Skip to content

Pedelec SDK

Give local AI agents a safe, typed bridge to your web application.

Pedelec is a browser SDK and local bridge for applications that want to work with AI coding agents such as Codex, Antigravity, OpenCode, Cursor, Claude Code, or an Ollama-backed agent.

A web application can use Pedelec to:

  • create an agent session on the user’s machine;
  • send user instructions and receive streamed assistant text;
  • expose narrowly scoped browser-side tools to the agent;
  • resume or end sessions; and
  • show connection, approval, provider, and lifecycle state in the UI.

A normal chat API accepts text and returns text. An agent integration often needs more:

  • The agent may need to inspect the page, editor, canvas, selection, or application state.
  • The application may need to ask the user for confirmation while an agent turn is paused.
  • The user may want to use a provider CLI that is already installed and authenticated locally.
  • The browser should not receive permission to launch arbitrary local processes directly.

Pedelec separates those responsibilities. Your web application owns the UI and the tool handlers. The Pedelec extension and desktop runtime own the local transport, session lifecycle, and provider process.

Web application
↓ @kaoruisaac/pedelec
Pedelec Chrome Extension
↓ Chrome Native Messaging
Pedelec native host
↓ local Core IPC
Pedelec Desktop Runtime
↓ provider process
Codex / Antigravity / OpenCode / Cursor / Claude Code / Ollama

The diagram is the control/event path. Asset file bodies follow a separate internal path: Web App fetch(PUT/GET) → Desktop-managed 127.0.0.1 loopback asset transfer server → session .pedelec-runtime/assets/. Use the asset APIs only; do not depend on the internal URL, port, token, or ticket lifecycle.

The Pedelec SDK must run in a browser page environment and requires:

  1. The user has installed the Pedelec Chrome Extension.
  2. The user has opened the Pedelec Desktop App at least once after installation, completing binary setup, Native Messaging host registration, and launch-config creation.
  3. After that initialization, the native host can try to start Desktop in the background for a Core request; this may still fail and must be handled as an unavailable Desktop/installation issue.
  4. The target provider is available on the user’s machine. CLI-backed providers use commands such as codex, agy, opencode, cursor-agent, or claude; the Ollama provider uses Pedelec’s bundled pedelec-agent.

The SDK is not suitable for direct use in Node.js, an SSR server, or a background worker; it needs extension runtime messaging from a Chrome page environment.


Install the published package:

Terminal window
npm install @kaoruisaac/pedelec

Import it from the Web App:

import { Pedelec, defineTool } from "@kaoruisaac/pedelec";

import { Pedelec, defineTool } from "@kaoruisaac/pedelec";
const pedelec = new Pedelec();
const session = await pedelec.createSession({
provider: "codex",
effortLevel: "high",
skills: {
guidance: "Use get_current_page when you need browser page context.",
tools: [
defineTool({
name: "get_current_page",
description: "Read the current browser page title and URL.",
argsSchema: {
type: "object",
properties: {},
required: [],
},
handler: () => ({
url: location.href,
title: document.title,
}),
}),
],
},
});
session.onChat((text) => {
// One completed logical assistant message.
console.log(text);
});
session.onChatDelta((delta) => {
// Optional best-effort incremental text for live UI.
console.log(delta);
});
session.onStatus((status) => {
// idle | running | waiting_tool_result | ended | error
console.log("status", status);
});
session.onError((error) => {
console.error(error.code, error.message, error.details);
});
await session.sendText("Please help me analyze the current page state");

sendText() resolves after Core reports the matching semantic operation as completed. An idle status or bridge request response alone does not complete it. If the session is already handling a previous prompt, the new sendText() call is rejected to prevent multiple concurrent requests from running in the same session.


The first time an origin calls createSession() or resumeSession(), the extension asks the user to approve that origin in the popup. After approval, the same origin can create sessions directly.

You can query the current origin’s approval status first to decide whether to show UI such as “Connect Pedelec”:

const status = await pedelec.getApprovalStatus();
console.log(status.installed, status.approved, status.origin);
const session = await pedelec.createSession({
provider: "opencode",
effortLevel: "high",
});

Currently supported provider codes in the SDK:

Provider Code
Codex codex
Antigravity antigravity
OpenCode opencode
Cursor cursor
Claude Code claude
Ollama ollama

Ollama sessions use the bundled pedelec-agent and the endpoint configured in Desktop Settings. The selected profile must contain a model; an empty selected profile returns MODEL_REQUIRED and does not fall back to default.

Desktop Settings also accepts an optional Tavily API key for Ollama sessions. When configured, the bundled agent can decide to use Tavily web search with basic search depth and up to five results per call. Without the key, the web-search tool is not exposed to the Ollama model.

Using the Desktop App’s Default Provider

Section titled “Using the Desktop App’s Default Provider”

If the Desktop App has a default provider configured, you can omit provider:

const session = await pedelec.createSession({
skills: {
guidance: "Use update_counter when the user asks to change the counter.",
tools: [
defineTool({
name: "update_counter",
description: "Update the visible counter by delta.",
argsSchema: {
type: "object",
required: ["delta"],
properties: {
delta: {
type: "number",
description: "Counter delta.",
},
},
},
}),
],
},
});

This first reads only defaultProvider from the Desktop App. If no default provider is configured, the SDK throws DEFAULT_PROVIDER_NOT_SET. The chosen effort profile is resolved by Core using Desktop settings.

const session = await pedelec.createSession({
provider: "codex",
});

When only provider is passed, the SDK sends that provider with effortLevel: "default". Non-Ollama providers may have an empty profile; Ollama requires a model in its selected profile and returns MODEL_REQUIRED otherwise.

SDK-created sessions are page-scoped by default. autoEndOnDisconnect defaults to true, so Pedelec automatically ends the Desktop thread when the last SDK connection for that session disconnects, such as on page refresh or tab close.

Use the default for demos and page-scoped apps. Set autoEndOnDisconnect: false only when you need to resume the same session after navigation or share it across pages:

const session = await pedelec.createSession({
provider: "codex",
autoEndOnDisconnect: false,
});

const providers = await pedelec.listProviders();
for (const provider of providers) {
console.log(provider.code, provider.available, provider.isDefault, provider.error);
}

Return format:

type ProviderInfo = {
name: string;
code: "codex" | "antigravity" | "opencode" | "cursor" | "claude" | "ollama";
available: boolean;
isDefault: boolean;
error: string | null;
};

isDefault identifies the provider selected by Desktop settings and is independent of available; an unavailable default provider still has isDefault: true. available: false usually means that the provider CLI is not installed or is not in PATH. For Ollama, available: true only means Pedelec can find pedelec-agent; it does not mean its endpoint is reachable, its credentials work, or the selected model is installed.


const settings = await pedelec.getSettings();
console.log(settings.defaultProvider);

Return format:

type PedelecSettings = {
defaultProvider: "codex" | "antigravity" | "opencode" | "cursor" | "claude" | "ollama" | null;
};

Use onChat() for completed logical assistant messages:

session.onChat((text) => {
saveCompletedAssistantMessage(text);
});

For a live streaming UI, subscribe separately with onChatDelta() and accumulate the fragments:

const chunks: string[] = [];
session.onChatDelta((delta) => {
chunks.push(delta);
render(chunks.join(""));
});

Delta delivery is best-effort and provider-dependent. Delta boundaries have no semantic meaning, and Pedelec does not synthesize missing suffixes from a later completed message.


session.onStatus((status) => {
switch (status) {
case "idle":
break;
case "running":
break;
case "waiting_tool_result":
break;
case "ended":
break;
case "error":
break;
}
});

Common statuses:

Status Meaning
idle The session can receive the next prompt
running The agent is processing user input
waiting_tool_result The agent has issued a tool call and is waiting for the frontend to return a result
ended The session has ended
error The session encountered an error

The public SDK reports errors through session.onError((error, ctx) => ...). error is a PedelecError; ctx.source describes whether the callback came from a Core-delivered event or from SDK-side handling:

  • "core": the error event arrived through the Core/provider execution path;
  • "sdk": the SDK generated the error while handling a local request, disconnect, or protocol condition.
type ErrorEventContext = PedelecEventContext & {
type: "error" | "sdk_error";
source: "core" | "sdk";
};
session.onError((error, ctx) => {
console.error(ctx.source, error.code, error.message, error.details);
});

The current public callback context does not expose a separate provider-responsibility field. Do not branch on ctx.source === "provider" or expect ctx.provider.

getSettings() and listProviders() require origin approval and may open its popup. Settings never expose provider credentials, and provider entries contain only the fields shown above. The SDK ignores unknown fields added to Extension responses for forward compatibility. Use getApprovalStatus().appConnected for a non-sensitive Desktop connectivity probe.


Tool Calling: Letting the Agent Operate on the Web App

Section titled “Tool Calling: Letting the Agent Operate on the Web App”

Pedelec’s tool calling flow is:

  1. The Web App provides skills: { guidance, tools } when creating a session.
  2. The Desktop Runtime validates the manifest, stores its ToolRegistry in memory, and injects the guidance and tool index into the first provider prompt.
  3. When the agent needs frontend data or an action, it uses pedelec-cli --thread-id <pedelec_thread_id> tool-spec <tool> for the full schema, then executes pedelec-cli --thread-id <pedelec_thread_id> tool-call ... locally.
  4. After the Desktop Runtime receives the tool call, it sends it back to the SDK through the native host and extension.
  5. The SDK triggers session.onTool().
  6. The Web App executes the corresponding tool and returns the result.
  7. The SDK automatically submits the result back to the Desktop Runtime, then hands it to the agent so reasoning can continue.

Each defineTool uses argsSchema to describe arguments for the provider and agent. The root schema must be an object. argsSchema is the Pedelec Tool Args Schema subset, not full JSON Schema: it supports common string, number, integer, boolean, array, object, and oneOf nodes with metadata such as description, default, examples, enum, numeric ranges, array bounds, and required. default guides the agent only; the SDK does not fill missing values. Shorthand input schemas are not supported. $defs, $ref, additionalProperties, exclusiveMinimum, exclusiveMaximum, multipleOf, and format are not supported; use TypeScript constants for reusable schema fragments.

Example:

session.onTool(async (tool, args) => {
if (tool === "get_current_page") {
return {
url: location.href,
title: document.title,
selectedText: window.getSelection()?.toString() ?? "",
};
}
if (tool === "update_counter") {
const { delta } = args as { delta: number };
counter.value += delta;
return {
counter: counter.value,
delta,
};
}
return {
error: {
code: "TOOL_NOT_FOUND",
message: `Unknown tool: ${tool}`,
},
};
});

The return value from onTool() must be JSON serializable. The SDK automatically sends the return value back to the runtime; you do not need to manually call submit_tool_result.


If you have saved a sessionId, you can reconnect to an existing session:

const session = await pedelec.resumeSession("thread_abc123");
session.onChat((text) => {
console.log(text);
});
await session.sendText("Continue the previous work");

pedelec.resumeSession(sessionId) only reattaches to an existing Core thread. It does not revive an ended thread. If the original, non-detached handle was explicitly ended, the same JavaScript object can reactivate it while Core still knows the thread and its recorded workspace still exists:

await session.end();
await session.resume();
console.log(session.getStatus()); // "idle"
await session.sendText("Continue the previous work");

This preserves the same thread ID, handlers, usage, and session metadata. Reactivation does not contact the provider runtime; provider work resumes lazily on the next operation. A missing recorded workspace returns WORKSPACE_OPEN_FAILED, and a thread lost during a Desktop/Core restart returns THREAD_NOT_FOUND; workspace contents alone cannot reconstruct it. A transport-detached handle must use pedelec.resumeSession(sessionId), which returns a new handle and leaves an ended thread ended.


await session.end();

After a session ends, sendText() cannot be called until the same attached handle successfully calls session.resume(), if the thread remains recoverable. Create a new session with createSession() for a separate conversation or when reactivation is no longer possible.

Ending a session does not immediately delete its workspace. Sessions without workspace.path use temporary Desktop-managed storage: managed workspaces are removed on normal Desktop App exit, and leftover directories are cleaned up on the next app launch. An explicit absolute workspace.path is application-managed and is never deleted by Pedelec on session end, app exit, or stale cleanup; multiple active sessions may share it. Closing the main window only hides the app and does not trigger managed cleanup. Ended sessions cannot access retained workspace assets through the asset APIs, and explicit shared workspaces do not receive filesystem conflict coordination.

If autoEndOnDisconnect is enabled, disconnect cleanup has the same goal as calling session.end(): the thread is ended and no longer treated as active.


It is recommended to wrap all SDK operations in try/catch and also register onError():

session.onError((error) => {
console.error("session error", error);
});
try {
await session.sendText("Please help me edit this content");
} catch (error) {
console.error("send failed", error);
}

Common errors:

code Possible cause
EXTENSION_UNAVAILABLE The SDK is not running in a browser page, or the extension cannot connect
EXTENSION_DISCONNECTED The extension connection was interrupted
SDK_BRIDGE_TIMEOUT The extension did not respond before the timeout
APPROVAL_REJECTED The user rejected Pedelec access for the current origin
APPROVAL_TIMEOUT The user did not complete origin approval in time
OPEN_POPUP_FAILED The extension could not automatically open the approval popup
NATIVE_HOST_UNAVAILABLE The Chrome Native Messaging host could not connect
DEFAULT_PROVIDER_NOT_SET The Desktop App has not configured a default provider
DEFAULT_PROVIDER_UNAVAILABLE The default provider is unavailable
SESSION_BUSY The same session already has a prompt running
SESSION_ENDED The session has ended
TOOL_HANDLER_NOT_FOUND The agent called a tool, but the Web App did not register a handler