@zetaloop/chappie 0.2.1 → 0.3.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 +27 -7
- package/docs/tools.md +55 -74
- package/package.json +3 -1
- package/src/broker.ts +193 -35
- package/src/config.ts +21 -0
- package/src/delivery.ts +25 -56
- package/src/index.ts +15 -10
- package/src/instructions.md +7 -5
- package/src/ipc.ts +142 -40
- package/src/provider.ts +3 -5
- package/src/question.html +384 -0
- package/src/questions.ts +95 -0
- package/src/server.ts +206 -62
- package/src/session.ts +111 -41
- package/src/state.ts +98 -7
- package/src/stdio.ts +11 -2
- package/src/tools.ts +3 -22
- package/src/transfer.ts +3 -3
package/src/session.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import type {
|
|
3
6
|
AssistantMessage,
|
|
4
|
-
Context,
|
|
5
7
|
ToolResultMessage,
|
|
6
8
|
UserMessage,
|
|
7
9
|
} from "@earendil-works/pi-ai";
|
|
8
10
|
import type {
|
|
9
11
|
ExtensionAPI,
|
|
10
12
|
ExtensionContext,
|
|
13
|
+
SessionEntry,
|
|
11
14
|
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
12
16
|
import type { DeliveryRecord } from "./delivery.ts";
|
|
13
17
|
import {
|
|
14
18
|
type BrokerMessage,
|
|
@@ -23,6 +27,11 @@ import { readSessionResource, rememberImages } from "./resources.ts";
|
|
|
23
27
|
|
|
24
28
|
type RemoteRequest = Extract<BrokerMessage, { type: "chat" | "call" }>;
|
|
25
29
|
|
|
30
|
+
interface Notice {
|
|
31
|
+
message: string;
|
|
32
|
+
type: "info" | "warning" | "error";
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
interface SyncRequest {
|
|
27
36
|
resolve(): void;
|
|
28
37
|
reject(error: Error): void;
|
|
@@ -38,13 +47,14 @@ interface ActiveRequest {
|
|
|
38
47
|
session: SessionDescription;
|
|
39
48
|
message: AssistantMessage;
|
|
40
49
|
completed: boolean;
|
|
41
|
-
cancelled:
|
|
50
|
+
cancelled: string | undefined;
|
|
42
51
|
toolResults: ToolResultMessage[];
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
export class LocalSession {
|
|
46
55
|
readonly #pi: ExtensionAPI;
|
|
47
56
|
readonly #agentDir: string;
|
|
57
|
+
readonly #connect: string | undefined;
|
|
48
58
|
readonly #syncs = new Map<number, SyncRequest>();
|
|
49
59
|
readonly #stores = new Map<string, StoreRequest>();
|
|
50
60
|
readonly #queue: RemoteRequest[] = [];
|
|
@@ -59,14 +69,27 @@ export class LocalSession {
|
|
|
59
69
|
#nextSyncId = 1;
|
|
60
70
|
#starting = false;
|
|
61
71
|
#sessionId: string | undefined;
|
|
72
|
+
#inputCursor: string | null = null;
|
|
62
73
|
#flushing = Promise.resolve();
|
|
63
74
|
|
|
64
|
-
constructor(pi: ExtensionAPI, agentDir: string) {
|
|
75
|
+
constructor(pi: ExtensionAPI, agentDir: string, connect?: string) {
|
|
65
76
|
this.#pi = pi;
|
|
66
77
|
this.#agentDir = agentDir;
|
|
78
|
+
this.#connect = connect;
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
install(): void {
|
|
82
|
+
this.#pi.registerEntryRenderer<Notice>(
|
|
83
|
+
"chappie.notice",
|
|
84
|
+
({ data }, _options, theme) => {
|
|
85
|
+
if (!data) return;
|
|
86
|
+
return new Text(
|
|
87
|
+
theme.fg(data.type === "info" ? "dim" : data.type, data.message),
|
|
88
|
+
1,
|
|
89
|
+
0,
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
);
|
|
70
93
|
this.#pi.on("session_start", (_event, context) => this.#update(context));
|
|
71
94
|
this.#pi.on("model_select", (event, context) =>
|
|
72
95
|
this.#update(context, event.model.provider === "chappie"),
|
|
@@ -75,11 +98,15 @@ export class LocalSession {
|
|
|
75
98
|
this.#context = context;
|
|
76
99
|
void this.#sync().catch(() => {});
|
|
77
100
|
});
|
|
78
|
-
this.#pi.on("context", (event) => ({
|
|
79
|
-
messages:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
this.#pi.on("context", (event, context) => ({
|
|
102
|
+
messages:
|
|
103
|
+
context.model?.provider === "chappie"
|
|
104
|
+
? []
|
|
105
|
+
: event.messages.filter(
|
|
106
|
+
(message) =>
|
|
107
|
+
message.role !== "custom" ||
|
|
108
|
+
message.customType !== "chappie.request",
|
|
109
|
+
),
|
|
83
110
|
}));
|
|
84
111
|
this.#pi.on("turn_end", (event, context) =>
|
|
85
112
|
this.#turnEnd(event.message, event.toolResults, context),
|
|
@@ -94,10 +121,12 @@ export class LocalSession {
|
|
|
94
121
|
this.#pi.on("session_shutdown", () => this.close());
|
|
95
122
|
}
|
|
96
123
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
#notify(message: string, type: Notice["type"] = "info"): void {
|
|
125
|
+
if (this.#context)
|
|
126
|
+
this.#pi.appendEntry<Notice>("chappie.notice", { message, type });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async start(output: ProviderOutput): Promise<void> {
|
|
101
130
|
const context = this.#context;
|
|
102
131
|
const connection = this.#connection;
|
|
103
132
|
if (context?.model?.provider !== "chappie" || !connection) {
|
|
@@ -156,7 +185,8 @@ export class LocalSession {
|
|
|
156
185
|
return;
|
|
157
186
|
}
|
|
158
187
|
if (!this.#connection) {
|
|
159
|
-
this.#
|
|
188
|
+
this.#notify("Connecting to Chappie…");
|
|
189
|
+
this.#connection = new IpcClient(this.#agentDir, this.#connect, {
|
|
160
190
|
onOpen: async () => {
|
|
161
191
|
await this.#sync();
|
|
162
192
|
await this.#flushDeliveries();
|
|
@@ -165,7 +195,8 @@ export class LocalSession {
|
|
|
165
195
|
onClose: (error) => {
|
|
166
196
|
this.#rejectSyncs(error);
|
|
167
197
|
this.#rejectStores(error);
|
|
168
|
-
this.#output
|
|
198
|
+
if (this.#output && !this.#output.closed) this.#output.fail(error);
|
|
199
|
+
else this.#notify(error.message, "error");
|
|
169
200
|
this.#active = undefined;
|
|
170
201
|
this.#queue.length = 0;
|
|
171
202
|
this.#starting = false;
|
|
@@ -182,13 +213,12 @@ export class LocalSession {
|
|
|
182
213
|
const context = this.#context;
|
|
183
214
|
if (!context) throw new Error("Chappie session is not available");
|
|
184
215
|
const name = this.#pi.getSessionName();
|
|
185
|
-
const sessionFile = context.sessionManager.getSessionFile();
|
|
186
216
|
return {
|
|
187
217
|
id: context.sessionManager.getSessionId(),
|
|
188
218
|
cwd: context.cwd,
|
|
219
|
+
device: hostname(),
|
|
189
220
|
status: this.#status,
|
|
190
221
|
...(name ? { name } : {}),
|
|
191
|
-
...(sessionFile ? { sessionFile } : {}),
|
|
192
222
|
};
|
|
193
223
|
}
|
|
194
224
|
|
|
@@ -219,13 +249,24 @@ export class LocalSession {
|
|
|
219
249
|
case "stored":
|
|
220
250
|
this.#stores.get(message.id)?.resolve();
|
|
221
251
|
break;
|
|
252
|
+
case "notice":
|
|
253
|
+
if (
|
|
254
|
+
message.sessionId === this.#context?.sessionManager.getSessionId()
|
|
255
|
+
) {
|
|
256
|
+
this.#notify(message.message);
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
222
259
|
case "inspect":
|
|
223
|
-
await this.#reply(message.id, message.sessionId, () =>
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
260
|
+
await this.#reply(message.id, message.sessionId, async () => {
|
|
261
|
+
const globalAgents = await this.#readGlobalAgents();
|
|
262
|
+
return {
|
|
263
|
+
type: "result",
|
|
264
|
+
id: message.id,
|
|
265
|
+
inspection: this.#inspection(),
|
|
266
|
+
inputs: this.#inputs(),
|
|
267
|
+
...(globalAgents ? { globalAgents } : {}),
|
|
268
|
+
};
|
|
269
|
+
});
|
|
229
270
|
break;
|
|
230
271
|
case "ackInputs":
|
|
231
272
|
if (
|
|
@@ -245,13 +286,28 @@ export class LocalSession {
|
|
|
245
286
|
const queued = this.#queue.findIndex(
|
|
246
287
|
(request) => request.id === message.id,
|
|
247
288
|
);
|
|
289
|
+
const request =
|
|
290
|
+
this.#queue[queued] ??
|
|
291
|
+
(this.#active?.request.id === message.id
|
|
292
|
+
? this.#active.request
|
|
293
|
+
: undefined);
|
|
294
|
+
if (!request) break;
|
|
295
|
+
const name =
|
|
296
|
+
request.type === "call"
|
|
297
|
+
? [...new Set(request.calls.map((call) => call.name))].join(", ")
|
|
298
|
+
: "chat";
|
|
299
|
+
this.#notify(
|
|
300
|
+
`${name} cancelled for ChatGPT ${request.chatId.slice(-4)}: ${message.reason}`,
|
|
301
|
+
"warning",
|
|
302
|
+
);
|
|
248
303
|
if (queued !== -1) {
|
|
249
304
|
this.#queue.splice(queued, 1);
|
|
250
305
|
break;
|
|
251
306
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
307
|
+
const active = this.#active;
|
|
308
|
+
if (!active) break;
|
|
309
|
+
active.cancelled = message.reason;
|
|
310
|
+
if (active.completed) await this.#completeActive();
|
|
255
311
|
else this.#context?.abort();
|
|
256
312
|
break;
|
|
257
313
|
}
|
|
@@ -286,6 +342,15 @@ export class LocalSession {
|
|
|
286
342
|
};
|
|
287
343
|
}
|
|
288
344
|
|
|
345
|
+
async #readGlobalAgents(): Promise<string | undefined> {
|
|
346
|
+
try {
|
|
347
|
+
return await readFile(join(this.#agentDir, "AGENTS.md"), "utf8");
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
289
354
|
#dispatch(): void {
|
|
290
355
|
if (this.#active) return;
|
|
291
356
|
const request = this.#queue[0];
|
|
@@ -302,7 +367,7 @@ export class LocalSession {
|
|
|
302
367
|
session: this.#description(),
|
|
303
368
|
message: output.message,
|
|
304
369
|
completed: false,
|
|
305
|
-
cancelled:
|
|
370
|
+
cancelled: undefined,
|
|
306
371
|
toolResults: [],
|
|
307
372
|
};
|
|
308
373
|
this.#status = "executing";
|
|
@@ -355,26 +420,17 @@ export class LocalSession {
|
|
|
355
420
|
if (!active?.completed) return;
|
|
356
421
|
this.#collectInputs();
|
|
357
422
|
const inputs = this.#inputs();
|
|
358
|
-
if (active.cancelled) {
|
|
359
|
-
const sessionFile = active.session.sessionFile;
|
|
423
|
+
if (active.cancelled !== undefined) {
|
|
360
424
|
const delivery: DeliveryRecord = {
|
|
361
425
|
id: randomUUID(),
|
|
362
426
|
chatId: active.request.chatId,
|
|
363
427
|
sessionId: active.session.id,
|
|
364
428
|
cwd: active.session.cwd,
|
|
365
|
-
|
|
366
|
-
|
|
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" }),
|
|
429
|
+
toolResults: active.toolResults,
|
|
430
|
+
error: active.cancelled,
|
|
375
431
|
};
|
|
376
432
|
this.#deliveries.set(delivery.id, delivery);
|
|
377
|
-
|
|
433
|
+
void this.#flushDeliveries().catch(() => {});
|
|
378
434
|
} else {
|
|
379
435
|
await this.#connection?.send({
|
|
380
436
|
type: "result",
|
|
@@ -395,13 +451,25 @@ export class LocalSession {
|
|
|
395
451
|
#collectInputs(): void {
|
|
396
452
|
const context = this.#context;
|
|
397
453
|
if (!context) return;
|
|
398
|
-
const
|
|
454
|
+
const sessionManager = context.sessionManager;
|
|
455
|
+
const sessionId = sessionManager.getSessionId();
|
|
399
456
|
if (this.#sessionId !== sessionId) {
|
|
400
457
|
this.#sessionId = sessionId;
|
|
458
|
+
this.#inputCursor = null;
|
|
401
459
|
this.#seenInputs.clear();
|
|
402
460
|
this.#pendingInputs.clear();
|
|
403
461
|
}
|
|
404
|
-
|
|
462
|
+
const leafId = sessionManager.getLeafId();
|
|
463
|
+
if (leafId === this.#inputCursor) return;
|
|
464
|
+
const entries: SessionEntry[] = [];
|
|
465
|
+
let current = sessionManager.getLeafEntry();
|
|
466
|
+
while (current && current.id !== this.#inputCursor) {
|
|
467
|
+
entries.push(current);
|
|
468
|
+
current = current.parentId
|
|
469
|
+
? sessionManager.getEntry(current.parentId)
|
|
470
|
+
: undefined;
|
|
471
|
+
}
|
|
472
|
+
for (const entry of entries.reverse()) {
|
|
405
473
|
if (
|
|
406
474
|
entry.type !== "message" ||
|
|
407
475
|
entry.message.role !== "user" ||
|
|
@@ -415,6 +483,7 @@ export class LocalSession {
|
|
|
415
483
|
this.#seenInputs.add(entry.id);
|
|
416
484
|
this.#pendingInputs.set(entry.id, { id: entry.id, sessionId, message });
|
|
417
485
|
}
|
|
486
|
+
this.#inputCursor = leafId;
|
|
418
487
|
}
|
|
419
488
|
|
|
420
489
|
#inputs(): SessionInput[] {
|
|
@@ -433,6 +502,7 @@ export class LocalSession {
|
|
|
433
502
|
await connection.send({ type: "delivery", delivery });
|
|
434
503
|
await completion.promise;
|
|
435
504
|
this.#deliveries.delete(delivery.id);
|
|
505
|
+
this.#notify(`Result saved for ChatGPT ${delivery.chatId.slice(-4)}`);
|
|
436
506
|
} finally {
|
|
437
507
|
this.#stores.delete(delivery.id);
|
|
438
508
|
}
|
package/src/state.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { DeliveryRecord } from "./delivery.ts";
|
|
4
|
+
import type { QuestionAnswer, QuestionRecord } from "./questions.ts";
|
|
4
5
|
|
|
5
6
|
interface StateFile {
|
|
6
7
|
bindings?: Record<string, string>;
|
|
8
|
+
workflows?: Record<string, string>;
|
|
7
9
|
deliveries?: DeliveryRecord[];
|
|
10
|
+
questions?: QuestionRecord[];
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
export class State {
|
|
11
14
|
readonly #path: string;
|
|
12
15
|
readonly #temporaryPath: string;
|
|
13
16
|
readonly #bindings = new Map<string, string>();
|
|
17
|
+
readonly #workflows = new Map<string, string>();
|
|
14
18
|
readonly #deliveries = new Map<string, DeliveryRecord>();
|
|
19
|
+
readonly #questions = new Map<string, QuestionRecord>();
|
|
15
20
|
#writes = Promise.resolve();
|
|
16
21
|
|
|
17
22
|
constructor(agentDir: string) {
|
|
@@ -31,15 +36,35 @@ export class State {
|
|
|
31
36
|
for (const [chatId, sessionId] of Object.entries(state.bindings ?? {})) {
|
|
32
37
|
if (typeof sessionId === "string") this.#bindings.set(chatId, sessionId);
|
|
33
38
|
}
|
|
39
|
+
for (const [chatId, id] of Object.entries(state.workflows ?? {})) {
|
|
40
|
+
this.#workflows.set(chatId, id);
|
|
41
|
+
}
|
|
34
42
|
for (const delivery of state.deliveries ?? []) {
|
|
35
43
|
if (delivery?.id) this.#deliveries.set(delivery.id, delivery);
|
|
36
44
|
}
|
|
45
|
+
for (const question of state.questions ?? [])
|
|
46
|
+
this.#questions.set(question.id, question);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
workflow(chatId: string): string | undefined {
|
|
50
|
+
return this.#workflows.get(chatId);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setWorkflow(chatId: string, id: string): Promise<void> {
|
|
54
|
+
this.#workflows.set(chatId, id);
|
|
55
|
+
return this.#save();
|
|
37
56
|
}
|
|
38
57
|
|
|
39
58
|
binding(chatId: string): string | undefined {
|
|
40
59
|
return this.#bindings.get(chatId);
|
|
41
60
|
}
|
|
42
61
|
|
|
62
|
+
chats(sessionId: string): string[] {
|
|
63
|
+
return [...this.#bindings]
|
|
64
|
+
.filter(([, target]) => target === sessionId)
|
|
65
|
+
.map(([chatId]) => chatId);
|
|
66
|
+
}
|
|
67
|
+
|
|
43
68
|
bindingCounts(): Map<string, number> {
|
|
44
69
|
const counts = new Map<string, number>();
|
|
45
70
|
for (const sessionId of this.#bindings.values()) {
|
|
@@ -64,23 +89,89 @@ export class State {
|
|
|
64
89
|
return this.#save();
|
|
65
90
|
}
|
|
66
91
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
92
|
+
question(chatId: string, id: string): QuestionRecord {
|
|
93
|
+
const question = this.#questions.get(id);
|
|
94
|
+
if (!question || question.chatId !== chatId)
|
|
95
|
+
throw new Error("Question not found in this ChatGPT conversation");
|
|
96
|
+
return question;
|
|
70
97
|
}
|
|
71
98
|
|
|
72
|
-
async
|
|
73
|
-
|
|
74
|
-
this.#deliveries.set(delivery.id, delivery);
|
|
99
|
+
async addQuestion(question: QuestionRecord): Promise<void> {
|
|
100
|
+
this.#questions.set(question.id, question);
|
|
75
101
|
await this.#save();
|
|
76
102
|
}
|
|
77
103
|
|
|
104
|
+
async answer(
|
|
105
|
+
chatId: string,
|
|
106
|
+
id: string,
|
|
107
|
+
answer: QuestionAnswer,
|
|
108
|
+
): Promise<QuestionRecord> {
|
|
109
|
+
const question = this.question(chatId, id);
|
|
110
|
+
const selections = [...new Set(answer.selections)].sort((a, b) => a - b);
|
|
111
|
+
if (selections.some((index) => !question.options[index]))
|
|
112
|
+
throw new Error("Unknown question option");
|
|
113
|
+
if (!question.allowMultiple && selections.length > 1)
|
|
114
|
+
throw new Error("Select one option");
|
|
115
|
+
if (answer.skipped && (selections.length > 0 || answer.text))
|
|
116
|
+
throw new Error("A skipped question cannot include an answer");
|
|
117
|
+
if (!answer.skipped && selections.length === 0 && !answer.text)
|
|
118
|
+
throw new Error("Select an option or enter an answer");
|
|
119
|
+
const value: QuestionAnswer = {
|
|
120
|
+
selections,
|
|
121
|
+
text: answer.text,
|
|
122
|
+
...(answer.skipped ? { skipped: true } : {}),
|
|
123
|
+
};
|
|
124
|
+
if (JSON.stringify(question.answer) === JSON.stringify(value))
|
|
125
|
+
return question;
|
|
126
|
+
const updated = { ...question, answer: value, delivered: false };
|
|
127
|
+
await this.addQuestion(updated);
|
|
128
|
+
return updated;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
answers(chatId: string): QuestionRecord[] {
|
|
132
|
+
return [...this.#questions.values()].filter(
|
|
133
|
+
(question) =>
|
|
134
|
+
question.chatId === chatId && question.answer && !question.delivered,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async acknowledge(
|
|
139
|
+
deliveries: DeliveryRecord[],
|
|
140
|
+
answers: QuestionRecord[],
|
|
141
|
+
signal: AbortSignal,
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
if (deliveries.length === 0 && answers.length === 0) return;
|
|
144
|
+
signal.throwIfAborted();
|
|
145
|
+
const delivered = new Map(
|
|
146
|
+
answers.map((question) => [question, { ...question, delivered: true }]),
|
|
147
|
+
);
|
|
148
|
+
for (const delivery of deliveries) this.#deliveries.delete(delivery.id);
|
|
149
|
+
for (const [question, updated] of delivered) {
|
|
150
|
+
if (this.#questions.get(question.id) === question)
|
|
151
|
+
this.#questions.set(question.id, updated);
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
await this.#save();
|
|
155
|
+
signal.throwIfAborted();
|
|
156
|
+
} catch (error) {
|
|
157
|
+
for (const delivery of deliveries)
|
|
158
|
+
this.#deliveries.set(delivery.id, delivery);
|
|
159
|
+
for (const [question, updated] of delivered) {
|
|
160
|
+
if (this.#questions.get(question.id) === updated)
|
|
161
|
+
this.#questions.set(question.id, question);
|
|
162
|
+
}
|
|
163
|
+
await this.#save();
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
78
168
|
#save(): Promise<void> {
|
|
79
169
|
const saved = this.#writes.then(async () => {
|
|
80
170
|
const bindings = Object.fromEntries(this.#bindings);
|
|
171
|
+
const workflows = Object.fromEntries(this.#workflows);
|
|
81
172
|
await writeFile(
|
|
82
173
|
this.#temporaryPath,
|
|
83
|
-
`${JSON.stringify({ bindings, deliveries: [...this.#deliveries.values()] }, null, 2)}\n`,
|
|
174
|
+
`${JSON.stringify({ bindings, workflows, deliveries: [...this.#deliveries.values()], questions: [...this.#questions.values()] }, null, 2)}\n`,
|
|
84
175
|
{
|
|
85
176
|
mode: 0o600,
|
|
86
177
|
},
|
package/src/stdio.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Writable } from "node:stream";
|
|
2
2
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import {
|
|
4
4
|
StdioServerTransport,
|
|
@@ -15,7 +15,15 @@ const terminationSignals =
|
|
|
15
15
|
export async function serveChappie(): Promise<never> {
|
|
16
16
|
const broker = new Broker(getAgentDir());
|
|
17
17
|
await broker.start();
|
|
18
|
-
const output =
|
|
18
|
+
const output = new Writable({
|
|
19
|
+
write(chunk, encoding, callback) {
|
|
20
|
+
Reflect.apply(Writable.prototype.write, process.stdout, [
|
|
21
|
+
chunk,
|
|
22
|
+
encoding,
|
|
23
|
+
callback,
|
|
24
|
+
]);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
19
27
|
const transport = new StdioServerTransport(process.stdin, output);
|
|
20
28
|
let stop: (() => void) | undefined;
|
|
21
29
|
const stopped = new Promise<void>((resolve) => {
|
|
@@ -23,6 +31,7 @@ export async function serveChappie(): Promise<never> {
|
|
|
23
31
|
});
|
|
24
32
|
const requestStop = (): void => stop?.();
|
|
25
33
|
|
|
34
|
+
output.once("error", requestStop);
|
|
26
35
|
process.stdin.once("end", requestStop);
|
|
27
36
|
process.stdin.once("close", requestStop);
|
|
28
37
|
for (const signal of terminationSignals) {
|
package/src/tools.ts
CHANGED
|
@@ -6,11 +6,9 @@ import {
|
|
|
6
6
|
createWriteToolDefinition,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { fromJsonSchema } from "@modelcontextprotocol/server";
|
|
9
|
+
import { toolResultsContent } from "./delivery.ts";
|
|
9
10
|
import type { SessionInput } from "./ipc.ts";
|
|
10
|
-
import {
|
|
11
|
-
contentWithImageReferences,
|
|
12
|
-
resourceDescriptors,
|
|
13
|
-
} from "./resources.ts";
|
|
11
|
+
import { contentWithImageReferences } from "./resources.ts";
|
|
14
12
|
import { transfer } from "./transfer.ts";
|
|
15
13
|
|
|
16
14
|
export interface ToolInput {
|
|
@@ -44,24 +42,7 @@ export function toolResult(
|
|
|
44
42
|
return {
|
|
45
43
|
content: [
|
|
46
44
|
{ type: "text" as const, text: JSON.stringify({ sessionId, cwd }) },
|
|
47
|
-
...toolResults
|
|
48
|
-
{
|
|
49
|
-
type: "text" as const,
|
|
50
|
-
text: JSON.stringify({
|
|
51
|
-
toolCallId: result.toolCallId,
|
|
52
|
-
toolName: result.toolName,
|
|
53
|
-
isError: result.isError,
|
|
54
|
-
}),
|
|
55
|
-
},
|
|
56
|
-
...contentWithImageReferences(sessionId, result.content),
|
|
57
|
-
...resourceDescriptors(result.details).map((resource) => ({
|
|
58
|
-
type: "resource_link" as const,
|
|
59
|
-
uri: resource.uri,
|
|
60
|
-
name: resource.name,
|
|
61
|
-
mimeType: resource.mimeType,
|
|
62
|
-
size: resource.size,
|
|
63
|
-
})),
|
|
64
|
-
]),
|
|
45
|
+
...toolResultsContent(toolResults, sessionId),
|
|
65
46
|
...inputContent(inputs),
|
|
66
47
|
],
|
|
67
48
|
isError: toolResults.some((result) => result.isError),
|
package/src/transfer.ts
CHANGED
|
@@ -28,18 +28,18 @@ export const transfer = {
|
|
|
28
28
|
name: "transfer",
|
|
29
29
|
label: "transfer",
|
|
30
30
|
description:
|
|
31
|
-
"Copy ChatGPT files into Pi paths
|
|
31
|
+
"Copy ChatGPT files into Pi paths when files is supplied. Otherwise, return resource links for Pi paths or Chappie image references.",
|
|
32
32
|
parameters: Type.Object({
|
|
33
33
|
paths: Type.Array(Type.String(), {
|
|
34
34
|
minItems: 1,
|
|
35
35
|
description:
|
|
36
|
-
"Pi
|
|
36
|
+
"Pi destinations for import; Pi paths or chappie:// image references for export",
|
|
37
37
|
}),
|
|
38
38
|
files: Type.Optional(
|
|
39
39
|
Type.Array(transferFile, {
|
|
40
40
|
minItems: 1,
|
|
41
41
|
description:
|
|
42
|
-
"ChatGPT files
|
|
42
|
+
"ChatGPT files paired with paths in order; omit for export",
|
|
43
43
|
}),
|
|
44
44
|
),
|
|
45
45
|
overwrite: Type.Optional(
|