@kaoruisaac/pedelec 0.4.6 → 0.4.8
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 +11 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +188 -71
- package/dist/index.type-test.js +15 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -286,6 +286,17 @@ console.log(session.workspace.path); // null for managed Workspace
|
|
|
286
286
|
|
|
287
287
|
`workspace.listFiles(path?)` and `workspace.listFolders(path?)` recursively return Workspace-relative paths. An omitted path lists from the Workspace root; `/` is the separator and results are lexicographically sorted. The `.pedelec-runtime` directory is included. Symlinks and junctions are neither followed nor returned. Results are not silently truncated: an oversized response fails with `WORKSPACE_LIST_TOO_LARGE`, so retry with a narrower path.
|
|
288
288
|
|
|
289
|
+
`workspace.uploadFile(file, targetPath?)` and `workspace.readFile(path, type)` transfer regular files anywhere in the Workspace, including `.pedelec-runtime`. Paths are Workspace-relative, use `/`, and do not start with `/`. `uploadFile(file)` stores the original filename at the Workspace root and does not add a generated prefix. The limit is 100 MiB. Workspace uploads may run concurrently; shared filesystem conflicts remain the application's responsibility. `readFile()` accepts `"text"`, `"json"`, or `"file"`.
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
const workspace = await pedelec.openWorkspace("C:\\workspace\\project");
|
|
293
|
+
const file = new File(["hello"], "hello.txt", { type: "text/plain" });
|
|
294
|
+
const path = await workspace.uploadFile(file, "references/photo.png");
|
|
295
|
+
const text = await workspace.readFile("README.md", "text");
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
`session.uploadAsset()`, `readAsset()`, and `listAssets()` stay available as Session-scoped conveniences. Their root is `.pedelec-runtime/assets`, which suits provider artifacts and Agent-shared assets. Use the Workspace methods for general filesystem access.
|
|
299
|
+
|
|
289
300
|
`workspace.run(script, options?)` executes application-supplied Deno code in the Workspace:
|
|
290
301
|
|
|
291
302
|
```ts
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,8 @@ export type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
|
9
9
|
[key: string]: JsonValue;
|
|
10
10
|
};
|
|
11
11
|
export type AssetPath = `/${string}`;
|
|
12
|
-
export type
|
|
12
|
+
export type ReadFileType = "text" | "json" | "file";
|
|
13
|
+
export type ReadAssetType = ReadFileType;
|
|
13
14
|
export type Asset = {
|
|
14
15
|
name: string;
|
|
15
16
|
path: AssetPath;
|
|
@@ -361,6 +362,11 @@ export declare class PedelecWorkspace {
|
|
|
361
362
|
createSession<const TTools extends readonly ToolDefinition[]>(input: CreateSessionInput<TTools>): Promise<PedelecSession<ToolNameOf<TTools>>>;
|
|
362
363
|
listFiles(path?: string): Promise<string[]>;
|
|
363
364
|
listFolders(path?: string): Promise<string[]>;
|
|
365
|
+
uploadFile(file: File): Promise<string>;
|
|
366
|
+
uploadFile(file: File, targetPath: string): Promise<string>;
|
|
367
|
+
readFile(path: string, type: "text"): Promise<string>;
|
|
368
|
+
readFile<T = JsonValue>(path: string, type: "json"): Promise<T>;
|
|
369
|
+
readFile(path: string, type: "file"): Promise<File>;
|
|
364
370
|
run(script: string, options?: WorkspaceRunOptions): Promise<WorkspaceRunResult>;
|
|
365
371
|
/** @internal */
|
|
366
372
|
get workspaceIdInternal(): string;
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,118 @@ function isValidAssetPath(path) {
|
|
|
31
31
|
return typeof path === "string" && /^\/(?:[^/\\\x00-\x1f]+)(?:\/[^/\\\x00-\x1f]+)*$/.test(path) &&
|
|
32
32
|
path.split("/").every((part, index) => index === 0 || (part !== "." && part !== ".."));
|
|
33
33
|
}
|
|
34
|
+
function isValidWorkspaceFilePath(path) {
|
|
35
|
+
return typeof path === "string" &&
|
|
36
|
+
/^(?:[^/\\:\x00-\x1f]+)(?:\/[^/\\:\x00-\x1f]+)*$/.test(path) &&
|
|
37
|
+
path.split("/").every((part) => part !== "." && part !== "..");
|
|
38
|
+
}
|
|
39
|
+
function isReadFileType(type) {
|
|
40
|
+
return type === "text" || type === "json" || type === "file";
|
|
41
|
+
}
|
|
42
|
+
function rejectBrowserFile(file, message) {
|
|
43
|
+
if (typeof File === "undefined" || !(file instanceof File) || !file.name || file.size < 0) {
|
|
44
|
+
return makeError("INVALID_INPUT", message);
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function uploadBrowserFile(client, request) {
|
|
49
|
+
return client.request(request.type, request.payload).then(async (ticket) => {
|
|
50
|
+
if (!ticket?.uploadUrl || !ticket.token) {
|
|
51
|
+
throw makeError("SDK_PROTOCOL_ERROR", "file upload ticket was invalid");
|
|
52
|
+
}
|
|
53
|
+
let response;
|
|
54
|
+
try {
|
|
55
|
+
response = await fetch(ticket.uploadUrl, {
|
|
56
|
+
method: "PUT",
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: `Bearer ${ticket.token}`,
|
|
59
|
+
"Content-Type": request.file.type || "application/octet-stream",
|
|
60
|
+
},
|
|
61
|
+
body: request.file,
|
|
62
|
+
credentials: "omit",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw normalizeError(error, request.failedCode, request.failedMessage);
|
|
67
|
+
}
|
|
68
|
+
let payload = null;
|
|
69
|
+
try {
|
|
70
|
+
payload = await response.json();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// normalized below
|
|
74
|
+
}
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw normalizeError(payload?.error, request.failedCode, request.failedMessage);
|
|
77
|
+
}
|
|
78
|
+
if (!request.acceptPath(payload?.path)) {
|
|
79
|
+
throw makeError("SDK_PROTOCOL_ERROR", "file upload response had an invalid path");
|
|
80
|
+
}
|
|
81
|
+
if (request.expectedPath !== undefined && payload?.path !== request.expectedPath) {
|
|
82
|
+
throw makeError("SDK_PROTOCOL_ERROR", "file upload response path did not match target path");
|
|
83
|
+
}
|
|
84
|
+
return payload?.path;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function downloadBrowserFile(client, request) {
|
|
88
|
+
return client.request(request.type, request.payload).then(async (ticket) => {
|
|
89
|
+
if (!ticket || typeof ticket !== "object") {
|
|
90
|
+
throw makeError("SDK_PROTOCOL_ERROR", "file download ticket was invalid");
|
|
91
|
+
}
|
|
92
|
+
const value = ticket;
|
|
93
|
+
if (typeof value.downloadUrl !== "string" ||
|
|
94
|
+
typeof value.token !== "string" ||
|
|
95
|
+
typeof value.name !== "string" ||
|
|
96
|
+
typeof value.mimeType !== "string" ||
|
|
97
|
+
typeof value.modifiedAt !== "number" ||
|
|
98
|
+
!Number.isFinite(value.modifiedAt) ||
|
|
99
|
+
!request.acceptPath(value.path)) {
|
|
100
|
+
throw makeError("SDK_PROTOCOL_ERROR", "file download ticket was invalid");
|
|
101
|
+
}
|
|
102
|
+
let response;
|
|
103
|
+
try {
|
|
104
|
+
response = await fetch(value.downloadUrl, {
|
|
105
|
+
headers: { Authorization: `Bearer ${value.token}` },
|
|
106
|
+
credentials: "omit",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
throw normalizeError(error, request.failedCode, request.failedMessage);
|
|
111
|
+
}
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
let payload;
|
|
114
|
+
try {
|
|
115
|
+
payload = await response.json();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// normalize below
|
|
119
|
+
}
|
|
120
|
+
throw normalizeError(payload?.error, request.failedCode, request.failedMessage);
|
|
121
|
+
}
|
|
122
|
+
const bytes = await response.arrayBuffer();
|
|
123
|
+
if (request.readType === "file") {
|
|
124
|
+
return new File([bytes], value.name, {
|
|
125
|
+
type: value.mimeType || "application/octet-stream",
|
|
126
|
+
lastModified: value.modifiedAt,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
let text;
|
|
130
|
+
try {
|
|
131
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
throw makeError(request.textDecodeCode, request.textDecodeMessage);
|
|
135
|
+
}
|
|
136
|
+
if (request.readType === "text")
|
|
137
|
+
return text;
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(text);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
throw makeError(request.invalidJsonCode, request.invalidJsonMessage);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
34
146
|
const MAX_ASSET_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
35
147
|
const TOOL_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_.-]*$/;
|
|
36
148
|
export function defineTool(tool) {
|
|
@@ -251,6 +363,52 @@ export class PedelecWorkspace {
|
|
|
251
363
|
listFolders(path) {
|
|
252
364
|
return this.list("workspace_list_folders", path);
|
|
253
365
|
}
|
|
366
|
+
uploadFile(file, targetPath) {
|
|
367
|
+
const invalidFile = rejectBrowserFile(file, "uploadFile requires a browser File with a filename");
|
|
368
|
+
if (invalidFile)
|
|
369
|
+
return Promise.reject(invalidFile);
|
|
370
|
+
if (file.size > MAX_ASSET_UPLOAD_BYTES) {
|
|
371
|
+
return Promise.reject(makeError("WORKSPACE_FILE_TOO_LARGE", "workspace file exceeds the 100 MiB limit"));
|
|
372
|
+
}
|
|
373
|
+
if (targetPath !== undefined && !isValidWorkspaceFilePath(targetPath)) {
|
|
374
|
+
return Promise.reject(makeError("WORKSPACE_PATH_INVALID", "workspace target path is invalid"));
|
|
375
|
+
}
|
|
376
|
+
if (targetPath === undefined && !isValidWorkspaceFilePath(file.name)) {
|
|
377
|
+
return Promise.reject(makeError("WORKSPACE_PATH_INVALID", "workspace filename is invalid"));
|
|
378
|
+
}
|
|
379
|
+
return uploadBrowserFile(this.client, {
|
|
380
|
+
type: "create_workspace_file_upload",
|
|
381
|
+
payload: {
|
|
382
|
+
workspaceId: this.workspaceId,
|
|
383
|
+
filename: file.name,
|
|
384
|
+
sizeBytes: file.size,
|
|
385
|
+
mimeType: file.type,
|
|
386
|
+
...(targetPath === undefined ? {} : { targetPath }),
|
|
387
|
+
},
|
|
388
|
+
file,
|
|
389
|
+
expectedPath: targetPath ?? file.name,
|
|
390
|
+
acceptPath: isValidWorkspaceFilePath,
|
|
391
|
+
failedCode: "WORKSPACE_FILE_UPLOAD_FAILED",
|
|
392
|
+
failedMessage: "workspace file upload failed",
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
readFile(path, type) {
|
|
396
|
+
if (!isValidWorkspaceFilePath(path) || !isReadFileType(type)) {
|
|
397
|
+
return Promise.reject(makeError("WORKSPACE_PATH_INVALID", "workspace path or read type is invalid"));
|
|
398
|
+
}
|
|
399
|
+
return downloadBrowserFile(this.client, {
|
|
400
|
+
type: "create_workspace_file_download",
|
|
401
|
+
payload: { workspaceId: this.workspaceId, path },
|
|
402
|
+
readType: type,
|
|
403
|
+
acceptPath: isValidWorkspaceFilePath,
|
|
404
|
+
failedCode: "WORKSPACE_FILE_READ_FAILED",
|
|
405
|
+
failedMessage: "workspace file read failed",
|
|
406
|
+
textDecodeCode: "WORKSPACE_FILE_TEXT_DECODE_FAILED",
|
|
407
|
+
textDecodeMessage: "workspace file is not valid UTF-8",
|
|
408
|
+
invalidJsonCode: "WORKSPACE_FILE_INVALID_JSON",
|
|
409
|
+
invalidJsonMessage: "workspace file does not contain valid JSON",
|
|
410
|
+
});
|
|
411
|
+
}
|
|
254
412
|
run(script, options = {}) {
|
|
255
413
|
if (typeof script !== "string") {
|
|
256
414
|
return Promise.reject(makeError("INVALID_INPUT", "script must be a string"));
|
|
@@ -1109,39 +1267,28 @@ export class PedelecSession {
|
|
|
1109
1267
|
if (this.uploadPromise) {
|
|
1110
1268
|
return Promise.reject(makeError("SESSION_BUSY", "session is busy", { sessionId: this.sessionId }));
|
|
1111
1269
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1270
|
+
const invalidFile = rejectBrowserFile(file, "uploadAsset requires a browser File with a filename");
|
|
1271
|
+
if (invalidFile)
|
|
1272
|
+
return Promise.reject(invalidFile);
|
|
1115
1273
|
if (file.size > MAX_ASSET_UPLOAD_BYTES)
|
|
1116
1274
|
return Promise.reject(makeError("ASSET_TOO_LARGE", "asset exceeds the 100 MiB limit"));
|
|
1117
1275
|
if (targetPath !== undefined && !isValidAssetPath(targetPath))
|
|
1118
1276
|
return Promise.reject(makeError("ASSET_PATH_INVALID", "asset target path is invalid"));
|
|
1119
|
-
this.uploadPromise = this.client
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
payload = await response.json();
|
|
1135
|
-
}
|
|
1136
|
-
catch { /* normalized below */ }
|
|
1137
|
-
if (!response.ok)
|
|
1138
|
-
throw normalizeError(payload?.error, "ASSET_UPLOAD_FAILED", "asset upload failed");
|
|
1139
|
-
if (!isValidAssetPath(payload?.path))
|
|
1140
|
-
throw makeError("SDK_PROTOCOL_ERROR", "asset upload response had an invalid path");
|
|
1141
|
-
if (targetPath !== undefined && payload.path !== targetPath)
|
|
1142
|
-
throw makeError("SDK_PROTOCOL_ERROR", "asset upload response path did not match target path");
|
|
1143
|
-
return payload.path;
|
|
1144
|
-
}).finally(() => { this.uploadPromise = null; });
|
|
1277
|
+
this.uploadPromise = uploadBrowserFile(this.client, {
|
|
1278
|
+
type: "create_asset_upload",
|
|
1279
|
+
payload: {
|
|
1280
|
+
sessionId: this.sessionId,
|
|
1281
|
+
filename: file.name,
|
|
1282
|
+
sizeBytes: file.size,
|
|
1283
|
+
mimeType: file.type,
|
|
1284
|
+
...(targetPath === undefined ? {} : { targetPath }),
|
|
1285
|
+
},
|
|
1286
|
+
file,
|
|
1287
|
+
expectedPath: targetPath,
|
|
1288
|
+
acceptPath: isValidAssetPath,
|
|
1289
|
+
failedCode: "ASSET_UPLOAD_FAILED",
|
|
1290
|
+
failedMessage: "asset upload failed",
|
|
1291
|
+
}).then((path) => path).finally(() => { this.uploadPromise = null; });
|
|
1145
1292
|
return this.uploadPromise;
|
|
1146
1293
|
}
|
|
1147
1294
|
listAssets() {
|
|
@@ -1153,50 +1300,20 @@ export class PedelecSession {
|
|
|
1153
1300
|
readAsset(path, type) {
|
|
1154
1301
|
if (this.transportDetached || this.ending || this.status === "ended")
|
|
1155
1302
|
return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
|
|
1156
|
-
if (!isValidAssetPath(path) || !
|
|
1303
|
+
if (!isValidAssetPath(path) || !isReadFileType(type)) {
|
|
1157
1304
|
return Promise.reject(makeError("ASSET_PATH_INVALID", "asset path or read type is invalid"));
|
|
1158
1305
|
}
|
|
1159
|
-
return this.client
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
}
|
|
1171
|
-
catch (error) {
|
|
1172
|
-
throw normalizeError(error, "ASSET_READ_FAILED", "asset read failed");
|
|
1173
|
-
}
|
|
1174
|
-
if (!response.ok) {
|
|
1175
|
-
let payload;
|
|
1176
|
-
try {
|
|
1177
|
-
payload = await response.json();
|
|
1178
|
-
}
|
|
1179
|
-
catch { /* normalize below */ }
|
|
1180
|
-
throw normalizeError(payload?.error, "ASSET_READ_FAILED", "asset read failed");
|
|
1181
|
-
}
|
|
1182
|
-
const bytes = await response.arrayBuffer();
|
|
1183
|
-
if (type === "file")
|
|
1184
|
-
return new File([bytes], value.name, { type: value.mimeType || "application/octet-stream", lastModified: value.modifiedAt });
|
|
1185
|
-
let text;
|
|
1186
|
-
try {
|
|
1187
|
-
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1188
|
-
}
|
|
1189
|
-
catch {
|
|
1190
|
-
throw makeError("ASSET_TEXT_DECODE_FAILED", "asset is not valid UTF-8");
|
|
1191
|
-
}
|
|
1192
|
-
if (type === "text")
|
|
1193
|
-
return text;
|
|
1194
|
-
try {
|
|
1195
|
-
return JSON.parse(text);
|
|
1196
|
-
}
|
|
1197
|
-
catch {
|
|
1198
|
-
throw makeError("ASSET_INVALID_JSON", "asset does not contain valid JSON");
|
|
1199
|
-
}
|
|
1306
|
+
return downloadBrowserFile(this.client, {
|
|
1307
|
+
type: "create_asset_download",
|
|
1308
|
+
payload: { sessionId: this.sessionId, path },
|
|
1309
|
+
readType: type,
|
|
1310
|
+
acceptPath: isValidAssetPath,
|
|
1311
|
+
failedCode: "ASSET_READ_FAILED",
|
|
1312
|
+
failedMessage: "asset read failed",
|
|
1313
|
+
textDecodeCode: "ASSET_TEXT_DECODE_FAILED",
|
|
1314
|
+
textDecodeMessage: "asset is not valid UTF-8",
|
|
1315
|
+
invalidJsonCode: "ASSET_INVALID_JSON",
|
|
1316
|
+
invalidJsonMessage: "asset does not contain valid JSON",
|
|
1200
1317
|
});
|
|
1201
1318
|
}
|
|
1202
1319
|
onError(handler) {
|
package/dist/index.type-test.js
CHANGED
|
@@ -251,6 +251,20 @@ async function workspacePublicTypeContract() {
|
|
|
251
251
|
// @ts-expect-error the removed picker API must not be public
|
|
252
252
|
pedelec.workspaceFolderPicker();
|
|
253
253
|
}
|
|
254
|
+
async function workspaceFileTransferTypes() {
|
|
255
|
+
const pedelec = new Pedelec();
|
|
256
|
+
const workspace = await pedelec.openWorkspace("C:\\workspace\\project");
|
|
257
|
+
const file = new File(["hello"], "hello.txt", { type: "text/plain" });
|
|
258
|
+
const generated = await workspace.uploadFile(file);
|
|
259
|
+
const nested = await workspace.uploadFile(file, "references/photo.png");
|
|
260
|
+
const text = await workspace.readFile("README.md", "text");
|
|
261
|
+
const parsed = await workspace.readFile("result.json", "json");
|
|
262
|
+
const downloaded = await workspace.readFile(generated, "file");
|
|
263
|
+
void nested;
|
|
264
|
+
void text;
|
|
265
|
+
void parsed;
|
|
266
|
+
void downloaded;
|
|
267
|
+
}
|
|
254
268
|
function directoryPickerIsRemoved() {
|
|
255
269
|
const pedelec = new Pedelec();
|
|
256
270
|
// @ts-expect-error directoryPicker was removed in favor of openWorkspace
|
|
@@ -277,6 +291,7 @@ void effortLevelPublicTypeContract;
|
|
|
277
291
|
void listAssetsHasPublicTypes;
|
|
278
292
|
void availabilityHasPublicType;
|
|
279
293
|
void workspacePublicTypeContract;
|
|
294
|
+
void workspaceFileTransferTypes;
|
|
280
295
|
void directoryPickerIsRemoved;
|
|
281
296
|
void publicSecurityTypesAreRestricted;
|
|
282
297
|
const baseContext = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.4.
|
|
1
|
+
export declare const SDK_VERSION = "0.4.8";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated from sdk/package.json. Do not edit manually.
|
|
2
|
-
export const SDK_VERSION = "0.4.
|
|
2
|
+
export const SDK_VERSION = "0.4.8";
|