@adeildo/pi-kit

中文文档
6.0.0发布于 2026/11/02MITES Module零依赖构建来源已验证近 30 天下载 3847

The app builder, settings and events that pi-harness packages are built on.

原始使用文档
文档目录8 节
  1. An app, and features in it
  2. What a feature gets
  3. What else it shares
  4. Settings
  5. The settings screen
  6. Turning a feature off
  7. Events and contracts
  8. Testing

@adeildo/pi-kit

npm

The library under every piece of pi-harness.
An app builder, a feature scope, settings that several extensions share, and the settings screen on Alt+S. The screen is the one thing it adds to Pi.

npm install @adeildo/pi-kit

It is a library and not a Pi package. Its API follows what the packages need, with no promise to anyone else.

An app, and features in it

import { createApp, defineFeature, matching, setting } from "@adeildo/pi-kit";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export const version = setting({
    id: "subscription.claudeCodeVersion",
    default: "2.1.280",
    decoder: matching(/^\d+\.\d+\.\d+$/, "a version like 2.1.280"),
});

export const subscription = defineFeature({
    id: "subscription",
    description: "Bill Anthropic OAuth requests to the Claude plan",
    settings: [version],
    setup(scope) {
        scope.on("before_provider_headers", (event) => {
            event.headers["user-agent"] = `claude-cli/${version.get(scope)}`;
        });
    },
});

export default function (pi: ExtensionAPI): void {
    createApp(pi, { name: "pi-harness" }).use(subscription).build();
}

Each extension a package declares is one app with one or more features. The app's name goes in front of every warning. The harness declares one extension per feature, so pi config turns each off on its own, and all of them read the same settings file.

What a feature gets

setup receives the feature's scope. It extends ExtensionAPI, so every pi method is there, with these wired to the feature:

ScopeDoes
scope.onpi.on, with errors that read pi-harness: permission: tool_call: boom. It still throws, because a tool_call handler that throws is how pi blocks a tool
scope.registerCommandpi.registerCommand. If another feature in the same app took the name, this one is skipped with a warning
scope.onSessionStartRuns once the session's settings are loaded. Anything that lasts, like a watcher or a child process, starts here and never in setup
scope.onShutdownRuns once per session, newest first, even when pi fires session_shutdown twice
scope.screenRows on the settings screen that are not settings
scope.warnShows a notification when there is a UI, and writes to stderr when there is not. Messages sent before a session starts wait for it
everything elseThe scope is the extension API, so scope.registerTool, scope.appendEntry, scope.events and the rest work as they always did

A setup that throws turns into a warning and the other features still mount. If two apps in one Pi process mount the same feature id, the first runs it and the second logs who has it.

What else it shares

Besides the app and the settings, the kit holds what every package would otherwise write twice.

ModuleWhat it gives
ui/frame.ts, ui/entry.ts, ui/glyphs.ts, ui/scroll.tsThe frame an entry wears, the lines and glyphs it is drawn with, and where a scroll bar's thumb sits
format.tsHow a token count, a price and a time of day read, the same in a footer, a panel and a table
betas.tsMerging the betas an Anthropic request carries, keeping the payload untouched when nothing had to be added
events/log.tsThe folder log every session appends to and reads
sources/A session to show, read from this pi or from its file, followed as it grows
app/renderers.tsThe tool and message renderers the features register, resolved as pi resolves them, so a view draws another session's entries the same way
contracts/, board/The names this suite writes into a pi session and the events that cross packages, plus the board's own types

Settings

Every app reads ~/.pi/agent/extensions/pi-harness/settings.json. A setting's id is its path in that file. A store reads only the settings its own features declared, so keys belonging to another app never produce a warning.

store.set writes one value and store.setAll several in one pass, which is what a feature does when it saves a whole config object. A value that already reads the same is not written, so the file holds what was decided and not a copy of every default, which would freeze them.

A setting with project: true also reads <project>/.pi/extensions/pi-harness/settings.json, but only once pi trusts the project, and the project value wins there. Anything without that flag is global, and a project file that tries to set it gets a warning, so a cloned repository cannot loosen what runs without asking.

A value that fails to decode is ignored, and the warning names the file and the key.

The settings screen

Alt+S or /harness opens it. Each feature is a tab, and the rows come from two places.

A setting with a ui gets a row:

export const cursor = setting({
    id: "look.frame.cursor",
    default: "bar",
    decoder: literal("block", "bar", "underline"),
    ui: { section: "Editor", label: "Cursor", description: "The editor's cursor." },
});

The decoder picks the editor: boolean is a toggle, literal a choice, integer a number, stringList a list. ui.control labels the options or adds presets.

Anything else goes through scope.screen: value for session state, action for something to run, info for a fact.

scope.screen.action({
    id: "judge.test",
    section: "Judge",
    label: "Test the judge",
    description: "Sends one real request.",
    run: (ctx) => probe(ctx),
});

A value typed on the screen goes through the same decoder as the file. The feature options tab and sections set the tab name and the section order.

Turning a feature off

Every feature also gets features.<id>.enabled, on by default. A feature that is off never runs setup, so it registers nothing and costs nothing. The change lands on the next /reload.

Events and contracts

// contracts/accounts.ts
export const ACCOUNT = defineContract<AccountState, string>("accounts:state", decoder);

ACCOUNT.provide(scope, (provider) => inUse(provider)); // the owner answers
ACCOUNT.current(scope, "anthropic"); // a reader asks while drawing
ACCOUNT.changed(scope, state, "anthropic"); // the owner announces a change
ACCOUNT.onChanged(scope, (state, provider) => …); // a bridge follows it

A contract is what one feature knows and the others read. It travels over pi.events, asked on harness:<name> and announced on harness:<name>:changed with the whole new value, so a bridge can follow every contract without polling. A contract with a key, like the ruling of a call or the account of a provider, takes the key as one more argument. Two features behave the same whether they share an app or come from different packages. The value may come from an older or newer version of the other package, so it is decoded when it arrives, and one that does not decode is dropped with a warning.

These rules hold for every contract:

  • It is declared in contracts/ here, never in the package that owns the state. The reader then does not depend on the owner, and either one works without the other.
  • The value is plain JSON, with no functions, classes or Date, so a remote control or a web UI can forward any contract without knowing what is inside.
  • A value can gain fields but never lose them.

A session to show is the exception: a SessionSource is an object with functions, so it travels on the raw channel harness:sources, where remote answers with its own. sourceFor asks there first and falls back to the session's file when the session is on this machine. Showing one over the screen goes through contracts/views.ts: openView asks on harness:view.open, and canOpenView says whether anyone would.

Testing

@adeildo/pi-kit/testing has fakePi(), fakeScope() and fakeContext(). Two fakes on one createEventBus() behave like two extensions in the same Pi process, and fire runs the handlers in order and returns what each one returned.

adeildo.dev
pi-harness is made by adeildo.dev

版本与发布记录

@adeildo/pi-kit 共发布 28 个版本,首次发布于 2026/09/30;过去 12 个月发布了 28 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
6.0.0latest2026/11/025 天前
5.15.02026/10/2414 天前
5.14.02026/10/2414 天前
5.13.02026/10/2315 天前
5.12.22026/10/2316 天前
5.12.12026/10/2216 天前

关于 @adeildo/pi-kit 的常见问题

@adeildo/pi-kit 是什么?

The app builder, settings and events that pi-harness packages are built on.

如何安装 @adeildo/pi-kit?

在项目目录运行 npm install @adeildo/pi-kit,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:typebox(*)、@earendil-works/pi-ai(*)、@earendil-works/pi-tui(*)、@earendil-works/pi-coding-agent(*)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@adeildo/pi-kit 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/adeildo__pi-kit 是否存在。

@adeildo/pi-kit 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@adeildo/pi-kit 可以商用吗?

@adeildo/pi-kit 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@adeildo/pi-kit 的下载量有多少?

根据 npm 官方下载统计,@adeildo/pi-kit 在 2026/10/05 – 2026/11/03 这 30 天内被下载 3,847 次,日均约 128 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@adeildo/pi-kit 还在维护吗?

最新版本 6.0.0 发布于 2026/11/02(5 天前);至今共发布 28 个版本,其中过去 12 个月发布了 28 个;首次发布于 2026/09/30。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。