@zetaloop/chappie 0.5.0 → 1.0.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/src/questions.ts DELETED
@@ -1,87 +0,0 @@
1
- import * as z from "zod";
2
-
3
- export const questionInstructions =
4
- "ask requests a question widget in ChatGPT; its result confirms creation of the request. Immediately call ask_assert with question.id to confirm loading. If the widget fails to load within 10 seconds, the assertion fails and records the question as skipped. Use an installed Pi interactive tool through call when an answer is needed. User answers arrive separately as webAnswer in normal tool results. Apply answers and revisions promptly; a user skip means proceed with available information.";
5
-
6
- export const questionInput = z.object({
7
- header: z.string().trim().min(1).optional().describe("Short topic label"),
8
- question: z
9
- .string()
10
- .trim()
11
- .min(1)
12
- .describe("One focused question for the user"),
13
- context: z
14
- .string()
15
- .optional()
16
- .describe("Background needed to answer the question"),
17
- options: z
18
- .array(
19
- z.object({
20
- title: z.string().trim().min(1).describe("Short choice label"),
21
- description: z
22
- .string()
23
- .optional()
24
- .describe("Brief consequence or tradeoff of this choice"),
25
- recommended: z
26
- .boolean()
27
- .optional()
28
- .describe("Preferred choice; place it first"),
29
- }),
30
- )
31
- .default([])
32
- .describe(
33
- "Distinct choices, usually two or three. Freeform answers and skipping are available separately.",
34
- ),
35
- allowMultiple: z
36
- .boolean()
37
- .default(false)
38
- .describe("Allow selecting several options"),
39
- });
40
-
41
- export const answerInput = z.object({
42
- selections: z.array(z.number().int().nonnegative()).default([]),
43
- text: z.string().trim().default(""),
44
- skipped: z.literal(true).optional().describe("The question was skipped"),
45
- });
46
-
47
- export const questionOutput = questionInput.extend({
48
- id: z.string().describe("Generated question ID for ask_assert"),
49
- sessionId: z.string(),
50
- cwd: z.string(),
51
- answer: answerInput.optional(),
52
- loaded: z.boolean().optional().describe("The widget has reported loading"),
53
- });
54
-
55
- export type QuestionInput = z.infer<typeof questionInput>;
56
- export type QuestionAnswer = z.infer<typeof answerInput>;
57
- export type Question = z.infer<typeof questionOutput>;
58
-
59
- export interface QuestionRecord extends Question {
60
- chatId: string;
61
- delivered: boolean;
62
- }
63
-
64
- export function questionView(record: QuestionRecord): Question {
65
- const { chatId: _chatId, delivered: _delivered, ...question } = record;
66
- return question;
67
- }
68
-
69
- export function answerContent(records: QuestionRecord[]) {
70
- return records.map((record) => ({
71
- type: "text" as const,
72
- text: JSON.stringify({
73
- webAnswer: {
74
- questionId: record.id,
75
- sessionId: record.sessionId,
76
- cwd: record.cwd,
77
- header: record.header,
78
- question: record.question,
79
- skipped: record.answer?.skipped,
80
- choices: record.answer?.selections.map(
81
- (index) => record.options[index],
82
- ),
83
- text: record.answer?.text,
84
- },
85
- }),
86
- }));
87
- }
package/src/resources.ts DELETED
@@ -1,209 +0,0 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { open, readFile, stat } from "node:fs/promises";
3
- import { basename } from "node:path";
4
- import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
5
- import mime from "mime";
6
-
7
- export interface ResourceDescriptor {
8
- uri: string;
9
- name: string;
10
- mimeType: string;
11
- size: number;
12
- }
13
-
14
- export interface ResourceData extends ResourceDescriptor {
15
- blob: string;
16
- }
17
-
18
- type ResourceEntry =
19
- | { type: "file"; path: string; descriptor: ResourceDescriptor }
20
- | { type: "image"; data: string; descriptor: ResourceDescriptor };
21
-
22
- const stores = new Map<string, Map<string, ResourceEntry>>();
23
-
24
- export async function registerFile(
25
- sessionId: string,
26
- path: string,
27
- ): Promise<ResourceDescriptor> {
28
- const info = await stat(path);
29
- if (!info.isFile()) throw new Error(`${path} is not a file`);
30
- const name = basename(path);
31
- const descriptor = resourceDescriptor(
32
- sessionId,
33
- "file",
34
- randomUUID(),
35
- name,
36
- mime.getType(path) ?? "application/octet-stream",
37
- info.size,
38
- );
39
- store(sessionId).set(descriptor.uri, { type: "file", path, descriptor });
40
- return descriptor;
41
- }
42
-
43
- export function rememberImages(
44
- sessionId: string,
45
- content: readonly (TextContent | ImageContent)[],
46
- ): void {
47
- for (const block of content) {
48
- if (block.type !== "image") continue;
49
- const descriptor = imageDescriptor(sessionId, block);
50
- store(sessionId).set(descriptor.uri, {
51
- type: "image",
52
- data: block.data,
53
- descriptor,
54
- });
55
- }
56
- }
57
-
58
- export function imageDescriptor(
59
- sessionId: string,
60
- image: ImageContent,
61
- ): ResourceDescriptor {
62
- const digest = createHash("sha256")
63
- .update(image.data, "base64")
64
- .digest("hex");
65
- const extension = mime.getExtension(image.mimeType) ?? "bin";
66
- return resourceDescriptor(
67
- sessionId,
68
- "image",
69
- digest,
70
- `image.${extension}`,
71
- image.mimeType,
72
- Buffer.byteLength(image.data, "base64"),
73
- );
74
- }
75
-
76
- export function contentWithImageReferences(
77
- sessionId: string,
78
- content: readonly (TextContent | ImageContent)[],
79
- ): (TextContent | ImageContent)[] {
80
- return content.flatMap((block) =>
81
- block.type === "image"
82
- ? [
83
- block,
84
- {
85
- type: "text" as const,
86
- text: JSON.stringify({
87
- piImage: imageDescriptor(sessionId, block).uri,
88
- }),
89
- },
90
- ]
91
- : [block],
92
- );
93
- }
94
-
95
- export function resourceDescriptors(details: unknown): ResourceDescriptor[] {
96
- if (!details || typeof details !== "object") return [];
97
- const resources = (details as { resources?: unknown }).resources;
98
- if (!Array.isArray(resources)) return [];
99
- return resources.filter((resource): resource is ResourceDescriptor => {
100
- if (!resource || typeof resource !== "object") return false;
101
- const value = resource as Partial<ResourceDescriptor>;
102
- return (
103
- typeof value.uri === "string" &&
104
- typeof value.name === "string" &&
105
- typeof value.mimeType === "string" &&
106
- typeof value.size === "number"
107
- );
108
- });
109
- }
110
-
111
- export function describeResource(
112
- sessionId: string,
113
- uri: string,
114
- ): ResourceDescriptor {
115
- const parsed = parseResourceUri(uri);
116
- if (parsed.sessionId !== sessionId) {
117
- throw new Error("The resource belongs to another Pi session");
118
- }
119
- const entry = store(sessionId).get(uri);
120
- if (!entry) throw new Error(`Unknown Chappie resource: ${uri}`);
121
- return entry.descriptor;
122
- }
123
-
124
- export async function readSessionResource(
125
- sessionId: string,
126
- uri: string,
127
- offset?: number,
128
- ): Promise<ResourceData> {
129
- const descriptor = describeResource(sessionId, uri);
130
- const entry = store(sessionId).get(uri);
131
- if (!entry) throw new Error(`Unknown Chappie resource: ${uri}`);
132
- if (offset === undefined) {
133
- const blob =
134
- entry.type === "file"
135
- ? (await readFile(entry.path)).toString("base64")
136
- : entry.data;
137
- return { ...descriptor, blob };
138
- }
139
- if (!Number.isSafeInteger(offset) || offset < 0)
140
- throw new Error("Resource offset must be a nonnegative integer");
141
- const length = Math.min(1024 * 1024, Math.max(0, descriptor.size - offset));
142
- let data: Buffer;
143
- if (entry.type === "file") {
144
- await using file = await open(entry.path, "r");
145
- const { buffer, bytesRead } = await file.read(
146
- Buffer.alloc(length),
147
- 0,
148
- length,
149
- offset,
150
- );
151
- data = buffer.subarray(0, bytesRead);
152
- } else {
153
- data = Buffer.from(entry.data, "base64").subarray(offset, offset + length);
154
- }
155
- return { ...descriptor, blob: data.toString("base64") };
156
- }
157
-
158
- export function resourceSessionId(uri: string): string {
159
- return parseResourceUri(uri).sessionId;
160
- }
161
-
162
- function store(sessionId: string): Map<string, ResourceEntry> {
163
- let resources = stores.get(sessionId);
164
- if (!resources) {
165
- resources = new Map();
166
- stores.set(sessionId, resources);
167
- }
168
- return resources;
169
- }
170
-
171
- function resourceDescriptor(
172
- sessionId: string,
173
- kind: "file" | "image",
174
- id: string,
175
- name: string,
176
- mimeType: string,
177
- size: number,
178
- ): ResourceDescriptor {
179
- return {
180
- uri: `chappie://session/${encodeURIComponent(sessionId)}/${kind}/${encodeURIComponent(id)}/${encodeURIComponent(name)}`,
181
- name,
182
- mimeType,
183
- size,
184
- };
185
- }
186
-
187
- function parseResourceUri(uri: string): {
188
- sessionId: string;
189
- kind: string;
190
- id: string;
191
- name: string;
192
- } {
193
- const parsed = new URL(uri);
194
- if (parsed.protocol !== "chappie:" || parsed.hostname !== "session") {
195
- throw new Error(`Unsupported Chappie resource: ${uri}`);
196
- }
197
- const parts = parsed.pathname
198
- .slice(1)
199
- .split("/")
200
- .map((part) => decodeURIComponent(part));
201
- if (parts.length !== 4 || !parts.every(Boolean)) {
202
- throw new Error(`Invalid Chappie resource: ${uri}`);
203
- }
204
- const [sessionId, kind, id, name] = parts;
205
- if (!sessionId || !kind || !id || !name) {
206
- throw new Error(`Invalid Chappie resource: ${uri}`);
207
- }
208
- return { sessionId, kind, id, name };
209
- }