@zetaloop/chappie 0.2.1 → 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/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,17 +47,17 @@ interface ActiveRequest {
38
47
  session: SessionDescription;
39
48
  message: AssistantMessage;
40
49
  completed: boolean;
41
- cancelled: boolean;
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[] = [];
51
- readonly #seenInputs = new Set<string>();
52
61
  readonly #pendingInputs = new Map<string, SessionInput>();
53
62
  readonly #deliveries = new Map<string, DeliveryRecord>();
54
63
  #context: ExtensionContext | undefined;
@@ -59,14 +68,27 @@ export class LocalSession {
59
68
  #nextSyncId = 1;
60
69
  #starting = false;
61
70
  #sessionId: string | undefined;
71
+ #inputCursor: string | null = null;
62
72
  #flushing = Promise.resolve();
63
73
 
64
- constructor(pi: ExtensionAPI, agentDir: string) {
74
+ constructor(pi: ExtensionAPI, agentDir: string, connect?: string) {
65
75
  this.#pi = pi;
66
76
  this.#agentDir = agentDir;
77
+ this.#connect = connect;
67
78
  }
68
79
 
69
80
  install(): void {
81
+ this.#pi.registerEntryRenderer<Notice>(
82
+ "chappie.notice",
83
+ ({ data }, _options, theme) => {
84
+ if (!data) return;
85
+ return new Text(
86
+ theme.fg(data.type === "info" ? "dim" : data.type, data.message),
87
+ 1,
88
+ 0,
89
+ );
90
+ },
91
+ );
70
92
  this.#pi.on("session_start", (_event, context) => this.#update(context));
71
93
  this.#pi.on("model_select", (event, context) =>
72
94
  this.#update(context, event.model.provider === "chappie"),
@@ -75,11 +97,21 @@ export class LocalSession {
75
97
  this.#context = context;
76
98
  void this.#sync().catch(() => {});
77
99
  });
78
- this.#pi.on("context", (event) => ({
79
- messages: event.messages.filter(
80
- (message) =>
81
- message.role !== "custom" || message.customType !== "chappie.request",
82
- ),
100
+ this.#pi.on("session_tree", (event, context) => {
101
+ this.#context = context;
102
+ if (context.model?.provider === "chappie") {
103
+ this.#resetInputs(context, event.newLeafId);
104
+ }
105
+ });
106
+ this.#pi.on("context", (event, context) => ({
107
+ messages:
108
+ context.model?.provider === "chappie"
109
+ ? []
110
+ : event.messages.filter(
111
+ (message) =>
112
+ message.role !== "custom" ||
113
+ message.customType !== "chappie.request",
114
+ ),
83
115
  }));
84
116
  this.#pi.on("turn_end", (event, context) =>
85
117
  this.#turnEnd(event.message, event.toolResults, context),
@@ -94,10 +126,12 @@ export class LocalSession {
94
126
  this.#pi.on("session_shutdown", () => this.close());
95
127
  }
96
128
 
97
- async start(
98
- output: ProviderOutput,
99
- _providerContext: Context,
100
- ): Promise<void> {
129
+ #notify(message: string, type: Notice["type"] = "info"): void {
130
+ if (this.#context)
131
+ this.#pi.appendEntry<Notice>("chappie.notice", { message, type });
132
+ }
133
+
134
+ async start(output: ProviderOutput): Promise<void> {
101
135
  const context = this.#context;
102
136
  const connection = this.#connection;
103
137
  if (context?.model?.provider !== "chappie" || !connection) {
@@ -142,6 +176,7 @@ export class LocalSession {
142
176
  this.#connection?.close();
143
177
  this.#connection = undefined;
144
178
  this.#context = undefined;
179
+ this.#resetInputs();
145
180
  this.#rejectSyncs(new Error("Chappie session ended"));
146
181
  this.#rejectStores(new Error("Chappie session ended"));
147
182
  }
@@ -155,8 +190,12 @@ export class LocalSession {
155
190
  this.close();
156
191
  return;
157
192
  }
193
+ if (this.#sessionId !== context.sessionManager.getSessionId()) {
194
+ this.#resetInputs(context);
195
+ }
158
196
  if (!this.#connection) {
159
- this.#connection = new IpcClient(this.#agentDir, {
197
+ this.#notify("Connecting to Chappie…");
198
+ this.#connection = new IpcClient(this.#agentDir, this.#connect, {
160
199
  onOpen: async () => {
161
200
  await this.#sync();
162
201
  await this.#flushDeliveries();
@@ -165,7 +204,8 @@ export class LocalSession {
165
204
  onClose: (error) => {
166
205
  this.#rejectSyncs(error);
167
206
  this.#rejectStores(error);
168
- this.#output?.fail(error);
207
+ if (this.#output && !this.#output.closed) this.#output.fail(error);
208
+ else this.#notify(error.message, "error");
169
209
  this.#active = undefined;
170
210
  this.#queue.length = 0;
171
211
  this.#starting = false;
@@ -182,13 +222,12 @@ export class LocalSession {
182
222
  const context = this.#context;
183
223
  if (!context) throw new Error("Chappie session is not available");
184
224
  const name = this.#pi.getSessionName();
185
- const sessionFile = context.sessionManager.getSessionFile();
186
225
  return {
187
226
  id: context.sessionManager.getSessionId(),
188
227
  cwd: context.cwd,
228
+ device: hostname(),
189
229
  status: this.#status,
190
230
  ...(name ? { name } : {}),
191
- ...(sessionFile ? { sessionFile } : {}),
192
231
  };
193
232
  }
194
233
 
@@ -219,13 +258,24 @@ export class LocalSession {
219
258
  case "stored":
220
259
  this.#stores.get(message.id)?.resolve();
221
260
  break;
261
+ case "notice":
262
+ if (
263
+ message.sessionId === this.#context?.sessionManager.getSessionId()
264
+ ) {
265
+ this.#notify(message.message);
266
+ }
267
+ break;
222
268
  case "inspect":
223
- await this.#reply(message.id, message.sessionId, () => ({
224
- type: "result",
225
- id: message.id,
226
- inspection: this.#inspection(),
227
- inputs: this.#inputs(),
228
- }));
269
+ await this.#reply(message.id, message.sessionId, async () => {
270
+ const globalAgents = await this.#readGlobalAgents();
271
+ return {
272
+ type: "result",
273
+ id: message.id,
274
+ inspection: this.#inspection(),
275
+ inputs: this.#inputs(),
276
+ ...(globalAgents ? { globalAgents } : {}),
277
+ };
278
+ });
229
279
  break;
230
280
  case "ackInputs":
231
281
  if (
@@ -245,13 +295,28 @@ export class LocalSession {
245
295
  const queued = this.#queue.findIndex(
246
296
  (request) => request.id === message.id,
247
297
  );
298
+ const request =
299
+ this.#queue[queued] ??
300
+ (this.#active?.request.id === message.id
301
+ ? this.#active.request
302
+ : undefined);
303
+ if (!request) break;
304
+ const name =
305
+ request.type === "call"
306
+ ? [...new Set(request.calls.map((call) => call.name))].join(", ")
307
+ : "chat";
308
+ this.#notify(
309
+ `${name} cancelled for ChatGPT ${request.chatId.slice(-4)}: ${message.reason}`,
310
+ "warning",
311
+ );
248
312
  if (queued !== -1) {
249
313
  this.#queue.splice(queued, 1);
250
314
  break;
251
315
  }
252
- if (this.#active?.request.id !== message.id) break;
253
- this.#active.cancelled = true;
254
- if (this.#active.completed) await this.#completeActive();
316
+ const active = this.#active;
317
+ if (!active) break;
318
+ active.cancelled = message.reason;
319
+ if (active.completed) await this.#completeActive();
255
320
  else this.#context?.abort();
256
321
  break;
257
322
  }
@@ -286,6 +351,15 @@ export class LocalSession {
286
351
  };
287
352
  }
288
353
 
354
+ async #readGlobalAgents(): Promise<string | undefined> {
355
+ try {
356
+ return await readFile(join(this.#agentDir, "AGENTS.md"), "utf8");
357
+ } catch (error) {
358
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
359
+ throw error;
360
+ }
361
+ }
362
+
289
363
  #dispatch(): void {
290
364
  if (this.#active) return;
291
365
  const request = this.#queue[0];
@@ -302,7 +376,7 @@ export class LocalSession {
302
376
  session: this.#description(),
303
377
  message: output.message,
304
378
  completed: false,
305
- cancelled: false,
379
+ cancelled: undefined,
306
380
  toolResults: [],
307
381
  };
308
382
  this.#status = "executing";
@@ -355,26 +429,17 @@ export class LocalSession {
355
429
  if (!active?.completed) return;
356
430
  this.#collectInputs();
357
431
  const inputs = this.#inputs();
358
- if (active.cancelled) {
359
- const sessionFile = active.session.sessionFile;
432
+ if (active.cancelled !== undefined) {
360
433
  const delivery: DeliveryRecord = {
361
434
  id: randomUUID(),
362
435
  chatId: active.request.chatId,
363
436
  sessionId: active.session.id,
364
437
  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" }),
438
+ toolResults: active.toolResults,
439
+ error: active.cancelled,
375
440
  };
376
441
  this.#deliveries.set(delivery.id, delivery);
377
- await this.#flushDeliveries().catch(() => {});
442
+ void this.#flushDeliveries().catch(() => {});
378
443
  } else {
379
444
  await this.#connection?.send({
380
445
  type: "result",
@@ -392,29 +457,47 @@ export class LocalSession {
392
457
  void this.#sync().catch(() => {});
393
458
  }
394
459
 
460
+ #resetInputs(
461
+ context?: ExtensionContext,
462
+ cursor = context?.sessionManager.getLeafId() ?? null,
463
+ ): void {
464
+ this.#sessionId = context?.sessionManager.getSessionId();
465
+ this.#inputCursor = cursor;
466
+ this.#pendingInputs.clear();
467
+ }
468
+
395
469
  #collectInputs(): void {
396
470
  const context = this.#context;
397
471
  if (!context) return;
398
- const sessionId = context.sessionManager.getSessionId();
472
+ const sessionManager = context.sessionManager;
473
+ const sessionId = sessionManager.getSessionId();
399
474
  if (this.#sessionId !== sessionId) {
400
- this.#sessionId = sessionId;
401
- this.#seenInputs.clear();
402
- this.#pendingInputs.clear();
475
+ this.#resetInputs(context);
476
+ return;
477
+ }
478
+ const leafId = sessionManager.getLeafId();
479
+ if (leafId === this.#inputCursor) return;
480
+ const entries: SessionEntry[] = [];
481
+ let current = sessionManager.getLeafEntry();
482
+ while (current && current.id !== this.#inputCursor) {
483
+ entries.push(current);
484
+ current = current.parentId
485
+ ? sessionManager.getEntry(current.parentId)
486
+ : undefined;
487
+ }
488
+ if (this.#inputCursor !== null && !current) {
489
+ this.#resetInputs(context);
490
+ return;
403
491
  }
404
- for (const entry of context.sessionManager.getBranch()) {
405
- if (
406
- entry.type !== "message" ||
407
- entry.message.role !== "user" ||
408
- this.#seenInputs.has(entry.id)
409
- )
410
- continue;
492
+ for (const entry of entries.reverse()) {
493
+ if (entry.type !== "message" || entry.message.role !== "user") continue;
411
494
  const message = entry.message as UserMessage;
412
495
  if (typeof message.content !== "string") {
413
496
  rememberImages(sessionId, message.content);
414
497
  }
415
- this.#seenInputs.add(entry.id);
416
498
  this.#pendingInputs.set(entry.id, { id: entry.id, sessionId, message });
417
499
  }
500
+ this.#inputCursor = leafId;
418
501
  }
419
502
 
420
503
  #inputs(): SessionInput[] {
@@ -433,6 +516,7 @@ export class LocalSession {
433
516
  await connection.send({ type: "delivery", delivery });
434
517
  await completion.promise;
435
518
  this.#deliveries.delete(delivery.id);
519
+ this.#notify(`Result saved for ChatGPT ${delivery.chatId.slice(-4)}`);
436
520
  } finally {
437
521
  this.#stores.delete(delivery.id);
438
522
  }
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
- removeDeliveries(ids: string[]): Promise<void> {
68
- for (const id of ids) this.#deliveries.delete(id);
69
- return this.#save();
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 restoreDeliveries(deliveries: DeliveryRecord[]): Promise<void> {
73
- for (const delivery of deliveries)
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 { createWriteStream } from "node:fs";
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 = createWriteStream("", { fd: 1, autoClose: false });
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.flatMap((result) => [
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, or export Pi paths and Chappie image references as MCP resources. The paths field always names Pi-side sources or destinations.",
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 paths to import into or export from; chappie:// image references can be exported",
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 matched to paths by index; omit to export Pi paths",
42
+ "ChatGPT files paired with paths in order; omit for export",
43
43
  }),
44
44
  ),
45
45
  overwrite: Type.Optional(