@kaoruisaac/pedelec 0.1.15 → 0.1.17

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
@@ -128,14 +128,22 @@ await session.sendText("Please help me analyze the current page state");
128
128
 
129
129
  `sendText()` resolves after the current agent response completes. If the session is already handling a previous prompt, the new `sendText()` call is rejected to prevent multiple concurrent requests from running in the same session.
130
130
 
131
- ## Listing uploaded assets
131
+ ## Session assets
132
132
 
133
133
  ```ts
134
134
  const path = await session.uploadAsset(file);
135
135
  const assets = await session.listAssets();
136
136
  ```
137
137
 
138
- `listAssets()` lists only the first level of `input/`, ordered by filesystem modification time (newest first). Each item's `name` is the actual sandbox filename and may differ from the original `File.name`; `modifiedAt` is a filesystem modification timestamp, not an exact upload time. `path` is an agent-sandbox-relative `input/...` path and never an absolute local path. It may be called while the agent is running. Ended sessions reject with `SESSION_ENDED`. Recursive listing, pagination, download, deletion, rename, and move are not supported.
138
+ `assets/` is shared by uploads and files written by the agent. `listAssets()` lists only its first level, ordered by filesystem modification time (newest first), and may run while the agent runs. One session can only upload one file at a time, but uploads can run alongside prepare or agent execution.
139
+
140
+ ```ts
141
+ const text = await session.readAsset("assets/report.txt", "text");
142
+ const json = await session.readAsset<{ ok: boolean }>("assets/result.json", "json");
143
+ const result = await session.readAsset("assets/model.glb", "file");
144
+ ```
145
+
146
+ Reads support nested `assets/...` paths up to 100 MiB. `text` and `json` require valid UTF-8; binary content is transferred over a loopback download ticket rather than extension messages.
139
147
 
140
148
  ---
141
149
 
@@ -143,6 +151,8 @@ const assets = await session.listAssets();
143
151
 
144
152
  The first time an origin calls `createSession()` or `resumeSession()`, the extension asks the user to approve that origin in the popup. After approval, the same origin can create sessions directly.
145
153
 
154
+ Sessions created through the SDK are bound to the creating origin. The same origin may resume them after reload, in another tab, or from another SDK instance; a different scheme, host, subdomain, or port cannot use the ID. A `sessionId` is an identifier, not a cross-origin bearer credential, and applications cannot supply or override its owner origin.
155
+
146
156
  You can query the current origin's approval status first to decide whether to show UI such as "Connect Pedelec":
147
157
 
148
158
  ```ts
@@ -151,7 +161,11 @@ const status = await pedelec.getApprovalStatus();
151
161
  console.log(status.installed, status.approved, status.origin);
152
162
  ```
153
163
 
154
- For a complete Extension, approval, and Desktop readiness probe, use `checkAvailability()`. It never creates/resumes a session or opens approval. When approved it validates Desktop by calling `getSettings()`, which may invoke the existing Native Host auto-launch fallback.
164
+ `getApprovalStatus()` includes `appConnected`, a non-sensitive Core `ping` result. It does not require approval or open the approval popup, but it may use the Native Host's existing Desktop auto-launch fallback. `appConnected` does not mean the origin is approved or a provider is ready.
165
+
166
+ `getSettings()` and `listProviders()` require origin approval and may open the approval popup. Settings expose only defaults (never provider credentials); providers expose only `name`, `code`, `available`, and `error`. Use `getApprovalStatus().appConnected` rather than `listProviders()` as a connection probe.
167
+
168
+ For a complete Extension, approval, and Desktop readiness probe, use `checkAvailability()`. It never creates/resumes a session or opens approval. Its status ping may attempt Desktop launch even for an unapproved origin.
155
169
 
156
170
  ```ts
157
171
  const availability = await pedelec.checkAvailability();
package/dist/index.d.ts CHANGED
@@ -6,7 +6,8 @@ export type JsonPrimitive = string | number | boolean | null;
6
6
  export type JsonValue = JsonPrimitive | JsonValue[] | {
7
7
  [key: string]: JsonValue;
8
8
  };
9
- export type SandboxAssetPath = `input/${string}`;
9
+ export type SandboxAssetPath = `assets/${string}`;
10
+ export type ReadAssetType = "text" | "json" | "file";
10
11
  export type SandboxAsset = {
11
12
  name: string;
12
13
  path: SandboxAssetPath;
@@ -97,7 +98,6 @@ export type CreateSessionInput<TTools extends readonly ToolDefinition[] = readon
97
98
  export type ProviderInfo = {
98
99
  name: string;
99
100
  code: ProviderCode;
100
- path: string | null;
101
101
  available: boolean;
102
102
  error: string | null;
103
103
  };
@@ -109,6 +109,7 @@ export type ApprovalStatus = {
109
109
  installed: boolean;
110
110
  approved: boolean;
111
111
  origin: string | null;
112
+ appConnected: boolean;
112
113
  };
113
114
  export type PedelecAvailability = {
114
115
  available: boolean;
@@ -258,6 +259,7 @@ export declare class PedelecSession<TToolName extends string = string> {
258
259
  private pendingPrepare;
259
260
  private preparePromise;
260
261
  private uploadPromise;
262
+ private ending;
261
263
  private prepared;
262
264
  private activeTurn;
263
265
  private genericToolHandler;
@@ -275,6 +277,9 @@ export declare class PedelecSession<TToolName extends string = string> {
275
277
  onTool<TArgs = unknown, TResult = unknown>(toolName: TToolName, handler: ToolSpecificHandler<TArgs, TResult>): () => void;
276
278
  uploadAsset(file: File): Promise<SandboxAssetPath>;
277
279
  listAssets(): Promise<SandboxAsset[]>;
280
+ readAsset(path: SandboxAssetPath, type: "text"): Promise<string>;
281
+ readAsset<T = JsonValue>(path: SandboxAssetPath, type: "json"): Promise<T>;
282
+ readAsset(path: SandboxAssetPath, type: "file"): Promise<File>;
278
283
  onError(handler: ErrorHandler): () => void;
279
284
  onStatus(handler: StatusHandler): () => void;
280
285
  onEnded(handler: EndedHandler): () => void;
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ function normalizeListAssetsResponse(response) {
12
12
  return invalidListAssetsResponse({ index, asset });
13
13
  const { name, path, sizeBytes, modifiedAt } = asset;
14
14
  const validName = typeof name === "string" && name.length > 0 && name !== "." && name !== ".." && !name.includes("/") && !name.includes("\\");
15
- const validPath = typeof path === "string" && !path.includes("\\") && path === `input/${name}` && !path.split("/").includes("..");
15
+ const validPath = typeof path === "string" && !path.includes("\\") && path === `assets/${name}` && !path.split("/").includes("..");
16
16
  const validNumber = (value) => typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0;
17
17
  if (!validName || !validPath || !validNumber(sizeBytes) || !validNumber(modifiedAt)) {
18
18
  return invalidListAssetsResponse({ index, asset });
@@ -23,6 +23,10 @@ function normalizeListAssetsResponse(response) {
23
23
  function invalidListAssetsResponse(details) {
24
24
  throw makeError("SDK_PROTOCOL_ERROR", "list_assets response had an invalid shape", { response: details });
25
25
  }
26
+ function isValidAssetPath(path) {
27
+ return typeof path === "string" && path.startsWith("assets/") && path.length > "assets/".length && !path.includes("\\") &&
28
+ path.split("/").every((part, index) => index === 0 || (part.length > 0 && part !== "." && part !== ".."));
29
+ }
26
30
  const MAX_ASSET_UPLOAD_BYTES = 100 * 1024 * 1024;
27
31
  const TOOL_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_.-]*$/;
28
32
  export function defineTool(tool) {
@@ -147,15 +151,10 @@ export class Pedelec {
147
151
  return this.registerSession(result.sessionId, resolvedInput.provider, resolvedInput.model, resolvedInput.inlineToolHandlers);
148
152
  }
149
153
  async listProviders() {
150
- const result = await this.request("list_providers");
151
- if (!Array.isArray(result)) {
152
- throw makeError("SDK_PROTOCOL_ERROR", "list_providers response was not an array");
153
- }
154
- return result;
154
+ return normalizeProviderInfoList(await this.request("list_providers"));
155
155
  }
156
156
  async getSettings() {
157
- const result = await this.request("get_settings");
158
- return result;
157
+ return normalizePedelecSettings(await this.request("get_settings"));
159
158
  }
160
159
  async getApprovalStatus() {
161
160
  if (this.disconnectedError) {
@@ -163,13 +162,11 @@ export class Pedelec {
163
162
  installed: false,
164
163
  approved: false,
165
164
  origin: getCurrentOrigin(this.pageWindow),
165
+ appConnected: false,
166
166
  };
167
167
  }
168
168
  try {
169
169
  const result = await this.request("get_approval_status");
170
- if (!isApprovalStatus(result)) {
171
- throw makeError("SDK_PROTOCOL_ERROR", "get_approval_status response had invalid shape");
172
- }
173
170
  return result;
174
171
  }
175
172
  catch (err) {
@@ -179,6 +176,7 @@ export class Pedelec {
179
176
  installed: false,
180
177
  approved: false,
181
178
  origin: getCurrentOrigin(this.pageWindow),
179
+ appConnected: false,
182
180
  };
183
181
  }
184
182
  throw error;
@@ -287,7 +285,7 @@ export class Pedelec {
287
285
  available: false,
288
286
  extension: { available: true },
289
287
  approval: { approved: false, origin: approval.origin },
290
- desktop: { available: false, launchAttempted: false },
288
+ desktop: { available: approval.appConnected, launchAttempted: true },
291
289
  };
292
290
  }
293
291
  try {
@@ -442,6 +440,7 @@ export class PedelecSession {
442
440
  pendingPrepare = null;
443
441
  preparePromise = null;
444
442
  uploadPromise = null;
443
+ ending = false;
445
444
  prepared = false;
446
445
  activeTurn = null;
447
446
  genericToolHandler = null;
@@ -462,7 +461,7 @@ export class PedelecSession {
462
461
  if (this.status === "ended") {
463
462
  return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
464
463
  }
465
- if (this.pendingSend || this.uploadPromise) {
464
+ if (this.pendingSend) {
466
465
  return Promise.reject(makeError("SESSION_BUSY", "session is already running", { sessionId: this.sessionId }));
467
466
  }
468
467
  if (this.prepared) {
@@ -498,9 +497,6 @@ export class PedelecSession {
498
497
  return this.preparePromise;
499
498
  }
500
499
  async sendText(text) {
501
- if (this.uploadPromise) {
502
- return Promise.reject(makeError("SESSION_BUSY", "an asset upload is in progress", { sessionId: this.sessionId }));
503
- }
504
500
  if (this.preparePromise) {
505
501
  try {
506
502
  await this.preparePromise;
@@ -564,9 +560,9 @@ export class PedelecSession {
564
560
  };
565
561
  }
566
562
  uploadAsset(file) {
567
- if (this.status === "ended")
563
+ if (this.ending || this.status === "ended")
568
564
  return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
569
- if (this.pendingSend || this.pendingPrepare || this.preparePromise || this.uploadPromise || this.status !== "idle") {
565
+ if (this.uploadPromise) {
570
566
  return Promise.reject(makeError("SESSION_BUSY", "session is busy", { sessionId: this.sessionId }));
571
567
  }
572
568
  if (typeof File === "undefined" || !(file instanceof File) || !file.name || file.size < 0) {
@@ -593,18 +589,67 @@ export class PedelecSession {
593
589
  catch { /* normalized below */ }
594
590
  if (!response.ok)
595
591
  throw normalizeError(payload?.error, "ASSET_UPLOAD_FAILED", "asset upload failed");
596
- if (typeof payload?.path !== "string" || !payload.path.startsWith("input/") || payload.path.includes(".."))
592
+ if (!isValidAssetPath(payload?.path))
597
593
  throw makeError("SDK_PROTOCOL_ERROR", "asset upload response had an invalid path");
598
594
  return payload.path;
599
595
  }).finally(() => { this.uploadPromise = null; });
600
596
  return this.uploadPromise;
601
597
  }
602
598
  listAssets() {
603
- if (this.status === "ended") {
599
+ if (this.ending || this.status === "ended") {
604
600
  return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
605
601
  }
606
602
  return this.client.request("list_assets", { sessionId: this.sessionId }).then(normalizeListAssetsResponse);
607
603
  }
604
+ readAsset(path, type) {
605
+ if (this.ending || this.status === "ended")
606
+ return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
607
+ if (!isValidAssetPath(path) || !["text", "json", "file"].includes(type)) {
608
+ return Promise.reject(makeError("ASSET_PATH_INVALID", "asset path or read type is invalid"));
609
+ }
610
+ return this.client.request("create_asset_download", { sessionId: this.sessionId, path }).then(async (ticket) => {
611
+ if (!ticket || typeof ticket !== "object")
612
+ throw makeError("SDK_PROTOCOL_ERROR", "asset download ticket was invalid");
613
+ const value = ticket;
614
+ if (typeof value.downloadUrl !== "string" || typeof value.token !== "string" || typeof value.name !== "string" ||
615
+ typeof value.mimeType !== "string" || typeof value.modifiedAt !== "number" || !Number.isFinite(value.modifiedAt) || !isValidAssetPath(value.path)) {
616
+ throw makeError("SDK_PROTOCOL_ERROR", "asset download ticket was invalid");
617
+ }
618
+ let response;
619
+ try {
620
+ response = await fetch(value.downloadUrl, { headers: { Authorization: `Bearer ${value.token}` }, credentials: "omit" });
621
+ }
622
+ catch (error) {
623
+ throw normalizeError(error, "ASSET_READ_FAILED", "asset read failed");
624
+ }
625
+ if (!response.ok) {
626
+ let payload;
627
+ try {
628
+ payload = await response.json();
629
+ }
630
+ catch { /* normalize below */ }
631
+ throw normalizeError(payload?.error, "ASSET_READ_FAILED", "asset read failed");
632
+ }
633
+ const bytes = await response.arrayBuffer();
634
+ if (type === "file")
635
+ return new File([bytes], value.name, { type: value.mimeType || "application/octet-stream", lastModified: value.modifiedAt });
636
+ let text;
637
+ try {
638
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
639
+ }
640
+ catch {
641
+ throw makeError("ASSET_TEXT_DECODE_FAILED", "asset is not valid UTF-8");
642
+ }
643
+ if (type === "text")
644
+ return text;
645
+ try {
646
+ return JSON.parse(text);
647
+ }
648
+ catch {
649
+ throw makeError("ASSET_INVALID_JSON", "asset does not contain valid JSON");
650
+ }
651
+ });
652
+ }
608
653
  onError(handler) {
609
654
  this.errorHandlers.add(handler);
610
655
  return () => this.errorHandlers.delete(handler);
@@ -621,14 +666,16 @@ export class PedelecSession {
621
666
  return this.status;
622
667
  }
623
668
  async end() {
624
- if (this.status === "ended")
669
+ if (this.status === "ended" || this.ending)
625
670
  return;
671
+ this.ending = true;
626
672
  try {
627
673
  await this.client.request("end_session", {
628
674
  sessionId: this.sessionId,
629
675
  });
630
676
  }
631
677
  catch (err) {
678
+ this.ending = false;
632
679
  const error = normalizeError(err, "END_SESSION_FAILED", "end failed");
633
680
  this.emitError(error, { source: "sdk" });
634
681
  throw error;
@@ -894,13 +941,38 @@ function isSessionEvent(message) {
894
941
  message.type === "error" ||
895
942
  message.type === "ended");
896
943
  }
897
- function isApprovalStatus(value) {
898
- if (!value || typeof value !== "object")
899
- return false;
900
- const status = value;
901
- return (typeof status.installed === "boolean" &&
902
- typeof status.approved === "boolean" &&
903
- (status.origin === null || typeof status.origin === "string"));
944
+ function isProviderCode(value) {
945
+ return value === "codex" || value === "antigravity" || value === "opencode" ||
946
+ value === "cursor" || value === "claude" || value === "ollama";
947
+ }
948
+ function isPlainObject(value) {
949
+ return !!value && typeof value === "object" && !Array.isArray(value);
950
+ }
951
+ function normalizePedelecSettings(value) {
952
+ if (!isPlainObject(value) || (value.defaultProvider !== null && !isProviderCode(value.defaultProvider)) ||
953
+ !isPlainObject(value.defaultModels)) {
954
+ throw makeError("SDK_PROTOCOL_ERROR", "get_settings response had invalid shape");
955
+ }
956
+ const defaultModels = {};
957
+ for (const [code, model] of Object.entries(value.defaultModels)) {
958
+ if (!isProviderCode(code) || typeof model !== "string") {
959
+ throw makeError("SDK_PROTOCOL_ERROR", "get_settings response had invalid shape");
960
+ }
961
+ defaultModels[code] = model;
962
+ }
963
+ return { defaultProvider: value.defaultProvider, defaultModels };
964
+ }
965
+ function normalizeProviderInfoList(value) {
966
+ if (!Array.isArray(value))
967
+ throw makeError("SDK_PROTOCOL_ERROR", "list_providers response was not an array");
968
+ return value.map((item) => {
969
+ if (!isPlainObject(item) || typeof item.name !== "string" || item.name.length === 0 ||
970
+ !isProviderCode(item.code) || typeof item.available !== "boolean" ||
971
+ (item.error !== null && typeof item.error !== "string")) {
972
+ throw makeError("SDK_PROTOCOL_ERROR", "list_providers response had invalid provider data");
973
+ }
974
+ return { name: item.name, code: item.code, available: item.available, error: item.error };
975
+ });
904
976
  }
905
977
  function makeError(code, message, details) {
906
978
  return details === undefined ? { code, message } : { code, message, details };
@@ -103,11 +103,24 @@ async function availabilityHasPublicType() {
103
103
  void availability;
104
104
  void promise;
105
105
  }
106
+ function publicSecurityTypesAreRestricted() {
107
+ const status = {
108
+ installed: true,
109
+ approved: true,
110
+ origin: "https://app.example.test",
111
+ appConnected: true,
112
+ };
113
+ const provider = {};
114
+ // @ts-expect-error SDK provider metadata must not expose executable paths
115
+ provider.path;
116
+ return status;
117
+ }
106
118
  void typedOnToolNameFromCreateSession;
107
119
  void resumedSessionFallsBackToString;
108
120
  void noSkillsFallsBackToString;
109
121
  void listAssetsHasPublicTypes;
110
122
  void availabilityHasPublicType;
123
+ void publicSecurityTypesAreRestricted;
111
124
  const baseContext = {
112
125
  sessionId: "thread_1",
113
126
  provider: "codex",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaoruisaac/pedelec",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",