@kaoruisaac/pedelec 0.4.6 → 0.4.7

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 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 ReadAssetType = "text" | "json" | "file";
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
- if (typeof File === "undefined" || !(file instanceof File) || !file.name || file.size < 0) {
1113
- return Promise.reject(makeError("INVALID_INPUT", "uploadAsset requires a browser File with a filename"));
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.request("create_asset_upload", {
1120
- sessionId: this.sessionId, filename: file.name, sizeBytes: file.size, mimeType: file.type,
1121
- ...(targetPath === undefined ? {} : { targetPath }),
1122
- }).then(async (ticket) => {
1123
- if (!ticket?.uploadUrl || !ticket.token)
1124
- throw makeError("SDK_PROTOCOL_ERROR", "asset upload ticket was invalid");
1125
- let response;
1126
- try {
1127
- response = await fetch(ticket.uploadUrl, { method: "PUT", headers: { Authorization: `Bearer ${ticket.token}`, "Content-Type": file.type || "application/octet-stream" }, body: file, credentials: "omit" });
1128
- }
1129
- catch (error) {
1130
- throw normalizeError(error, "ASSET_UPLOAD_FAILED", "asset upload failed");
1131
- }
1132
- let payload = null;
1133
- try {
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) || !["text", "json", "file"].includes(type)) {
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.request("create_asset_download", { sessionId: this.sessionId, path }).then(async (ticket) => {
1160
- if (!ticket || typeof ticket !== "object")
1161
- throw makeError("SDK_PROTOCOL_ERROR", "asset download ticket was invalid");
1162
- const value = ticket;
1163
- if (typeof value.downloadUrl !== "string" || typeof value.token !== "string" || typeof value.name !== "string" ||
1164
- typeof value.mimeType !== "string" || typeof value.modifiedAt !== "number" || !Number.isFinite(value.modifiedAt) || !isValidAssetPath(value.path)) {
1165
- throw makeError("SDK_PROTOCOL_ERROR", "asset download ticket was invalid");
1166
- }
1167
- let response;
1168
- try {
1169
- response = await fetch(value.downloadUrl, { headers: { Authorization: `Bearer ${value.token}` }, credentials: "omit" });
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) {
@@ -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.6";
1
+ export declare const SDK_VERSION = "0.4.7";
@@ -1,2 +1,2 @@
1
1
  // Generated from sdk/package.json. Do not edit manually.
2
- export const SDK_VERSION = "0.4.6";
2
+ export const SDK_VERSION = "0.4.7";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaoruisaac/pedelec",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",