@zetaloop/chappie 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -25,7 +25,7 @@ Start otunnel with this configuration and add its tunnel as a developer-mode app
25
25
  pi --provider chappie --model chatgpt
26
26
  ```
27
27
 
28
- Send a task in Pi, then ask ChatGPT to call `init`. Chappie pairs the chat with a ready Pi session.
28
+ Open Pi with the Chappie provider, then ask ChatGPT to call `init`. Chappie pairs the chat with an online Pi session; the first remote operation starts its turn.
29
29
 
30
30
  ## Usage
31
31
 
package/docs/tools.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  | Tool | Purpose |
4
4
  |---|---|
5
- | `init` | Connect to a Pi session and read its environment, tools, skills, global `AGENTS.md`, and pending input. |
5
+ | `init` | Connect to a Pi session and read its environment, tool catalog, skills, global `AGENTS.md`, and pending input. |
6
6
  | `sessions` | List connected Pi sessions and the chat's default session. |
7
- | `tools` | Read the selected session's active tool definitions. |
7
+ | `tools` | Read complete definitions for selected active tools. |
8
8
  | `chat` | Send an assistant message to Pi. |
9
9
  | `call` | Run one or more tools as a Pi batch. |
10
10
  | `read` | Read local text or images. |
@@ -13,9 +13,17 @@
13
13
  | `write` | Write text to a file. |
14
14
  | `transfer` | Copy files between ChatGPT and Pi, or export a Pi image as a file. |
15
15
 
16
+ ## Model environment
17
+
18
+ The active model is the current ChatGPT conversation. A Pi tool that starts another `chappie/chatgpt` agent cannot create a new browser conversation, so that child waits without a model response. Subagents configured with another provider use that provider normally.
19
+
20
+ Use ChatGPT's web search, connectors, and cloud tools for remote research and cloud-side work. Chappie tools operate on local files, processes, Pi extensions, and Pi user interfaces. Pi project-memory tools access their local stores; Pi context-reduction tools do not alter the current ChatGPT conversation.
21
+
22
+ Use `chat` for progress or results that should appear in Pi. When a Pi user decision is needed, load the installed interactive tool definition with `tools` and invoke it through `call`.
23
+
16
24
  ## Sessions
17
25
 
18
- Call `init` with `{}` to reuse the chat's session or pair with the next ready, unbound Pi session. Either side can arrive first. Sending a task in Pi starts its Chappie provider request.
26
+ Call `init` with `{}` to reuse the chat's session or pair with the first online, unbound Pi session. A newly opened Pi session can be selected before its first user message; the first remote operation starts its Chappie provider turn.
19
27
 
20
28
  `sessions` lists session IDs, working directories, names, and status. `ready` means the provider is accepting output, `executing` means Pi is handling an operation, and `idle` means the next operation will start a turn.
21
29
 
@@ -31,15 +39,21 @@ The optional `sessionId` on other tools selects a session for that operation. Fo
31
39
  { "path": "package.json", "sessionId": "<session-id>" }
32
40
  ```
33
41
 
34
- `sessions({ sessionId })` shows the selected session and retrieves available input and deferred results. It can be called while Pi is executing a tool batch.
42
+ `sessions({ sessionId })` filters the online list and retrieves available input when that session is connected. The call returns immediately when the selected or bound session is offline; the saved binding is still shown, and deferred results remain available.
35
43
 
36
44
  Several chats can select the same Pi session, and one chat can address several sessions. Defaults are saved in `chappie.state.json` under Pi's agent directory. An existing binding waits for its Pi session to reconnect; `init` with another ID selects a different target.
37
45
 
38
46
  ## Tool calls
39
47
 
40
- `read`, `bash`, `edit`, and `write` accept Pi's tool parameters plus `sessionId`. Their descriptions provide the current schemas. For installed extension tools, call `tools` and use the returned name and parameters in `call`.
48
+ `read`, `bash`, `edit`, and `write` accept Pi's tool parameters plus `sessionId`. Their descriptions provide the current schemas. `init` lists every active tool by name with a short description. Load complete definitions for installed extension tools before calling them:
49
+
50
+ ```json
51
+ { "names": ["ask_user", "ctx_search"] }
52
+ ```
53
+
54
+ Omit `names` to return every active definition. Definitions already present in the current ChatGPT context can be reused without another query.
41
55
 
42
- A single tool uses a one-item `calls` array. To request a batch:
56
+ A single extension tool uses a one-item `calls` array. To request a batch:
43
57
 
44
58
  ```json
45
59
  {
@@ -64,7 +78,7 @@ Call `chat` to display a reply in Pi:
64
78
  { "text": "Updated the parser and its callers." }
65
79
  ```
66
80
 
67
- Each call completes one assistant message. Later operations start another turn when Pi is idle. Use `chat` for text that should appear in Pi.
81
+ Each call completes one assistant message. Later operations start another turn when Pi is idle. The result returns the target session and any new Pi input without repeating the message text.
68
82
 
69
83
  User messages consumed by Pi accompany later Chappie replies, including images. Steering is delivered when Pi consumes it; follow-up uses Pi's normal follow-up timing.
70
84
 
package/package.json CHANGED
@@ -1,54 +1,55 @@
1
1
  {
2
- "name": "@zetaloop/chappie",
3
- "version": "0.1.0",
4
- "description": "Connect ChatGPT to Pi",
5
- "devDependencies": {
6
- "@earendil-works/pi-ai": "^0.85.1",
7
- "@earendil-works/pi-coding-agent": "^0.85.1",
8
- "@types/node": "^26.5.1",
9
- "typebox": "^1.3.30",
10
- "typescript": "^7.0.2"
11
- },
12
- "keywords": [
13
- "pi-package",
14
- "chatgpt",
15
- "mcp"
16
- ],
17
- "author": "zetaloop",
18
- "license": "MIT",
19
- "type": "module",
20
- "engines": {
21
- "node": ">=26",
22
- "pnpm": ">=12"
23
- },
24
- "pi": {
25
- "extensions": [
26
- "./src/index.ts"
27
- ]
28
- },
29
- "peerDependencies": {
30
- "@earendil-works/pi-ai": "*",
31
- "@earendil-works/pi-coding-agent": "*",
32
- "typebox": "*"
33
- },
34
- "dependencies": {
35
- "@modelcontextprotocol/server": "^2.0.0",
36
- "mime": "^4.1.0",
37
- "zod": "^4.6.4"
38
- },
39
- "files": [
40
- "src",
41
- "docs"
42
- ],
43
- "publishConfig": {
44
- "access": "public"
45
- },
46
- "repository": {
47
- "type": "git",
48
- "url": "git+https://github.com/zetaloop/chappie.git"
49
- },
50
- "scripts": {
51
- "format": "biome check --write .",
52
- "check": "biome check . && tsc"
53
- }
54
- }
2
+ "name": "@zetaloop/chappie",
3
+ "version": "0.2.0",
4
+ "description": "Connect ChatGPT to Pi",
5
+ "devDependencies": {
6
+ "@earendil-works/pi-ai": "^0.85.1",
7
+ "@earendil-works/pi-coding-agent": "^0.85.1",
8
+ "@types/node": "^26.5.1",
9
+ "typebox": "^1.3.30",
10
+ "typescript": "^7.0.2"
11
+ },
12
+ "scripts": {
13
+ "format": "biome check --write .",
14
+ "check": "biome check . && tsc"
15
+ },
16
+ "keywords": [
17
+ "pi-package",
18
+ "chatgpt",
19
+ "mcp"
20
+ ],
21
+ "author": "zetaloop",
22
+ "license": "MIT",
23
+ "packageManager": "pnpm@12.4.1",
24
+ "type": "module",
25
+ "engines": {
26
+ "node": ">=26",
27
+ "pnpm": ">=12"
28
+ },
29
+ "pi": {
30
+ "extensions": [
31
+ "./src/index.ts"
32
+ ]
33
+ },
34
+ "peerDependencies": {
35
+ "@earendil-works/pi-ai": "*",
36
+ "@earendil-works/pi-coding-agent": "*",
37
+ "typebox": "*"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/server": "^2.0.0",
41
+ "mime": "^4.1.0",
42
+ "zod": "^4.6.4"
43
+ },
44
+ "files": [
45
+ "src",
46
+ "docs"
47
+ ],
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/zetaloop/chappie.git"
54
+ }
55
+ }
package/src/broker.ts CHANGED
@@ -1,11 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- import type {
5
- AssistantMessage,
6
- ToolCall,
7
- ToolResultMessage,
8
- } from "@earendil-works/pi-ai";
4
+ import type { ToolCall, ToolResultMessage } from "@earendil-works/pi-ai";
9
5
  import {
10
6
  type DeliveryRecord,
11
7
  type ResolvedDelivery,
@@ -45,9 +41,10 @@ interface ChangeWaiter {
45
41
  onAbort(): void;
46
42
  }
47
43
 
48
- export interface InitializedSession extends SessionInspection {
44
+ export interface InitializedSession extends Omit<SessionInspection, "tools"> {
49
45
  globalAgents?: string;
50
46
  inputs: SessionInput[];
47
+ tools: { name: string; description: string }[];
51
48
  }
52
49
 
53
50
  export interface InspectedSession extends SessionInspection {
@@ -55,7 +52,7 @@ export interface InspectedSession extends SessionInspection {
55
52
  }
56
53
 
57
54
  export interface ChatResult {
58
- message: AssistantMessage;
55
+ sessionId: string;
59
56
  inputs: SessionInput[];
60
57
  }
61
58
 
@@ -70,7 +67,6 @@ export class Broker {
70
67
  readonly #ipc: IpcServer;
71
68
  readonly #state: State;
72
69
  readonly #sessions = new Map<string, RegisteredSession>();
73
- readonly #ready = new Set<string>();
74
70
  readonly #pending = new Map<number, PendingRequest>();
75
71
  readonly #waiters = new Set<ChangeWaiter>();
76
72
  #nextRequestId = 1;
@@ -102,7 +98,6 @@ export class Broker {
102
98
  }
103
99
  this.#waiters.clear();
104
100
  this.#sessions.clear();
105
- this.#ready.clear();
106
101
  await this.#ipc.close();
107
102
  }
108
103
 
@@ -127,7 +122,15 @@ export class Broker {
127
122
  const { inspection, inputs } = await this.#inspect(target, signal);
128
123
  const globalAgents = await this.#readGlobalAgents();
129
124
  await this.#ackInputs(target, inputs);
130
- return { ...inspection, inputs, ...(globalAgents ? { globalAgents } : {}) };
125
+ return {
126
+ ...inspection,
127
+ tools: inspection.tools.map(({ name, description }) => ({
128
+ name,
129
+ description: description.split("\n", 1)[0] ?? description,
130
+ })),
131
+ inputs,
132
+ ...(globalAgents ? { globalAgents } : {}),
133
+ };
131
134
  }
132
135
 
133
136
  async chat(
@@ -144,7 +147,7 @@ export class Broker {
144
147
  );
145
148
  if ("message" in result) {
146
149
  await this.#ackInputs(target, result.inputs);
147
- return { message: result.message, inputs: result.inputs };
150
+ return { sessionId: target, inputs: result.inputs };
148
151
  }
149
152
  throw new Error("Pi session returned no assistant message");
150
153
  }
@@ -152,12 +155,20 @@ export class Broker {
152
155
  async tools(
153
156
  chatId: string,
154
157
  sessionId: string | undefined,
158
+ names: string[] | undefined,
155
159
  signal: AbortSignal,
156
160
  ): Promise<InspectedSession> {
157
161
  const target = await this.#selectSession(chatId, sessionId, signal, false);
158
162
  const { inspection, inputs } = await this.#inspect(target, signal);
159
163
  await this.#ackInputs(target, inputs);
160
- return { ...inspection, inputs };
164
+ const selected = names ? new Set(names) : undefined;
165
+ return {
166
+ ...inspection,
167
+ tools: selected
168
+ ? inspection.tools.filter(({ name }) => selected.has(name))
169
+ : inspection.tools,
170
+ inputs,
171
+ };
161
172
  }
162
173
 
163
174
  async call(
@@ -201,8 +212,7 @@ export class Broker {
201
212
  signal: AbortSignal,
202
213
  ): Promise<SessionInput[]> {
203
214
  const target = sessionId ?? this.#state.binding(chatId);
204
- if (!target) return [];
205
- await this.#waitForSession(target, signal);
215
+ if (!target || !this.#sessions.has(target)) return [];
206
216
  const { inputs } = await this.#inspect(target, signal);
207
217
  await this.#ackInputs(target, inputs);
208
218
  return inputs;
@@ -242,17 +252,10 @@ export class Broker {
242
252
  ): Promise<void> {
243
253
  switch (message.type) {
244
254
  case "sync": {
245
- const previous = this.#sessions.get(message.session.id);
246
255
  this.#sessions.set(message.session.id, {
247
256
  description: message.session,
248
257
  peer,
249
258
  });
250
- if (message.session.status === "ready") {
251
- if (previous?.description.status !== "ready")
252
- this.#ready.add(message.session.id);
253
- } else {
254
- this.#ready.delete(message.session.id);
255
- }
256
259
  this.#notifyChange();
257
260
  await peer.send({
258
261
  type: "synced",
@@ -307,9 +310,8 @@ export class Broker {
307
310
 
308
311
  for (;;) {
309
312
  const occupied = this.#state.boundSessions();
310
- const candidate = [...this.#ready].find(
311
- (sessionId) =>
312
- this.#sessions.has(sessionId) && !occupied.has(sessionId),
313
+ const candidate = [...this.#sessions.keys()].find(
314
+ (sessionId) => !occupied.has(sessionId),
313
315
  );
314
316
  if (candidate) {
315
317
  await this.#state.setBinding(chatId, candidate);
@@ -427,7 +429,6 @@ export class Broker {
427
429
 
428
430
  #removeSession(sessionId: string): void {
429
431
  this.#sessions.delete(sessionId);
430
- this.#ready.delete(sessionId);
431
432
  this.#notifyChange();
432
433
  }
433
434
 
@@ -1,7 +1,11 @@
1
- Chappie connects this ChatGPT conversation to local Pi sessions. Call init before starting work. Omitting sessionId reuses the current binding or pairs with the next Pi session waiting for ChatGPT; specifying sessionId on init selects that Pi session as the new default. A sessionId on any other tool affects only that operation. Use sessions to inspect connected sessions without changing the binding.
1
+ Chappie connects this ChatGPT conversation to local Pi sessions. Call init before starting work. Omitting sessionId reuses the current binding or pairs with the first online, unbound Pi session; specifying sessionId on init selects that Pi session as the new default. A sessionId on any other tool affects only that operation. Use sessions to inspect connected sessions without changing the binding.
2
2
 
3
- Use chat to send one complete assistant message to Pi. Use read, bash, edit, and write directly. Use tools to inspect other active Pi tools and call to execute one or more of them as one native Pi batch. Separate calls remain separate Pi turns; use a call array when tools should share a batch.
3
+ The active model is this existing ChatGPT conversation. A Pi tool that starts another chappie/chatgpt agent has no ChatGPT conversation to attach to and will wait indefinitely. Subagents targeting another configured model keep that provider's normal behavior.
4
+
5
+ Prefer ChatGPT's web search, connectors, and cloud tools for remote research and cloud-side work. Use Chappie for local files, processes, Pi extensions, and Pi user interfaces. Pi project-memory tools operate on their local stores; Pi context-reduction tools do not change this ChatGPT conversation.
6
+
7
+ Use chat to send progress and final messages that should appear in Pi. Use read, bash, edit, and write directly. init lists active tools with short descriptions; call tools with their names to load complete definitions before using other Pi tools through call. Use an installed interactive tool through call when input is needed in Pi. Separate calls remain separate Pi turns; use a call array when tools should share a batch.
4
8
 
5
9
  Use transfer with paths and files to copy ChatGPT files into Pi. Omit files to expose existing Pi paths or chappie:// image references as MCP resources. Relative paths use the Pi working directory, existing targets require overwrite: true, and resource materialization may require host confirmation.
6
10
 
7
- Tool replies may contain user input consumed by Pi or results from an earlier explicitly cancelled request. Continue from those results instead of repeating completed work. Use the local persistent-process facilities for operations that need to outlive one tool request.
11
+ Tool replies may contain user input consumed by Pi or results from an earlier explicitly cancelled request. Continue from those results instead of repeating completed work. Host request deadlines include queueing and execution; use local persistent-process facilities for work intended to outlive one request.
package/src/server.ts CHANGED
@@ -36,7 +36,8 @@ export function createServer(broker: Broker): McpServer {
36
36
  "init",
37
37
  {
38
38
  title: "Connect to Pi",
39
- description: "Connect this ChatGPT conversation to a Pi session.",
39
+ description:
40
+ "Connect this ChatGPT conversation to an online Pi session. A sessionId on init becomes the new default.",
40
41
  inputSchema: z.object({
41
42
  sessionId: z
42
43
  .string()
@@ -44,6 +45,7 @@ export function createServer(broker: Broker): McpServer {
44
45
  .describe("Pi session to select explicitly"),
45
46
  }),
46
47
  annotations: {
48
+ destructiveHint: false,
47
49
  openWorldHint: false,
48
50
  },
49
51
  },
@@ -62,7 +64,7 @@ export function createServer(broker: Broker): McpServer {
62
64
  "chat",
63
65
  {
64
66
  title: "Reply in Pi",
65
- description: "Send one complete assistant message to a Pi session.",
67
+ description: "Display one complete assistant message in Pi.",
66
68
  inputSchema: z.object({
67
69
  text: z.string().min(1).describe("Assistant message to display in Pi"),
68
70
  sessionId: z
@@ -71,18 +73,19 @@ export function createServer(broker: Broker): McpServer {
71
73
  .describe("Pi session for this operation only"),
72
74
  }),
73
75
  annotations: {
76
+ destructiveHint: false,
74
77
  openWorldHint: false,
75
78
  },
76
79
  },
77
80
  async (args, context) => {
78
81
  const chatId = requireChatId(context);
79
- const { message, inputs } = await broker.chat(
82
+ const { sessionId, inputs } = await broker.chat(
80
83
  chatId,
81
84
  args.sessionId,
82
85
  args.text,
83
86
  context.mcpReq.signal,
84
87
  );
85
- return finishResult(broker, context, textResult({ message }, inputs));
88
+ return finishResult(broker, context, textResult({ sessionId }, inputs));
86
89
  },
87
90
  );
88
91
 
@@ -90,8 +93,14 @@ export function createServer(broker: Broker): McpServer {
90
93
  "tools",
91
94
  {
92
95
  title: "Pi tools",
93
- description: "List the tools currently active in a Pi session.",
96
+ description:
97
+ "Return complete definitions for active Pi tools. Provide names to inspect only those tools.",
94
98
  inputSchema: z.object({
99
+ names: z
100
+ .array(z.string())
101
+ .min(1)
102
+ .optional()
103
+ .describe("Tool names to describe; omit to return every active tool"),
95
104
  sessionId: z
96
105
  .string()
97
106
  .optional()
@@ -99,6 +108,7 @@ export function createServer(broker: Broker): McpServer {
99
108
  }),
100
109
  annotations: {
101
110
  readOnlyHint: true,
111
+ destructiveHint: false,
102
112
  idempotentHint: true,
103
113
  openWorldHint: false,
104
114
  },
@@ -107,6 +117,7 @@ export function createServer(broker: Broker): McpServer {
107
117
  const { inputs, ...inspected } = await broker.tools(
108
118
  requireChatId(context),
109
119
  args.sessionId,
120
+ args.names,
110
121
  context.mcpReq.signal,
111
122
  );
112
123
  return finishResult(
@@ -124,7 +135,8 @@ export function createServer(broker: Broker): McpServer {
124
135
  "call",
125
136
  {
126
137
  title: "Call Pi tools",
127
- description: "Execute one or more tools as a native Pi tool batch.",
138
+ description:
139
+ "Execute one or more active Pi tools as one native batch. Arguments must match definitions returned by tools.",
128
140
  inputSchema: z.object({
129
141
  calls: z
130
142
  .array(
@@ -140,6 +152,7 @@ export function createServer(broker: Broker): McpServer {
140
152
  .describe("Pi session for this operation only"),
141
153
  }),
142
154
  annotations: {
155
+ destructiveHint: true,
143
156
  openWorldHint: true,
144
157
  },
145
158
  },
@@ -167,8 +180,9 @@ export function createServer(broker: Broker): McpServer {
167
180
  inputSchema: tool.inputSchema,
168
181
  annotations: {
169
182
  readOnlyHint: tool.name === "read",
183
+ destructiveHint: tool.name !== "read",
170
184
  idempotentHint: tool.name === "read",
171
- openWorldHint: tool.name === "bash",
185
+ openWorldHint: tool.name === "bash" || tool.name === "transfer",
172
186
  },
173
187
  ...(tool.fileParams
174
188
  ? { _meta: { "openai/fileParams": tool.fileParams } }
@@ -201,15 +215,16 @@ export function createServer(broker: Broker): McpServer {
201
215
  {
202
216
  title: "Local sessions",
203
217
  description:
204
- "List connected Pi sessions and the current conversation binding.",
218
+ "List online Pi sessions and the current conversation binding without waiting for offline sessions.",
205
219
  inputSchema: z.object({
206
220
  sessionId: z
207
221
  .string()
208
222
  .optional()
209
- .describe("Return only this Pi session when it is online"),
223
+ .describe("Return this Pi session when it is online"),
210
224
  }),
211
225
  annotations: {
212
226
  readOnlyHint: true,
227
+ destructiveHint: false,
213
228
  idempotentHint: true,
214
229
  openWorldHint: false,
215
230
  },
package/src/transfer.ts CHANGED
@@ -18,8 +18,8 @@ interface TransferDetails {
18
18
  }
19
19
 
20
20
  export const transferFile = Type.Object({
21
- file_id: Type.String(),
22
- download_url: Type.String(),
21
+ file_id: Type.String({ description: "Host file identifier" }),
22
+ download_url: Type.String({ description: "Host-provided download URL" }),
23
23
  file_name: Type.Optional(Type.String()),
24
24
  mime_type: Type.Optional(Type.String()),
25
25
  });
@@ -28,10 +28,20 @@ export const transfer = {
28
28
  name: "transfer",
29
29
  label: "transfer",
30
30
  description:
31
- "Transfer files between ChatGPT and the current Pi session. Provide files to write them to paths; omit files to export existing paths or Chappie image references.",
31
+ "Copy ChatGPT files into Pi paths, or export Pi paths and Chappie image references as MCP resources. The paths field always names Pi-side sources or destinations.",
32
32
  parameters: Type.Object({
33
- paths: Type.Array(Type.String(), { minItems: 1 }),
34
- files: Type.Optional(Type.Array(transferFile, { minItems: 1 })),
33
+ paths: Type.Array(Type.String(), {
34
+ minItems: 1,
35
+ description:
36
+ "Pi paths to import into or export from; chappie:// image references can be exported",
37
+ }),
38
+ files: Type.Optional(
39
+ Type.Array(transferFile, {
40
+ minItems: 1,
41
+ description:
42
+ "ChatGPT files matched to paths by index; omit to export Pi paths",
43
+ }),
44
+ ),
35
45
  overwrite: Type.Optional(
36
46
  Type.Boolean({ description: "Overwrite existing target files" }),
37
47
  ),