@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 +17 -3
- package/dist/index.d.ts +7 -2
- package/dist/index.js +100 -28
- package/dist/index.type-test.js +13 -0
- package/package.json +1 -1
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
|
-
##
|
|
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
|
|
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
|
-
|
|
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 = `
|
|
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 === `
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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.
|
|
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 (
|
|
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
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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 };
|
package/dist/index.type-test.js
CHANGED
|
@@ -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",
|