@zetaloop/chappie 0.1.0 → 0.2.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/README.md +1 -1
- package/docs/tools.md +32 -12
- package/package.json +54 -53
- package/src/broker.ts +73 -40
- package/src/delivery.ts +2 -0
- package/src/instructions.md +9 -3
- package/src/ipc.ts +2 -1
- package/src/server.ts +78 -26
- package/src/session.ts +32 -38
- package/src/state.ts +6 -2
- package/src/tools.ts +3 -1
- package/src/transfer.ts +15 -5
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
|
-
|
|
28
|
+
Open Pi with the Chappie provider, then ask ChatGPT to call `init`. A chat without a default is paired with the first online, unbound Pi session. To resume work in a new chat or branch, use `init` with the original Pi session ID; `sessions` lists projects when the target ID is unknown.
|
|
29
29
|
|
|
30
30
|
## Usage
|
|
31
31
|
|
package/docs/tools.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
| Tool | Purpose |
|
|
4
4
|
|---|---|
|
|
5
|
-
| `init` | Connect to a Pi session and read its environment,
|
|
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
|
|
8
|
-
| `chat` |
|
|
7
|
+
| `tools` | Read complete definitions for selected active tools. |
|
|
8
|
+
| `chat` | Display Markdown as an assistant message in Pi. |
|
|
9
9
|
| `call` | Run one or more tools as a Pi batch. |
|
|
10
10
|
| `read` | Read local text or images. |
|
|
11
11
|
| `bash` | Execute a shell command. |
|
|
@@ -13,33 +13,51 @@
|
|
|
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
|
-
|
|
26
|
+
For a new task without a specific target, call `init` with `{}` to reuse this chat's default or allocate the first online, unbound Pi session. Sessions register while `chappie/chatgpt` is selected. They can be blank or already contain a task; a remote operation starts a turn when Pi is idle.
|
|
27
|
+
|
|
28
|
+
`sessions` lists session IDs, working directories, names, status, and `bindingCount`, the number of saved chat defaults pointing to each session. Zero means the session can be allocated automatically. The count includes closed chats; execution status describes Pi activity: `ready` accepts provider output, `executing` handles an operation, and `idle` starts a turn on the next operation.
|
|
19
29
|
|
|
20
|
-
`
|
|
30
|
+
`init.selection` reports how the target was chosen: `existing` reuses this chat's default, `explicit` uses the supplied session ID, and `automatic` allocates the first online session with no saved bindings. Explicit selection also accepts sessions already used by other chats.
|
|
21
31
|
|
|
22
|
-
|
|
32
|
+
To continue existing work in a new chat or branch, pass the Pi session ID associated with that task in the inherited context:
|
|
23
33
|
|
|
24
34
|
```json
|
|
25
35
|
{ "sessionId": "<session-id>" }
|
|
26
36
|
```
|
|
27
37
|
|
|
38
|
+
This establishes the new chat's default, even when another chat already uses the same Pi session. For a requested project or session without a known ID, select it from `sessions` by working directory or name. When the target is absent or ambiguous, clarify the intended session before running tools. A session being the only one online does not establish that it is the requested target.
|
|
39
|
+
|
|
28
40
|
The optional `sessionId` on other tools selects a session for that operation. For example, `read` can inspect another project:
|
|
29
41
|
|
|
30
42
|
```json
|
|
31
43
|
{ "path": "package.json", "sessionId": "<session-id>" }
|
|
32
44
|
```
|
|
33
45
|
|
|
34
|
-
`sessions({ sessionId })`
|
|
46
|
+
`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
47
|
|
|
36
48
|
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
49
|
|
|
38
50
|
## Tool calls
|
|
39
51
|
|
|
40
|
-
`read`, `bash`, `edit`, and `write` accept Pi's tool parameters plus `sessionId`. Their descriptions provide the current schemas.
|
|
52
|
+
`read`, `bash`, `edit`, and `write` accept Pi's tool parameters plus `sessionId`. Their descriptions provide the current schemas. The catalog in `init.tools` lists Pi's native and extension tools available through `call`. Chappie's `init`, `sessions`, `tools`, and `chat` are separate top-level MCP tools. Load complete definitions for installed extension tools before calling them:
|
|
41
53
|
|
|
42
|
-
|
|
54
|
+
```json
|
|
55
|
+
{ "names": ["ask_user", "ctx_search"] }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Omit `names` to return every active definition. Definitions already present in the current ChatGPT context can be reused without another query.
|
|
59
|
+
|
|
60
|
+
A single extension tool uses a one-item `calls` array. To request a batch:
|
|
43
61
|
|
|
44
62
|
```json
|
|
45
63
|
{
|
|
@@ -52,7 +70,9 @@ A single tool uses a one-item `calls` array. To request a batch:
|
|
|
52
70
|
|
|
53
71
|
Each batch returns its results together. Pi determines how its tools run within the batch. Separate calls run in order within one Pi session; different sessions can work independently.
|
|
54
72
|
|
|
55
|
-
Extension tools execute through Pi, including their interactive prompts.
|
|
73
|
+
Extension tools execute through Pi, including their interactive prompts. Each batch starts with its executing `sessionId` and Pi `cwd`, followed by each tool's name, call ID, error status, and original text or image content. The directory is captured when the batch starts; a shell `cd` changes that command's working directory, while the operation stays in the same Pi session.
|
|
74
|
+
|
|
75
|
+
Every tool declares a `{ text: string }` output. `structuredContent.text` contains the complete text in result order, including Pi input, deferred results, and image references. The same text remains in `content` alongside native images and resource links.
|
|
56
76
|
|
|
57
77
|
ChatGPT file inputs use the direct `transfer` tool. Its top-level `files` parameter lets the host prepare the files before sending them to Pi.
|
|
58
78
|
|
|
@@ -64,11 +84,11 @@ Call `chat` to display a reply in Pi:
|
|
|
64
84
|
{ "text": "Updated the parser and its callers." }
|
|
65
85
|
```
|
|
66
86
|
|
|
67
|
-
Each call completes one assistant message. Later operations start another turn when Pi is idle.
|
|
87
|
+
Pi renders the supplied Markdown, including fenced code blocks, and appends the message to the session transcript. Each call completes one assistant message. Later operations start another turn when Pi is idle. The result returns the target `sessionId`, Pi `cwd`, and any new Pi input without repeating the message text.
|
|
68
88
|
|
|
69
89
|
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
90
|
|
|
71
|
-
Explicit cancellation removes a queued request or asks Pi to stop its active batch. Available results from that batch accompany a later reply to the originating chat. `sessions` can retrieve them before another tool call. When ChatGPT stops without sending cancellation, local execution continues.
|
|
91
|
+
Explicit cancellation removes a queued request or asks Pi to stop its active batch. Available results from that batch accompany a later reply to the originating chat, with the original session ID and working directory. `sessions` can retrieve them before another tool call. When ChatGPT stops without sending cancellation, local execution continues.
|
|
72
92
|
|
|
73
93
|
Host request deadlines include time spent in the queue. Use local facilities such as tmux for work intended to outlive one call.
|
|
74
94
|
|
package/package.json
CHANGED
|
@@ -1,54 +1,55 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
2
|
+
"name": "@zetaloop/chappie",
|
|
3
|
+
"version": "0.2.1",
|
|
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,11 @@ interface ChangeWaiter {
|
|
|
45
41
|
onAbort(): void;
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
export interface InitializedSession extends SessionInspection {
|
|
44
|
+
export interface InitializedSession extends Omit<SessionInspection, "tools"> {
|
|
45
|
+
selection: "existing" | "explicit" | "automatic";
|
|
49
46
|
globalAgents?: string;
|
|
50
47
|
inputs: SessionInput[];
|
|
48
|
+
tools: { name: string; description: string }[];
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
export interface InspectedSession extends SessionInspection {
|
|
@@ -55,12 +53,14 @@ export interface InspectedSession extends SessionInspection {
|
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
export interface ChatResult {
|
|
58
|
-
|
|
56
|
+
sessionId: string;
|
|
57
|
+
cwd: string;
|
|
59
58
|
inputs: SessionInput[];
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
export interface CallResult {
|
|
63
62
|
sessionId: string;
|
|
63
|
+
cwd: string;
|
|
64
64
|
toolResults: ToolResultMessage[];
|
|
65
65
|
inputs: SessionInput[];
|
|
66
66
|
}
|
|
@@ -70,7 +70,6 @@ export class Broker {
|
|
|
70
70
|
readonly #ipc: IpcServer;
|
|
71
71
|
readonly #state: State;
|
|
72
72
|
readonly #sessions = new Map<string, RegisteredSession>();
|
|
73
|
-
readonly #ready = new Set<string>();
|
|
74
73
|
readonly #pending = new Map<number, PendingRequest>();
|
|
75
74
|
readonly #waiters = new Set<ChangeWaiter>();
|
|
76
75
|
#nextRequestId = 1;
|
|
@@ -102,16 +101,19 @@ export class Broker {
|
|
|
102
101
|
}
|
|
103
102
|
this.#waiters.clear();
|
|
104
103
|
this.#sessions.clear();
|
|
105
|
-
this.#ready.clear();
|
|
106
104
|
await this.#ipc.close();
|
|
107
105
|
}
|
|
108
106
|
|
|
109
|
-
listSessions(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
listSessions(
|
|
108
|
+
sessionId?: string,
|
|
109
|
+
): (SessionDescription & { bindingCount: number })[] {
|
|
110
|
+
const counts = this.#state.bindingCounts();
|
|
111
|
+
return [...this.#sessions.values()]
|
|
112
|
+
.filter(({ description }) => !sessionId || description.id === sessionId)
|
|
113
|
+
.map(({ description }) => ({
|
|
114
|
+
...description,
|
|
115
|
+
bindingCount: counts.get(description.id) ?? 0,
|
|
116
|
+
}));
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
binding(chatId: string): string | undefined {
|
|
@@ -123,11 +125,25 @@ export class Broker {
|
|
|
123
125
|
sessionId: string | undefined,
|
|
124
126
|
signal: AbortSignal,
|
|
125
127
|
): Promise<InitializedSession> {
|
|
126
|
-
const target = await this.#selectSession(
|
|
128
|
+
const { sessionId: target, selection } = await this.#selectSession(
|
|
129
|
+
chatId,
|
|
130
|
+
sessionId,
|
|
131
|
+
signal,
|
|
132
|
+
true,
|
|
133
|
+
);
|
|
127
134
|
const { inspection, inputs } = await this.#inspect(target, signal);
|
|
128
135
|
const globalAgents = await this.#readGlobalAgents();
|
|
129
136
|
await this.#ackInputs(target, inputs);
|
|
130
|
-
return {
|
|
137
|
+
return {
|
|
138
|
+
selection,
|
|
139
|
+
...inspection,
|
|
140
|
+
tools: inspection.tools.map(({ name, description }) => ({
|
|
141
|
+
name,
|
|
142
|
+
description: description.split("\n", 1)[0] ?? description,
|
|
143
|
+
})),
|
|
144
|
+
inputs,
|
|
145
|
+
...(globalAgents ? { globalAgents } : {}),
|
|
146
|
+
};
|
|
131
147
|
}
|
|
132
148
|
|
|
133
149
|
async chat(
|
|
@@ -136,7 +152,12 @@ export class Broker {
|
|
|
136
152
|
text: string,
|
|
137
153
|
signal: AbortSignal,
|
|
138
154
|
): Promise<ChatResult> {
|
|
139
|
-
const target = await this.#selectSession(
|
|
155
|
+
const { sessionId: target } = await this.#selectSession(
|
|
156
|
+
chatId,
|
|
157
|
+
sessionId,
|
|
158
|
+
signal,
|
|
159
|
+
false,
|
|
160
|
+
);
|
|
140
161
|
const result = await this.#request(
|
|
141
162
|
target,
|
|
142
163
|
(id) => ({ type: "chat", id, chatId, sessionId: target, text }),
|
|
@@ -144,7 +165,7 @@ export class Broker {
|
|
|
144
165
|
);
|
|
145
166
|
if ("message" in result) {
|
|
146
167
|
await this.#ackInputs(target, result.inputs);
|
|
147
|
-
return {
|
|
168
|
+
return { sessionId: target, cwd: result.cwd, inputs: result.inputs };
|
|
148
169
|
}
|
|
149
170
|
throw new Error("Pi session returned no assistant message");
|
|
150
171
|
}
|
|
@@ -152,12 +173,25 @@ export class Broker {
|
|
|
152
173
|
async tools(
|
|
153
174
|
chatId: string,
|
|
154
175
|
sessionId: string | undefined,
|
|
176
|
+
names: string[] | undefined,
|
|
155
177
|
signal: AbortSignal,
|
|
156
178
|
): Promise<InspectedSession> {
|
|
157
|
-
const target = await this.#selectSession(
|
|
179
|
+
const { sessionId: target } = await this.#selectSession(
|
|
180
|
+
chatId,
|
|
181
|
+
sessionId,
|
|
182
|
+
signal,
|
|
183
|
+
false,
|
|
184
|
+
);
|
|
158
185
|
const { inspection, inputs } = await this.#inspect(target, signal);
|
|
159
186
|
await this.#ackInputs(target, inputs);
|
|
160
|
-
|
|
187
|
+
const selected = names ? new Set(names) : undefined;
|
|
188
|
+
return {
|
|
189
|
+
...inspection,
|
|
190
|
+
tools: selected
|
|
191
|
+
? inspection.tools.filter(({ name }) => selected.has(name))
|
|
192
|
+
: inspection.tools,
|
|
193
|
+
inputs,
|
|
194
|
+
};
|
|
161
195
|
}
|
|
162
196
|
|
|
163
197
|
async call(
|
|
@@ -166,7 +200,12 @@ export class Broker {
|
|
|
166
200
|
calls: ToolInput[],
|
|
167
201
|
signal: AbortSignal,
|
|
168
202
|
): Promise<CallResult> {
|
|
169
|
-
const target = await this.#selectSession(
|
|
203
|
+
const { sessionId: target } = await this.#selectSession(
|
|
204
|
+
chatId,
|
|
205
|
+
sessionId,
|
|
206
|
+
signal,
|
|
207
|
+
false,
|
|
208
|
+
);
|
|
170
209
|
const toolCalls: ToolCall[] = calls.map((call) => ({
|
|
171
210
|
type: "toolCall",
|
|
172
211
|
id: `chappie-${randomUUID()}`,
|
|
@@ -188,6 +227,7 @@ export class Broker {
|
|
|
188
227
|
await this.#ackInputs(target, result.inputs);
|
|
189
228
|
return {
|
|
190
229
|
sessionId: target,
|
|
230
|
+
cwd: result.cwd,
|
|
191
231
|
toolResults: result.toolResults,
|
|
192
232
|
inputs: result.inputs,
|
|
193
233
|
};
|
|
@@ -201,8 +241,7 @@ export class Broker {
|
|
|
201
241
|
signal: AbortSignal,
|
|
202
242
|
): Promise<SessionInput[]> {
|
|
203
243
|
const target = sessionId ?? this.#state.binding(chatId);
|
|
204
|
-
if (!target) return [];
|
|
205
|
-
await this.#waitForSession(target, signal);
|
|
244
|
+
if (!target || !this.#sessions.has(target)) return [];
|
|
206
245
|
const { inputs } = await this.#inspect(target, signal);
|
|
207
246
|
await this.#ackInputs(target, inputs);
|
|
208
247
|
return inputs;
|
|
@@ -242,17 +281,10 @@ export class Broker {
|
|
|
242
281
|
): Promise<void> {
|
|
243
282
|
switch (message.type) {
|
|
244
283
|
case "sync": {
|
|
245
|
-
const previous = this.#sessions.get(message.session.id);
|
|
246
284
|
this.#sessions.set(message.session.id, {
|
|
247
285
|
description: message.session,
|
|
248
286
|
peer,
|
|
249
287
|
});
|
|
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
288
|
this.#notifyChange();
|
|
257
289
|
await peer.send({
|
|
258
290
|
type: "synced",
|
|
@@ -289,32 +321,34 @@ export class Broker {
|
|
|
289
321
|
requestedId: string | undefined,
|
|
290
322
|
signal: AbortSignal,
|
|
291
323
|
bindRequested: boolean,
|
|
292
|
-
): Promise<
|
|
324
|
+
): Promise<{
|
|
325
|
+
sessionId: string;
|
|
326
|
+
selection: InitializedSession["selection"];
|
|
327
|
+
}> {
|
|
293
328
|
if (requestedId) {
|
|
294
329
|
await this.#waitForSession(requestedId, signal);
|
|
295
330
|
if (bindRequested && this.#state.binding(chatId) !== requestedId) {
|
|
296
331
|
await this.#state.setBinding(chatId, requestedId);
|
|
297
332
|
this.#notifyChange();
|
|
298
333
|
}
|
|
299
|
-
return requestedId;
|
|
334
|
+
return { sessionId: requestedId, selection: "explicit" };
|
|
300
335
|
}
|
|
301
336
|
|
|
302
337
|
const boundId = this.#state.binding(chatId);
|
|
303
338
|
if (boundId) {
|
|
304
339
|
await this.#waitForSession(boundId, signal);
|
|
305
|
-
return boundId;
|
|
340
|
+
return { sessionId: boundId, selection: "existing" };
|
|
306
341
|
}
|
|
307
342
|
|
|
308
343
|
for (;;) {
|
|
309
|
-
const occupied = this.#state.
|
|
310
|
-
const candidate = [...this.#
|
|
311
|
-
(sessionId) =>
|
|
312
|
-
this.#sessions.has(sessionId) && !occupied.has(sessionId),
|
|
344
|
+
const occupied = this.#state.bindingCounts();
|
|
345
|
+
const candidate = [...this.#sessions.keys()].find(
|
|
346
|
+
(sessionId) => !occupied.has(sessionId),
|
|
313
347
|
);
|
|
314
348
|
if (candidate) {
|
|
315
349
|
await this.#state.setBinding(chatId, candidate);
|
|
316
350
|
this.#notifyChange();
|
|
317
|
-
return candidate;
|
|
351
|
+
return { sessionId: candidate, selection: "automatic" };
|
|
318
352
|
}
|
|
319
353
|
await this.#waitForChange(signal);
|
|
320
354
|
}
|
|
@@ -427,7 +461,6 @@ export class Broker {
|
|
|
427
461
|
|
|
428
462
|
#removeSession(sessionId: string): void {
|
|
429
463
|
this.#sessions.delete(sessionId);
|
|
430
|
-
this.#ready.delete(sessionId);
|
|
431
464
|
this.#notifyChange();
|
|
432
465
|
}
|
|
433
466
|
|
package/src/delivery.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface DeliveryRecord {
|
|
|
9
9
|
id: string;
|
|
10
10
|
chatId: string;
|
|
11
11
|
sessionId: string;
|
|
12
|
+
cwd: string;
|
|
12
13
|
sessionFile?: string;
|
|
13
14
|
toolCallIds: string[];
|
|
14
15
|
inlineResults?: ToolResultMessage[];
|
|
@@ -57,6 +58,7 @@ export function deliveryContent(deliveries: ResolvedDelivery[]) {
|
|
|
57
58
|
text: JSON.stringify({
|
|
58
59
|
deferredResult: delivery.id,
|
|
59
60
|
sessionId: delivery.sessionId,
|
|
61
|
+
cwd: delivery.cwd,
|
|
60
62
|
error: delivery.error,
|
|
61
63
|
}),
|
|
62
64
|
},
|
package/src/instructions.md
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
Chappie connects this ChatGPT conversation to local Pi sessions.
|
|
1
|
+
Chappie connects this ChatGPT conversation to local Pi sessions. To continue existing work, call init with the Pi sessionId associated with that task in the context. A new chat or branch establishes its own binding to that ID. For a requested project or session without a known ID, use sessions to match its cwd or name, then init with the chosen ID. If the intended target is absent or ambiguous, resolve the selection with the user before starting work. Pi sessions register while the chappie/chatgpt provider is selected.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
For work without a specific target, init without sessionId reuses the current binding or allocates the first online session with bindingCount zero. These sessions may be blank or already contain a task. Saved bindings persist across broker restarts; explicit init(sessionId) can share a session already used by another chat. init reports its selection as existing, explicit, or automatic. A sessionId on any other tool affects only that operation.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
Use chat to display Markdown progress and final messages in Pi, including code examples. Use read, bash, edit, and write directly. init.tools lists Pi's native and extension tools for call; Chappie's init, sessions, tools, and chat are separate top-level MCP tools. Use tools with names to load complete definitions before calling other Pi tools. 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
10
|
|
|
5
11
|
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
12
|
|
|
7
|
-
Tool replies
|
|
13
|
+
Tool replies identify the executing sessionId and Pi cwd. A shell command can access another directory while its session and transcript remain the same. Replies provide their complete text in structuredContent.text, including user input consumed by Pi and results from an earlier explicitly cancelled request. Images and file resources accompany the text as native content blocks. 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/ipc.ts
CHANGED
|
@@ -44,9 +44,10 @@ export interface SessionInput {
|
|
|
44
44
|
|
|
45
45
|
export type SessionResult =
|
|
46
46
|
| { inspection: SessionInspection; inputs: SessionInput[] }
|
|
47
|
-
| { message: AssistantMessage; inputs: SessionInput[] }
|
|
47
|
+
| { message: AssistantMessage; cwd: string; inputs: SessionInput[] }
|
|
48
48
|
| {
|
|
49
49
|
message: AssistantMessage;
|
|
50
|
+
cwd: string;
|
|
50
51
|
toolResults: ToolResultMessage[];
|
|
51
52
|
inputs: SessionInput[];
|
|
52
53
|
}
|
package/src/server.ts
CHANGED
|
@@ -16,6 +16,14 @@ const instructions = readFileSync(
|
|
|
16
16
|
"utf8",
|
|
17
17
|
).trim();
|
|
18
18
|
|
|
19
|
+
const outputSchema = z.object({
|
|
20
|
+
text: z
|
|
21
|
+
.string()
|
|
22
|
+
.describe(
|
|
23
|
+
"Complete text output, including Pi user input and deferred results. Images and file resources accompany it as native content blocks.",
|
|
24
|
+
),
|
|
25
|
+
});
|
|
26
|
+
|
|
19
27
|
interface RequestContext {
|
|
20
28
|
mcpReq: {
|
|
21
29
|
_meta?: Record<string, unknown>;
|
|
@@ -36,14 +44,20 @@ export function createServer(broker: Broker): McpServer {
|
|
|
36
44
|
"init",
|
|
37
45
|
{
|
|
38
46
|
title: "Connect to Pi",
|
|
39
|
-
description:
|
|
47
|
+
description:
|
|
48
|
+
"Bind this chat to a Pi session. To resume work or a ChatGPT branch, pass that task's Pi sessionId from context; use sessions to locate a requested target when its ID is unknown. Omit sessionId to reuse this chat's default or allocate the first online, unbound session. Returns the selection source.",
|
|
49
|
+
outputSchema,
|
|
40
50
|
inputSchema: z.object({
|
|
41
51
|
sessionId: z
|
|
42
52
|
.string()
|
|
43
53
|
.optional()
|
|
44
|
-
.describe(
|
|
54
|
+
.describe(
|
|
55
|
+
"Pi session ID to set as this chat's default, including an already-bound session when resuming work",
|
|
56
|
+
),
|
|
45
57
|
}),
|
|
46
58
|
annotations: {
|
|
59
|
+
readOnlyHint: false,
|
|
60
|
+
destructiveHint: false,
|
|
47
61
|
openWorldHint: false,
|
|
48
62
|
},
|
|
49
63
|
},
|
|
@@ -62,27 +76,38 @@ export function createServer(broker: Broker): McpServer {
|
|
|
62
76
|
"chat",
|
|
63
77
|
{
|
|
64
78
|
title: "Reply in Pi",
|
|
65
|
-
description:
|
|
79
|
+
description:
|
|
80
|
+
"Display the supplied Markdown in Pi and append it to the session transcript. Code blocks are displayed as text.",
|
|
81
|
+
outputSchema,
|
|
66
82
|
inputSchema: z.object({
|
|
67
|
-
text: z
|
|
83
|
+
text: z
|
|
84
|
+
.string()
|
|
85
|
+
.min(1)
|
|
86
|
+
.describe("Markdown message, including prose and code examples"),
|
|
68
87
|
sessionId: z
|
|
69
88
|
.string()
|
|
70
89
|
.optional()
|
|
71
90
|
.describe("Pi session for this operation only"),
|
|
72
91
|
}),
|
|
73
92
|
annotations: {
|
|
93
|
+
readOnlyHint: false,
|
|
94
|
+
destructiveHint: false,
|
|
74
95
|
openWorldHint: false,
|
|
75
96
|
},
|
|
76
97
|
},
|
|
77
98
|
async (args, context) => {
|
|
78
99
|
const chatId = requireChatId(context);
|
|
79
|
-
const {
|
|
100
|
+
const { sessionId, cwd, inputs } = await broker.chat(
|
|
80
101
|
chatId,
|
|
81
102
|
args.sessionId,
|
|
82
103
|
args.text,
|
|
83
104
|
context.mcpReq.signal,
|
|
84
105
|
);
|
|
85
|
-
return finishResult(
|
|
106
|
+
return finishResult(
|
|
107
|
+
broker,
|
|
108
|
+
context,
|
|
109
|
+
textResult({ sessionId, cwd }, inputs),
|
|
110
|
+
);
|
|
86
111
|
},
|
|
87
112
|
);
|
|
88
113
|
|
|
@@ -90,8 +115,15 @@ export function createServer(broker: Broker): McpServer {
|
|
|
90
115
|
"tools",
|
|
91
116
|
{
|
|
92
117
|
title: "Pi tools",
|
|
93
|
-
description:
|
|
118
|
+
description:
|
|
119
|
+
"Return complete definitions for active Pi tools invoked through call. Provide names to inspect selected tools. Chappie's own MCP controls, including init, sessions, and chat, are exposed separately.",
|
|
120
|
+
outputSchema,
|
|
94
121
|
inputSchema: z.object({
|
|
122
|
+
names: z
|
|
123
|
+
.array(z.string())
|
|
124
|
+
.min(1)
|
|
125
|
+
.optional()
|
|
126
|
+
.describe("Tool names to describe; omit to return every active tool"),
|
|
95
127
|
sessionId: z
|
|
96
128
|
.string()
|
|
97
129
|
.optional()
|
|
@@ -99,6 +131,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
99
131
|
}),
|
|
100
132
|
annotations: {
|
|
101
133
|
readOnlyHint: true,
|
|
134
|
+
destructiveHint: false,
|
|
102
135
|
idempotentHint: true,
|
|
103
136
|
openWorldHint: false,
|
|
104
137
|
},
|
|
@@ -107,6 +140,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
107
140
|
const { inputs, ...inspected } = await broker.tools(
|
|
108
141
|
requireChatId(context),
|
|
109
142
|
args.sessionId,
|
|
143
|
+
args.names,
|
|
110
144
|
context.mcpReq.signal,
|
|
111
145
|
);
|
|
112
146
|
return finishResult(
|
|
@@ -124,7 +158,9 @@ export function createServer(broker: Broker): McpServer {
|
|
|
124
158
|
"call",
|
|
125
159
|
{
|
|
126
160
|
title: "Call Pi tools",
|
|
127
|
-
description:
|
|
161
|
+
description:
|
|
162
|
+
"Execute one or more active Pi tools as one native batch. Arguments must match definitions returned by tools.",
|
|
163
|
+
outputSchema,
|
|
128
164
|
inputSchema: z.object({
|
|
129
165
|
calls: z
|
|
130
166
|
.array(
|
|
@@ -140,6 +176,8 @@ export function createServer(broker: Broker): McpServer {
|
|
|
140
176
|
.describe("Pi session for this operation only"),
|
|
141
177
|
}),
|
|
142
178
|
annotations: {
|
|
179
|
+
readOnlyHint: false,
|
|
180
|
+
destructiveHint: true,
|
|
143
181
|
openWorldHint: true,
|
|
144
182
|
},
|
|
145
183
|
},
|
|
@@ -153,7 +191,12 @@ export function createServer(broker: Broker): McpServer {
|
|
|
153
191
|
return finishResult(
|
|
154
192
|
broker,
|
|
155
193
|
context,
|
|
156
|
-
toolResult(
|
|
194
|
+
toolResult(
|
|
195
|
+
result.toolResults,
|
|
196
|
+
result.sessionId,
|
|
197
|
+
result.cwd,
|
|
198
|
+
result.inputs,
|
|
199
|
+
),
|
|
157
200
|
);
|
|
158
201
|
},
|
|
159
202
|
);
|
|
@@ -164,11 +207,13 @@ export function createServer(broker: Broker): McpServer {
|
|
|
164
207
|
{
|
|
165
208
|
title: tool.name,
|
|
166
209
|
description: tool.description,
|
|
210
|
+
outputSchema,
|
|
167
211
|
inputSchema: tool.inputSchema,
|
|
168
212
|
annotations: {
|
|
169
213
|
readOnlyHint: tool.name === "read",
|
|
214
|
+
destructiveHint: tool.name !== "read",
|
|
170
215
|
idempotentHint: tool.name === "read",
|
|
171
|
-
openWorldHint: tool.name === "bash",
|
|
216
|
+
openWorldHint: tool.name === "bash" || tool.name === "transfer",
|
|
172
217
|
},
|
|
173
218
|
...(tool.fileParams
|
|
174
219
|
? { _meta: { "openai/fileParams": tool.fileParams } }
|
|
@@ -190,7 +235,12 @@ export function createServer(broker: Broker): McpServer {
|
|
|
190
235
|
return finishResult(
|
|
191
236
|
broker,
|
|
192
237
|
context,
|
|
193
|
-
toolResult(
|
|
238
|
+
toolResult(
|
|
239
|
+
result.toolResults,
|
|
240
|
+
result.sessionId,
|
|
241
|
+
result.cwd,
|
|
242
|
+
result.inputs,
|
|
243
|
+
),
|
|
194
244
|
);
|
|
195
245
|
},
|
|
196
246
|
);
|
|
@@ -201,15 +251,17 @@ export function createServer(broker: Broker): McpServer {
|
|
|
201
251
|
{
|
|
202
252
|
title: "Local sessions",
|
|
203
253
|
description:
|
|
204
|
-
"List
|
|
254
|
+
"List online Pi sessions, their saved binding counts, and this conversation's default. A zero bindingCount permits automatic pairing; status describes execution. Offline sessions do not delay the listing.",
|
|
255
|
+
outputSchema,
|
|
205
256
|
inputSchema: z.object({
|
|
206
257
|
sessionId: z
|
|
207
258
|
.string()
|
|
208
259
|
.optional()
|
|
209
|
-
.describe("Return
|
|
260
|
+
.describe("Return this Pi session when it is online"),
|
|
210
261
|
}),
|
|
211
262
|
annotations: {
|
|
212
263
|
readOnlyHint: true,
|
|
264
|
+
destructiveHint: false,
|
|
213
265
|
idempotentHint: true,
|
|
214
266
|
openWorldHint: false,
|
|
215
267
|
},
|
|
@@ -226,7 +278,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
226
278
|
},
|
|
227
279
|
inputs,
|
|
228
280
|
);
|
|
229
|
-
return
|
|
281
|
+
return finishResult(broker, context, result);
|
|
230
282
|
},
|
|
231
283
|
);
|
|
232
284
|
|
|
@@ -268,19 +320,19 @@ function textResult(
|
|
|
268
320
|
};
|
|
269
321
|
}
|
|
270
322
|
|
|
271
|
-
async function finishResult<
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
):
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
323
|
+
async function finishResult<
|
|
324
|
+
T extends { content: ReturnType<typeof toolResult>["content"] },
|
|
325
|
+
>(broker: Broker, context: RequestContext, result: T) {
|
|
326
|
+
const chatId = requestChatId(context);
|
|
327
|
+
const deliveries = chatId ? await broker.deliveries(chatId) : [];
|
|
328
|
+
const content = [...result.content, ...deliveryContent(deliveries)];
|
|
329
|
+
const structuredContent = {
|
|
330
|
+
text: content
|
|
331
|
+
.flatMap((block) => (block.type === "text" ? [block.text] : []))
|
|
332
|
+
.join("\n"),
|
|
333
|
+
};
|
|
282
334
|
await broker.acknowledgeDeliveries(deliveries, context.mcpReq.signal);
|
|
283
|
-
return
|
|
335
|
+
return { ...result, content, structuredContent };
|
|
284
336
|
}
|
|
285
337
|
|
|
286
338
|
function requestChatId(context: RequestContext): string | undefined {
|
package/src/session.ts
CHANGED
|
@@ -35,6 +35,7 @@ interface StoreRequest {
|
|
|
35
35
|
|
|
36
36
|
interface ActiveRequest {
|
|
37
37
|
request: RemoteRequest;
|
|
38
|
+
session: SessionDescription;
|
|
38
39
|
message: AssistantMessage;
|
|
39
40
|
completed: boolean;
|
|
40
41
|
cancelled: boolean;
|
|
@@ -298,6 +299,7 @@ export class LocalSession {
|
|
|
298
299
|
this.#queue.shift();
|
|
299
300
|
this.#active = {
|
|
300
301
|
request,
|
|
302
|
+
session: this.#description(),
|
|
301
303
|
message: output.message,
|
|
302
304
|
completed: false,
|
|
303
305
|
cancelled: false,
|
|
@@ -342,7 +344,7 @@ export class LocalSession {
|
|
|
342
344
|
this.#context = context;
|
|
343
345
|
const active = this.#active;
|
|
344
346
|
if (!active || message !== active.message) return;
|
|
345
|
-
const sessionId =
|
|
347
|
+
const sessionId = active.session.id;
|
|
346
348
|
for (const result of toolResults) rememberImages(sessionId, result.content);
|
|
347
349
|
active.completed = true;
|
|
348
350
|
active.toolResults = toolResults;
|
|
@@ -354,44 +356,36 @@ export class LocalSession {
|
|
|
354
356
|
this.#collectInputs();
|
|
355
357
|
const inputs = this.#inputs();
|
|
356
358
|
if (active.cancelled) {
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
active.request.
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
await this.#flushDeliveries().catch(() => {});
|
|
377
|
-
}
|
|
359
|
+
const sessionFile = active.session.sessionFile;
|
|
360
|
+
const delivery: DeliveryRecord = {
|
|
361
|
+
id: randomUUID(),
|
|
362
|
+
chatId: active.request.chatId,
|
|
363
|
+
sessionId: active.session.id,
|
|
364
|
+
cwd: active.session.cwd,
|
|
365
|
+
toolCallIds:
|
|
366
|
+
active.request.type === "call"
|
|
367
|
+
? active.request.calls.map(({ id }) => id)
|
|
368
|
+
: [],
|
|
369
|
+
...(sessionFile
|
|
370
|
+
? { sessionFile }
|
|
371
|
+
: { inlineResults: active.toolResults }),
|
|
372
|
+
...(active.message.errorMessage
|
|
373
|
+
? { error: active.message.errorMessage }
|
|
374
|
+
: { error: "Request cancelled" }),
|
|
375
|
+
};
|
|
376
|
+
this.#deliveries.set(delivery.id, delivery);
|
|
377
|
+
await this.#flushDeliveries().catch(() => {});
|
|
378
378
|
} else {
|
|
379
|
-
await this.#connection?.send(
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
type: "result",
|
|
390
|
-
id: active.request.id,
|
|
391
|
-
message: active.message,
|
|
392
|
-
inputs,
|
|
393
|
-
},
|
|
394
|
-
);
|
|
379
|
+
await this.#connection?.send({
|
|
380
|
+
type: "result",
|
|
381
|
+
id: active.request.id,
|
|
382
|
+
cwd: active.session.cwd,
|
|
383
|
+
message: active.message,
|
|
384
|
+
inputs,
|
|
385
|
+
...(active.request.type === "call"
|
|
386
|
+
? { toolResults: active.toolResults }
|
|
387
|
+
: {}),
|
|
388
|
+
});
|
|
395
389
|
}
|
|
396
390
|
this.#active = undefined;
|
|
397
391
|
this.#status = "idle";
|
package/src/state.ts
CHANGED
|
@@ -40,8 +40,12 @@ export class State {
|
|
|
40
40
|
return this.#bindings.get(chatId);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
bindingCounts(): Map<string, number> {
|
|
44
|
+
const counts = new Map<string, number>();
|
|
45
|
+
for (const sessionId of this.#bindings.values()) {
|
|
46
|
+
counts.set(sessionId, (counts.get(sessionId) ?? 0) + 1);
|
|
47
|
+
}
|
|
48
|
+
return counts;
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
setBinding(chatId: string, sessionId: string): Promise<void> {
|
package/src/tools.ts
CHANGED
|
@@ -38,10 +38,12 @@ export const directTools = definitions.map((definition) => ({
|
|
|
38
38
|
export function toolResult(
|
|
39
39
|
toolResults: ToolResultMessage[],
|
|
40
40
|
sessionId: string,
|
|
41
|
+
cwd: string,
|
|
41
42
|
inputs: SessionInput[] = [],
|
|
42
43
|
) {
|
|
43
44
|
return {
|
|
44
45
|
content: [
|
|
46
|
+
{ type: "text" as const, text: JSON.stringify({ sessionId, cwd }) },
|
|
45
47
|
...toolResults.flatMap((result) => [
|
|
46
48
|
{
|
|
47
49
|
type: "text" as const,
|
|
@@ -70,7 +72,7 @@ export function inputContent(inputs: SessionInput[]) {
|
|
|
70
72
|
return inputs.flatMap(({ id, sessionId, message }) => [
|
|
71
73
|
{
|
|
72
74
|
type: "text" as const,
|
|
73
|
-
text: JSON.stringify({ piInput: id }),
|
|
75
|
+
text: JSON.stringify({ piInput: id, sessionId }),
|
|
74
76
|
},
|
|
75
77
|
...(typeof message.content === "string"
|
|
76
78
|
? [{ type: "text" as const, text: message.content }]
|
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
|
-
"
|
|
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(), {
|
|
34
|
-
|
|
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
|
),
|