Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hone Documentation

Hone is a native, AI-powered code editor built in TypeScript and compiled to native binaries via the Perry compiler — a TypeScript-to-native AOT compiler written in Rust. Hone runs without V8 or Node.js at runtime; the generated binaries link directly against platform UI frameworks.

Who these docs are for

  • Hone contributors — developers working on the IDE itself, its editor, services, or built-in extensions
  • Plugin developers — anyone building V2 plugins for the Hone marketplace using @hone/sdk

How to navigate

SectionAudienceWhat you’ll find
Getting StartedContributorsBuild instructions, project layout, Perry constraint cheat sheet
ArchitectureContributorsDeep-dives into how each major subsystem works
PackagesContributorsReference docs for every package in the monorepo
Plugin DevelopmentPlugin devsGuides for building, testing, and publishing plugins
Perry IntegrationContributorsDetailed Perry AOT constraints, FFI conventions, and build commands
ServicesOperatorsHow to run and deploy Hone’s backend services
TestingContributorsPer-package test runners and UI testing tools
ContributingContributorsDev environment setup and project conventions

Key concepts

  • Perry AOT — TypeScript is compiled ahead-of-time to native machine code. There is no JavaScript runtime. This imposes specific constraints on the TypeScript you can write.
  • Monorepo, no workspace manager — Each package (hone-core, hone-editor, hone-ide, etc.) is independent with its own package.json. There is no top-level package.json or Yarn/pnpm workspace.
  • TS-authoritative rendering — TypeScript owns all document state. Native platform code (Rust FFI) is a rendering cache that receives computed lines and viewport state.
  • Two extension systems — V1 built-in extensions ship with the IDE. V2 plugins are independently compiled native binaries distributed via the marketplace.

Building Hone

Prerequisites

  • Bun – used as the test runner for most packages
  • Rust toolchain – required to build the Perry compiler and its runtime libraries
  • Perry compiler binary – compiles TypeScript to native binaries (no V8/Node.js at runtime)

Install Bun: https://bun.sh Install Rust: https://rustup.rs

Building Perry

Before compiling any Hone package to a native binary, you need a working Perry compiler.

# Build Perry (always disable LTO)
cd ../perry && CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry

# Build Perry UI library (macOS)
cd ../perry && CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry-ui-macos

# Rebuild Perry stdlib (after changing perry-runtime source)
cd ../perry && cargo clean -p perry-runtime --release && \
  CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry-stdlib -p perry-ui-macos -p perry

Critical: Always set CARGO_PROFILE_RELEASE_LTO=off for all Perry Rust builds. Thin LTO produces bitcode that the macOS clang linker cannot read.

Running Tests

Each package has its own test suite. There is no monorepo-wide test runner.

cd hone-core && bun test              # 649+ tests
cd hone-editor && bun test            # 353 tests
cd hone-terminal && bun test          # 163 tests
cd hone-relay && bun test             # 48 tests
cd hone-build && bun test             # 21 tests
cd hone-themes && npm test            # 452 tests (Jest)
cd hone-extensions && npm test        # Vitest
cd hone-api && npm test               # Type-check only (tsc --noEmit)

Test runner rules:

  • hone-core, hone-editor, hone-terminal, hone-relay, and hone-build use bun test – not vitest, not npx.
  • hone-themes uses Jest.
  • hone-extensions uses Vitest.

Run a single test file:

bun test tests/buffer.test.ts

Type Checking

cd <package> && bun run typecheck
# or
cd <package> && npx tsc --noEmit

Compiling Native Binaries

Use the Perry compiler to produce platform-specific binaries from TypeScript entry points.

IDE

# macOS native binary
cd hone-ide && perry compile src/app.ts --output hone-ide

# iOS Simulator
cd hone-ide && perry compile src/app.ts --target ios-simulator --output Hone

# Web
cd hone-ide && perry compile src/app.ts --target web --output hone-ide.html

Services

cd hone-auth && perry compile src/app.ts --output hone-auth
cd hone-marketplace && perry compile src/app.ts --output hone-marketplace
cd hone-build && perry compile src/app.ts --output hone-build

UI Testing (macOS)

geisterhand screenshot --output /tmp/shot.png   # Take screenshot
geisterhand click x y                            # Click at coordinates

For iOS Simulator, use osascript (AppleScript) instead of geisterhand.

Project Structure

Hone is a monorepo of independent packages. There is no top-level package.json and no workspace manager (no npm workspaces, no Turborepo, no Lerna). Each package stands alone with its own dependencies and build configuration.

Package Directory

DirectoryPurposeRuntime
hone-core/Headless IDE services (workspace, settings, git, search, LSP, DAP, AI, extensions)Bun (tests only)
hone-editor/Embeddable code editor – piece table buffer, multi-cursor, syntax highlighting, diffBun (tests), Perry (native)
hone-ide/IDE workbench shell – activity bar, sidebar, tabs, panels, theme enginePerry (native binary)
hone-terminal/Terminal emulator – VT parser, PTY, cross-platform Rust FFIBun (tests), Perry (native)
hone-auth/Auth service (magic-link login, device pairing, subscriptions) – Fastify serverPerry (native binary, 2.8 MB)
hone-relay/WebSocket sync relay (cross-device delta sync, SQLite persistence)Bun / Perry
hone-build/Plugin build coordinator (submits to perry-hub for cross-platform compilation)Perry (native binary)
hone-marketplace/Plugin marketplace server (marketplace.hone.codes)Perry (native binary)
hone-api/Public extension API types (@honeide/api) – pure declarations, zero runtimetsc only
hone-extensions/11 built-in IDE extensions (TypeScript, Python, Rust, Go, etc.)Perry
hone-extension/V2 plugin SDK (@hone/sdk), Rust plugin host, marketplace client, CLIMixed
hone-themes/15 VSCode-compatible color themes (@honeide/themes) – pure JSON dataJest
hone-brand/Logos, colors, typography, brand guidelinesStatic assets
landing/Landing page (hone.codes) – single index.html, no build stepStatic
account.hone.codes/Account dashboard SPAStatic

How Packages Relate

IDE application

hone-ide is the main application binary. It depends on:

  • hone-editor – the embeddable editor component (buffer, cursors, viewport, syntax highlighting, diff)
  • hone-core – headless IDE services (workspace management, settings, git integration, search, LSP/DAP clients, AI, extension host)
  • hone-terminal – the terminal emulator (VT parsing, PTY management, platform-specific Rust FFI)

Embeddable editor

hone-editor is designed to be used independently of the full IDE. Its core/ directory is entirely platform-independent. Platform-specific rendering lives in native/, with Rust FFI crates for macOS, iOS, Windows, Linux, Android, and Web.

Services

The backend services are standalone binaries, each compiled separately with Perry:

  • hone-auth (port 8445) – authentication, device pairing, JWT tokens
  • hone-relay (port 8443/8444) – WebSocket rooms for cross-device sync
  • hone-marketplace (port 8446) – plugin search, download, publish
  • hone-build (port 8447) – plugin cross-compilation via perry-hub workers

All services run on webserver.skelpo.net with MySQL for persistent storage and SQLite for relay delta persistence.

Extensions and themes

  • hone-extensions/ contains 11 built-in extensions that ship with the IDE.
  • hone-extension/ provides the V2 plugin SDK (@hone/sdk) for third-party extension authors, plus the Rust plugin host and CLI tooling.
  • hone-api/ defines the public extension API surface (@honeide/api) as pure TypeScript declarations with no runtime code.
  • hone-themes/ ships 15 VSCode-compatible color themes as JSON data.

Static sites

  • landing/ is the hone.codes landing page – a single index.html with no build step.
  • account.hone.codes/ is the account dashboard SPA.
  • hone-brand/ holds logos, color definitions, typography, and brand guidelines.

Perry Constraints (Quick Reference)

Perry compiles TypeScript directly to native machine code via Rust codegen. Several JavaScript patterns that rely on V8 runtime behavior are not supported. This page lists each constraint with concrete before/after examples.

For full explanations, see the detailed constraints page.

Constraint Table

PatternUse Instead
obj[variable] dynamic key accessif/else if per key
?. optional chainingExplicit null checks
?? nullish coalescingif (x !== undefined)
/regex/.test()indexOf or char checks
{ key } ES6 shorthand{ key: key } explicit
array.map(fn) on class fieldsfor loop
for...of on arraysfor (let i = 0; i < arr.length; i++)
c >= 'a' && c <= 'z' char rangesALPHA_STR.indexOf(c) >= 0
Closures capturing this methodsModule-level functions + module-level vars
requestAnimationFramesetInterval
setTimeout self-recursionsetInterval
String-returning functions in asyncInline string operations
new Date() in asyncDate.now()

Examples

No dynamic key access

// Don't
const value = obj[key];

// Do
let value: string;
if (key === "name") {
  value = obj.name;
} else if (key === "type") {
  value = obj.type;
}

No optional chaining

// Don't
const name = user?.profile?.name;

// Do
let name: string | undefined;
if (user !== null && user !== undefined) {
  if (user.profile !== null && user.profile !== undefined) {
    name = user.profile.name;
  }
}

No nullish coalescing

// Don't
const port = config.port ?? 8080;

// Do
let port = 8080;
if (config.port !== undefined) {
  port = config.port;
}

No regex

// Don't
if (/^[a-z]+$/.test(input)) { ... }

// Do
const ALPHA = "abcdefghijklmnopqrstuvwxyz";
let allAlpha = true;
for (let i = 0; i < input.length; i++) {
  if (ALPHA.indexOf(input[i]) < 0) {
    allAlpha = false;
    break;
  }
}

No ES6 shorthand properties

// Don't
const obj = { name, age };

// Do
const obj = { name: name, age: age };

No array.map() on class fields

// Don't
class Editor {
  lines: string[] = [];
  getUpperLines() {
    return this.lines.map((l) => l.toUpperCase());
  }
}

// Do
class Editor {
  lines: string[] = [];
  getUpperLines(): string[] {
    const result: string[] = [];
    for (let i = 0; i < this.lines.length; i++) {
      result.push(this.lines[i].toUpperCase());
    }
    return result;
  }
}

No for...of on arrays

// Don't
for (const item of items) {
  process(item);
}

// Do
for (let i = 0; i < items.length; i++) {
  process(items[i]);
}

No character range comparisons

// Don't
if (c >= 'a' && c <= 'z') { ... }

// Do
const ALPHA_LOWER = "abcdefghijklmnopqrstuvwxyz";
if (ALPHA_LOWER.indexOf(c) >= 0) { ... }

No closures capturing this

// Don't
class App {
  count = 0;
  start() {
    setInterval(() => {
      this.count++;       // closure captures `this` by value
      this.render();
    }, 16);
  }
}

// Do
let count = 0;

function tick() {
  count++;
  render();
}

function render() { /* ... */ }

setInterval(tick, 16);

Closure rule: Perry captures closure variables by value, not by reference. Mutations inside a closure do not affect the outer variable. Store mutable state in module-level let variables and access them through module-level named functions.

No requestAnimationFrame

// Don't
function loop() {
  update();
  requestAnimationFrame(loop);  // never fires in Perry
}

// Do
setInterval(update, 16);

No setTimeout self-recursion

// Don't
function poll() {
  fetchData();
  setTimeout(poll, 1000);  // only fires once in Perry
}

// Do
setInterval(fetchData, 1000);

No string-returning functions in async

// Don't
async function getName(): Promise<string> {
  return buildName(first, last);  // returns NaN-boxed pointer
}

// Do
async function getName(): Promise<string> {
  const name = first + " " + last;  // inline the string operation
  return name;
}

No new Date() in async

// Don't
async function logTime() {
  const now = new Date();
  console.log(now.toISOString());
}

// Do
async function logTime() {
  const now = Date.now();
  console.log(now.toString());
}

Architecture Overview

Hone is a native IDE built in TypeScript and compiled to native binaries via the Perry AOT compiler. Perry compiles TypeScript directly to machine code through Rust codegen – no V8 or Node.js exists at runtime. The generated binaries link against libperry_stdlib.a (the Perry runtime) and platform-specific UI libraries (perry-ui-macos, perry-ui-ios, perry-ui-windows).

Compilation Pipeline

TypeScript source
       |
       v
 Perry compiler (TS → Rust codegen)
       |
       v
    LLVM IR
       |
       v
 Native binary
   + libperry_stdlib.a    (runtime library)
   + perry-ui-{platform}  (macOS / iOS / Windows / Linux / Android / Web)

Package Composition

The IDE is assembled from three main packages plus backend services:

hone-ide  (workbench shell)
  ├── hone-editor  (embeddable code editor)
  │     ├── core/        — platform-independent (buffer, cursor, search, diff, LSP, DAP)
  │     ├── view-model/  — reactive state bridge (EditorViewModel)
  │     └── native/      — Rust FFI per platform (Metal, Direct2D, Cairo, WASM)
  ├── hone-core    (headless IDE services)
  │     ├── workspace    — project management, file watching
  │     ├── settings     — layered settings (language > workspace > user > defaults)
  │     ├── git          — built-in git operations
  │     ├── search       — workspace-wide search
  │     ├── LSP          — language server protocol management
  │     ├── DAP          — debug adapter protocol management
  │     ├── AI           — provider registry, inline completion, chat, agent, review
  │     ├── extensions   — extension registry and lifecycle
  │     ├── tasks        — task runner
  │     ├── formatting   — code formatting
  │     ├── telemetry    — usage telemetry
  │     ├── sync         — cross-device sync client
  │     └── changes queue — batched change processing
  └── hone-terminal (terminal emulator)
        ├── VT parser
        ├── PTY management
        └── cross-platform Rust FFI

Backend Services

Four server-side services support the IDE:

ServiceHostPortPurpose
hone-authauth.hone.codes8445Magic-link login, device pairing, JWT tokens, subscriptions
hone-relaysync.hone.codes8443/8444WebSocket rooms for cross-device delta sync, SQLite persistence
hone-marketplacemarketplace.hone.codes8446Plugin search, download, publish
hone-build8447Plugin cross-compilation via perry-hub workers

All backend services are also compiled to native binaries via Perry and run on webserver.skelpo.net (84.32.223.50, Ubuntu 24.04).

Supporting Packages

PackagePurpose
hone-apiPublic extension API types (@honeide/api) – pure declarations, zero runtime
hone-extensionV2 plugin SDK (@hone/sdk), Rust plugin host, marketplace client, CLI
hone-extensions11 built-in language extensions (TypeScript, Python, Rust, Go, etc.)
hone-themes15 VSCode-compatible color themes (@honeide/themes)
hone-brandLogos, colors, typography, brand guidelines

Testing

Each package is tested independently – there is no monorepo test runner:

  • hone-core: 649+ tests via bun test
  • hone-editor: 353 tests via bun test
  • hone-terminal: 163 tests via bun test
  • hone-relay: 48 tests via bun test
  • hone-build: 21 tests via bun test
  • hone-themes: 452 tests via Jest
  • hone-extensions: Vitest
  • hone-api: type-check only (tsc --noEmit)

Compilation Model

Hone compiles TypeScript to native binaries using the Perry AOT compiler – a TypeScript-to-native ahead-of-time compiler written in Rust. There is no V8, no JIT, and no JavaScript runtime at execution time.

How Perry Compiles TypeScript

Perry performs whole-program compilation:

  1. Parse – TypeScript source is parsed and type-checked
  2. Codegen – Rust codegen emits LLVM IR from the typed AST
  3. Link – LLVM produces a native binary linked against libperry_stdlib.a and platform UI libraries
# Compile the IDE for macOS
cd hone-ide && perry compile src/app.ts --output hone-ide

# Compile for iOS simulator
cd hone-ide && perry compile src/app.ts --target ios-simulator --output Hone

# Compile for web (WASM)
cd hone-ide && perry compile src/app.ts --target web --output hone-ide.html

NaN-Boxing

All values at runtime are 64-bit doubles. Pointers, strings, objects, and other non-numeric types are encoded inside the NaN payload bits of IEEE 754 doubles. This means:

  • Every variable is a single f64 at the machine level
  • Type checks are bitwise operations on the NaN payload
  • No heap tagging or boxed value types
  • String parameters across FFI are NaN-boxed StringHeader pointers

No Dynamic Dispatch

All types are resolved at compile time. Perry does not support:

  • Dynamic property access (obj[variable] – use if/else if chains)
  • Optional chaining (?. – use explicit null checks)
  • Nullish coalescing (?? – use explicit if (x !== undefined))
  • Regular expressions (/regex/.test() – use indexOf or character checks)

Closure Semantics

Perry captures closure variables by value, not by reference. This is the most important semantic difference from standard JavaScript/TypeScript:

// WRONG -- Perry captures count by value at closure creation time
let count = 0;
function increment() {
  count++; // Always increments a stale copy
}

// CORRECT -- Use module-level variables and module-level functions
let count = 0;
function increment() {
  count++; // Module-level function reads the live module-level variable
}

Store mutable state in module-level let variables and access them through module-level named functions.

package.json Perry Section

Each package declares its Perry configuration in package.json:

{
  "perry": {
    "nativeLibrary": {
      "functions": [
        "createWindow",
        "drawText",
        "handleInput"
      ]
    },
    "targets": ["macos", "ios", "windows", "linux", "web"]
  }
}

FFI functions listed here are resolved at link time. Perry generates __wrapper_<function_name> symbols (double underscore prefix) for each declared function.

FFI Conventions

  • String parameters are NaN-boxed StringHeader pointers. Rust receives *const u8 and uses str_from_header() to decode.
  • Use f64 for numeric FFI parameters.
  • Use i64 for string/pointer FFI parameters.
  • Do not use i32 – it causes verifier errors.

Perry-Specific Patterns to Avoid

PatternUse Instead
obj[variable] dynamic key accessif/else if per key
?. optional chainingExplicit null checks
?? nullish coalescingif (x !== undefined)
/regex/.test()indexOf or char checks
{ key } ES6 shorthand{ key: key }
array.map(fn) on class fieldsfor loop
for...of on arraysfor (let i = 0; i < arr.length; i++)
c >= 'a' && c <= 'z' char rangesALPHA_STR.indexOf(c) >= 0
Closures capturing this methodsModule-level functions + module-level vars
requestAnimationFramesetInterval
setTimeout self-recursionsetInterval
String-returning functions in asyncInline string operations
new Date() in asyncDate.now()

Building Perry Itself

When modifying Perry’s Rust source (located at ../perry/):

# Build the compiler
cd ../perry && CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry

# Build the macOS UI library
cd ../perry && CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry-ui-macos

# Rebuild stdlib after changing perry-runtime source
cd ../perry && cargo clean -p perry-runtime --release && \
  CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry-stdlib -p perry-ui-macos -p perry

Always use CARGO_PROFILE_RELEASE_LTO=off – thin LTO produces bitcode that the macOS clang linker cannot read.

Editor Architecture

hone-editor is the embeddable code editor component (@honeide/editor). It is used by hone-ide but can also be embedded independently. The editor has three layers: core (platform-independent logic), view-model (reactive state bridge), and native (Rust FFI rendering per platform).

core/ – Platform-Independent

All editor logic lives here. No platform dependencies, no FFI, no rendering.

Buffer (buffer/)

Text storage uses a piece table backed by a rope with a line index:

  • Piece table tracks original and add buffers with a sequence of pieces
  • Rope provides O(log n) random access and efficient insertions/deletions
  • Line index maintains a mapping from line numbers to buffer offsets
  • Supports large files without loading entire content into a flat string

Cursor (cursor/)

Multi-cursor management:

  • Multiple independent cursors with selection ranges
  • Word boundary detection for double-click selection and ctrl+arrow navigation
  • Cursor affinity (left/right) for ambiguous positions at line wraps
  • Selection operations: expand to word, line, bracket, all

History (history/)

Undo/redo with time-based coalescing:

  • Changes within a 500ms window are grouped into a single undo step
  • Maximum undo stack depth: 10,000 entries
  • Branching undo (redo stack preserved until a new edit diverges)

Viewport (viewport/)

Virtual scrolling for performance with large files:

  • Only renders visible lines plus a 10-line buffer zone above and below
  • Hidden line tracking for code folding integration
  • Smooth scrolling with pixel-level offset tracking
  • Viewport resize handling

Tokenizer (tokenizer/)

Incremental syntax highlighting via two engines:

  • Lezer parser integration – 8 language grammars: TypeScript, HTML, CSS, JSON, Markdown, Python, Rust, C++
  • Keyword-based fallback engine – for languages without a Lezer grammar
  • Incremental re-tokenization on edits (only re-parses changed regions)
  • Token types map to theme color scopes

Find and replace across the document:

  • Literal and regex search modes
  • Chunked search for large files (avoids blocking the main thread)
  • Incremental live search (results update as the user types)
  • Match highlighting with current-match distinction

Folding (folding/)

Two folding strategies:

  • Indent-based – folds regions where indentation increases
  • Syntax-based – uses Lezer parse tree to identify foldable nodes (functions, classes, blocks)
  • Nested folding with level tracking

Diff (diff/)

Myers diff algorithm implementation:

  • O(ND) complexity for computing minimal edit scripts
  • Hunk-based operations (apply hunk, revert hunk)
  • Inline character-level diff within changed lines
  • Used by the git diff view and AI review

Document (document/)

Wraps TextBuffer with document metadata:

  • URI, languageId, version counter
  • isDirty tracking
  • Encoding detection (UTF-8, UTF-16, etc.)
  • Line-ending detection and normalization (LF, CRLF)

Commands (commands/)

Command registry with 5 command modules:

  • Editing – insert, delete, indent, comment toggle, auto-close brackets
  • Navigation – go to line, go to definition, go to bracket
  • Selection – select all, expand selection, shrink selection
  • Clipboard – cut, copy, paste with multi-cursor support
  • Multi-cursor – add cursor above/below, add cursor at next occurrence

LSP Client (lsp/)

Language Server Protocol client implementation:

  • JSON-RPC 2.0 transport layer
  • Typed request/response methods:
    • textDocument/completion – autocomplete
    • textDocument/hover – hover information
    • textDocument/publishDiagnostics – errors and warnings
    • textDocument/codeAction – quick fixes and refactorings
    • textDocument/definition – go to definition
    • textDocument/references – find all references
    • textDocument/formatting – document formatting

DAP Client (dap/)

Debug Adapter Protocol client:

  • Session management: launch and attach configurations
  • Breakpoint management (line, conditional, logpoint)
  • Execution control: continue, step over, step into, step out, pause
  • Stack frame inspection
  • Variable inspection with scopes (local, closure, global)

Snippets (snippets/)

  • Template processing with tab stops and placeholders
  • Language-specific snippet registries
  • Variable expansion ($TM_FILENAME, $CLIPBOARD, etc.)

view-model/ – Reactive State Bridge

EditorViewModel orchestrates all core subsystems and exposes reactive state for rendering:

  • Cursor state – positions, selections, blink timer
  • Decorations – inline decorations, line decorations, overlay widgets
  • Diff view model – side-by-side and inline diff rendering state
  • Find widget – search state, match counts, replace mode
  • Ghost text – AI inline completion overlay
  • Gutter – line numbers, fold indicators, breakpoint markers, diagnostics
  • Line layout – computed line heights, wrapped line mapping
  • Minimap – scaled-down document overview with viewport indicator
  • Overlays – autocomplete popover, hover card, parameter hints
  • Theme – resolved token colors and editor colors

native/ – Rust FFI Per Platform

The editor uses a TS-authoritative model: TypeScript is the single source of truth for all document state. The Rust layer is a rendering cache that receives pre-computed line content and viewport state via FFI.

NativeEditorFFI Interface

93+ FFI functions organized by concern:

  • Document operations (set content, apply edit, get line)
  • Viewport management (scroll, resize, set visible range)
  • Cursor rendering (set positions, blink state)
  • Selection rendering (set selection ranges)
  • Decoration management (add/remove/update decorations)
  • Theme application (set colors, font metrics)
  • Input handling (key events, mouse events, IME)

Platform Crates

PlatformRenderingText Layout
macOSMetalCoreText
iOSUIKit / MetalCoreText
WindowsDirect2DDirectWrite
LinuxCairoPango
AndroidSkiaHarfBuzz
WebWASM + CanvasBrowser layout

Each platform crate implements the same NativeEditorFFI interface, so the TypeScript layer is entirely platform-agnostic.

Workbench Architecture

hone-ide is the IDE workbench shell. It assembles the editor, terminal, and core services into the full IDE experience. Compiled to a native binary via Perry.

Entry Point

src/app.ts is the Perry App() entry point:

App({
  title: 'Hone',
  width: 1200,
  height: 800,
  icon: 'hone-icon.png',
  body: body
})

Bootstrap sequence:

  1. Load and apply theme
  2. Detect platform (__platform__ compile-time constant: 0=macOS, 1=iOS, 2=Android)
  3. Register commands and keybindings
  4. Initialize panel registry
  5. Check for first-run (show setup screen) or launch normal workbench
  6. Initialize plugin system (conditional on __plugins__ compile flag)

Render Tree

src/workbench/render.ts defines the main UI layout:

+--+---------------------+------------------+
|  |                     |                  |
|A |     Sidebar         |   Editor Area    |
|c |  (explorer, search, |   (tab bar +     |
|t |   git, debug, etc.) |    editors)      |
|i |                     |                  |
|v |                     |                  |
|i |                     +------------------+
|t |                     |   Panels         |
|y |                     |  (terminal,      |
|  |                     |   output, debug) |
|B |                     |                  |
|a |                     |                  |
|r |                     |                  |
+--+---------------------+------------------+
|          Status Bar                       |
+-------------------------------------------+
  • Activity bar – Left icon strip for switching sidebar views
  • Sidebar – Contextual panel selected by activity bar
  • Editor area – Tab bar with open files, editor instances below
  • Panels – Bottom area for terminal, output, debug console
  • Status bar – Language mode, cursor position, encoding, git branch, sync status

Views

25+ views organized in src/workbench/views/:

ViewPurpose
ai-chatAI chat panel with multi-turn conversation
ai-inlineAI inline completion UI
debugDebugger controls, call stack, variables, watch
diffSide-by-side and inline diff viewer
explorerFile tree, open editors
extensionsInstalled and available extensions
findFind and replace across workspace
gitStaging, commits, branches, remotes
lspLanguage server UI components:
autocomplete: completion popover
diagnostics: error/warning list
hover: hover information card
signature: function signature help
notificationsToast notifications
pluginsV2 plugin management
pr-reviewPull request review with AI annotations
quick-openFuzzy file finder (Cmd+P)
recentRecently opened files and workspaces
searchWorkspace-wide text search
settings-uiVisual settings editor
setupFirst-run configuration wizard
syncCross-device sync status and settings
tabsTab bar and tab management
terminalIntegrated terminal
updateApplication update notifications
welcomeWelcome tab with getting-started content
status-barStatus bar segments

Command System

Commands are the primary way to expose IDE functionality:

  • Command registry – central map of command ID to handler function
  • Keybindings – modifier keys, chord sequences (e.g., Ctrl+K Ctrl+C), platform-specific defaults
  • When-clauses – conditional activation (e.g., editorTextFocus, inDebugMode)
  • Commands are contributed by core, views, and extensions

Native Menu Bar

Platform-native menu bar setup with standard menus (File, Edit, Selection, View, Go, Run, Terminal, Help). Menu items map to commands.

Grid Layout System

The workbench uses a grid layout for arranging sidebar, editor area, and panels:

  • Resizable splits between sidebar and editor area
  • Resizable splits between editor area and bottom panels
  • Collapsible sidebar and panels
  • Persisted layout dimensions in settings

Panel Registry

Built-in panels are registered at startup:

  • Terminal panel
  • Output panel
  • Debug console panel
  • Additional panels contributed by extensions

Theme Engine

Loads and applies VSCode-compatible JSON themes:

  • Theme colors accessed via getter functions (getEditorBackground(), getSideBarBackground(), etc.)
  • Applied at two levels: workbench chrome colors and editor token colors
  • Theme switching without restart
  • See Theme Architecture for details

Settings

Layered settings with precedence:

  1. Language-specific (highest priority)
  2. Workspace (.hone/settings.json)
  3. User (~/.hone/settings.json)
  4. Defaults (lowest priority)

Perry UI Considerations

Perry’s UI system creates widgets once and mutates them imperatively (not declarative/virtual-DOM). Key constraints:

  • Widgets are created in the render function and updated via property mutations
  • Closures must follow Perry’s by-value capture rule – use module-level state and module-level functions
  • No requestAnimationFrame – use setInterval for animation loops
  • No setTimeout self-recursion – use setInterval

Sync Architecture

Cross-device sync for the Hone IDE. Edit on desktop and see changes on mobile (and vice versa) in real time. Sync is off by default – users opt in via Settings > Sync.

Components

Auth Service (auth.hone.codes, port 8445)

Magic-link login, device token management, project entitlements. Built with Fastify, backed by MySQL on webserver.skelpo.net.

Relay Server (sync.hone.codes, ports 8443/8444)

WebSocket message router between devices in the same room. SQLite persistence for buffering messages during disconnects.

Auth Flow

1. User enters email in IDE settings
2. IDE calls GET /auth/login?email=...
3. Auth creates 64-char random hex token, stores in magic_links table (15-min expiry)
4. Email sent to user (or logged to console in dev mode)
5. User clicks link: GET /auth/verify?token=...&deviceName=...&platform=...
6. Auth validates token, creates user + device records, generates device token
7. IDE stores deviceToken locally

Device Token Format

userId:deviceId:timestamp.hash

Where hash is a double-djb2 HMAC. A shared secret between auth and relay allows the relay to validate tokens locally without calling back to auth.

Projects and Tiers

Projects map local workspaces to relay rooms. Each project has a projectKey, name, roomId, and userId.

TierSynced Projects
free0
personal1
prounlimited
teamunlimited

Project registration is idempotent – registering the same project key twice returns the existing record.

Relay Protocol

Join

{
  "type": "join",
  "room": "<roomId>",
  "device": "<deviceId>",
  "token": "<deviceToken>"
}

Joined (response)

{
  "type": "joined",
  "room": "<roomId>",
  "device": "<deviceId>"
}

Messages

{
  "from": "<deviceId>",
  "to": "host | broadcast | <targetDeviceId>",
  "room": "<roomId>",
  "payload": { ... }
}

Relay Behavior

  • Room-based routing – room IDs hashed via djb2 for slot assignment
  • Slot tracking – each connection occupies a slot in a room
  • Host election – first device to join a room becomes host
  • 60-second message buffer – messages buffered for reconnecting devices (covers brief disconnects)
  • Rate limiting – per-connection rate limiting to prevent abuse
  • Auth bypass – when auth.secret is empty, token validation is skipped (dev mode only)

Auth Endpoints

MethodPathPurpose
GET/auth/loginInitiate magic-link login (params: email)
GET/auth/verifyVerify magic link and register device (params: token, deviceName, platform)
GET/auth/deviceGet device info (header: Authorization)
GET/auth/devicesList all devices for the authenticated user
DELETE/auth/device/:idRemove a device
POST/auth/projectRegister a project for sync
GET/auth/projectsList synced projects
DELETE/auth/project/:idRemove a synced project
GET/auth/subscriptionGet current subscription tier

All endpoints except /auth/login and /auth/verify require the Authorization: Bearer <deviceToken> header.

Database Schema

users

CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(255) NOT NULL UNIQUE,
  createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
);

devices

CREATE TABLE devices (
  id INT AUTO_INCREMENT PRIMARY KEY,
  userId INT NOT NULL,
  deviceName VARCHAR(255) NOT NULL,
  platform VARCHAR(50) NOT NULL,
  token VARCHAR(255) NOT NULL UNIQUE,
  createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
  lastSeen DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE magic_links (
  id INT AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(255) NOT NULL,
  token VARCHAR(255) NOT NULL UNIQUE,
  expiresAt DATETIME NOT NULL,
  used BOOLEAN DEFAULT FALSE,
  createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
);

projects

CREATE TABLE projects (
  id INT AUTO_INCREMENT PRIMARY KEY,
  userId INT NOT NULL,
  projectKey VARCHAR(255) NOT NULL,
  name VARCHAR(255) NOT NULL,
  roomId VARCHAR(255) NOT NULL UNIQUE,
  createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE,
  UNIQUE KEY unique_user_project (userId, projectKey)
);

subscriptions

CREATE TABLE subscriptions (
  id INT AUTO_INCREMENT PRIMARY KEY,
  userId INT NOT NULL UNIQUE,
  tier ENUM('free', 'personal', 'pro', 'team') DEFAULT 'free',
  expiresAt DATETIME,
  createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
);

All identifiers use camelCase (matching Hone’s database convention).

IDE Settings

SettingDefaultDescription
syncEnabledfalseEnable cross-device sync
syncRelayUrlwss://sync.hone.codesRelay server WebSocket URL
syncAuthUrlhttps://auth.hone.codesAuth service URL
syncDeviceToken(empty)Stored device token after login

Deployment

Both services run on 84.32.223.50 (Ubuntu 24.04):

  • TLS termination via Let’s Encrypt + nginx reverse proxy
  • Auth config: auth.conf (KEY=VALUE format)
  • Relay config: relay.conf (KEY=VALUE format)
  • MySQL database: host=webserver.skelpo.net, user=hone

Config File Format

# auth.conf
db.host=webserver.skelpo.net
db.user=hone
db.password=...
db.name=hone
auth.secret=<shared-secret>
smtp.host=...
smtp.port=587
smtp.user=...
smtp.password=...
# relay.conf
auth.secret=<shared-secret>
sqlite.path=/var/lib/hone-relay/relay.db

Self-Hosted

Users can run their own relay and auth services. Both are single native binaries:

  1. Compile: perry compile src/app.ts --output hone-auth / hone-relay
  2. Create config file (auth.conf / relay.conf)
  3. Run the binary

Point the IDE settings (syncRelayUrl, syncAuthUrl) at your own server.

AI Architecture

Hone integrates AI at multiple levels: inline code completion, multi-turn chat, automated code review, and an autonomous agent. The AI system is provider-agnostic, with a registry of adapters for major LLM providers.

Provider System

ProviderRegistry

Central registry managing AI provider adapters. Each adapter implements a common interface for chat completions and streaming.

Adapters:

ProviderDescription
AnthropicClaude models (direct API)
OpenAIGPT models (direct API)
GoogleGemini models
OllamaLocal models via Ollama
Azure OpenAIOpenAI models via Azure
BedrockAWS Bedrock (Anthropic, etc.)
VertexGoogle Cloud Vertex AI

ModelRouter

Selects the appropriate model for a given task based on:

  • User preferences (configured default model)
  • Task type (completion vs. chat vs. review)
  • Model capabilities (context window, tool use support)
  • Provider availability

Token Estimation

Estimates token counts before sending requests to:

  • Stay within context window limits
  • Truncate or chunk context when necessary
  • Provide usage feedback to the user

AI Surfaces

1. Inline Completion (ai/inline/)

Ghost text suggestions that appear as the user types.

  • Fill-In-Middle (FIM) formatting – constructs prompts with prefix, suffix, and cursor position for infilling
  • Debouncing – waits for a pause in typing before sending requests (avoids flooding the provider)
  • Caching – recently generated completions are cached and reused when the user types matching characters
  • Request lifecycle – manages in-flight requests, cancels stale requests when the cursor moves

The completion appears as dimmed ghost text. The user accepts with Tab or dismisses by continuing to type.

2. Chat (ai/chat/)

Multi-turn AI chat panel integrated into the IDE.

  • Multi-turn conversation – maintains conversation history with system prompt, user messages, and assistant responses
  • Context collection – automatically gathers relevant context:
    • Current file content and cursor position
    • Selected text
    • Visible viewport
    • Diagnostics (errors/warnings)
    • Open file list
  • Code block extraction – parses assistant responses to identify code blocks with language and file path annotations
  • Streaming renderer – renders assistant responses token-by-token as they stream in

3. Review (ai/review/)

AI-powered code review for diffs and pull requests.

  • Diff chunking – splits large diffs into reviewable chunks that fit within the model’s context window
  • Annotation parsing – extracts structured annotations (line number, severity, suggestion) from the model’s response
  • Review engine – orchestrates the review process:
    1. Collects diff hunks
    2. Chunks them for the model
    3. Sends each chunk with review instructions
    4. Parses and aggregates annotations
    5. Displays inline annotations in the diff view

Agent System (ai/agent/)

Autonomous task execution with tool calling.

Plan and Execute Loop

  1. User describes a task in natural language
  2. Agent creates a plan (sequence of steps)
  3. Agent executes steps using tools:
    • File read/write
    • Terminal commands
    • Search (workspace-wide)
    • LSP queries (find references, go to definition)
  4. Agent observes results and adjusts plan
  5. Repeats until task is complete or blocked

Approval Flows

  • Auto-approve – read-only operations (file read, search) execute without confirmation
  • Prompt for approval – write operations (file edit, terminal commands) require user confirmation
  • Batch approval – user can approve all remaining steps in a plan

Error Recovery

When a tool call fails, the agent:

  1. Observes the error message
  2. Adjusts its approach
  3. Retries with a different strategy
  4. Escalates to the user if repeated failures occur

Activity Logging

All agent actions are logged for transparency:

  • Tool calls with arguments and results
  • Plan steps with status (pending, in-progress, completed, failed)
  • Token usage per step
  • Total elapsed time

Extension Architecture

Hone has two extension systems that coexist: V1 built-in extensions for language support that ships with the IDE, and V2 plugins for third-party and community extensions distributed via the marketplace.

V1 Built-in Extensions (hone-extensions/)

Language Packs

11 built-in language extensions ship with the IDE binary:

ExtensionLanguages
TypeScriptTypeScript, JavaScript, JSX, TSX
PythonPython
RustRust
GoGo
C++C, C++, Objective-C
HTML/CSSHTML, CSS, SCSS, Less
JSONJSON, JSONC
MarkdownMarkdown
DockerDockerfile, Docker Compose
TOML/YAMLTOML, YAML
GitGit commit, Git rebase, .gitignore

Manifest Format

Each V1 extension declares a hone-extension.json manifest:

{
  "name": "typescript",
  "displayName": "TypeScript Language",
  "version": "1.0.0",
  "activationEvents": [
    "onLanguage:typescript",
    "onLanguage:javascript"
  ],
  "contributes": {
    "languages": [
      {
        "id": "typescript",
        "extensions": [".ts", ".tsx"],
        "aliases": ["TypeScript"]
      }
    ],
    "lspServer": {
      "command": "typescript-language-server",
      "args": ["--stdio"],
      "languages": ["typescript", "javascript"]
    },
    "commands": [],
    "snippets": [
      {
        "language": "typescript",
        "path": "./snippets/typescript.json"
      }
    ]
  }
}

Loading

V1 extensions are loaded by ExtensionRegistry in hone-core:

  1. Scan hone-extensions/ directory for hone-extension.json manifests
  2. Parse manifests and register language contributions
  3. Activate extensions lazily based on activation events (e.g., when a TypeScript file is opened)
  4. Start LSP servers as needed

V2 Plugin SDK (hone-extension/)

Overview

V2 plugins are independent packages compiled to native binaries via Perry. They use the @hone/sdk package for the plugin API.

Plugin Structure

import { HonePlugin, HoneHost } from '@hone/sdk';

class MyPlugin extends HonePlugin {
  activate(host: HoneHost): void {
    host.commands.registerCommand('myPlugin.hello', () => {
      host.window.showInformationMessage('Hello from my plugin!');
    });
  }

  deactivate(): void {
    // cleanup
  }
}

HoneHost Interface

HoneHost provides 80+ types covering:

  • commands – register and execute commands
  • window – show messages, create output channels, status bar items
  • workspace – access files, folders, settings
  • editor – manipulate the active editor (selections, decorations, edits)
  • languages – register language features (completions, hover, diagnostics)
  • debug – register debug adapters
  • terminal – create and manage terminal instances

Execution Tiers

Plugins run in one of three tiers based on their declared permissions:

TierCapabilitiesIsolation
InProcessUI-only (commands, decorations, status bar)Runs in main process
PluginHostEditor access, file system readSeparate plugin host process
IsolatedProcessNetwork, file system write, spawn processesFully sandboxed process

The tier is declared in the plugin’s package.json:

{
  "hone": {
    "tier": "PluginHost",
    "permissions": ["fs.read", "editor"]
  }
}

Distribution

V2 plugins are distributed via the Hone marketplace (marketplace.hone.codes):

  1. Buildhone-build submits the plugin source to perry-hub workers for cross-platform compilation
  2. Publish – compiled binaries are uploaded to the marketplace
  3. Install – IDE downloads the platform-appropriate binary from the marketplace
  4. Update – marketplace notifies IDE of available updates

CLI

The hone-extension package includes a CLI for plugin development:

  • hone ext init – scaffold a new plugin project
  • hone ext build – compile the plugin
  • hone ext publish – publish to the marketplace
  • hone ext test – run plugin tests

Coexistence

Both systems can:

  • Register commands in the command registry
  • Contribute UI elements (sidebar views, status bar items, decorations)
  • Interact with the editor (read/write documents, manage cursors)
  • Respond to lifecycle events

V1 extensions handle core language support and ship bundled with the IDE. V2 plugins extend the IDE with third-party functionality and are installed independently by users. Both register with the same underlying command and contribution systems, so they interoperate seamlessly.

Theme Architecture

Hone uses VSCode-compatible JSON themes for both workbench colors and syntax highlighting. Themes are applied at two levels and can be switched at runtime without restarting the IDE.

Theme Format

Themes use the standard VSCode JSON theme format, making it possible to import any existing VSCode theme directly.

{
  "name": "Hone Dark",
  "type": "dark",
  "colors": {
    "editor.background": "#1e1e1e",
    "editor.foreground": "#d4d4d4",
    "sideBar.background": "#252526",
    "statusBar.background": "#007acc",
    "activityBar.background": "#333333"
  },
  "tokenColors": [
    {
      "scope": ["keyword", "storage.type"],
      "settings": {
        "foreground": "#569cd6"
      }
    },
    {
      "scope": ["string"],
      "settings": {
        "foreground": "#ce9178"
      }
    }
  ]
}

Two-Layer System

UI Theme (Workbench Colors)

The colors object defines colors for every workbench element:

  • Editor: background, foreground, line highlight, selection, cursor
  • Sidebar: background, foreground, border
  • Activity bar: background, foreground, badge
  • Status bar: background, foreground (normal, debugging, no-folder)
  • Tabs: active/inactive background and foreground, border
  • Panel: background, border
  • Title bar: background, foreground
  • Input fields, dropdowns, buttons, scrollbars, minimap, etc.

Colors are accessed through getter functions:

getEditorBackground()
getEditorForeground()
getSideBarBackground()
getStatusBarBackground()
getActivityBarBackground()
getTabActiveBackground()
getTabInactiveBackground()
getPanelBackground()
// ... and many more

Token Theme (Syntax Highlighting)

The tokenColors array maps TextMate scopes to colors and font styles:

  • keyword – language keywords (if, else, return, etc.)
  • string – string literals
  • comment – comments
  • variable – variable names
  • function – function names and calls
  • type – type names, classes, interfaces
  • constant – constants, numbers, booleans
  • operator – operators
  • punctuation – brackets, semicolons, commas

Token scopes support dot-separated specificity (e.g., entity.name.function.typescript is more specific than entity.name.function).

Built-in Themes

15 themes ship with the IDE (hone-themes package):

ThemeType
Hone Darkdark
Hone Lightlight
Catppuccindark
Draculadark
GitHub Darkdark
GitHub Lightlight
Gruvbox Darkdark
High Contrast Darkdark (high contrast)
High Contrast Lightlight (high contrast)
Monokaidark
Norddark
One Darkdark
Solarized Darkdark
Solarized Lightlight
Tokyo Nightdark

WCAG Contrast Validation

The hone-themes package includes 452 tests (via Jest) that validate:

  • Schema validation – every theme conforms to the expected JSON structure
  • WCAG contrast ratios – text colors meet accessibility contrast requirements against their backgrounds
    • Normal text: minimum 4.5:1 contrast ratio (AA)
    • Large text: minimum 3:1 contrast ratio (AA)
  • Required color keys – all themes define the minimum set of required colors
  • Token color coverage – all themes provide colors for core syntax scopes

Run theme tests:

cd hone-themes && npm test

Importing VSCode Themes

Any VSCode JSON theme file can be imported directly:

  1. Place the .json theme file in the themes directory
  2. The theme engine reads and parses the VSCode format natively
  3. No conversion or transformation step is needed

This compatibility means the thousands of themes available in the VSCode ecosystem work in Hone out of the box.

Theme Application

When a theme is applied:

  1. The theme JSON is parsed and colors are resolved (including defaults for missing keys)
  2. Workbench colors are set on all UI elements via getter functions
  3. Token colors are compiled into a scope-to-style lookup table for the tokenizer
  4. The editor re-renders with the new colors
  5. The selected theme is persisted in user settings

Theme switching is instant – no restart required.

hone-api

Package: @honeide/api — pure TypeScript declarations, zero runtime code. Defines the public extension API surface that all Hone extensions program against.

Namespaces

Each namespace corresponds to a separate source file:

NamespaceSourcePurpose
commandscommands.tsCommand registry and execution
workspaceworkspace.tsWorkspace, folders, and file operations
windowui.ts (exported as window)UI components and window management
editoreditor.tsEditor state, selections, viewport
languageslanguages.tsLanguage registration and configuration
debugdebug.tsDebugger protocol and control
terminalterminal.tsTerminal emulator and PTY
aiai.tsAI chat/completion APIs
uiui.tsUI component APIs (panels, status bar, notifications)
syncsync.tsCross-device sync types

Core Types

Defined in types.ts:

Disposable

Cleanup interface returned by registrations and subscriptions.

ExtensionContext

Passed to the extension’s activate function. Provides:

  • subscriptions — array of Disposable objects; disposed when the extension deactivates
  • extensionPath — absolute path to the extension’s install directory
  • storagePath — per-workspace storage path
  • globalStoragePath — global (cross-workspace) storage path

ActivateFunction

(context: ExtensionContext) => void | Promise<void>

DeactivateFunction

() => void | Promise<void>

Testing

cd hone-api && npm test

Type-check only via tsc --noEmit. No runtime tests — the package contains no executable code.

hone-core

Headless IDE services. Runtime: Bun (tests only). 649+ tests.

cd hone-core && bun test

Service Modules

workspace/

Multi-root workspace management. Tracks root folders, filesystem entries, and workspace configuration (.hone/ directory).

Exports: Workspace, WorkspaceFolder, FileEntry, FileWatcher, FileIndex

settings/

Layered settings resolution with 4-layer priority (highest to lowest):

  1. Language overrides
  2. Workspace overrides
  3. User settings
  4. Defaults

VS Code-compatible format. Exports: SettingsStore, KeybindingResolver, schema validation.

git/

Git integration via child_process. Parses status, diff, log, and blame output. Includes platform clients for GitHub, GitLab, and Bitbucket.

Exports: GitClient, diff/blame/log parsers

Workspace-wide text search. Supports literal and regex modes with case sensitivity and whole-word matching. Ripgrep integration for performance.

Exports: searchFileContent, RipgrepSearcher

protocols/lsp/

Language Server Protocol implementation. JSON-RPC 2.0 message framing.

Exports: JsonRpcRequest, JsonRpcResponse, JsonRpcNotification, ClientCapabilities, LSP types

protocols/dap/

Debug Adapter Protocol implementation. DAP message structure with Content-Length framing.

Exports: DapRequest, DapResponse, DapEvent, BreakpointManager, DapManager, DapClient

ai/provider/

Multi-provider AI system with dynamic routing.

  • ProviderRegistry — registers and resolves AI providers
  • ModelRouter — selects provider/model based on task requirements
  • Token estimation utilities

Adapters: Anthropic, OpenAI, Google, Ollama, Azure OpenAI, Bedrock, Vertex

ai/inline/

Inline code completion using Fill-In-Middle (FIM) formatting. Debouncing prevents excessive requests; caching avoids redundant completions.

Exports: InlineCompletionProvider, CompletionDebouncer, CompletionCache

ai/chat/

Multi-turn chat with streaming support. Manages conversation history, collects editor/workspace context, extracts code blocks from responses.

Exports: ChatModel, ContextCollector, StreamingRenderer

ai/agent/

Autonomous agent tasks. Executes a plan-then-act loop with tool calling, user approval flows, and error recovery.

Exports: AgentOrchestrator, AgentPlan, ActivityLog, AGENT_TOOLS

ai/review/

AI-powered pull request review. Splits diffs into reviewable chunks, parses annotations from AI responses.

Exports: ReviewEngine, chunkDiff, parseAnnotations

extensions/

Built-in extension management (V1 system). Loads, activates, and deactivates extensions based on parsed manifests.

Exports: ExtensionRegistry, ExtensionHost, parseManifest

tasks/

Task configuration and execution. Parses tasks.json format (VS Code-compatible), executes shell and process tasks.

Exports: parseTaskConfig, TaskRunner

sync/

Cross-device sync protocol. Handles device pairing, encryption, and transport (LAN discovery and relay fallback). Room-based sync with envelope format and sequence numbers.

queue/

Changes queue (review inbox). Tracks proposed changes with pending/accepted/rejected states. Detects conflicts before applying diffs.

Exports: ChangesQueue, DiffApplier, ConflictDetector, TrustConfig

formatting/

Document formatting rules. Perry-safe pure TypeScript (no regex). Handles trailing whitespace trimming, final newline insertion, and indentation normalization.

telemetry/

Anonymous usage telemetry. Batches events using the Chirp protocol (max 50 events per batch). No PII collected.

hone-editor

Package: @honeide/editor — embeddable code editor. 353 tests.

cd hone-editor && bun test

The editor is structured in three layers: core (platform-independent logic), view-model (reactive state bridge), and native (Rust FFI per platform).

Core

buffer/

Piece table backed by a rope with a line index. TextBuffer wraps rope internals with a clean API for editing and querying.

cursor/

Multi-cursor management. CursorManager handles primary and secondary cursors with merging, sorting, selection ranges, and word boundary detection.

history/

UndoManager with time-based coalescing (500ms window, max depth 10000). Each operation records: edits, deleted texts, cursor state before and after. Supports computeInverseEdits for redo.

viewport/

ViewportManager for virtual scrolling. Computes visible line range with a 10-line buffer zone. Tracks hidden lines from code folding. Includes ScrollController and LineHeightCache.

tokenizer/

SyntaxEngine via Lezer parser integration. Incremental tokenization — only re-tokenizes affected regions after edits. Token theme resolution maps parser tokens to editor colors. KeywordSyntaxEngine serves as a fallback when no grammar is available.

Bundled grammars: TypeScript, HTML, CSS, JSON, Markdown, Python, Rust, C++

search/

SearchEngine supporting literal and regex modes, chunked for large files. Replace supports regex capture group expansion. IncrementalSearch powers the live find widget.

folding/

Two folding strategies:

  • Indent-based — fallback for languages without a grammar
  • Syntax-based — uses Lezer parse tree

FoldState tracks collapsed ranges.

diff/

Myers diff algorithm with O(ND) complexity. DiffResult contains hunks typed as add, delete, or modify. Utilities: mergeAdjacentHunks, splitHunk, navigateHunks. Supports inline character-level diff within hunks.

document/

EditorDocument combines TextBuffer with metadata: URI, languageId, version, isDirty, encoding, and line-ending detection. EditBuilder provides a transaction API for batching edits.

commands/

CommandRegistry maps string IDs to handler functions. Five command modules:

  1. Editing (insert, delete, indent)
  2. Navigation (move, scroll, go-to)
  3. Selection (select word, line, all)
  4. Clipboard (cut, copy, paste)
  5. Multicursor (add cursor above/below, select occurrences)

lsp-client/

LSPClient wrapping JSON-RPC transport. Typed methods for completion, hover, diagnostics, and code actions. Manages the initialize/initialized lifecycle. ServerCapabilityChecker queries what the server supports.

dap-client/

DAPClient for debug sessions. Supports launch/attach configurations, breakpoint management, stepping (into/over/out), stack frame inspection, and variable evaluation.

snippets/

Snippet template processing with tab stops, placeholders, and variable expansion. Language-specific snippet registries.

View-Model

Reactive state bridge between core subsystems and rendering.

EditorViewModel is the main orchestrator. It connects buffer, cursor, viewport, tokenizer, search, and folding into a unified state that the native layer consumes.

Sub-models:

ModelResponsibility
CursorStateCursor positions and selection visuals
DecorationsInline and margin decorations
DiffViewModelSide-by-side and inline diff state
FindWidgetFind/replace UI state
GhostTextInline completion preview
GutterLine numbers, fold markers, breakpoints
LineLayoutLine wrapping and layout metrics
MinimapDocument overview rendering state
OverlaysHover cards, autocomplete popups, signature help
EditorThemeToken and UI color resolution

Native (Rust FFI)

NativeEditorFFI interface defines 93+ FFI functions covering rendering, input handling, events, font metrics, and color management.

Key components:

  • NativeRenderCoordinator — orchestrates rendering calls to the platform layer
  • TouchInputHandler — processes touch events for mobile platforms
  • Word wrap computation

Six platform crates:

PlatformRendering Stack
macOSMetal, CoreGraphics, CoreText
iOSUIKit
WindowsDirect2D, DirectWrite
LinuxCairo, Pango
AndroidNative .so
WebWASM

hone-ide

IDE workbench shell. Compiles to a native binary via Perry.

# macOS
cd hone-ide && perry compile src/app.ts --output hone-ide

# iOS Simulator
cd hone-ide && perry compile src/app.ts --target ios-simulator --output Hone

# Web
cd hone-ide && perry compile src/app.ts --target web --output hone-ide.html

Entry Point

src/app.ts performs startup in order:

  1. Load themes from @honeide/themes
  2. Detect platform and screen dimensions
  3. Register built-in commands and panels
  4. Build the visual workbench (or show first-run setup screen)
  5. Call App({ title: 'Hone', width: 1200, height: 800, icon: 'hone-icon.png', body: body })

The plugin system initializes conditionally based on the __plugins__ compile flag.

Commands

src/commands.ts provides registerCommand() with:

  • id — unique string identifier
  • title — display name
  • category — grouping (e.g., “File”, “Edit”, “View”)
  • handler — function to execute
  • showInPalette — whether the command appears in the command palette

Commands are the central mechanism for keybindings, menus, and the command palette.

Keybindings

src/keybindings.ts handles keyboard shortcut resolution.

  • Modifier support: Ctrl, Shift, Alt, Meta (Cmd on macOS)
  • Chord sequences (e.g., Ctrl+K then Ctrl+C)
  • When-clauses for context-dependent activation
  • CmdOrCtrl normalizes to Cmd on macOS, Ctrl on other platforms

Views

25+ views in src/workbench/views/:

ViewPurpose
ai-chatAI conversation panel
ai-inlineInline AI completion UI
debugDebugger controls and state
diffSide-by-side and inline diff
explorerFile tree browser
extensionsExtension management
findFind and replace
gitGit status, staging, commit
lspAutocomplete, diagnostics, hover, signature help
notificationsToast notifications
pluginsPlugin management (V2)
pr-reviewPull request review
quick-openFuzzy file finder
recentRecent files and workspaces
searchWorkspace-wide search
settings-uiVisual settings editor
setupFirst-run configuration
syncCross-device sync status
tabsEditor tab bar
terminalIntegrated terminal
updateUpdate notifications
welcomeWelcome/start page
status-barBottom status bar

Theme Engine

Loads VS Code-compatible JSON theme files. Ships with 15 built-in themes.

  • UI colors accessed via getter functions
  • Dynamic theme switching at runtime
  • Token colors resolved through the tokenizer’s theme mapping

hone-terminal

Package: @honeide/terminal — terminal emulator. 163 tests.

cd hone-terminal && bun test

Components

vt-parser/

VT100/xterm escape sequence parser. Handles:

  • CSI (Control Sequence Introducer) — cursor movement, erase, scroll, SGR attributes
  • OSC (Operating System Command) — window title, clipboard, hyperlinks
  • DCS (Device Control String) — device-specific sequences

pty/

PTY management with platform-specific backends:

  • Unix (macOS/Linux) — POSIX PTY via openpty/forkpty
  • Windows — ConPTY for modern terminal support

Spawns shell processes and manages bidirectional I/O between the emulator and the child process.

input/

Input encoding layer:

  • Key encoder — maps keyboard events to VT escape sequences
  • Mouse encoder — translates mouse events to xterm mouse reporting sequences

emulator.ts

Terminal emulator core. Ties together the parser, screen buffer, and input handler into a unified terminal instance. Manages:

  • Screen buffer (primary and alternate)
  • Cursor state and attributes
  • Scroll region and scrollback
  • Character set handling

Rendering

Cross-platform rendering via Rust FFI, following the same pattern as hone-editor’s native layer. The TypeScript emulator owns the terminal state; Rust handles platform-specific drawing.

hone-extension

V2 plugin SDK package. Contains the SDK, Rust plugin host, marketplace client, and CLI tooling.

SDK (@hone/sdk)

Base Classes and Interfaces

  • HonePlugin — base class that plugins extend
  • HoneHost — host interface type (capabilities the IDE exposes to plugins)
  • HoneHostImpl — host implementation
  • CanvasContext — custom UI rendering context for plugin panels

Types

80+ type definitions organized by domain:

Editor

BufferId, Position, Range, Selection, TextEdit

Filesystem

FileStat, FileInfo, FileWatchEvent

UI

PanelElement, StatusBarItemOptions, DecorationTypeOptions, GutterProviderOptions, ContextMenuOptions, NotificationOptions

Process and Network

SpawnOptions, HttpRequest, HttpResponse

Configuration

ConfigSchema

Events

DocumentOpenEvent, FormatDocumentEvent, and others

Manifest

PluginManifest, PluginCapabilities, PluginTier

Helper Functions

Edit construction:

insertEdit(position, text)
replaceEdit(range, text)
deleteEdit(range)

Position/range construction:

pos(line, character)
range(startLine, startChar, endLine, endChar)

Manifest utilities:

deriveTier(manifest)        // Determines plugin tier from capabilities
validateManifest(manifest)  // Validates manifest structure
parsePluginManifest(json)   // Parses raw JSON into typed manifest

Testing Utilities

  • MockHost — mock implementation of HoneHost for unit testing plugins
  • createTestBuffer — creates an in-memory buffer for test scenarios

Rust Plugin Host

Loads compiled plugin binaries at runtime. Exposes HoneHostAPI struct that implements the host interface, bridging plugin calls to IDE services.

Marketplace Client

API client for marketplace.hone.codes. Handles plugin search, download, and publish operations.

CLI

Command-line tools for plugin development: scaffolding, building, testing, and publishing plugins.

hone-extensions

11 built-in language extension packs (V1 system). Tests via npm test (Vitest).

Extensions

#DirectoryLanguages
1typescript/TypeScript, JavaScript, JSX, TSX
2python/Python
3rust/Rust
4go/Go
5cpp/C, C++
6html-css/HTML, CSS, SCSS
7json/JSON, JSONC
8markdown/Markdown
9docker/Dockerfile
10toml-yaml/TOML, YAML
11git/Git commit messages, gitignore, etc.

Manifest Format

Each extension has a hone-extension.json manifest with the following structure:

Metadata:

  • id — unique extension identifier
  • name — display name
  • version — semver version string
  • publisher — publisher identifier
  • description — short description
  • license — SPDX license identifier
  • engines — compatible Hone version range
  • main — entry point file

Activation events:

Extensions declare when they activate. Examples:

  • onLanguage:typescript
  • onLanguage:python
  • onLanguage:rust

Contributes:

  • languages — language definitions (id, aliases, file extensions, language configuration)
  • lspServers — LSP server configurations
  • commands — contributed commands
  • snippets — snippet files per language
  • configuration — settings schema
  • keybindings — keyboard shortcuts

Adding a New Extension

  1. Create a directory under extensions/
  2. Add a hone-extension.json manifest
  3. Define language contributions (file extensions, aliases, language configuration)
  4. Optionally configure an LSP server, commands, snippets
  5. Add to the extension registry

hone-themes

Package: @honeide/themes — 15 VSCode-compatible color themes as pure JSON data. 452 tests via npm test (Jest).

Themes

ThemeFileType
Hone Darkhone-dark.jsonDark
Hone Lighthone-light.jsonLight
Catppuccincatppuccin.jsonDark
Draculadracula.jsonDark
GitHub Darkgithub-dark.jsonDark
GitHub Lightgithub-light.jsonLight
Gruvbox Darkgruvbox-dark.jsonDark
High Contrast Darkhigh-contrast-dark.jsonDark
High Contrast Lighthigh-contrast-light.jsonLight
Monokaimonokai.jsonDark
Nordnord.jsonDark
One Darkone-dark.jsonDark
Solarized Darksolarized-dark.jsonDark
Solarized Lightsolarized-light.jsonLight
Tokyo Nighttokyo-night.jsonDark

JSON Schema

Each theme file follows the VSCode color theme format:

  • type"dark" or "light"
  • colors — object mapping UI color keys to hex values:
    • editor.background, editor.foreground
    • activityBar.background
    • sideBar.background
    • statusBar.background
    • tab.activeBackground
    • and many more
  • tokenColors — array of token color rules, each with:
    • scope — TextMate scope selector(s)
    • settingsforeground (hex color), fontStyle (bold, italic, underline)

Tests

  • Schema validation — every theme conforms to the expected structure
  • WCAG contrast — foreground/background color pairs meet accessibility contrast ratios
  • Completeness — all required color keys are present in every theme

Importing Themes

Any VSCode JSON theme can be imported directly — the format is compatible.

hone-auth

Auth service for Hone sync. Perry-compiled native binary (2.8 MB) with Fastify + MySQL.

Endpoints

EndpointMethodPurpose
/auth/infoGETDiscovery — returns available auth methods
/auth/login?email=...GETCreates magic link, sends email
/auth/verify?token=...&deviceName=...&platform=...GETVerifies magic link, creates user + device, returns device token
/auth/validate?token=...GETValidates a device token (returns userId + tier)
/auth/me?token=...GETReturns user profile
/projects?token=...GETLists user’s registered projects
/projects/register?token=...&projectKey=...&name=...&roomId=...GETRegisters project for sync
/devices?token=...GETLists user’s registered devices
/healthGETHealth check
  1. Create a 64-char random hex token, store in the magic_links table with a 15-minute expiry.
  2. If SMTP is configured, send the link via email. Otherwise, the token is logged server-side (dev mode).
  3. On verify: validate the link (not already used, not expired), mark it as used, find or create the user, create a device record, and generate a device token.

Device Token Format

userId:deviceId:timestamp.hash

The hash is a double-djb2 HMAC:

djb2(String(djb2(secret + "|" + payload)) + "|" + secret + "|" + payload)

Database Schema

MySQL database on webserver.skelpo.net (user: hone). Tables:

  • users
  • devices
  • magic_links
  • projects
  • subscriptions

All identifiers use camelCase.

Configuration

File: auth.conf

DB_HOST=webserver.skelpo.net
DB_USER=hone
DB_PASS=<password>
DB_NAME=hone
PORT=8445
AUTH_SECRET=<shared-secret>
AUTH_BASE_URL=https://auth.hone.codes
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=<sendgrid-key>
SMTP_FROM=Hone <noreply@hone.dev>

Build

cd hone-auth && perry compile src/app.ts --output hone-auth

hone-relay

WebSocket sync relay. Routes messages between devices in the same room. 48 tests via bun test.

Protocol

Join a room:

{"type": "join", "room": "<roomId>", "device": "<deviceId>", "token": "<deviceToken>"}

Response:

{"type": "joined", "room": "<roomId>", "device": "<deviceId>"}

Send a message:

{"from": "<deviceId>", "to": "host|broadcast|<targetDeviceId>", "room": "<roomId>", "payload": {...}}

Message Targets

  • "host" — send to the room’s host device
  • "broadcast" — send to all devices in the room except the sender
  • "<deviceId>" — send directly to a specific device

Internals

  • Room-based routing — messages are routed within rooms identified by the djb2 hash of roomId
  • Slot tracking — each connection gets a slot with wsId, deviceId, and roomId
  • Host election — the first device to join a room becomes the host
  • Buffer — messages are buffered for 60 seconds for reconnecting devices
  • Auth bypass — when auth.secret is empty, token validation is skipped (dev mode)
  • Rate limiting — per-connection with configurable windows
  • Stateless — rooms exist only while clients are connected

Configuration

File: relay.conf

host=0.0.0.0
port=8443
auth.secret=<shared-secret>

Ports

  • 8443 — HTTP
  • 8444 — WebSocket

Build

Perry-compiled or runs on Bun:

cd hone-relay && perry compile src/app.ts --output hone-relay

hone-build

Plugin build coordinator. Submits plugin source to perry-hub workers for cross-platform compilation. Perry-compiled native binary. 21 tests via bun test.

Purpose

When a plugin developer publishes a plugin, it needs to be compiled for all supported platforms. hone-build coordinates this by submitting compilation jobs to perry-hub workers, which run the Perry compiler for each target platform.

Features

  • Artifact storage — stores compiled plugin binaries per platform
  • Platform normalization — maps platform identifiers to Perry compilation targets
  • Cross-platform compilation orchestration — fans out builds to perry-hub workers and collects results

Configuration

File: build.conf

PORT=8447

Build

cd hone-build && perry compile src/app.ts --output hone-build

hone-marketplace

Plugin marketplace server at marketplace.hone.codes. Perry-compiled native binary.

Features

  • Plugin search and discovery — query available plugins by name, category, or keyword
  • Plugin download — serve compiled binaries per platform
  • Plugin publishing — submit plugins for review and distribution
  • Version management — track and serve multiple versions per plugin

Port

8446

Build

cd hone-marketplace && perry compile src/app.ts --output hone-marketplace

Deployment

Runs on webserver.skelpo.net alongside the auth and relay services.

Plugin Development Overview

Hone supports two paths for extending the editor, each suited to different use cases.

V1 Built-in Extensions

Located in hone-extensions/. These ship with the IDE binary.

  • Primarily for language support: LSP servers, syntax definitions, snippets
  • Defined by a hone-extension.json manifest
  • Loaded by ExtensionRegistry in hone-core
  • 11 built-in extensions ship today (TypeScript, Python, Rust, Go, C++, HTML/CSS, JSON, Markdown, Docker, TOML/YAML, Git)

Best for: core language features that every user needs out of the box.

V2 Independent Plugins

Located in hone-extension/ (the SDK package). These are standalone native plugins distributed through the Hone marketplace.

  • Compiled to native binaries via Perry
  • Use @hone/sdk (HonePlugin base class)
  • Distributed and installed through the Hone marketplace
  • Three execution tiers based on declared capabilities (InProcess, PluginHost, IsolatedProcess)
  • Full access to editor, filesystem, UI, network, and process APIs depending on tier

Best for: third-party tools, custom UI panels, external integrations, anything beyond built-in language support.

Which Path to Choose

CriteriaV1 Built-inV2 Plugin
Ships with IDEYesNo (marketplace)
Language support (LSP, syntax)Primary use casePossible but not typical
Custom UI panelsNoYes
Network accessNoYes (Tier 3)
Process spawningNoYes (Tier 3)
Marketplace distributionNoYes
Requires Perry compilationNo (data-driven)Yes

Choose V1 if you are adding a language to the IDE itself. Choose V2 if you are building a standalone tool, integration, or any plugin for the marketplace.

V1 Built-in Extensions

Built-in extensions use hone-extension.json manifests and ship with the IDE binary. They are loaded by the ExtensionRegistry in hone-core.

Manifest Structure

Each extension lives in its own directory under extensions/<lang>/ and is defined by a hone-extension.json file:

{
  "id": "hone.typescript",
  "name": "TypeScript",
  "version": "1.0.0",
  "publisher": "hone",
  "description": "TypeScript and JavaScript language support",
  "license": "MIT",
  "engines": { "hone": ">=1.0.0" },
  "main": "src/index.ts",
  "activationEvents": [
    "onLanguage:typescript",
    "onLanguage:javascript",
    "onLanguage:typescriptreact",
    "onLanguage:javascriptreact"
  ],
  "contributes": {
    "languages": [
      {
        "id": "typescript",
        "aliases": ["TypeScript", "ts"],
        "extensions": [".ts", ".mts", ".cts"],
        "configuration": "./language-configuration.json"
      }
    ],
    "lspServers": [
      {
        "id": "typescript-language-server",
        "command": "typescript-language-server",
        "args": ["--stdio"],
        "languages": ["typescript", "javascript", "typescriptreact", "javascriptreact"]
      }
    ],
    "commands": [],
    "snippets": [],
    "configuration": {},
    "keybindings": []
  }
}

Manifest Fields

FieldRequiredDescription
idYesUnique identifier (convention: hone.<language>)
nameYesDisplay name
versionYesSemver version string
publisherYesPublisher identifier
descriptionYesShort description
licenseYesSPDX license identifier
engines.honeYesMinimum compatible Hone version
mainNoEntry point file (if extension has runtime code)
activationEventsYesEvents that trigger extension loading

Contribution Points

The contributes object declares what the extension provides:

  • languages – Language definitions with file extensions, aliases, and configuration
  • lspServers – LSP server configurations (command, args, supported languages)
  • commands – Commands registered in the command palette
  • snippets – Code snippets for the language
  • configuration – Settings the extension exposes
  • keybindings – Default keyboard shortcuts

Walkthrough: Adding a New Language

  1. Create a directory: extensions/<lang>/
  2. Create hone-extension.json with the language metadata
  3. Define language contributions: file extensions, aliases, language configuration
  4. Configure an LSP server if one exists (specify the command, args, and supported language IDs)
  5. Add commands, snippets, and keybindings as needed
  6. Register the extension in the extension system

Language Configuration

The language-configuration.json file controls editor behavior for the language:

{
  "comments": {
    "lineComment": "//",
    "blockComment": ["/*", "*/"]
  },
  "brackets": [
    ["{", "}"],
    ["[", "]"],
    ["(", ")"]
  ],
  "autoClosingPairs": [
    { "open": "{", "close": "}" },
    { "open": "[", "close": "]" },
    { "open": "(", "close": ")" },
    { "open": "\"", "close": "\"" },
    { "open": "'", "close": "'" }
  ],
  "surroundingPairs": [
    ["{", "}"],
    ["[", "]"],
    ["(", ")"],
    ["\"", "\""],
    ["'", "'"]
  ]
}

Existing Built-in Extensions

ExtensionLanguagesLSP Server
TypeScriptTypeScript, JavaScript, TSX, JSXtypescript-language-server
PythonPythonpylsp or pyright
RustRustrust-analyzer
GoGogopls
C++C, C++, Objective-Cclangd
HTML/CSSHTML, CSS, SCSS, Lessvscode-html-languageserver
JSONJSON, JSONCvscode-json-languageserver
MarkdownMarkdown
DockerDockerfile, Docker Compose
TOML/YAMLTOML, YAML
GitGit commit, Git rebase, .gitignore

V2 Plugin SDK

The V2 plugin system uses @hone/sdk to build native Hone plugins compiled by Perry and distributed via the marketplace.

Installation

npm install @hone/sdk

Plugin Lifecycle

  1. init – Host calls hone_plugin_init(host_api). The plugin stores the host API pointer for later use.
  2. activate – Host calls hone_plugin_activate(). The plugin initializes its state, registers commands, and sets up event handlers.
  3. hooks – Host calls hook methods as events occur (e.g., hone_plugin_on_document_format(event_ptr), hone_plugin_on_command(cmd_ptr)).
  4. deactivate – Host calls hone_plugin_deactivate(). The plugin cleans up resources.
  5. unload – Host calls dlclose(). The plugin binary is unloaded from memory.

Basic Plugin Structure

import { HonePlugin } from '@hone/sdk';
import type { HoneHost } from '@hone/sdk';

export class MyPlugin extends HonePlugin {
  activate(host: HoneHost): void {
    host.log('info', 'My plugin activated');

    host.commands.register('my-plugin.greet', 'Greet User', () => {
      host.ui.notify({ message: 'Hello from my plugin!' });
    });
  }

  deactivate(): void {
    // Clean up resources
  }
}

Host APIs

The HoneHost interface provides access to IDE functionality. Available APIs depend on declared capabilities – undeclared capabilities have null function pointers and will not be linked.

Editor

Requires editor.read and/or editor.write capabilities.

// Read buffer content
const text = host.editor.getBufferText(bufferId);
const lines = host.editor.getLines(bufferId, startLine, endLine);
const selection = host.editor.getSelection(bufferId);
const lineCount = host.editor.getLineCount(bufferId);

// Write edits (requires editor.write)
host.editor.submitEdits(bufferId, [
  { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } }, text: 'replacement' }
]);

Filesystem

Requires filesystem.read and/or filesystem.write capabilities.

// Read (patterns must match declared globs)
const content = host.fs.readText('/path/to/file.txt');
const exists = host.fs.exists('/path/to/file.txt');
const entries = host.fs.listDirectory('/path/to/dir');

// Write (requires filesystem.write)
host.fs.writeText('/path/to/file.txt', 'content');
host.fs.delete('/path/to/file.txt');
host.fs.createDirectory('/path/to/dir', { recursive: true });

UI

Requires various ui.* capabilities.

// Status bar (ui.statusbar)
const item = host.ui.createStatusBarItem({ text: 'My Plugin', alignment: 'left' });
host.ui.updateStatusBarItem(item, { text: 'Updated' });

// Panels (ui.panel)
const panel = host.ui.createPanel({
  title: 'My Panel',
  icon: 'list',
  content: [
    { type: 'Heading', text: 'Results', level: 2 },
    { type: 'List', items: ['Item 1', 'Item 2'] }
  ]
});

// Notifications (ui.notifications)
host.ui.notify({ message: 'Operation complete', type: 'info' });

// Command palette (ui.commandPalette)
host.commands.register('my-plugin.action', 'My Action', callback);

Process

Requires process.spawn capability with allowlisted binaries.

const result = host.process.spawn('git', ['status', '--porcelain'], {
  cwd: host.getWorkspacePath()
});

Network

Requires network capability (forces Tier 3 execution).

const response = host.network.httpRequest({
  method: 'GET',
  url: 'https://api.example.com/data',
  headers: { 'Authorization': 'Bearer token' }
});

Custom UI Panels

Plugins can create panels with structured content using PanelElement types:

ElementDescription
TextPlain or styled text
HeadingSection heading (levels 1-4)
ListOrdered or unordered list
TreeCollapsible tree structure
TableTabular data
InputText input field
ButtonClickable button
SeparatorVisual divider
ProgressProgress bar or spinner
CodeBlockSyntax-highlighted code
GroupContainer for other elements

Testing

Use MockHost and createTestBuffer from the SDK to test plugin logic without the full IDE:

import { MockHost, createTestBuffer } from '@hone/sdk';

const host = new MockHost();
const buffer = createTestBuffer('hello world');

const plugin = new MyPlugin();
plugin.activate(host);

// Assert plugin behavior against mock host

Compiling

Plugins are compiled to native shared libraries using Perry:

perry compile src/index.ts --output my-plugin

The output is a platform-specific shared library (.dylib on macOS, .dll on Windows, .so on Linux) that the Hone plugin host loads via dlopen/dlclose.

Perry AOT Constraints

Plugin code is compiled by Perry and must follow its constraints. Avoid:

  • Optional chaining (?.) – use explicit null checks
  • Nullish coalescing (??) – use if (x !== undefined)
  • Dynamic key access (obj[variable]) – use if/else if per key
  • for...of on arrays – use index-based for loops
  • ES6 shorthand properties ({ key }) – use { key: key }
  • Closures capturing this – use module-level functions and variables

See the Perry constraints reference for the full list.

Capabilities & Tiers

Every V2 plugin declares its capabilities in the manifest. The declared capabilities determine the plugin’s execution tier, which controls isolation and security boundaries.

Execution Tiers

TierNameDescriptionExecution Model
1InProcessUI-only (themes, keymaps, color schemes)Loaded in main process
2PluginHostEditor access, filesystem reads, UI elementsShared plugin host process
3IsolatedProcessNetwork, filesystem writes, process spawning, terminal, webviewsOwn sandboxed process

Tiers are derived automatically from declared capabilities. You do not choose a tier directly.

Tier Derivation Rules

Tier 3 Triggers

Any of these capabilities forces the plugin into its own sandboxed process:

  • network: true
  • filesystem.write: true
  • process.spawn: [...] (non-empty array)
  • terminal: true
  • ui.webview: true

Tier 2 Triggers

These capabilities place the plugin in the shared plugin host (unless a Tier 3 trigger is also present):

  • editor.read, editor.write, editor.decorations
  • filesystem.read: [...] (non-empty array)
  • Any ui.* capability (panel, statusbar, gutter, commandPalette, contextMenu, notifications)

Tier 1

No code capabilities declared. The plugin is purely declarative data: theme JSON, keymap JSON, or other static contributions. Loaded directly in the main process with no isolation overhead.

All Capabilities

CapabilityTypeDescription
editor.readbooleanRead buffer text, selections, language ID, line count
editor.writebooleanSubmit edits (routed through the Changes Queue)
editor.decorationsbooleanUnderlines, highlights, gutter icons
filesystem.readstring[]Read files matching declared glob patterns
filesystem.writebooleanWrite and delete files and directories
networkbooleanHTTP requests to external services
process.spawnstring[]Spawn allowlisted external binaries
terminalbooleanInteractive terminal access
ui.panelbooleanCreate side panels with structured content
ui.statusbarbooleanAdd and update status bar items
ui.gutterbooleanGutter icons next to line numbers
ui.commandPalettebooleanRegister commands in the command palette
ui.contextMenubooleanAdd items to right-click context menus
ui.notificationsbooleanToast notifications
ui.webviewbooleanEmbedded webviews (Tier 3 only)

Declaring Capabilities

Capabilities are declared in plugin.json:

{
  "id": "com.example.my-formatter",
  "name": "My Formatter",
  "version": "1.0.0",
  "capabilities": {
    "editor": {
      "read": true,
      "write": true,
      "decorations": true
    },
    "filesystem": {
      "read": ["**/*.config.json"]
    },
    "ui": {
      "statusbar": true,
      "commandPalette": true,
      "notifications": true
    }
  }
}

This plugin would be assigned Tier 2: it uses editor and UI capabilities but does not declare network, filesystem write, process spawn, terminal, or webview.

Enforcement Layers

Capabilities are enforced at three independent layers. All three must agree for an API call to succeed.

1. Compile-time (Perry)

Only API functions matching declared capabilities are linked into the plugin binary. Attempting to import an undeclared API causes a compile error. This is the earliest and strictest check.

2. Runtime (OS Sandbox)

OS-level restrictions are applied before the plugin binary loads:

PlatformMechanism
macOSsandbox-exec profiles
Linuxseccomp-bpf filters
WindowsJob Objects + AppContainer

The sandbox profile is generated from the declared capabilities. A Tier 3 plugin with network: true but no filesystem.write will have network access allowed but filesystem writes blocked at the OS level.

3. Host API (Null Pointers)

In the C ABI HoneHostAPI struct, function pointers for undeclared capabilities are set to null. If a plugin somehow bypasses the first two layers and calls an undeclared function pointer, it dereferences null and crashes. This is the final safety net.

Principle of Least Privilege

Declare only the capabilities your plugin actually needs. Fewer capabilities means:

  • Lower tier (less isolation overhead, better performance)
  • Smaller attack surface
  • Easier review for marketplace approval
  • More user trust (users see capability declarations before installing)

Host API (C ABI)

The plugin host communicates with plugins through a C ABI struct of function pointers. This page documents the raw interface between the host and plugin binaries.

Entry Point

Every plugin compiled by Perry exposes a single initialization function:

void hone_plugin_init(HoneHostAPI* host);

The host passes a HoneHostAPI struct populated with function pointers. Function pointers for undeclared capabilities are null.

HoneHostAPI Struct

typedef struct {
    // ── Always available ──────────────────────────────────────
    void    (*log)(int32_t level, const uint8_t* msg, uint32_t msg_len);
    int64_t (*get_config)(const uint8_t* key, uint32_t key_len);
    int64_t (*get_workspace_path)(void);

    // ── editor.read ───────────────────────────────────────────
    int64_t (*buffer_get_text)(int64_t buffer_id);
    int64_t (*buffer_get_lines)(int64_t buffer_id, int32_t start, int32_t end);
    int64_t (*buffer_get_selection)(int64_t buffer_id);
    int64_t (*buffer_get_selections)(int64_t buffer_id);
    int64_t (*buffer_get_language_id)(int64_t buffer_id);
    int64_t (*buffer_get_file_path)(int64_t buffer_id);
    int32_t (*buffer_get_line_count)(int64_t buffer_id);
    int64_t (*get_active_buffer_id)(void);
    int64_t (*get_open_buffer_ids)(void);

    // ── editor.write ──────────────────────────────────────────
    int64_t (*buffer_submit_edits)(int64_t buffer_id, int64_t edits_ptr);
    void    (*buffer_set_selection)(int64_t buffer_id, int64_t sel_ptr);
    void    (*buffer_set_selections)(int64_t buffer_id, int64_t sels_ptr);

    // ── editor.decorations ────────────────────────────────────
    int32_t (*create_decoration_type)(int64_t opts_ptr);
    void    (*set_decorations)(int64_t buffer_id, int32_t type_id, int64_t ranges_ptr);
    void    (*clear_decorations)(int32_t type_id);

    // ── filesystem.read ───────────────────────────────────────
    int64_t (*file_read_text)(const uint8_t* path, uint32_t path_len);
    int32_t (*file_exists)(const uint8_t* path, uint32_t path_len);
    int64_t (*file_stat)(const uint8_t* path, uint32_t path_len);
    int64_t (*directory_list)(const uint8_t* path, uint32_t path_len);

    // ── filesystem.write ──────────────────────────────────────
    void    (*file_write_text)(const uint8_t* path, uint32_t path_len,
                               const uint8_t* content, uint32_t content_len);
    void    (*file_delete)(const uint8_t* path, uint32_t path_len);
    void    (*directory_create)(const uint8_t* path, uint32_t path_len,
                                int32_t recursive);

    // ── process.spawn ─────────────────────────────────────────
    int64_t (*spawn)(const uint8_t* cmd, uint32_t cmd_len,
                     int64_t args_ptr, int64_t opts_ptr);

    // ── network ───────────────────────────────────────────────
    int64_t (*http_request)(int64_t req_ptr);

    // ── ui.statusbar ──────────────────────────────────────────
    int32_t (*statusbar_create_item)(int64_t opts_ptr);
    void    (*statusbar_update_item)(int32_t id, int64_t opts_ptr);
    void    (*statusbar_remove_item)(int32_t id);

    // ── ui.panel ──────────────────────────────────────────────
    int32_t (*panel_create)(int64_t opts_ptr);
    void    (*panel_update)(int32_t id, int64_t content_ptr);
    void    (*panel_dispose)(int32_t id);

    // ── ui.commandPalette ─────────────────────────────────────
    void    (*command_register)(const uint8_t* id, uint32_t id_len,
                                const uint8_t* title, uint32_t title_len);
    void    (*command_unregister)(const uint8_t* id, uint32_t id_len);

    // ── ui.notifications ──────────────────────────────────────
    void    (*notify)(int64_t opts_ptr);
} HoneHostAPI;

String Encoding

All strings in the C ABI are UTF-8 encoded with explicit length. There are no null terminators.

Perry uses NaN-boxed StringHeader pointers internally. On the Rust side, the host extracts strings using str_from_header(), which reads the pointer and length from the NaN-boxed value.

Passing strings to the host: Provide a const uint8_t* pointer and a uint32_t length.

Receiving strings from the host: The host returns a NaN-boxed StringHeader pointer (int64_t). Use Perry’s string utilities to read the content.

Return Value Encoding

Return TypeC TypeNotes
Stringsint64_tNaN-boxed StringHeader pointer, allocated by host
Arraysint64_tNaN-boxed array pointer
Booleansint32_t0 or 1
IDsint32_tOpaque handle (decoration types, status bar items, panels)
Complex structsint64_tPointer to host-allocated struct
VoidvoidNo return value

Plugin Lifecycle Functions

The host expects these exported symbols from the plugin binary:

SymbolWhen CalledPurpose
hone_plugin_initLoad timeReceives host API pointer
hone_plugin_activateAfter initPlugin initializes state
hone_plugin_deactivateBefore unloadPlugin cleans up resources

Hook Functions

The host calls optional hook functions when relevant events occur. Plugins export only the hooks they handle:

SymbolEvent
hone_plugin_on_document_openDocument opened in editor
hone_plugin_on_document_closeDocument closed
hone_plugin_on_document_changeDocument content changed
hone_plugin_on_document_saveDocument saved
hone_plugin_on_document_formatFormat request
hone_plugin_on_commandRegistered command invoked
hone_plugin_on_selection_changeCursor/selection changed

Each hook receives an int64_t pointer to an event struct allocated by the host. The plugin reads event data from this struct and must not free it.

FFI Conventions

These Perry FFI rules apply to all plugin code:

  • String parameters are NaN-boxed StringHeader pointers. Rust receives *const u8 and uses str_from_header().
  • Perry generates __wrapper_<function_name> symbols (double underscore prefix).
  • All FFI functions must be listed in package.json under perry.nativeLibrary.functions.
  • Use f64 for numeric FFI parameters and i64 for string/pointer parameters. Using i32 causes verifier errors.

Publishing Plugins

This page covers how to prepare, build, and publish a V2 plugin to the Hone marketplace.

Manifest Requirements

Every published plugin needs a complete plugin.json:

{
  "id": "com.example.my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "description": "A short description of what the plugin does",
  "author": {
    "name": "Your Name",
    "email": "you@example.com"
  },
  "license": "MIT",
  "engines": {
    "hone": ">=1.0.0"
  },
  "main": "src/index.ts",
  "capabilities": {
    "editor": {
      "read": true,
      "write": true
    },
    "ui": {
      "commandPalette": true,
      "notifications": true
    }
  },
  "contributes": {
    "commands": [
      {
        "id": "my-plugin.run",
        "title": "Run My Plugin"
      }
    ]
  }
}

Required Fields

FieldDescription
idGlobally unique identifier (reverse-domain convention)
nameDisplay name shown in the marketplace
versionSemver version string
descriptionShort description (shown in search results)
authorAuthor name and contact
licenseSPDX license identifier
engines.honeMinimum compatible Hone version
mainEntry point TypeScript file
capabilitiesDeclared capabilities (determines execution tier)

Validating the Manifest

Use validateManifest from @hone/sdk to check your manifest before submission:

import { validateManifest } from '@hone/sdk';

const errors = validateManifest('./plugin.json');
if (errors.length > 0) {
  console.error('Manifest errors:', errors);
}

Cross-platform Compilation

When you submit a plugin, the build service (hone-build, port 8447) sends it to perry-hub workers for compilation. You submit TypeScript source code; the service produces native binaries for all supported platforms:

PlatformArchitectures
macOSx86_64, aarch64
Windowsx86_64
Linuxx86_64, aarch64
iOSaarch64 (if applicable)
Androidaarch64 (if applicable)

You do not need to compile for each platform yourself. The build service handles cross-compilation.

Publishing Flow

  1. Develop and test locally. Compile with perry compile src/index.ts --output my-plugin and test against a local Hone instance.

  2. Validate the manifest. Run validateManifest to catch issues before submission.

  3. Submit to the marketplace. Use the CLI or the marketplace client from @hone/sdk:

    hone plugin publish
    

    This uploads your source to the build service, which compiles for all platforms and registers the result with the marketplace server (hone-marketplace, port 8446).

  4. Build service compiles. perry-hub workers produce native shared libraries for each platform/architecture combination.

  5. Plugin goes live. Once all platform builds succeed, the plugin appears in the Hone marketplace. Users can discover and install it from within the IDE.

Versioning

Follow semver:

  • Patch (1.0.0 -> 1.0.1): Bug fixes, no API changes
  • Minor (1.0.0 -> 1.1.0): New features, backward compatible
  • Major (1.0.0 -> 2.0.0): Breaking changes

Each published version creates a new marketplace entry. Previous versions remain available. Users can:

  • Auto-update within a major version (default)
  • Pin to a specific version
  • Roll back to any previously published version

Pre-publication Checklist

  • plugin.json passes validateManifest with no errors
  • Plugin compiles locally with perry compile
  • All declared capabilities are actually used
  • No unnecessary capabilities declared (principle of least privilege)
  • Version number incremented from previous release
  • Description and metadata are accurate
  • License is specified and correct

Perry Constraints (Detailed)

Perry is an AOT (ahead-of-time) compiler that transforms TypeScript directly into native machine code. Because there is no runtime interpreter or JIT, certain JavaScript/TypeScript patterns that rely on runtime dynamism are not supported. This guide documents every constraint with explanations and corrected code.

1. Dynamic Key Access

Pattern: obj[variable]

Perry resolves all property accesses at compile time. A dynamic key like obj[key] requires runtime dispatch to determine which property to read — Perry has no mechanism for this.

// BROKEN — Perry cannot resolve the property at compile time
function getConfig(config: Config, key: string): string {
  return config[key];
}

// FIXED — enumerate every possible key explicitly
function getConfig(config: Config, key: string): string {
  if (key === 'host') return config.host;
  else if (key === 'port') return config.port;
  else if (key === 'protocol') return config.protocol;
  return '';
}

This also applies to dynamic writes (obj[key] = value) and computed property access on arrays of objects.

2. Optional Chaining (?.)

Pattern: obj?.prop, arr?.[0], fn?.()

Perry does not implement the optional chaining operator. The compiler will fail or produce incorrect code.

// BROKEN
const name = user?.profile?.name;

// FIXED — explicit null/undefined checks
let name = '';
if (user !== null && user !== undefined) {
  if (user.profile !== null && user.profile !== undefined) {
    name = user.profile.name;
  }
}

Every level of the chain needs its own guard. This is verbose but produces correct native code.

3. Nullish Coalescing (??)

Pattern: value ?? fallback

Not implemented in Perry’s codegen. The operator is not recognized.

// BROKEN
const port = config.port ?? 8080;

// FIXED
let port = 8080;
if (config.port !== undefined) {
  port = config.port;
}

If you also need to guard against null:

let port = 8080;
if (config.port !== null && config.port !== undefined) {
  port = config.port;
}

4. Regular Expressions

Pattern: /regex/, RegExp, .test(), .match(), .replace() with regex

Perry has no regex engine. Any code that constructs or uses a RegExp will fail.

// BROKEN
if (/^[a-z]+$/.test(input)) {
  // ...
}

// FIXED — manual character checking
const ALPHA = 'abcdefghijklmnopqrstuvwxyz';
let allAlpha = true;
for (let i = 0; i < input.length; i++) {
  if (ALPHA.indexOf(input.charAt(i)) < 0) {
    allAlpha = false;
    break;
  }
}

For string splitting or searching, use indexOf, substring, and manual parsing loops.

// BROKEN
const parts = line.split(/\s+/);

// FIXED — split on single space, trim first
const trimmed = line.trim();
const parts: string[] = [];
let start = 0;
for (let i = 0; i <= trimmed.length; i++) {
  if (i === trimmed.length || trimmed.charAt(i) === ' ') {
    if (i > start) {
      parts.push(trimmed.substring(start, i));
    }
    start = i + 1;
  }
}

5. ES6 Shorthand Properties

Pattern: { name, age }

Perry requires explicit key-value pairs in object literals.

// BROKEN
const name = 'Alice';
const age = 30;
const user = { name, age };

// FIXED
const user = { name: name, age: age };

This applies everywhere: return statements, function arguments, variable declarations.

6. Array.map on Class Fields

Pattern: this.items.map(fn)

Perry cannot dispatch .map() on arrays stored as class instance fields. The method resolution fails at compile time.

// BROKEN
class UserList {
  users: User[] = [];

  getNames(): string[] {
    return this.users.map(u => u.name);
  }
}

// FIXED — use a for loop
class UserList {
  users: User[] = [];

  getNames(): string[] {
    const names: string[] = [];
    for (let i = 0; i < this.users.length; i++) {
      names.push(this.users[i].name);
    }
    return names;
  }
}

This also applies to .filter(), .reduce(), .find(), and other higher-order array methods on class fields. Use indexed for loops instead.

7. for...of on Arrays

Pattern: for (const item of items)

Perry does not implement the iterator protocol. for...of loops will not work on arrays.

// BROKEN
for (const item of items) {
  process(item);
}

// FIXED
for (let i = 0; i < items.length; i++) {
  const item = items[i];
  process(item);
}

Also avoid for...in on objects — use explicit property access instead.

8. Character Range Comparisons

Pattern: c >= 'a' && c <= 'z'

Perry does not support ordering comparisons on characters (strings of length 1). The comparison operators work on numbers, not character codes.

// BROKEN
function isLowerAlpha(c: string): boolean {
  return c >= 'a' && c <= 'z';
}

// FIXED
const LOWER_ALPHA = 'abcdefghijklmnopqrstuvwxyz';
function isLowerAlpha(c: string): boolean {
  return LOWER_ALPHA.indexOf(c) >= 0;
}

For digits:

const DIGITS = '0123456789';
function isDigit(c: string): boolean {
  return DIGITS.indexOf(c) >= 0;
}

9. Closure Capture Semantics

Pattern: Closures that read or mutate outer variables

Perry captures closure variables by value at the time the closure is created. Any subsequent mutation to the outer variable is invisible to the closure, and any mutation inside the closure does not affect the outer variable.

// BROKEN — closure captures initial value of this.count
class Counter {
  count = 0;

  setup() {
    setInterval(() => {
      this.count++;       // increments a stale copy, not the real field
      console.log(this.count);
    }, 1000);
  }
}

// FIXED — module-level state with named functions
let _count = 0;

function incrementCount() {
  _count++;
  console.log(_count);
}

setInterval(incrementCount, 1000);

Rules:

  • Store mutable state in module-level let variables.
  • Access that state through module-level named functions (not closures).
  • Never capture this in a closure passed to setInterval, event handlers, or callbacks.

10. requestAnimationFrame

Pattern: requestAnimationFrame(callback)

RAF never fires in Perry. The Perry runtime does not have a browser-style event loop with frame callbacks.

// BROKEN — callback never executes
function animate() {
  updateFrame();
  requestAnimationFrame(animate);
}

// FIXED
setInterval(updateFrame, 16); // ~60fps

11. setTimeout Self-Recursion

Pattern: setTimeout called inside its own callback

When setTimeout is called inside the callback of a previous setTimeout, the new timeout only fires once and then stops. This breaks recursive polling patterns.

// BROKEN — fires once, then stops
function poll() {
  doWork();
  setTimeout(poll, 1000);
}
setTimeout(poll, 1000);

// FIXED
setInterval(doWork, 1000);

If you need to stop the interval later, capture the return value:

const id = setInterval(doWork, 1000);
// later:
clearInterval(id);

12. String-Returning Functions in Async Context

Pattern: Returning a string from an async function or a function called in async context

String values returned from async contexts are NaN-boxed StringHeader pointers. When the receiving code treats the return value as a string, it gets a numeric pointer instead.

// BROKEN
async function getName(): Promise<string> {
  return fetchName(); // returns NaN-boxed pointer
}

// FIXED — inline the string operation at the call site
// instead of returning strings through async boundaries

Where possible, perform string operations directly where the result is needed rather than passing strings across async boundaries.

13. new Date() in Async Context

Pattern: new Date() inside an async function

The Date constructor malfunctions in async contexts due to how Perry handles object allocation across execution frames.

// BROKEN
async function getTimestamp(): Promise<number> {
  const now = new Date();
  return now.getTime();
}

// FIXED — Date.now() returns a plain number, no object allocation
async function getTimestamp(): Promise<number> {
  return Date.now();
}

Date.now() is safe everywhere because it returns a primitive number, avoiding the object allocation issue.

Quick Reference

AvoidUse Instead
obj[variable]if/else if per key
?. optional chainingExplicit null checks
?? nullish coalescingif (x !== undefined)
/regex/.test()indexOf or char checks
{ key } shorthand{ key: key }
this.arr.map(fn)for loop
for...of on arraysfor (let i = 0; ...)
c >= 'a' && c <= 'z'ALPHA.indexOf(c) >= 0
Closures capturing thisModule-level functions + state
requestAnimationFramesetInterval
setTimeout recursionsetInterval
String returns in asyncInline string ops
new Date() in asyncDate.now()

FFI Conventions

Perry’s FFI (Foreign Function Interface) bridges TypeScript code and Rust native libraries. This is how the editor, terminal, and other packages call into platform-specific Rust code for rendering, input handling, and OS integration.

How It Works

TypeScript code calls functions declared as external. Perry compiles these calls into native function invocations against symbols in a linked Rust static library (.a file). The Rust side exports C-ABI functions that Perry can call directly.

NaN-Boxed String Parameters

Perry represents all values as 64-bit NaN-boxed floats internally. Strings are no exception — a string parameter is a NaN-boxed pointer to a StringHeader struct in memory.

On the Rust side, string parameters arrive as raw pointers:

#![allow(unused)]
fn main() {
use perry_runtime::str_from_header;

#[no_mangle]
pub extern "C" fn __wrapper_setTitle(title_ptr: i64) {
    let title = unsafe { str_from_header(title_ptr as *const u8) };
    // title is now a &str
}
}

The str_from_header() utility (from perry-runtime) extracts the string data from the NaN-boxed pointer.

Function Naming Convention

Perry generates wrapper symbols with a double-underscore prefix. A TypeScript declaration like:

declare function renderLine(text: string, lineNum: number, x: number, y: number): void;

becomes a call to the native symbol __wrapper_renderLine. The Rust implementation must use this exact name:

#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn __wrapper_renderLine(
    text: i64,    // NaN-boxed string pointer
    line_num: f64,
    x: f64,
    y: f64,
) {
    let text_str = unsafe { str_from_header(text as *const u8) };
    // render using platform APIs
}
}

package.json FFI Declarations

Every FFI function must be registered in the package’s package.json under perry.nativeLibrary.functions. This tells the Perry compiler which external symbols to expect during linking.

{
  "name": "@honeide/editor",
  "perry": {
    "nativeLibrary": {
      "name": "hone_editor_macos",
      "functions": [
        {
          "name": "renderLine",
          "params": ["i64", "i64", "f64", "f64"],
          "returns": "void"
        },
        {
          "name": "setViewportSize",
          "params": ["f64", "f64"],
          "returns": "void"
        },
        {
          "name": "getClipboardText",
          "params": [],
          "returns": "i64"
        },
        {
          "name": "setClipboardText",
          "params": ["i64"],
          "returns": "void"
        }
      ]
    }
  }
}

The name field is the Rust crate name (and the resulting .a library file). The functions array lists every function the TypeScript side calls.

Parameter Type Rules

FFI typeUse forRust type
f64Numbers (integers, floats, booleans)f64
i64Strings, pointersi64 (cast to *const u8 for strings)
voidNo return value()

Do not use i32. It causes verifier errors in Perry’s code generator. Always use f64 for numeric values and i64 for string/pointer values, even if the logical type is a 32-bit integer.

#![allow(unused)]
fn main() {
// WRONG — i32 causes verifier errors
pub extern "C" fn __wrapper_setLine(line: i32) { ... }

// CORRECT
pub extern "C" fn __wrapper_setLine(line: f64) {
    let line_num = line as i32; // cast inside the function
}
}

Return Values

Functions can return f64 (numbers), i64 (strings/pointers), or void. To return a string from Rust to TypeScript, allocate a StringHeader and return its pointer as i64:

#![allow(unused)]
fn main() {
use perry_runtime::alloc_string;

#[no_mangle]
pub extern "C" fn __wrapper_getClipboardText() -> i64 {
    let text = get_clipboard_contents();
    alloc_string(&text) as i64
}
}

Per-Platform Crates

Each platform has its own FFI crate that implements the same set of declared functions using native APIs:

PlatformCrateLocation
macOShone_editor_macoshone-editor/native/macos/
iOShone_editor_ioshone-editor/native/ios/
Windowshone_editor_windowshone-editor/native/windows/
Linuxhone_editor_linuxhone-editor/native/linux/
Androidhone_editor_androidhone-editor/native/android/

All crates export the same __wrapper_* symbols, so the TypeScript code is platform-agnostic. Perry links the correct crate based on the --target flag.

Building Perry

Perry is the TypeScript-to-native AOT compiler that powers Hone. It lives in a sibling directory (../perry/) and is written in Rust. This page covers building Perry itself and using it to compile Hone packages.

Prerequisites

  • Rust toolchain (install via rustup)
  • Xcode Command Line Tools (macOS) for the system linker
  • The Perry source at ../perry/ relative to the Hone repo root

Building Perry Components

The Perry Compiler

cd ../perry && CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry

This produces the perry binary used to compile TypeScript to native code.

Perry UI Library (macOS)

cd ../perry && CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry-ui-macos

Required for compiling any package that renders UI on macOS (hone-ide, hone-editor).

Perry Standard Library

Rebuild after changing perry-runtime source:

cd ../perry && cargo clean -p perry-runtime --release && \
  CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry-stdlib -p perry-ui-macos -p perry

The cargo clean step is necessary because Cargo may not detect changes to the runtime’s generated code.

LTO Warning

Always use CARGO_PROFILE_RELEASE_LTO=off for all Perry Rust builds. Without this flag, Cargo defaults to thin LTO, which produces LLVM bitcode that the macOS clang linker cannot read. The build will succeed but linking Hone packages will fail with cryptic errors.

You can also set this permanently in ../perry/.cargo/config.toml:

[profile.release]
lto = false

Compiling Hone Packages

Once Perry is built, use it to compile Hone’s TypeScript packages into native binaries.

IDE

# macOS native binary
cd hone-ide && perry compile src/app.ts --output hone-ide

# iOS Simulator
cd hone-ide && perry compile src/app.ts --target ios-simulator --output Hone

# Web (WASM)
cd hone-ide && perry compile src/app.ts --target web --output hone-ide.html

Auth Server

cd hone-auth && perry compile src/app.ts --output hone-auth

Produces a ~2.8MB static binary with no runtime dependencies.

Marketplace

cd hone-marketplace && perry compile src/app.ts --output hone-marketplace

Build Server

cd hone-build && perry compile src/app.ts --output hone-build

Build Order

When building everything from scratch:

  1. Perry compilercargo build --release -p perry
  2. Perry stdlibcargo build --release -p perry-stdlib
  3. Perry UI librarycargo build --release -p perry-ui-macos (or target platform)
  4. FFI crates — Build the native crates in hone-editor/native/ and hone-terminal/native/
  5. Hone packagesperry compile each package

Steps 1-3 only need to be repeated when Perry itself changes. Step 4 is needed when FFI code changes. Step 5 is the normal development cycle.

Troubleshooting

“unknown file type” linker error: You forgot CARGO_PROFILE_RELEASE_LTO=off. Rebuild Perry with LTO disabled.

“undefined symbol _wrapper*” linker error: A function declared in package.json under perry.nativeLibrary.functions is not exported by the Rust FFI crate. Check that the #[no_mangle] pub extern "C" fn __wrapper_<name> exists and matches exactly.

“verifier error” during compilation: An FFI function uses i32 parameters. Change them to f64 (numbers) or i64 (strings/pointers).

Platform Targets

Perry compiles TypeScript to native binaries for multiple platforms. Each target uses platform-specific UI libraries and rendering backends.

Supported Platforms

PlatformTarget FlagUI LibraryRendering Backend
macOS(default, no flag)perry-ui-macosMetal, CoreGraphics, CoreText
iOS--target ios-simulatorperry-ui-iosUIKit, CoreGraphics, CoreText
Windows--target windowsperry-ui-windowsDirect2D, DirectWrite, DirectComposition
Linux--target linuxperry-ui-linuxCairo, Pango
Android--target androidperry-ui-androidAndroid NDK
Web--target web(WASM)WebAssembly, Canvas/WebGL

Compile-Time Platform Detection

Perry provides the __platform__ compile-time constant. Code guarded by platform checks is dead-code-eliminated for non-matching platforms.

ValuePlatform
0macOS
1iOS
2Android
declare const __platform__: number;

function getDefaultFontSize(): number {
  if (__platform__ === 0) {
    return 13;  // macOS — standard desktop size
  } else if (__platform__ === 1) {
    return 16;  // iOS — larger for touch
  } else if (__platform__ === 2) {
    return 16;  // Android — larger for touch
  }
  return 13;
}

Because __platform__ is resolved at compile time, the non-matching branches are stripped entirely from the binary. This means platform-specific imports and FFI calls in dead branches do not need to link.

Per-Platform FFI Crates

Each platform has dedicated Rust crates that implement the FFI functions using native APIs. These crates all export the same __wrapper_* symbols, so TypeScript code remains platform-agnostic.

Editor FFI crates:

PlatformCrateKey APIs
macOShone-editor/native/macos/CoreText for text shaping, Metal for GPU rendering
iOShone-editor/native/ios/CoreText for text, UIKit for input
Windowshone-editor/native/windows/DirectWrite for text, Direct2D for rendering
Linuxhone-editor/native/linux/Pango for text, Cairo for rendering
Androidhone-editor/native/android/Android NDK Canvas, Skia

Terminal FFI crates follow the same pattern in hone-terminal/native/.

Compilation Examples

# macOS (default target)
perry compile src/app.ts --output hone-ide

# iOS Simulator
perry compile src/app.ts --target ios-simulator --output Hone

# Web
perry compile src/app.ts --target web --output hone-ide.html

UI Testing by Platform

macOS — geisterhand

geisterhand is the UI automation tool for macOS Perry binaries.

# Capture screenshot
geisterhand screenshot --output /tmp/shot.png

# Click at pixel coordinates
geisterhand click 200 350

# Type text
geisterhand type "hello world"

iOS Simulator — AppleScript

For iOS Simulator targets, use osascript (AppleScript) to drive the Simulator app:

# Tap at coordinates in Simulator
osascript -e 'tell application "Simulator" to activate'
# Use Accessibility APIs or xcrun simctl for interaction

geisterhand does not work with the iOS Simulator — it only targets native macOS windows.

Output Artifacts

TargetOutputSize (typical)
macOSStatic binary5-15 MB
iOS.app bundle8-20 MB
Web.html + .wasm2-5 MB
Auth server (macOS)Static binary~2.8 MB

All native binaries are statically linked — no runtime dependencies, no Node.js, no V8. The binary includes the Perry stdlib, platform UI library, and all FFI crates.

Running the Auth Service

The auth service (hone-auth) handles user authentication, device pairing, and subscription management. It runs as a native Perry binary on port 8445.

Configuration

Create auth.conf in the working directory with KEY=VALUE pairs:

DB_HOST=webserver.skelpo.net
DB_USER=hone
DB_PASS=<password>
DB_NAME=hone
PORT=8445
AUTH_SECRET=<shared-secret>
AUTH_BASE_URL=https://auth.hone.codes
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=<sendgrid-key>
SMTP_FROM=Hone <noreply@hone.dev>
KeyRequiredDescription
DB_HOSTYesMySQL server hostname
DB_USERYesMySQL username
DB_PASSYesMySQL password
DB_NAMEYesMySQL database name
PORTNoListen port (default: 8445)
AUTH_SECRETYesShared secret for JWT signing. Must match the relay service’s auth.secret.
AUTH_BASE_URLYesPublic URL for magic-link callbacks
SMTP_HOSTNoSMTP server for sending emails
SMTP_PORTNoSMTP port (default: 587)
SMTP_USERNoSMTP username
SMTP_PASSNoSMTP password
SMTP_FROMNoFrom address for emails

Database Setup

The auth service uses MySQL. Create the database and user:

CREATE DATABASE hone;
CREATE USER 'hone'@'%' IDENTIFIED BY '<password>';
GRANT ALL PRIVILEGES ON hone.* TO 'hone'@'%';

Tables are created automatically on first startup. The schema uses camelCase for all identifiers (table names, column names).

Running

cd /opt/hone-auth && ./hone-auth

The service reads auth.conf from its working directory.

Health Check

GET /health

Returns 200 OK when the service is running and the database connection is healthy.

Authentication Flow

  1. Magic link request: Client sends email address to POST /auth/magic-link. The service generates a one-time token and emails a login link.
  2. Token verification: User clicks the link, which hits GET /auth/verify?token=<token>. The service validates the token and returns a JWT.
  3. Device pairing: Client registers a device with POST /auth/devices using the JWT. The service returns a device token used for relay authentication.

Development Mode

If SMTP configuration is omitted, magic-link tokens are logged to stdout instead of emailed. This enables local development without an email provider.

[dev] Magic link token for user@example.com: abc123def456

Shared Secret

The AUTH_SECRET value is critical for the system to work end-to-end:

  • The auth service uses it to sign JWTs and device tokens.
  • The relay service uses the same secret (as auth.secret in relay.conf) to validate device tokens locally, without making HTTP calls back to the auth service.

If these values do not match, devices will fail to authenticate with the relay.

Running the Relay Service

The relay service (hone-relay) provides real-time cross-device synchronization via WebSocket. It manages rooms, buffers messages for reconnecting devices, and persists deltas to SQLite.

Configuration

Create relay.conf in the working directory:

host=0.0.0.0
port=8443
auth.secret=<shared-secret>
KeyRequiredDescription
hostNoBind address (default: 0.0.0.0)
portNoHTTP port (default: 8443)
auth.secretNoShared secret for token validation. Must match auth service’s AUTH_SECRET.

Ports

PortProtocolPurpose
8443HTTPREST API, health checks
8444WebSocketReal-time sync connections

Running

cd /opt/hone-relay && ./hone-relay

The service reads relay.conf from its working directory.

Development Mode

Leave auth.secret empty or omit it entirely to disable token validation. All connections will be accepted without authentication. This is useful for local development.

host=127.0.0.1
port=8443
auth.secret=

Sync Protocol

Devices connect via WebSocket and join rooms identified by project ID. The relay:

  1. Authenticates the device token against the shared secret (or skips in dev mode).
  2. Assigns the connection to a project room.
  3. Forwards deltas from any device in the room to all other devices.
  4. Persists deltas to SQLite for offline reconciliation.
  5. Buffers messages for 60 seconds for devices that temporarily disconnect.

Message Buffering

When a device disconnects, the relay holds its pending messages for 60 seconds. If the device reconnects within that window, it receives the buffered messages and resumes without data loss. After 60 seconds, the buffer is dropped and the device must do a full reconciliation from SQLite on reconnect.

Rate Limiting

Rate limiting is per-connection with configurable windows. Under default settings, a single device can send up to 100 messages per second. This is sufficient for real-time typing sync without overwhelming other devices in the room.

SQLite Persistence

Deltas are stored in a local SQLite database (relay.db in the working directory). This provides:

  • Offline reconciliation: Devices that were offline for longer than the 60-second buffer window can catch up from persisted deltas.
  • Audit trail: All changes are recorded with timestamps and device IDs.

The database file is created automatically on first startup.

Tests

cd hone-relay && bun test

Runs 48 tests covering auth, buffer management, WebSocket hub, and configuration parsing.

Running the Marketplace

The marketplace service (hone-marketplace) powers marketplace.hone.codes — the plugin registry for Hone V2 extensions.

Overview

PropertyValue
Binaryhone-marketplace
Port8446
Domainmarketplace.hone.codes

Running

cd /opt/hone-marketplace && ./hone-marketplace

Capabilities

The marketplace provides:

  • Search: Full-text search of published plugins by name, description, and tags.
  • Download: Retrieve compiled plugin binaries for a specific platform target.
  • Publish: Upload new plugin versions (authenticated via auth service tokens).
  • Metadata: Plugin details, version history, download counts, ratings.

Plugin Lifecycle

  1. A plugin author develops a V2 plugin using the @hone/sdk package (hone-extension/).
  2. The author submits the plugin source to the build service, which compiles it for all supported platforms via perry-hub workers.
  3. The compiled artifacts are uploaded to the marketplace via POST /plugins.
  4. Users discover plugins through the IDE’s extensions panel or the marketplace website.
  5. The IDE downloads the appropriate platform binary from the marketplace.

Compilation

cd hone-marketplace && perry compile src/app.ts --output hone-marketplace

Running the Build Service

The build service (hone-build) coordinates cross-platform plugin compilation. It accepts plugin source code and dispatches compilation jobs to perry-hub workers.

Overview

PropertyValue
Binaryhone-build
Port8447

Configuration

Create build.conf in the working directory:

PORT=8447

Running

cd /opt/hone-build && ./hone-build

How It Works

  1. A plugin author submits source code to the build service.
  2. The build service creates compilation jobs for each supported platform target (macOS, iOS, Windows, Linux, Android, Web).
  3. Jobs are dispatched to perry-hub workers — remote machines with the Perry compiler installed for each target platform.
  4. Workers compile the plugin and return the platform-specific binary artifacts.
  5. The build service collects all artifacts and stores them for retrieval by the marketplace.

Compilation

cd hone-build && perry compile src/app.ts --output hone-build

Tests

cd hone-build && bun test

Runs 21 tests covering artifact storage and platform normalization.

Deployment

All Hone backend services run on a single server and are managed by systemd. TLS is terminated by nginx with Let’s Encrypt certificates.

Server

PropertyValue
IP84.32.223.50
OSUbuntu 24.04
Hostwebserver.skelpo.net

Services

ServiceBinary PathPortSystemd UnitDomain
Auth/opt/hone-auth/hone-auth8445hone-auth.serviceauth.hone.codes
Relay (HTTP)/opt/hone-relay/hone-relay8443hone-relay.servicesync.hone.codes
Relay (WS)(same binary)8444(same)sync.hone.codes/ws
Marketplace/opt/hone-marketplace/hone-marketplace8446hone-marketplace.servicemarketplace.hone.codes
Build/opt/hone-build/hone-build8447hone-build.service

DNS

A records pointing to 84.32.223.50:

  • auth.hone.codes
  • sync.hone.codes
  • marketplace.hone.codes

TLS

Certificates are provisioned by Let’s Encrypt via certbot and auto-renewed. nginx terminates TLS and proxies to the service ports over localhost HTTP.

nginx Configuration

Standard HTTP Service (Auth, Marketplace)

server {
    listen 443 ssl;
    server_name auth.hone.codes;

    ssl_certificate /etc/letsencrypt/live/auth.hone.codes/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/auth.hone.codes/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8445;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

WebSocket Service (Relay)

server {
    listen 443 ssl;
    server_name sync.hone.codes;

    ssl_certificate /etc/letsencrypt/live/sync.hone.codes/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/sync.hone.codes/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8443;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /ws {
        proxy_pass http://127.0.0.1:8444;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 86400s;
    }
}

The proxy_read_timeout on the WebSocket location prevents nginx from closing idle connections.

Systemd Units

Each service has a systemd unit file in /etc/systemd/system/.

Example: hone-auth.service

[Unit]
Description=Hone Auth Service
After=network.target mysql.service

[Service]
Type=simple
WorkingDirectory=/opt/hone-auth
ExecStart=/opt/hone-auth/hone-auth
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Common Operations

# Start a service
sudo systemctl start hone-auth

# Stop a service
sudo systemctl stop hone-auth

# Restart after deploying a new binary
sudo systemctl restart hone-auth

# View logs
sudo journalctl -u hone-auth -f

# Enable service on boot
sudo systemctl enable hone-auth

Database

MySQL runs on the same server (webserver.skelpo.net). Only the auth service connects to MySQL directly. The relay uses local SQLite.

PropertyValue
Hostwebserver.skelpo.net
Userhone
Databasehone

Landing Page

The landing page (hone.codes) is a static index.html with no build step:

scp landing/index.html root@webserver.skelpo.net:/var/www/hone.codes/

Deploying a New Binary

  1. Compile the package on the build machine:

    cd hone-auth && perry compile src/app.ts --output hone-auth
    
  2. Upload the binary:

    scp hone-auth root@webserver.skelpo.net:/opt/hone-auth/hone-auth
    
  3. Restart the service:

    ssh root@webserver.skelpo.net 'systemctl restart hone-auth'
    
  4. Verify:

    curl https://auth.hone.codes/health
    

Test Runners

Hone uses per-package test runners. There is no monorepo-level test command — each package must be tested independently.

Test Matrix

PackageRunnerCommandTest CountImport
hone-coreBunbun test649+import { describe, it, expect } from 'bun:test'
hone-editorBunbun test353import { describe, it, expect } from 'bun:test'
hone-terminalBunbun test163import { describe, it, expect } from 'bun:test'
hone-relayBunbun test48import { describe, it, expect } from 'bun:test'
hone-buildBunbun test21import { describe, it, expect } from 'bun:test'
hone-themesJestnpm test452import { describe, it, expect } from '@jest/globals'
hone-extensionsVitestnpm testimport { describe, it, expect } from 'vitest'
hone-apitscnpm test(type-check only, no runtime tests)

Running Tests

All tests in a package

cd hone-core && bun test
cd hone-editor && bun test
cd hone-terminal && bun test
cd hone-relay && bun test
cd hone-build && bun test
cd hone-themes && npm test
cd hone-extensions && npm test
cd hone-api && npm test

A single test file

cd hone-editor && bun test tests/buffer.test.ts

With Bun, pass the file path as an argument. Bun matches on filename, so partial names also work:

bun test buffer       # runs all test files containing "buffer" in the name

Type checking

cd <package> && bun run typecheck
# or
npx tsc --noEmit

Important: Use the Correct Runner

The runner matters. Do not use npx vitest or npx jest in Bun-based packages — they will fail because test files import from bun:test, which only Bun provides.

If the test file imports from…Use this runner
bun:testbun test
@jest/globals or no import (Jest globals)npm test (Jest)
vitestnpm test (Vitest)

Writing Tests (Bun Packages)

import { describe, it, expect, beforeEach } from 'bun:test';

describe('PieceTable', () => {
  let table: PieceTable;

  beforeEach(() => {
    table = new PieceTable('hello world');
  });

  it('should insert text', () => {
    table.insert(5, ' beautiful');
    expect(table.getText()).toBe('hello beautiful world');
  });

  it('should delete text', () => {
    table.delete(5, 6);
    expect(table.getText()).toBe('hello');
  });
});

What Each Package Tests

PackageCoverage Areas
hone-coreWorkspace management, settings, git integration, search, LSP client, DAP client, AI service, extension host
hone-editorPiece table buffer, multi-cursor editing, undo/redo, viewport calculations, tokenizer, search & replace, code folding, diff algorithm, LSP integration, DAP integration
hone-terminalVT100/VT220 escape sequence parser, terminal buffer, keyboard input handling, terminal emulator integration
hone-relayToken authentication, message buffering, WebSocket hub, configuration parsing
hone-buildArtifact storage, platform target normalization
hone-themesJSON schema validation for all 11 themes, WCAG contrast ratio compliance
hone-extensionsBuilt-in extension functionality
hone-apiType correctness of the public extension API (compile-time only)

UI Testing

Hone’s UI is rendered natively on each platform. Since there is no browser DOM, UI testing uses platform-specific automation tools.

macOS — geisterhand

geisterhand is the UI automation tool for testing Perry-compiled macOS binaries. It interacts with native windows at the pixel level.

Taking Screenshots

geisterhand screenshot --output /tmp/shot.png

Captures the frontmost window to a PNG file. Use this to verify visual state during tests.

Clicking

geisterhand click 200 350

Sends a mouse click at the specified (x, y) pixel coordinates relative to the screen.

Typical Test Flow

  1. Launch the Perry-compiled binary.
  2. Wait for the window to appear.
  3. Take a screenshot to verify initial state.
  4. Perform interactions (clicks, key presses).
  5. Take another screenshot to verify the result.
# Launch the IDE
./hone-ide &

# Wait for window
sleep 2

# Screenshot the initial state
geisterhand screenshot --output /tmp/initial.png

# Click the file explorer icon in the activity bar
geisterhand click 25 60

# Screenshot after interaction
geisterhand screenshot --output /tmp/after-click.png

iOS Simulator

For iOS Simulator targets, use AppleScript (osascript) to automate interactions. geisterhand does not work with the Simulator.

# Activate the Simulator
osascript -e 'tell application "Simulator" to activate'

For programmatic interaction with the simulated device, use xcrun simctl:

# Boot a simulator
xcrun simctl boot "iPhone 15 Pro"

# Install the app
xcrun simctl install booted Hone.app

# Launch the app
xcrun simctl launch booted codes.hone.ide

# Take a screenshot
xcrun simctl io booted screenshot /tmp/ios-shot.png

Limitations

  • No DOM testing: There is no browser, no document, no querySelector. All UI assertions are visual (screenshot comparison) or coordinate-based.
  • No accessibility tree (yet): Automated assertions currently rely on pixel coordinates and screenshot diffs rather than semantic element queries.
  • Platform-specific: Each platform requires its own automation toolchain. macOS uses geisterhand, iOS uses simctl/osascript, and other platforms have their own tools.

Dev Environment Setup

Hone is a monorepo of independent packages with no workspace manager. Each package manages its own dependencies.

Prerequisites

ToolPurposeInstall
BunRuntime and test runner for core packagescurl -fsSL https://bun.sh/install | bash
RustBuilding Perry compiler and native FFI cratescurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Node.jsRuntime for hone-themes (Jest), hone-extensions (Vitest), hone-api (tsc)Download from nodejs.org or use nvm
Xcode CLI ToolsmacOS system linker (required for Perry)xcode-select --install

Clone the Repository

git clone https://github.com/HoneIDE/hone.git
cd hone

Build the Perry Compiler

Perry lives in a sibling directory. Clone and build it first:

cd ..
git clone https://github.com/nicktypemern/perry.git
cd perry
CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry
CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry-ui-macos
cd ../hone

The CARGO_PROFILE_RELEASE_LTO=off flag is mandatory — without it, the macOS linker cannot process the resulting bitcode.

Install Dependencies

Each package manages its own node_modules. Install them individually:

# Bun-based packages
cd hone-core && bun install && cd ..
cd hone-editor && bun install && cd ..
cd hone-terminal && bun install && cd ..
cd hone-relay && bun install && cd ..
cd hone-build && bun install && cd ..

# Node-based packages
cd hone-themes && npm install && cd ..
cd hone-extensions && npm install && cd ..
cd hone-api && npm install && cd ..

There is no top-level package.json or npm install at the repo root.

Verify the Setup

Run tests in a couple of packages to confirm everything works:

cd hone-core && bun test
cd ../hone-editor && bun test

If tests pass, your environment is ready.

Optional: Build the IDE

Once Perry is built and dependencies are installed, you can compile the IDE:

cd hone-ide && perry compile src/app.ts --output hone-ide
./hone-ide

This produces a native macOS binary that launches the full IDE.

Troubleshooting

bun: command not found: Bun is not on your PATH. Run the install script again or add ~/.bun/bin to your PATH.

perry: command not found: The Perry binary is at ../perry/target/release/perry. Either add it to your PATH or use the full path.

Linker errors when compiling with Perry: Rebuild Perry with LTO disabled: CARGO_PROFILE_RELEASE_LTO=off cargo build --release -p perry.

Test import errors (Cannot find module 'bun:test'): You are running the test with the wrong runner. Use bun test, not npx vitest or npx jest, for packages that import from bun:test.

Conventions

Coding and project conventions for contributing to Hone.

Perry-Safe TypeScript

All code that will be compiled by Perry must avoid the constrained patterns. The key rules:

  • No optional chaining (?.) — use explicit null checks.
  • No nullish coalescing (??) — use if (x !== undefined).
  • No dynamic key access (obj[variable]) — use if/else chains.
  • No for...of on arrays — use indexed for loops.
  • No regex — use indexOf and character checks.
  • No ES6 shorthand properties — write { key: key }.
  • No Array.map/filter/reduce on class fields — use for loops.
  • No closures capturing this — use module-level state and named functions.

When in doubt, prefer explicit and verbose over concise and clever.

Database Schemas

All database identifiers use camelCase:

CREATE TABLE magicLinks (
    id INT PRIMARY KEY AUTO_INCREMENT,
    userId INT NOT NULL,
    tokenHash VARCHAR(64) NOT NULL,
    expiresAt BIGINT NOT NULL,
    createdAt BIGINT NOT NULL
);

This applies to table names, column names, and index names.

Test Imports

Bun-based packages (hone-core, hone-editor, hone-terminal, hone-relay, hone-build) import test utilities from bun:test:

import { describe, it, expect, beforeEach, afterEach } from 'bun:test';

Do not import from vitest, jest, or @jest/globals in these packages.

Platform Detection

Use the __platform__ compile-time constant for platform-specific code:

declare const __platform__: number;

// 0 = macOS, 1 = iOS, 2 = Android
if (__platform__ === 0) {
  // macOS-specific code
}

Non-matching branches are dead-code-eliminated at compile time.

Configuration Files

Service configuration uses KEY=VALUE format in .conf files:

PORT=8445
DB_HOST=webserver.skelpo.net
AUTH_SECRET=my-secret

No quotes around values. No section headers. One key-value pair per line.

Project Structure

  • No Rust in hone-ide. All Rust code is in ../perry/ (compiler, runtime, UI libraries) or in hone-editor/native/ and hone-terminal/native/ (FFI crates). The IDE package is pure TypeScript.
  • No workspace manager. Each package is independent with its own package.json, its own node_modules, and its own test runner.
  • No shared dependencies. Packages do not hoist or share node_modules. Install dependencies in each package individually.

Closure Rule

Perry captures closure variables by value at creation time. Mutations after closure creation are invisible to the closure.

// WRONG — Perry captures the initial value of count
let count = 0;
setInterval(() => { count++; }, 1000);  // always increments from 0

// RIGHT — module-level named function reads current state
let _count = 0;
function tick() { _count++; }
setInterval(tick, 1000);

Store mutable state in module-level let variables. Access it through module-level named functions. Never capture this in a closure.

FFI Conventions

  • Use f64 for numeric parameters, i64 for string/pointer parameters. Never i32.
  • Functions are exported as __wrapper_<name> symbols.
  • All FFI functions must be declared in package.json under perry.nativeLibrary.functions.

See FFI Conventions for full details.