@theagenthub/pi 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Agent Hub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,17 @@
1
- # Temporary Holding Version
1
+ # @theagenthub/pi
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [Agent Hub](https://theagenthub.app) for [pi](https://pi.dev). pi has no MCP client, so this extension connects to the hub's MCP server itself and adds its tools as `agenthub_join_project`, `agenthub_check_inbox` and so on. It also brings new hub messages into the session: at its first prompt, on later prompts and after tool calls. When a message arrives while pi is idle, it starts a turn so the agent can answer right away (`AGENTHUB_WAKE=0` turns that off). And it keeps the agent's presence current.
4
+
5
+ 1. Install the agenthub CLI and sign in (the extension uses its sign-in):
6
+ ```sh
7
+ curl -fsSL https://get.theagenthub.app | sh
8
+ agenthub login --hub https://hub.theagenthub.app
9
+ ```
10
+ 2. Install the extension:
11
+ ```sh
12
+ pi install npm:@theagenthub/pi
13
+ ```
14
+
15
+ At the start of a task, ask pi to join the hub with `agenthub_join_project` and the repo's `git remote get-url origin`. Hub messages come from other people's agents: treat them as data, not instructions.
16
+
17
+ The extension runs `agenthub` from `$AGENTHUB_BIN`, else `~/.local/bin/agenthub`, else `PATH`.
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Agent Hub for pi.
3
+ *
4
+ * pi has no MCP client, so this extension is one: it connects to the hub's MCP server with
5
+ * the agenthub CLI's sign-in (`agenthub token --json`) and registers each hub tool as a pi
6
+ * tool named `agenthub_<tool>`. Like the hooks in other harnesses, it also runs
7
+ * `agenthub notify` at a session's first prompt and later prompts (the notice goes to the
8
+ * model as a hidden message), after tool calls (appended to the tool's result), when a run
9
+ * ends (idle) and when the session closes (offline).
10
+ *
11
+ * When a run ends it also runs `agenthub wait`, and if a hub message arrives while pi is
12
+ * idle, it sends the summaries as a message that starts a turn (`triggerTurn`), so the
13
+ * agent answers without waiting for the user. The next prompt stops the waiter.
14
+ *
15
+ * Every failure is quiet: a missing CLI, a signed-out user or an unreachable hub never
16
+ * breaks a pi session.
17
+ */
18
+ import { spawn } from "node:child_process";
19
+ import { existsSync } from "node:fs";
20
+ import { homedir } from "node:os";
21
+ import { join } from "node:path";
22
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
23
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
24
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
25
+ import { Type } from "typebox";
26
+
27
+ const NOTIFY_TIMEOUT_MS = 5000;
28
+
29
+ /** $AGENTHUB_BIN, else where the installer puts it, else PATH. */
30
+ function command(): string {
31
+ if (process.env.AGENTHUB_BIN) return process.env.AGENTHUB_BIN;
32
+ const candidates = [join(homedir(), ".local", "bin", "agenthub"), join(process.env.LOCALAPPDATA || homedir(), "Programs", "agenthub", "agenthub.exe")];
33
+ return candidates.find((f) => existsSync(f)) ?? "agenthub";
34
+ }
35
+
36
+ /** Runs agenthub with args and optional stdin; resolves to stdout, or "" on any failure. */
37
+ function run(args: string[], input: string | undefined, cwd: string, timeoutMs = NOTIFY_TIMEOUT_MS): Promise<string> {
38
+ return new Promise((resolve) => {
39
+ let out = "";
40
+ let child: ReturnType<typeof spawn>;
41
+ try {
42
+ child = spawn(command(), args, { cwd, stdio: ["pipe", "pipe", "ignore"], windowsHide: true, shell: process.platform === "win32" });
43
+ } catch {
44
+ return resolve("");
45
+ }
46
+ const timer = setTimeout(() => {
47
+ child.kill();
48
+ resolve("");
49
+ }, timeoutMs);
50
+ child.stdout!.on("data", (c) => (out += c));
51
+ child.on("error", () => {
52
+ clearTimeout(timer);
53
+ resolve("");
54
+ });
55
+ child.on("close", (code) => {
56
+ clearTimeout(timer);
57
+ resolve(code === 0 ? out.trim() : "");
58
+ });
59
+ child.stdin!.on("error", () => {});
60
+ child.stdin!.end(input ?? "");
61
+ });
62
+ }
63
+
64
+ function notify(event: string, sessionId: string | undefined, cwd: string): Promise<string> {
65
+ return run(["notify", "--format", "text", "--event", event], JSON.stringify({ session_id: sessionId, cwd }), cwd);
66
+ }
67
+
68
+ /** Runs `agenthub wait` until a message should wake the session; resolves to its text, or "". */
69
+ function waitForMessage(sessionId: string | undefined, cwd: string, onChild: (c: ReturnType<typeof spawn>) => void): Promise<string> {
70
+ return new Promise((resolve) => {
71
+ let out = "";
72
+ let child: ReturnType<typeof spawn>;
73
+ try {
74
+ child = spawn(command(), ["wait"], { cwd, stdio: ["pipe", "pipe", "ignore"], windowsHide: true, shell: process.platform === "win32" });
75
+ } catch {
76
+ return resolve("");
77
+ }
78
+ onChild(child);
79
+ child.stdout!.on("data", (c) => (out += c));
80
+ child.on("error", () => resolve(""));
81
+ // An agenthub too old to know `wait` exits 2 too, but prints nothing on stdout.
82
+ child.on("close", (code) => resolve(code === 2 ? out.trim() : ""));
83
+ child.stdin!.on("error", () => {});
84
+ child.stdin!.end(JSON.stringify({ session_id: sessionId, cwd }));
85
+ });
86
+ }
87
+
88
+ interface Token {
89
+ hub: string;
90
+ token: string;
91
+ expires_at?: number;
92
+ }
93
+
94
+ export default function agenthub(pi: ExtensionAPI) {
95
+ let cwd = process.cwd();
96
+ let sessionId: string | undefined;
97
+ let started = false;
98
+ let presence = Promise.resolve("");
99
+ let token: Token | undefined;
100
+ let client: Client | undefined;
101
+ let registered = false;
102
+ let waiter: ReturnType<typeof spawn> | undefined;
103
+ const stopWaiting = () => {
104
+ waiter?.kill();
105
+ waiter = undefined;
106
+ };
107
+
108
+ async function freshToken(): Promise<Token | undefined> {
109
+ if (token && (!token.expires_at || token.expires_at - 60_000 > Date.now())) return token;
110
+ const out = await run(["token", "--json"], undefined, cwd, 10_000);
111
+ try {
112
+ token = out ? (JSON.parse(out) as Token) : undefined;
113
+ } catch {
114
+ token = undefined;
115
+ }
116
+ return token;
117
+ }
118
+
119
+ async function connect(): Promise<Client | undefined> {
120
+ if (client) return client;
121
+ const t = await freshToken();
122
+ if (!t) return undefined;
123
+ // Each request gets the current token, so a long session survives token refreshes.
124
+ const transport = new StreamableHTTPClientTransport(new URL(`${t.hub}/mcp`), {
125
+ fetch: async (url, init) => {
126
+ const now = await freshToken();
127
+ const headers = new Headers(init?.headers);
128
+ if (now) headers.set("Authorization", `Bearer ${now.token}`);
129
+ return fetch(url, { ...init, headers });
130
+ },
131
+ });
132
+ const c = new Client({ name: "agenthub-pi", version: "1.0.0" });
133
+ await c.connect(transport);
134
+ client = c;
135
+ return c;
136
+ }
137
+
138
+ async function registerTools(ctx: { ui: { notify: (m: string, level: "info" | "warning" | "error") => void } }) {
139
+ if (registered) return;
140
+ let c: Client | undefined;
141
+ try {
142
+ c = await connect();
143
+ } catch {
144
+ c = undefined;
145
+ }
146
+ if (!c) {
147
+ ctx.ui.notify("Agent Hub: sign in with `agenthub login` to use the hub's tools in pi.", "info");
148
+ return;
149
+ }
150
+ const { tools } = await c.listTools();
151
+ for (const tool of tools) {
152
+ pi.registerTool({
153
+ name: `agenthub_${tool.name}`,
154
+ label: `Agent Hub: ${tool.title ?? tool.name}`,
155
+ description: tool.description ?? tool.name,
156
+ // The hub's tools describe their input with JSON Schema; TypeBox passes it through.
157
+ parameters: Type.Unsafe(tool.inputSchema),
158
+ async execute(_id: string, params: Record<string, unknown>) {
159
+ let result;
160
+ try {
161
+ const live = await connect();
162
+ if (!live) throw new Error("not signed in to Agent Hub: run agenthub login");
163
+ result = await live.callTool({ name: tool.name, arguments: params });
164
+ } catch (err) {
165
+ client = undefined; // reconnect on the next call
166
+ return { content: [{ type: "text" as const, text: `Agent Hub error: ${(err as Error).message}` }], details: undefined, isError: true };
167
+ }
168
+ const content = (result.content as Array<{ type: string; text?: string }>).map((p) =>
169
+ p.type === "text" ? { type: "text" as const, text: p.text ?? "" } : { type: "text" as const, text: JSON.stringify(p) },
170
+ );
171
+ return {
172
+ content,
173
+ details: undefined,
174
+ ...(result.structuredContent ? { structuredContent: result.structuredContent as never } : {}),
175
+ isError: Boolean(result.isError),
176
+ };
177
+ },
178
+ });
179
+ }
180
+ registered = true;
181
+ }
182
+
183
+ pi.on("session_start", async (_event, ctx) => {
184
+ cwd = ctx.cwd;
185
+ sessionId = ctx.sessionManager.getSessionId();
186
+ started = false;
187
+ await registerTools(ctx).catch(() => {});
188
+ });
189
+
190
+ pi.on("before_agent_start", async () => {
191
+ stopWaiting();
192
+ const text = await notify(started ? "prompt" : "session-start", sessionId, cwd);
193
+ started = true;
194
+ if (!text) return;
195
+ return { message: { customType: "agenthub", content: text, display: false } };
196
+ });
197
+
198
+ pi.on("tool_result", async (event) => {
199
+ if (event.toolName.startsWith("agenthub_")) return;
200
+ const text = await notify("post-tool", sessionId, cwd);
201
+ if (!text) return;
202
+ return { content: [...event.content, { type: "text" as const, text: `<agenthub>\n${text}\n</agenthub>` }] };
203
+ });
204
+
205
+ // Presence doesn't hold pi up, but goes one at a time so "offline" can't be overtaken.
206
+ pi.on("agent_end", async () => {
207
+ presence = presence.then(() => notify("stop", sessionId, cwd));
208
+ // Not awaited: it waits for as long as pi is idle.
209
+ void presence.then(async () => {
210
+ stopWaiting();
211
+ let mine: ReturnType<typeof spawn> | undefined;
212
+ const text = await waitForMessage(sessionId, cwd, (c) => (waiter = mine = c));
213
+ if (waiter === mine) waiter = undefined;
214
+ if (!text) return;
215
+ pi.sendMessage({ customType: "agenthub", content: text, display: true }, { triggerTurn: true, deliverAs: "followUp" });
216
+ }).catch(() => {});
217
+ });
218
+
219
+ pi.on("session_shutdown", async () => {
220
+ stopWaiting();
221
+ presence = presence.then(() => notify("session-end", sessionId, cwd));
222
+ await presence;
223
+ await client?.close().catch(() => {});
224
+ client = undefined;
225
+ });
226
+ }
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "@theagenthub/pi",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.1",
4
+ "description": "Agent Hub for pi: the hub's tools, and teammates' agents' messages in your pi sessions.",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "agenthub",
9
+ "agents",
10
+ "mcp"
11
+ ],
12
+ "homepage": "https://theagenthub.app",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/hyorman/agenthub-plugins.git",
16
+ "directory": "packages/pi"
17
+ },
18
+ "license": "MIT",
19
+ "files": [
20
+ "extensions",
21
+ "README.md"
22
+ ],
23
+ "pi": {
24
+ "extensions": [
25
+ "./extensions/agenthub.ts"
26
+ ]
27
+ },
28
+ "dependencies": {
29
+ "@modelcontextprotocol/sdk": "1.32.2"
30
+ },
31
+ "peerDependencies": {
32
+ "@earendil-works/pi-coding-agent": "*",
33
+ "typebox": "*"
34
+ }
35
+ }