@kaoruisaac/pedelec 0.2.1 → 0.2.3

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
@@ -24,6 +24,7 @@ Pedelec is a browser SDK and local bridge for applications that want to work wit
24
24
  A web application can use Pedelec to:
25
25
 
26
26
  - create an agent session on the user's machine;
27
+ - choose an application-managed workspace with the native directory picker;
27
28
  - send user instructions and receive streamed assistant text;
28
29
  - expose narrowly scoped browser-side tools to the agent;
29
30
  - resume or end sessions; and
@@ -168,6 +169,25 @@ const session = await pedelec.createSession({
168
169
 
169
170
  Pedelec creates missing `skills/`, `assets/`, `logs/`, and `tmp/` directories and preserves existing files. It never deletes an explicit workspace, and multiple active sessions may share it. Filesystem write conflicts are the application's responsibility. An explicit path must not overlap Pedelec's managed sandbox root.
170
171
 
172
+ ## Selecting an Application Workspace
173
+
174
+ Use `sandboxFolderPicker()` when the application wants the user to choose a local workspace before creating a session:
175
+
176
+ ```ts
177
+ const folder = await pedelec.sandboxFolderPicker();
178
+ if (!folder) return; // The user cancelled the native picker.
179
+
180
+ if (!folder.isEmptyFolder && !folder.hasSandboxConfig) {
181
+ // The application decides whether to warn the user.
182
+ }
183
+
184
+ const session = await pedelec.createSession({
185
+ sandbox: { path: folder.path },
186
+ });
187
+ ```
188
+
189
+ `sandboxFolderPicker()` returns `Promise<SandboxFolderPickerResult | null>`. It is a read-only snapshot: it does not validate or initialize the selected folder, create workspace directories, or create the sandbox marker. `isEmptyFolder` only reports whether the selected folder root has any filesystem entry. `hasSandboxConfig` is true only when `.pedelec-sandbox.json` exists as a regular file; its JSON is not parsed. The picker requires the same origin approval as other sensitive Desktop APIs, and cancellation returns `null`. `createSession()` still applies the full sandbox rules and creates the marker after successful custom sandbox initialization. The marker currently records the SDK version and normalized caller origin but does not restrict reuse by origin or SDK version.
190
+
171
191
  ---
172
192
 
173
193
  ## Creating a Session
package/dist/index.d.ts CHANGED
@@ -86,6 +86,11 @@ export type SerializableSkillsManifest = {
86
86
  export type CreateSessionSandboxInput = {
87
87
  path: string;
88
88
  };
89
+ export interface SandboxFolderPickerResult {
90
+ path: string;
91
+ isEmptyFolder: boolean;
92
+ hasSandboxConfig: boolean;
93
+ }
89
94
  type CreateSessionInputWithProvider<TTools extends readonly ToolDefinition[] = readonly ToolDefinition[]> = {
90
95
  provider: ProviderCode;
91
96
  effortLevel?: EffortLevel;
@@ -233,11 +238,13 @@ export declare class Pedelec {
233
238
  constructor(options?: PedelecOptions);
234
239
  createSession(): Promise<PedelecSession<string>>;
235
240
  createSession<const TTools extends readonly ToolDefinition[]>(input: CreateSessionInput<TTools>): Promise<PedelecSession<ToolNameOf<TTools>>>;
241
+ sandboxFolderPicker(): Promise<SandboxFolderPickerResult | null>;
236
242
  listProviders(): Promise<ProviderInfo[]>;
237
243
  getSettings(): Promise<PedelecSettings>;
238
244
  getApprovalStatus(): Promise<ApprovalStatus>;
239
245
  resumeSession(sessionId: string): Promise<PedelecSession<string>>;
240
246
  request<T>(type: string, payload?: Record<string, unknown>): Promise<T>;
247
+ private requestWithOptions;
241
248
  unregisterSession(sessionId: string): void;
242
249
  private resolveCreateSessionInput;
243
250
  checkAvailability(): Promise<PedelecAvailability>;
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { PEDELEC_EXTENSION_ID } from "./extension-id.js";
2
+ import { SDK_VERSION } from "./version.generated.js";
2
3
  const SDK_EXTERNAL_PORT_NAME = "pedelec-sdk-external";
3
4
  const DEFAULT_BRIDGE_TIMEOUT_MS = 30_000;
4
5
  function normalizeListAssetsResponse(response) {
@@ -149,6 +150,10 @@ export class Pedelec {
149
150
  }
150
151
  return this.registerSession(result.sessionId, resolvedInput.provider, resolvedInput.effortLevel, resolvedInput.inlineToolHandlers);
151
152
  }
153
+ async sandboxFolderPicker() {
154
+ const result = await this.requestWithOptions("pick_sandbox_folder", {}, { timeoutMs: null });
155
+ return normalizeSandboxFolderPickerResponse(result);
156
+ }
152
157
  async listProviders() {
153
158
  return normalizeProviderInfoList(await this.request("list_providers"));
154
159
  }
@@ -186,6 +191,9 @@ export class Pedelec {
186
191
  return this.registerSession(result.sessionId, "", undefined);
187
192
  }
188
193
  request(type, payload = {}) {
194
+ return this.requestWithOptions(type, payload);
195
+ }
196
+ requestWithOptions(type, payload = {}, options = {}) {
189
197
  let port;
190
198
  try {
191
199
  port = this.ensureExtensionPort();
@@ -194,15 +202,24 @@ export class Pedelec {
194
202
  return Promise.reject(normalizeError(err, "EXTENSION_UNAVAILABLE", "Pedelec extension is unavailable."));
195
203
  }
196
204
  const requestId = `sdk_${Date.now()}_${this.nextRequestNumber++}`;
197
- const message = { channelId: this.channelId, type, requestId, ...payload };
205
+ const message = {
206
+ channelId: this.channelId,
207
+ type,
208
+ requestId,
209
+ ...payload,
210
+ ...(type === "create_session" ? { callerSdkVersion: SDK_VERSION } : {}),
211
+ };
198
212
  return new Promise((resolve, reject) => {
199
- const timeoutId = setTimeout(() => {
200
- this.pendingRequests.delete(requestId);
201
- reject(makeError("SDK_BRIDGE_TIMEOUT", "Pedelec extension did not respond.", {
202
- requestId,
203
- type,
204
- }));
205
- }, this.bridgeTimeoutMs);
213
+ const timeoutMs = options.timeoutMs === undefined ? this.bridgeTimeoutMs : options.timeoutMs;
214
+ const timeoutId = timeoutMs === null
215
+ ? null
216
+ : setTimeout(() => {
217
+ this.pendingRequests.delete(requestId);
218
+ reject(makeError("SDK_BRIDGE_TIMEOUT", "Pedelec extension did not respond.", {
219
+ requestId,
220
+ type,
221
+ }));
222
+ }, timeoutMs);
206
223
  this.pendingRequests.set(requestId, {
207
224
  port,
208
225
  resolve: (value) => resolve(value),
@@ -214,7 +231,8 @@ export class Pedelec {
214
231
  }
215
232
  catch (err) {
216
233
  this.pendingRequests.delete(requestId);
217
- clearTimeout(timeoutId);
234
+ if (timeoutId !== null)
235
+ clearTimeout(timeoutId);
218
236
  const error = normalizeError(err, "EXTENSION_DISCONNECTED", "Pedelec extension disconnected.");
219
237
  if (this.port === port) {
220
238
  this.handleDisconnect(port, error);
@@ -398,7 +416,8 @@ export class Pedelec {
398
416
  if (!pending)
399
417
  return;
400
418
  this.pendingRequests.delete(message.requestId);
401
- clearTimeout(pending.timeoutId);
419
+ if (pending.timeoutId !== null)
420
+ clearTimeout(pending.timeoutId);
402
421
  if (message.ok) {
403
422
  pending.resolve(message.result);
404
423
  }
@@ -427,7 +446,8 @@ export class Pedelec {
427
446
  if (pending.port !== port)
428
447
  continue;
429
448
  this.pendingRequests.delete(requestId);
430
- clearTimeout(pending.timeoutId);
449
+ if (pending.timeoutId !== null)
450
+ clearTimeout(pending.timeoutId);
431
451
  pending.reject(error);
432
452
  }
433
453
  }
@@ -1013,6 +1033,23 @@ function normalizePedelecSettings(value) {
1013
1033
  }
1014
1034
  return { defaultProvider: value.defaultProvider };
1015
1035
  }
1036
+ function normalizeSandboxFolderPickerResponse(value) {
1037
+ if (!isPlainObject(value) || !("path" in value)) {
1038
+ throw makeError("SDK_PROTOCOL_ERROR", "pick_sandbox_folder response had an invalid shape");
1039
+ }
1040
+ const path = value.path;
1041
+ if (path === null)
1042
+ return null;
1043
+ if (typeof path !== "string" || path.length === 0 ||
1044
+ typeof value.isEmptyFolder !== "boolean" || typeof value.hasSandboxConfig !== "boolean") {
1045
+ throw makeError("SDK_PROTOCOL_ERROR", "pick_sandbox_folder response had an invalid shape");
1046
+ }
1047
+ return {
1048
+ path,
1049
+ isEmptyFolder: value.isEmptyFolder,
1050
+ hasSandboxConfig: value.hasSandboxConfig,
1051
+ };
1052
+ }
1016
1053
  function normalizeProviderInfoList(value) {
1017
1054
  if (!Array.isArray(value))
1018
1055
  throw makeError("SDK_PROTOCOL_ERROR", "list_providers response was not an array");
@@ -136,6 +136,16 @@ async function availabilityHasPublicType() {
136
136
  void availability;
137
137
  void promise;
138
138
  }
139
+ async function sandboxFolderPickerHasPublicType() {
140
+ const pedelec = new Pedelec();
141
+ const result = pedelec.sandboxFolderPicker();
142
+ void result;
143
+ }
144
+ function directoryPickerIsRemoved() {
145
+ const pedelec = new Pedelec();
146
+ // @ts-expect-error directoryPicker was removed in favor of sandboxFolderPicker
147
+ pedelec.directoryPicker();
148
+ }
139
149
  function publicSecurityTypesAreRestricted() {
140
150
  const status = {
141
151
  installed: true,
@@ -154,6 +164,8 @@ void noSkillsFallsBackToString;
154
164
  void effortLevelPublicTypeContract;
155
165
  void listAssetsHasPublicTypes;
156
166
  void availabilityHasPublicType;
167
+ void sandboxFolderPickerHasPublicType;
168
+ void directoryPickerIsRemoved;
157
169
  void publicSecurityTypesAreRestricted;
158
170
  const baseContext = {
159
171
  sessionId: "thread_1",
@@ -0,0 +1 @@
1
+ export declare const SDK_VERSION = "0.2.3";
@@ -0,0 +1,2 @@
1
+ // Generated from sdk/package.json. Do not edit manually.
2
+ export const SDK_VERSION = "0.2.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaoruisaac/pedelec",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -24,8 +24,11 @@
24
24
  "directory": "sdk"
25
25
  },
26
26
  "scripts": {
27
+ "prebuild": "node scripts/generate-version.mjs",
27
28
  "build": "tsc -p tsconfig.json",
29
+ "pretest": "node scripts/generate-version.mjs",
28
30
  "test": "vitest run",
31
+ "pretype-test": "node scripts/generate-version.mjs",
29
32
  "type-test": "tsc -p tsconfig.type-test.json",
30
33
  "prepublishOnly": "npm run test && npm run type-test && npm run build"
31
34
  },