@zetaloop/chappie 0.1.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/LICENSE +21 -0
- package/README.md +34 -0
- package/docs/tools.md +133 -0
- package/package.json +54 -0
- package/src/broker.ts +448 -0
- package/src/delivery.ts +82 -0
- package/src/index.ts +34 -0
- package/src/instructions.md +7 -0
- package/src/ipc.ts +361 -0
- package/src/provider.ts +195 -0
- package/src/resources.ts +189 -0
- package/src/server.ts +296 -0
- package/src/session.ts +484 -0
- package/src/state.ts +89 -0
- package/src/stdio.ts +49 -0
- package/src/tools.ts +98 -0
- package/src/transfer.ts +152 -0
package/src/transfer.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { mkdir, stat, unlink } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
7
|
+
import { pipeline } from "node:stream/promises";
|
|
8
|
+
import {
|
|
9
|
+
type ExtensionContext,
|
|
10
|
+
withFileMutationQueue,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { Type } from "typebox";
|
|
13
|
+
import { describeResource, registerFile } from "./resources.ts";
|
|
14
|
+
|
|
15
|
+
interface TransferDetails {
|
|
16
|
+
files: ({ path: string; bytes: number } | { path: string; error: string })[];
|
|
17
|
+
resources: import("./resources.ts").ResourceDescriptor[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const transferFile = Type.Object({
|
|
21
|
+
file_id: Type.String(),
|
|
22
|
+
download_url: Type.String(),
|
|
23
|
+
file_name: Type.Optional(Type.String()),
|
|
24
|
+
mime_type: Type.Optional(Type.String()),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const transfer = {
|
|
28
|
+
name: "transfer",
|
|
29
|
+
label: "transfer",
|
|
30
|
+
description:
|
|
31
|
+
"Transfer files between ChatGPT and the current Pi session. Provide files to write them to paths; omit files to export existing paths or Chappie image references.",
|
|
32
|
+
parameters: Type.Object({
|
|
33
|
+
paths: Type.Array(Type.String(), { minItems: 1 }),
|
|
34
|
+
files: Type.Optional(Type.Array(transferFile, { minItems: 1 })),
|
|
35
|
+
overwrite: Type.Optional(
|
|
36
|
+
Type.Boolean({ description: "Overwrite existing target files" }),
|
|
37
|
+
),
|
|
38
|
+
}),
|
|
39
|
+
async execute(
|
|
40
|
+
_id: string,
|
|
41
|
+
args: {
|
|
42
|
+
paths: string[];
|
|
43
|
+
files?: { download_url: string }[];
|
|
44
|
+
overwrite?: boolean;
|
|
45
|
+
},
|
|
46
|
+
signal: AbortSignal | undefined,
|
|
47
|
+
_update: unknown,
|
|
48
|
+
context: ExtensionContext,
|
|
49
|
+
): Promise<{
|
|
50
|
+
content: { type: "text"; text: string }[];
|
|
51
|
+
details: TransferDetails;
|
|
52
|
+
}> {
|
|
53
|
+
const sessionId = context.sessionManager.getSessionId();
|
|
54
|
+
if (!args.files) {
|
|
55
|
+
const resources = await Promise.all(
|
|
56
|
+
args.paths.map((requested) =>
|
|
57
|
+
requested.startsWith("chappie://")
|
|
58
|
+
? describeResource(sessionId, requested)
|
|
59
|
+
: registerFile(sessionId, localPath(requested, context.cwd)),
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
return {
|
|
63
|
+
content: [
|
|
64
|
+
{ type: "text" as const, text: JSON.stringify({ resources }) },
|
|
65
|
+
],
|
|
66
|
+
details: { files: [], resources },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (args.files.length !== args.paths.length) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"files and paths must contain the same number of entries",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const files = await Promise.all(
|
|
76
|
+
args.paths.map(async (requested, index) => {
|
|
77
|
+
const path = localPath(requested, context.cwd);
|
|
78
|
+
const source = args.files?.[index];
|
|
79
|
+
if (!source)
|
|
80
|
+
throw new Error("files and paths must correspond by index");
|
|
81
|
+
try {
|
|
82
|
+
const bytes = await importFile(
|
|
83
|
+
path,
|
|
84
|
+
source.download_url,
|
|
85
|
+
args.overwrite === true,
|
|
86
|
+
signal,
|
|
87
|
+
);
|
|
88
|
+
return { path, bytes };
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return {
|
|
91
|
+
path: requested,
|
|
92
|
+
error: error instanceof Error ? error.message : String(error),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
if (files.some((file) => "error" in file)) {
|
|
98
|
+
throw new Error(JSON.stringify({ files }));
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: "text" as const, text: JSON.stringify({ files }) }],
|
|
102
|
+
details: { files, resources: [] },
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function localPath(path: string, cwd: string): string {
|
|
108
|
+
if (path === "~") return homedir();
|
|
109
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) {
|
|
110
|
+
return resolve(homedir(), path.slice(2));
|
|
111
|
+
}
|
|
112
|
+
return resolve(cwd, path);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function importFile(
|
|
116
|
+
path: string,
|
|
117
|
+
url: string,
|
|
118
|
+
overwrite: boolean,
|
|
119
|
+
signal?: AbortSignal,
|
|
120
|
+
): Promise<number> {
|
|
121
|
+
return withFileMutationQueue(path, async () => {
|
|
122
|
+
signal?.throwIfAborted();
|
|
123
|
+
await mkdir(dirname(path), { recursive: true });
|
|
124
|
+
const writable = createWriteStream(path, {
|
|
125
|
+
flags: overwrite ? "w" : "wx",
|
|
126
|
+
});
|
|
127
|
+
let opened = false;
|
|
128
|
+
try {
|
|
129
|
+
await once(writable, "open");
|
|
130
|
+
opened = true;
|
|
131
|
+
const response = await fetch(url, signal ? { signal } : {});
|
|
132
|
+
if (!response.ok || !response.body) {
|
|
133
|
+
throw new Error(`Download failed with HTTP ${response.status}`);
|
|
134
|
+
}
|
|
135
|
+
const readable = Readable.from(
|
|
136
|
+
response.body as unknown as AsyncIterable<Uint8Array>,
|
|
137
|
+
);
|
|
138
|
+
if (signal) await pipeline(readable, writable, { signal });
|
|
139
|
+
else await pipeline(readable, writable);
|
|
140
|
+
signal?.throwIfAborted();
|
|
141
|
+
return (await stat(path)).size;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (!writable.closed) {
|
|
144
|
+
const closed = once(writable, "close");
|
|
145
|
+
writable.destroy();
|
|
146
|
+
await closed.catch(() => {});
|
|
147
|
+
}
|
|
148
|
+
if (opened) await unlink(path).catch(() => {});
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|