dsh-codex-subscription 3.0.0-beta.2 → 3.0.0-beta.4
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.en.md +9 -2
- package/README.md +9 -2
- package/lib/client.js +109 -8
- package/lib/index.js +1116 -153
- package/package.json +2 -1
package/lib/index.js
CHANGED
|
@@ -9,9 +9,9 @@ import { promisify } from "node:util";
|
|
|
9
9
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
10
10
|
import WebSocket from "ws";
|
|
11
11
|
import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
|
|
12
|
-
import { lstat, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
13
|
-
import { constants, readFileSync, statSync } from "node:fs";
|
|
14
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
12
|
+
import { lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
13
|
+
import { constants, readFileSync, readdirSync, statSync } from "node:fs";
|
|
14
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
15
15
|
import { pathToFileURL } from "node:url";
|
|
16
16
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
17
17
|
import { LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
@@ -845,6 +845,17 @@ function createCodexNetworkTransport(options = {}) {
|
|
|
845
845
|
}
|
|
846
846
|
//#endregion
|
|
847
847
|
//#region src/subscription-connection.js
|
|
848
|
+
/**
|
|
849
|
+
* Who a bearer token belongs to, stable across refreshes: the ChatGPT account id inside the token, so a
|
|
850
|
+
* refreshed token keeps the same prompt-cache key and socket. A token without one falls back to itself.
|
|
851
|
+
*/
|
|
852
|
+
function tokenScope(apiKey) {
|
|
853
|
+
try {
|
|
854
|
+
const account = JSON.parse(Buffer.from(String(apiKey).split(".")[1], "base64url").toString("utf8"))?.["https://api.openai.com/auth"]?.chatgpt_account_id;
|
|
855
|
+
if (typeof account === "string" && account.length > 0 && account.length <= 128) return `account:${account}`;
|
|
856
|
+
} catch {}
|
|
857
|
+
return `token:${apiKey}`;
|
|
858
|
+
}
|
|
848
859
|
function createSubscriptionConnection({ resolveMode = () => "sse", resolveProxy = resolveCodexOAuthProxy } = {}) {
|
|
849
860
|
const namespace = randomUUID();
|
|
850
861
|
const sessions = /* @__PURE__ */ new Set();
|
|
@@ -873,7 +884,7 @@ function createSubscriptionConnection({ resolveMode = () => "sse", resolveProxy
|
|
|
873
884
|
const sessionId = options.sessionId && `dsh-${createHash("sha256").update(JSON.stringify([
|
|
874
885
|
namespace,
|
|
875
886
|
options.sessionId,
|
|
876
|
-
options.apiKey,
|
|
887
|
+
tokenScope(options.apiKey),
|
|
877
888
|
proxy
|
|
878
889
|
])).digest("hex").slice(0, 56)}`;
|
|
879
890
|
if (sessionId) sessions.add(sessionId);
|
|
@@ -1124,6 +1135,8 @@ const RPC_ENDPOINTS = Object.freeze([
|
|
|
1124
1135
|
"remote/enable",
|
|
1125
1136
|
"remote/disable",
|
|
1126
1137
|
"remote/pair",
|
|
1138
|
+
"remote/clients",
|
|
1139
|
+
"remote/revoke",
|
|
1127
1140
|
"reset-credit/inspect",
|
|
1128
1141
|
"reset-credit/prepare",
|
|
1129
1142
|
"reset-credit/consume",
|
|
@@ -2062,7 +2075,7 @@ function inheritedOriginalImageRef(session, assetId) {
|
|
|
2062
2075
|
}
|
|
2063
2076
|
//#endregion
|
|
2064
2077
|
//#region src/version.js
|
|
2065
|
-
const PACKAGE_VERSION = "3.0.0-beta.
|
|
2078
|
+
const PACKAGE_VERSION = "3.0.0-beta.4";
|
|
2066
2079
|
const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
|
|
2067
2080
|
//#endregion
|
|
2068
2081
|
//#region src/codex-images.js
|
|
@@ -2235,7 +2248,7 @@ async function readSessionImage(attachmentId, attachments, signal, messages) {
|
|
|
2235
2248
|
};
|
|
2236
2249
|
}
|
|
2237
2250
|
async function editImages(values, attachments, signal, messages) {
|
|
2238
|
-
if (!Array.isArray(values) || values.length === 0 || values.length > MAX_REFERENCE_IMAGES) throw new Error(`referenceImages must contain between 1 and ${MAX_REFERENCE_IMAGES} images`);
|
|
2251
|
+
if (!Array.isArray(values) || values.length === 0 || values.length > MAX_REFERENCE_IMAGES) throw new Error(`referenceImages must contain between 1 and ${MAX_REFERENCE_IMAGES} images; omit it to generate a new image`);
|
|
2239
2252
|
const available = messages === void 0 ? void 0 : sessionImageReferences(messages);
|
|
2240
2253
|
const references = values.map((value) => {
|
|
2241
2254
|
const id = normalizeAttachmentId(value?.attachmentId);
|
|
@@ -2260,7 +2273,7 @@ function imageContent(value) {
|
|
|
2260
2273
|
const label = typeof value.size === "string" && value.size.length > 0 ? `Generated a ${value.size} image.` : "Generated an image.";
|
|
2261
2274
|
return [{
|
|
2262
2275
|
type: "text",
|
|
2263
|
-
text: value.localPath === void 0 ? label : `${label}\nOriginal PNG saved on the DSH host at: ${JSON.stringify(value.localPath)}. Read this file or copy it to the workspace with a .png extension; this host path is not a browser URL.`
|
|
2276
|
+
text: value.localPath === void 0 ? label : `${label}\nOriginal PNG saved on the DSH host at: ${JSON.stringify(value.localPath)}. Read this file or copy it to the workspace with a .png extension; this host path is not a browser URL. The image is already shown to the user, so do not embed it again; copy it rather than move it, and leave the original in place unless the user asks you to delete it.`
|
|
2264
2277
|
}, {
|
|
2265
2278
|
type: "image",
|
|
2266
2279
|
attachment: imageReference(value.image)
|
|
@@ -2364,6 +2377,41 @@ function imageOutputSchema() {
|
|
|
2364
2377
|
}
|
|
2365
2378
|
};
|
|
2366
2379
|
}
|
|
2380
|
+
const SUPPORT_ID = /^[A-Za-z0-9._-]{1,80}$/u;
|
|
2381
|
+
/** The request id Codex returns for an image call, kept on failures so a report can be traced. */
|
|
2382
|
+
function supportId(response) {
|
|
2383
|
+
const id = response.headers?.get?.("x-codex-imagegen-request-id");
|
|
2384
|
+
return typeof id === "string" && SUPPORT_ID.test(id) ? ` (request ${id})` : "";
|
|
2385
|
+
}
|
|
2386
|
+
/** What a 429 from the image endpoint says: when the limit resets, when the response tells us. */
|
|
2387
|
+
async function limitMessage(response, now = Date.now()) {
|
|
2388
|
+
const fallback = "Codex image generation quota is unavailable";
|
|
2389
|
+
let error;
|
|
2390
|
+
try {
|
|
2391
|
+
error = JSON.parse(await readTextWithin(response, 16 * 1024))?.error;
|
|
2392
|
+
} catch {
|
|
2393
|
+
return fallback;
|
|
2394
|
+
}
|
|
2395
|
+
if (!record$4(error)) return fallback;
|
|
2396
|
+
const at = Number.isSafeInteger(error.resets_at) && error.resets_at > 0 ? error.resets_at * 1e3 : Number.isSafeInteger(error.resets_in_seconds) && error.resets_in_seconds > 0 ? now + error.resets_in_seconds * 1e3 : void 0;
|
|
2397
|
+
if (at === void 0) return fallback;
|
|
2398
|
+
const minutes = Math.max(1, Math.ceil((at - now) / 6e4));
|
|
2399
|
+
return `Codex image generation limit reached; it resets in about ${minutes >= 120 ? `${Math.round(minutes / 60)} hours` : `${minutes} minutes`}`;
|
|
2400
|
+
}
|
|
2401
|
+
async function readTextWithin(response, maximumBytes) {
|
|
2402
|
+
if (response.body === null || response.body === void 0) return "";
|
|
2403
|
+
const reader = response.body.getReader();
|
|
2404
|
+
const chunks = [];
|
|
2405
|
+
let bytes = 0;
|
|
2406
|
+
while (bytes < maximumBytes) {
|
|
2407
|
+
const { done, value } = await reader.read();
|
|
2408
|
+
if (done) break;
|
|
2409
|
+
bytes += value.byteLength;
|
|
2410
|
+
chunks.push(value);
|
|
2411
|
+
}
|
|
2412
|
+
await reader.cancel().catch(() => {});
|
|
2413
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8").slice(0, maximumBytes);
|
|
2414
|
+
}
|
|
2367
2415
|
function responseMetadata(value) {
|
|
2368
2416
|
const data = Array.isArray(value?.data) ? value.data[0] : void 0;
|
|
2369
2417
|
const encoded = record$4(data) ? data.b64_json : void 0;
|
|
@@ -2481,6 +2529,10 @@ function createCodexImageTool(options) {
|
|
|
2481
2529
|
timeoutMs: 300 * 1e3,
|
|
2482
2530
|
isConcurrencySafe: () => false,
|
|
2483
2531
|
async execute(args, exec) {
|
|
2532
|
+
if (Array.isArray(args.referenceImages) && args.referenceImages.length === 0) {
|
|
2533
|
+
const { referenceImages: _empty, ...rest } = args;
|
|
2534
|
+
args = rest;
|
|
2535
|
+
}
|
|
2484
2536
|
assertImageOperation(options.getFeatures?.(), args.referenceImages !== void 0);
|
|
2485
2537
|
const defaults = readImageDefaults(options.getFeatures?.());
|
|
2486
2538
|
args = {
|
|
@@ -2529,8 +2581,9 @@ function createCodexImageTool(options) {
|
|
|
2529
2581
|
}
|
|
2530
2582
|
if (!response.ok) {
|
|
2531
2583
|
if (response.status === 401 || response.status === 403) throw new Error("ChatGPT sign-in needs to be renewed");
|
|
2532
|
-
|
|
2533
|
-
throw new Error(
|
|
2584
|
+
const requestId = supportId(response);
|
|
2585
|
+
if (response.status === 429) throw new Error(`${await limitMessage(response)}${requestId}`);
|
|
2586
|
+
throw new Error(`Codex image ${editing ? "edit" : "generation"} failed (HTTP ${response.status})${requestId}`);
|
|
2534
2587
|
}
|
|
2535
2588
|
const metadata = responseMetadata(await readJsonWithin(response, encodedLimit(maximumBytes) + RESPONSE_ENVELOPE_BYTES));
|
|
2536
2589
|
const data = decodeCodexPng(metadata.encoded, maximumBytes);
|
|
@@ -2892,6 +2945,21 @@ const CONFLICT_RENEW_MS = 15 * 6e4;
|
|
|
2892
2945
|
const MAX_RECONNECT_MS = 3e4;
|
|
2893
2946
|
const REQUEST_TIMEOUT_MS = 3e4;
|
|
2894
2947
|
const REFRESH_MARGIN_MS = 5 * 6e4;
|
|
2948
|
+
const PING_MS = 1e4;
|
|
2949
|
+
const PONG_TIMEOUT_MS = 6e4;
|
|
2950
|
+
const STABLE_MS = 6e4;
|
|
2951
|
+
const MAX_BUFFERED_ITEMS = 2e3;
|
|
2952
|
+
const MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
|
|
2953
|
+
const BUFFER_IDLE_MS = 30 * 6e4;
|
|
2954
|
+
const RETRY_AFTER_JITTER_MS = 3e4;
|
|
2955
|
+
/** Milliseconds a server asked us to wait (Retry-After as seconds or a date), when it did. */
|
|
2956
|
+
function retryAfterMs$1(value, at = Date.now()) {
|
|
2957
|
+
if (typeof value !== "string" || value.trim() === "") return void 0;
|
|
2958
|
+
const seconds = Number(value);
|
|
2959
|
+
if (Number.isFinite(seconds)) return Math.max(0, Math.round(seconds * 1e3));
|
|
2960
|
+
const date = Date.parse(value);
|
|
2961
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - at);
|
|
2962
|
+
}
|
|
2895
2963
|
var RemoteControlError = class extends Error {
|
|
2896
2964
|
constructor(code, message) {
|
|
2897
2965
|
super(message);
|
|
@@ -2915,6 +2983,7 @@ function remoteControlEndpoints(base = REMOTE_CONTROL_BASE_URL) {
|
|
|
2915
2983
|
enroll: at("wham/remote/control/server/enroll"),
|
|
2916
2984
|
refresh: at("wham/remote/control/server/refresh"),
|
|
2917
2985
|
pair: at("wham/remote/control/server/pair"),
|
|
2986
|
+
clients: (environment) => at(`wham/remote/control/environments/${encodeURIComponent(environment)}/clients`),
|
|
2918
2987
|
websocket
|
|
2919
2988
|
});
|
|
2920
2989
|
}
|
|
@@ -2965,6 +3034,8 @@ function createRemoteControlRelay(options) {
|
|
|
2965
3034
|
const clients = /* @__PURE__ */ new Map();
|
|
2966
3035
|
const assemblies = /* @__PURE__ */ new Map();
|
|
2967
3036
|
const sequences = /* @__PURE__ */ new Map();
|
|
3037
|
+
const outbound = /* @__PURE__ */ new Map();
|
|
3038
|
+
const lastInbound = /* @__PURE__ */ new Map();
|
|
2968
3039
|
const state = {
|
|
2969
3040
|
status: "stopped",
|
|
2970
3041
|
connectedAt: void 0,
|
|
@@ -2977,9 +3048,19 @@ function createRemoteControlRelay(options) {
|
|
|
2977
3048
|
redirect: "error",
|
|
2978
3049
|
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]) : AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
2979
3050
|
});
|
|
2980
|
-
if (!response.ok)
|
|
3051
|
+
if (!response.ok) {
|
|
3052
|
+
const error = new RemoteControlError(response.status === 401 || response.status === 403 ? "unauthorized" : "http-error", `Remote Control request failed (HTTP ${response.status})`);
|
|
3053
|
+
if (response.status === 429 || response.status >= 500) error.retryAfterMs = retryAfterMs$1(response.headers?.get?.("retry-after"), now());
|
|
3054
|
+
throw error;
|
|
3055
|
+
}
|
|
2981
3056
|
return response.json();
|
|
2982
3057
|
};
|
|
3058
|
+
const accountHeaders = (access, accountId) => ({
|
|
3059
|
+
authorization: `Bearer ${access}`,
|
|
3060
|
+
"chatgpt-account-id": accountId,
|
|
3061
|
+
accept: "application/json",
|
|
3062
|
+
"user-agent": options.userAgent
|
|
3063
|
+
});
|
|
2983
3064
|
const ensureEnrollment = async (signal) => {
|
|
2984
3065
|
if (enrolled && enrolled.expiresAt > now() + REFRESH_MARGIN_MS) return enrolled;
|
|
2985
3066
|
const { access, accountId } = await options.credentials(signal);
|
|
@@ -3032,45 +3113,134 @@ function createRemoteControlRelay(options) {
|
|
|
3032
3113
|
expiresAt
|
|
3033
3114
|
};
|
|
3034
3115
|
};
|
|
3116
|
+
/** The phones paired with this host, from the same account endpoint Codex uses. */
|
|
3117
|
+
const listClients = async (signal) => {
|
|
3118
|
+
const current = await ensureEnrollment(signal);
|
|
3119
|
+
const { access, accountId } = await options.credentials(signal);
|
|
3120
|
+
if (!access || !accountId) throw new RemoteControlError("not-signed-in", "ChatGPT subscription is not signed in");
|
|
3121
|
+
const value = await request(endpoints.clients(current.environmentId), {
|
|
3122
|
+
method: "GET",
|
|
3123
|
+
headers: accountHeaders(access, accountId)
|
|
3124
|
+
}, signal);
|
|
3125
|
+
return (Array.isArray(value?.items) ? value.items : []).filter((item) => text(item?.client_id)).map((item) => ({
|
|
3126
|
+
clientId: item.client_id,
|
|
3127
|
+
name: text(item.display_name) ?? text(item.device_model) ?? null,
|
|
3128
|
+
platform: text(item.platform) ?? null,
|
|
3129
|
+
osVersion: text(item.os_version) ?? null,
|
|
3130
|
+
appVersion: text(item.app_version) ?? null,
|
|
3131
|
+
lastSeenAt: Number.isFinite(Date.parse(item.last_seen_at)) ? Date.parse(item.last_seen_at) : null
|
|
3132
|
+
}));
|
|
3133
|
+
};
|
|
3134
|
+
/** Remove one paired phone; it has to be paired again to come back. */
|
|
3135
|
+
const revokeClient = async (clientId, signal) => {
|
|
3136
|
+
if (typeof clientId !== "string" || clientId === "" || clientId.length > 200) throw new RemoteControlError("bad-request", "A client id is required");
|
|
3137
|
+
const current = await ensureEnrollment(signal);
|
|
3138
|
+
const { access, accountId } = await options.credentials(signal);
|
|
3139
|
+
if (!access || !accountId) throw new RemoteControlError("not-signed-in", "ChatGPT subscription is not signed in");
|
|
3140
|
+
const target = new URL(`${endpoints.clients(current.environmentId).href}/${encodeURIComponent(clientId)}`);
|
|
3141
|
+
const response = await doFetch(target, {
|
|
3142
|
+
method: "DELETE",
|
|
3143
|
+
headers: accountHeaders(access, accountId),
|
|
3144
|
+
redirect: "error",
|
|
3145
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]) : AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
3146
|
+
});
|
|
3147
|
+
if (!response.ok) throw new RemoteControlError(response.status === 401 || response.status === 403 ? "unauthorized" : "http-error", `Remote Control request failed (HTTP ${response.status})`);
|
|
3148
|
+
closeClient(clientId);
|
|
3149
|
+
};
|
|
3035
3150
|
const sendEnvelope = async (envelope) => {
|
|
3036
3151
|
if (socket?.readyState !== 1) throw new RemoteControlError("not-connected", "Remote Control is not connected");
|
|
3037
3152
|
await new Promise((resolve, reject) => socket.send(JSON.stringify(envelope), (error) => error ? reject(error) : resolve()));
|
|
3038
3153
|
};
|
|
3154
|
+
const remember = (key, sequence, segment, encoded) => {
|
|
3155
|
+
const stream = outbound.get(key) ?? {
|
|
3156
|
+
items: [],
|
|
3157
|
+
bytes: 0,
|
|
3158
|
+
touched: now()
|
|
3159
|
+
};
|
|
3160
|
+
outbound.set(key, stream);
|
|
3161
|
+
stream.items.push({
|
|
3162
|
+
sequence,
|
|
3163
|
+
segment,
|
|
3164
|
+
encoded
|
|
3165
|
+
});
|
|
3166
|
+
stream.bytes += encoded.length;
|
|
3167
|
+
stream.touched = now();
|
|
3168
|
+
while (stream.items.length > MAX_BUFFERED_ITEMS || stream.bytes > MAX_BUFFERED_BYTES) stream.bytes -= stream.items.shift().encoded.length;
|
|
3169
|
+
};
|
|
3170
|
+
const acknowledge = (key, sequence, segment) => {
|
|
3171
|
+
const stream = outbound.get(key);
|
|
3172
|
+
if (!stream) return;
|
|
3173
|
+
stream.items = stream.items.filter((item) => !(item.sequence < sequence || item.sequence === sequence && (segment === void 0 || (item.segment ?? 0) <= segment)));
|
|
3174
|
+
stream.bytes = stream.items.reduce((total, item) => total + item.encoded.length, 0);
|
|
3175
|
+
if (stream.items.length === 0) outbound.delete(key);
|
|
3176
|
+
};
|
|
3177
|
+
const replay = (ws) => {
|
|
3178
|
+
for (const [key, stream] of outbound) {
|
|
3179
|
+
if (now() - stream.touched > BUFFER_IDLE_MS) {
|
|
3180
|
+
outbound.delete(key);
|
|
3181
|
+
continue;
|
|
3182
|
+
}
|
|
3183
|
+
for (const item of stream.items) ws.send(item.encoded, () => {});
|
|
3184
|
+
}
|
|
3185
|
+
};
|
|
3039
3186
|
const sendServerMessage = async (clientId, streamId, message) => {
|
|
3040
3187
|
const key = `${clientId}\u0000${streamId}`;
|
|
3188
|
+
const isRequest = message?.method !== void 0 && message?.id !== void 0;
|
|
3189
|
+
if (isRequest && socket?.readyState !== 1) throw new RemoteControlError("not-connected", "Remote Control is not connected");
|
|
3041
3190
|
const sequence = sequences.get(key) ?? 1;
|
|
3042
3191
|
sequences.set(key, sequence + 1);
|
|
3043
|
-
const
|
|
3192
|
+
const encoded = JSON.stringify({
|
|
3044
3193
|
client_id: clientId,
|
|
3045
3194
|
message,
|
|
3046
3195
|
seq_id: sequence,
|
|
3047
3196
|
stream_id: streamId,
|
|
3048
3197
|
type: "server_message"
|
|
3198
|
+
});
|
|
3199
|
+
const put = async (text) => {
|
|
3200
|
+
if (isRequest) return new Promise((resolve, reject) => socket.send(text, (error) => error ? reject(error) : resolve()));
|
|
3201
|
+
remember(key, sequence, void 0, text);
|
|
3202
|
+
if (socket?.readyState === 1) await new Promise((resolve) => socket.send(text, () => resolve()));
|
|
3049
3203
|
};
|
|
3050
|
-
|
|
3051
|
-
if (Buffer.byteLength(encoded) <= MAX_SEGMENT_BYTES) return sendEnvelope(envelope);
|
|
3204
|
+
if (Buffer.byteLength(encoded) <= MAX_SEGMENT_BYTES) return put(encoded);
|
|
3052
3205
|
const bytes = Buffer.from(JSON.stringify(message));
|
|
3053
3206
|
if (bytes.byteLength > MAX_MESSAGE_BYTES) throw new RemoteControlError("too-large", "Remote Control message is too large");
|
|
3054
3207
|
const chunks = [];
|
|
3055
3208
|
for (let offset = 0; offset < bytes.byteLength; offset += TARGET_SEGMENT_BYTES) chunks.push(bytes.subarray(offset, offset + TARGET_SEGMENT_BYTES).toString("base64"));
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3209
|
+
for (const [segmentId, chunk] of chunks.entries()) {
|
|
3210
|
+
const text = JSON.stringify({
|
|
3211
|
+
client_id: clientId,
|
|
3212
|
+
message_chunk_base64: chunk,
|
|
3213
|
+
message_size_bytes: bytes.byteLength,
|
|
3214
|
+
segment_count: chunks.length,
|
|
3215
|
+
segment_id: segmentId,
|
|
3216
|
+
seq_id: sequence,
|
|
3217
|
+
stream_id: streamId,
|
|
3218
|
+
type: "server_message_chunk"
|
|
3219
|
+
});
|
|
3220
|
+
if (isRequest) await put(text);
|
|
3221
|
+
else {
|
|
3222
|
+
remember(key, sequence, segmentId, text);
|
|
3223
|
+
if (socket?.readyState === 1) await new Promise((resolve) => socket.send(text, () => resolve()));
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3066
3226
|
};
|
|
3067
3227
|
const closeClient = (clientId, streamId) => {
|
|
3068
3228
|
for (const [key, client] of clients) if (key.startsWith(`${clientId}\u0000`) && (streamId === void 0 || key === `${clientId}\u0000${streamId}`)) {
|
|
3069
3229
|
clients.delete(key);
|
|
3070
3230
|
sequences.delete(key);
|
|
3231
|
+
outbound.delete(key);
|
|
3232
|
+
lastInbound.delete(key);
|
|
3071
3233
|
client.close();
|
|
3072
3234
|
}
|
|
3073
3235
|
};
|
|
3236
|
+
const resetStreams = () => {
|
|
3237
|
+
for (const [, client] of clients) client.close();
|
|
3238
|
+
clients.clear();
|
|
3239
|
+
sequences.clear();
|
|
3240
|
+
outbound.clear();
|
|
3241
|
+
lastInbound.clear();
|
|
3242
|
+
assemblies.clear();
|
|
3243
|
+
};
|
|
3074
3244
|
const deliver = async (envelope) => {
|
|
3075
3245
|
const streamId = envelope.stream_id ?? randomUUID();
|
|
3076
3246
|
const key = `${envelope.client_id}\u0000${streamId}`;
|
|
@@ -3095,7 +3265,10 @@ function createRemoteControlRelay(options) {
|
|
|
3095
3265
|
};
|
|
3096
3266
|
clients.set(key, client);
|
|
3097
3267
|
}
|
|
3098
|
-
|
|
3268
|
+
if (!(Number.isInteger(envelope.seq_id) && (lastInbound.get(key) ?? -1) >= envelope.seq_id)) {
|
|
3269
|
+
if (Number.isInteger(envelope.seq_id)) lastInbound.set(key, envelope.seq_id);
|
|
3270
|
+
client.receive(envelope.message);
|
|
3271
|
+
}
|
|
3099
3272
|
if (Number.isInteger(envelope.seq_id)) await sendEnvelope({
|
|
3100
3273
|
client_id: envelope.client_id,
|
|
3101
3274
|
seq_id: envelope.seq_id,
|
|
@@ -3140,6 +3313,8 @@ function createRemoteControlRelay(options) {
|
|
|
3140
3313
|
else if (envelope.type === "client_message_chunk") {
|
|
3141
3314
|
const message = receiveChunk(envelope);
|
|
3142
3315
|
if (message) await deliver(message);
|
|
3316
|
+
} else if (envelope.type === "ack") {
|
|
3317
|
+
if (typeof envelope.stream_id === "string" && Number.isInteger(envelope.seq_id)) acknowledge(`${envelope.client_id}\u0000${envelope.stream_id}`, envelope.seq_id, Number.isInteger(envelope.segment_id) ? envelope.segment_id : void 0);
|
|
3143
3318
|
} else if (envelope.type === "client_closed") closeClient(envelope.client_id, envelope.stream_id);
|
|
3144
3319
|
else if (envelope.type === "ping") await sendEnvelope({
|
|
3145
3320
|
client_id: envelope.client_id,
|
|
@@ -3164,25 +3339,60 @@ function createRemoteControlRelay(options) {
|
|
|
3164
3339
|
let queue = Promise.resolve();
|
|
3165
3340
|
const onAbort = () => ws.close();
|
|
3166
3341
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
3342
|
+
let ping;
|
|
3343
|
+
let silent;
|
|
3344
|
+
const hearFrom = () => {
|
|
3345
|
+
clearTimeout(silent);
|
|
3346
|
+
silent = setTimeout(() => {
|
|
3347
|
+
state.lastError = "timeout";
|
|
3348
|
+
try {
|
|
3349
|
+
(ws.terminate ?? ws.close).call(ws);
|
|
3350
|
+
} catch {}
|
|
3351
|
+
}, options.pongTimeoutMs ?? PONG_TIMEOUT_MS);
|
|
3352
|
+
};
|
|
3353
|
+
const stopBeat = () => {
|
|
3354
|
+
clearInterval(ping);
|
|
3355
|
+
clearTimeout(silent);
|
|
3356
|
+
};
|
|
3167
3357
|
ws.on("open", () => {
|
|
3168
3358
|
state.status = "connected";
|
|
3169
3359
|
state.connectedAt = now();
|
|
3170
3360
|
state.lastError = void 0;
|
|
3361
|
+
replay(ws);
|
|
3362
|
+
if (typeof ws.ping === "function") {
|
|
3363
|
+
hearFrom();
|
|
3364
|
+
ping = setInterval(() => {
|
|
3365
|
+
try {
|
|
3366
|
+
ws.ping();
|
|
3367
|
+
} catch {}
|
|
3368
|
+
}, options.pingMs ?? PING_MS);
|
|
3369
|
+
}
|
|
3370
|
+
});
|
|
3371
|
+
ws.on("pong", hearFrom);
|
|
3372
|
+
ws.on("unexpected-response", (request, response) => {
|
|
3373
|
+
stopBeat();
|
|
3374
|
+
const error = new RemoteControlError("socket", `Unexpected server response: ${response?.statusCode}`);
|
|
3375
|
+
if (response?.statusCode === 429 || response?.statusCode >= 500) error.retryAfterMs = retryAfterMs$1(String(response.headers?.["retry-after"] ?? ""), now());
|
|
3376
|
+
try {
|
|
3377
|
+
request?.destroy?.();
|
|
3378
|
+
} catch {}
|
|
3379
|
+
reject(error);
|
|
3171
3380
|
});
|
|
3172
3381
|
ws.on("message", (data) => {
|
|
3173
3382
|
queue = queue.then(() => receive(String(data))).catch((error) => {
|
|
3174
3383
|
state.lastError = error?.code ?? "protocol";
|
|
3175
3384
|
});
|
|
3176
3385
|
});
|
|
3177
|
-
ws.on("error", (error) =>
|
|
3386
|
+
ws.on("error", (error) => {
|
|
3387
|
+
stopBeat();
|
|
3388
|
+
reject(new RemoteControlError("socket", error?.message ?? "socket error"));
|
|
3389
|
+
});
|
|
3178
3390
|
ws.on("close", () => {
|
|
3391
|
+
stopBeat();
|
|
3179
3392
|
signal.removeEventListener("abort", onAbort);
|
|
3180
3393
|
resolve();
|
|
3181
3394
|
});
|
|
3182
3395
|
});
|
|
3183
|
-
for (const [, client] of clients) client.close();
|
|
3184
|
-
clients.clear();
|
|
3185
|
-
sequences.clear();
|
|
3186
3396
|
assemblies.clear();
|
|
3187
3397
|
socket = void 0;
|
|
3188
3398
|
};
|
|
@@ -3190,12 +3400,15 @@ function createRemoteControlRelay(options) {
|
|
|
3190
3400
|
let delay = INITIAL_RECONNECT_MS;
|
|
3191
3401
|
let conflicts = 0;
|
|
3192
3402
|
let conflictSince = 0;
|
|
3403
|
+
let asked;
|
|
3193
3404
|
while (!signal.aborted) {
|
|
3194
3405
|
state.status = "connecting";
|
|
3195
3406
|
try {
|
|
3407
|
+
state.connectedAt = void 0;
|
|
3196
3408
|
await connectOnce(signal);
|
|
3197
|
-
delay = INITIAL_RECONNECT_MS;
|
|
3409
|
+
if (state.connectedAt !== void 0 && now() - state.connectedAt >= STABLE_MS) delay = INITIAL_RECONNECT_MS;
|
|
3198
3410
|
} catch (error) {
|
|
3411
|
+
asked = error?.retryAfterMs;
|
|
3199
3412
|
state.lastError = error?.code ?? "network";
|
|
3200
3413
|
if (options.debug) state.detail = String(error?.message ?? error).slice(0, 160);
|
|
3201
3414
|
state.status = "error";
|
|
@@ -3206,6 +3419,7 @@ function createRemoteControlRelay(options) {
|
|
|
3206
3419
|
installation = await options.renewInstallation();
|
|
3207
3420
|
enrolled = void 0;
|
|
3208
3421
|
conflicts = 0;
|
|
3422
|
+
resetStreams();
|
|
3209
3423
|
}
|
|
3210
3424
|
if (error?.code === "not-signed-in") {
|
|
3211
3425
|
state.status = "stopped";
|
|
@@ -3214,12 +3428,16 @@ function createRemoteControlRelay(options) {
|
|
|
3214
3428
|
}
|
|
3215
3429
|
if (signal.aborted) break;
|
|
3216
3430
|
state.reconnects += 1;
|
|
3217
|
-
|
|
3431
|
+
const spread = Math.round(delay * (.5 + (options.random ?? Math.random)() / 2));
|
|
3432
|
+
await wait(asked === void 0 ? spread : Math.max(spread, asked + Math.round((options.random ?? Math.random)() * RETRY_AFTER_JITTER_MS)), signal);
|
|
3433
|
+
asked = void 0;
|
|
3218
3434
|
delay = Math.min(delay * 2, MAX_RECONNECT_MS);
|
|
3219
3435
|
}
|
|
3220
3436
|
};
|
|
3221
3437
|
return Object.freeze({
|
|
3222
3438
|
pair,
|
|
3439
|
+
listClients,
|
|
3440
|
+
revokeClient,
|
|
3223
3441
|
status: () => ({
|
|
3224
3442
|
...state,
|
|
3225
3443
|
enrolled: enrolled !== void 0,
|
|
@@ -3240,6 +3458,7 @@ function createRemoteControlRelay(options) {
|
|
|
3240
3458
|
stop?.abort();
|
|
3241
3459
|
socket?.close();
|
|
3242
3460
|
await running;
|
|
3461
|
+
resetStreams();
|
|
3243
3462
|
state.status = "stopped";
|
|
3244
3463
|
}
|
|
3245
3464
|
});
|
|
@@ -3318,6 +3537,13 @@ function createRemoteControl(options) {
|
|
|
3318
3537
|
async pair(signal) {
|
|
3319
3538
|
return (await ensureRelay()).pair(signal);
|
|
3320
3539
|
},
|
|
3540
|
+
async clients(signal) {
|
|
3541
|
+
return (await ensureRelay()).listClients(signal);
|
|
3542
|
+
},
|
|
3543
|
+
async revoke(clientId, signal) {
|
|
3544
|
+
await (await ensureRelay()).revokeClient(clientId, signal);
|
|
3545
|
+
return this.clients(signal);
|
|
3546
|
+
},
|
|
3321
3547
|
status() {
|
|
3322
3548
|
return {
|
|
3323
3549
|
...relay?.status() ?? {
|
|
@@ -3334,6 +3560,212 @@ function createRemoteControl(options) {
|
|
|
3334
3560
|
});
|
|
3335
3561
|
}
|
|
3336
3562
|
//#endregion
|
|
3563
|
+
//#region src/remote-control-git.js
|
|
3564
|
+
const DEFAULT_TIMEOUT_MS$2 = 15e3;
|
|
3565
|
+
const MAX_TIMEOUT_MS = 3e4;
|
|
3566
|
+
const DEFAULT_OUTPUT_BYTES = 1024 * 1024;
|
|
3567
|
+
const MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
3568
|
+
const READ_ONLY = /* @__PURE__ */ new Set([
|
|
3569
|
+
"status",
|
|
3570
|
+
"diff",
|
|
3571
|
+
"rev-parse",
|
|
3572
|
+
"ls-files",
|
|
3573
|
+
"show",
|
|
3574
|
+
"log",
|
|
3575
|
+
"rev-list",
|
|
3576
|
+
"merge-base",
|
|
3577
|
+
"diff-tree",
|
|
3578
|
+
"ls-tree",
|
|
3579
|
+
"cat-file",
|
|
3580
|
+
"describe",
|
|
3581
|
+
"shortlog",
|
|
3582
|
+
"blame",
|
|
3583
|
+
"diff-files",
|
|
3584
|
+
"diff-index",
|
|
3585
|
+
"for-each-ref",
|
|
3586
|
+
"count-objects"
|
|
3587
|
+
]);
|
|
3588
|
+
const EXACT = [
|
|
3589
|
+
["branch", "--show-current"],
|
|
3590
|
+
["remote"],
|
|
3591
|
+
["remote", "-v"],
|
|
3592
|
+
["symbolic-ref", "HEAD"],
|
|
3593
|
+
[
|
|
3594
|
+
"symbolic-ref",
|
|
3595
|
+
"--short",
|
|
3596
|
+
"HEAD"
|
|
3597
|
+
],
|
|
3598
|
+
[
|
|
3599
|
+
"symbolic-ref",
|
|
3600
|
+
"-q",
|
|
3601
|
+
"HEAD"
|
|
3602
|
+
]
|
|
3603
|
+
];
|
|
3604
|
+
const FORBIDDEN = /^(?:-o$|--output(?:=|$)|--ext-diff|--textconv|--exec|--upload-pack|--receive-pack|--git-dir|--work-tree|--namespace|--super-prefix|--open-files-in-pager|-O|--paginate|-c$|-C$|--config)/u;
|
|
3605
|
+
const TEXTUAL = /* @__PURE__ */ new Set([
|
|
3606
|
+
"diff",
|
|
3607
|
+
"show",
|
|
3608
|
+
"log",
|
|
3609
|
+
"blame",
|
|
3610
|
+
"diff-tree",
|
|
3611
|
+
"diff-files",
|
|
3612
|
+
"diff-index"
|
|
3613
|
+
]);
|
|
3614
|
+
/** The argv to run for a phone request, or a reason it is refused. */
|
|
3615
|
+
function gitArguments(command) {
|
|
3616
|
+
if (!Array.isArray(command) || command.length < 2 || command.some((part) => typeof part !== "string" || part.length > 4096 || part.includes("\0"))) return { error: "not a git command" };
|
|
3617
|
+
if (!/^git(?:\.exe)?$/iu.test(basename(command[0]))) return { error: "only git is available" };
|
|
3618
|
+
let rest = command.slice(1);
|
|
3619
|
+
if (rest[0] === "--no-pager") rest = rest.slice(1);
|
|
3620
|
+
const [subcommand, ...args] = rest;
|
|
3621
|
+
if (subcommand === void 0) return { error: "no git subcommand" };
|
|
3622
|
+
if (args.some((arg) => FORBIDDEN.test(arg))) return { error: `option not allowed with git ${subcommand}` };
|
|
3623
|
+
const exact = EXACT.some((form) => form.length === rest.length && form.every((part, index) => part === rest[index]));
|
|
3624
|
+
if (!READ_ONLY.has(subcommand) && !exact) return { error: `git ${subcommand} is not available` };
|
|
3625
|
+
return { argv: [
|
|
3626
|
+
"-c",
|
|
3627
|
+
"core.fsmonitor=false",
|
|
3628
|
+
"-c",
|
|
3629
|
+
"core.pager=cat",
|
|
3630
|
+
"-c",
|
|
3631
|
+
"diff.external=",
|
|
3632
|
+
"--no-pager",
|
|
3633
|
+
subcommand,
|
|
3634
|
+
...TEXTUAL.has(subcommand) ? ["--no-ext-diff", "--no-textconv"] : [],
|
|
3635
|
+
...args
|
|
3636
|
+
] };
|
|
3637
|
+
}
|
|
3638
|
+
const inside = (folder, root) => {
|
|
3639
|
+
const path = relative(resolve(root), resolve(folder));
|
|
3640
|
+
return path === "" || !path.startsWith("..") && !path.startsWith(`..${sep}`) && !/^[A-Za-z]:/u.test(path);
|
|
3641
|
+
};
|
|
3642
|
+
/**
|
|
3643
|
+
* @param {{ command: string[], cwd?: string, timeoutMs?: number, outputBytesCap?: number, tty?: boolean, streamStdin?: boolean, streamStdoutStderr?: boolean }} params
|
|
3644
|
+
* @param {{ folders: string[], run?: typeof execFile }} options folders: the conversation folders git may run in
|
|
3645
|
+
*/
|
|
3646
|
+
function runGit(params, { folders, run = execFile }) {
|
|
3647
|
+
const parsed = gitArguments(params?.command);
|
|
3648
|
+
if (parsed.error) return Promise.reject(Object.assign(new Error(parsed.error), { code: "not-allowed" }));
|
|
3649
|
+
if (params.tty || params.streamStdin || params.streamStdoutStderr) return Promise.reject(Object.assign(/* @__PURE__ */ new Error("streaming is not available"), { code: "not-allowed" }));
|
|
3650
|
+
const cwd = localFolder(params.cwd);
|
|
3651
|
+
if (!cwd || !folders.some((root) => inside(cwd, root))) return Promise.reject(Object.assign(/* @__PURE__ */ new Error("git only runs in a DSH conversation folder"), { code: "not-allowed" }));
|
|
3652
|
+
const timeout = Math.min(Number.isFinite(params.timeoutMs) && params.timeoutMs > 0 ? params.timeoutMs : DEFAULT_TIMEOUT_MS$2, MAX_TIMEOUT_MS);
|
|
3653
|
+
const cap = Math.min(Number.isInteger(params.outputBytesCap) && params.outputBytesCap > 0 ? params.outputBytesCap : DEFAULT_OUTPUT_BYTES, MAX_OUTPUT_BYTES);
|
|
3654
|
+
const env = {
|
|
3655
|
+
...process.env,
|
|
3656
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
3657
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
3658
|
+
GIT_PAGER: "cat",
|
|
3659
|
+
GIT_EXTERNAL_DIFF: "",
|
|
3660
|
+
GIT_ASKPASS: "",
|
|
3661
|
+
GCM_INTERACTIVE: "never"
|
|
3662
|
+
};
|
|
3663
|
+
return new Promise((resolvePromise) => {
|
|
3664
|
+
run("git", parsed.argv, {
|
|
3665
|
+
cwd,
|
|
3666
|
+
env,
|
|
3667
|
+
timeout,
|
|
3668
|
+
maxBuffer: cap,
|
|
3669
|
+
windowsHide: true,
|
|
3670
|
+
encoding: "utf8"
|
|
3671
|
+
}, (error, stdout, stderr) => {
|
|
3672
|
+
resolvePromise({
|
|
3673
|
+
exitCode: error ? typeof error.code === "number" ? error.code : 1 : 0,
|
|
3674
|
+
stdout: String(stdout ?? "").slice(0, cap),
|
|
3675
|
+
stderr: error && typeof error.code !== "number" ? String(error.message).slice(0, 500) : String(stderr ?? "").slice(0, cap)
|
|
3676
|
+
});
|
|
3677
|
+
});
|
|
3678
|
+
});
|
|
3679
|
+
}
|
|
3680
|
+
//#endregion
|
|
3681
|
+
//#region src/remote-control-files.js
|
|
3682
|
+
const SKIP = /* @__PURE__ */ new Set([
|
|
3683
|
+
"node_modules",
|
|
3684
|
+
".git",
|
|
3685
|
+
".hg",
|
|
3686
|
+
".svn",
|
|
3687
|
+
"dist",
|
|
3688
|
+
"build",
|
|
3689
|
+
"target",
|
|
3690
|
+
"__pycache__",
|
|
3691
|
+
".venv",
|
|
3692
|
+
"venv",
|
|
3693
|
+
".next",
|
|
3694
|
+
".cache",
|
|
3695
|
+
"coverage"
|
|
3696
|
+
]);
|
|
3697
|
+
const MAX_ENTRIES = 3e4;
|
|
3698
|
+
const MAX_DEPTH = 8;
|
|
3699
|
+
const BUDGET_MS = 2e3;
|
|
3700
|
+
const RESULTS = 50;
|
|
3701
|
+
/** Subsequence match with a bonus for runs, word starts and the file name; undefined when the query is not in the text. */
|
|
3702
|
+
function fuzzyScore(query, text) {
|
|
3703
|
+
const needle = query.toLowerCase();
|
|
3704
|
+
const hay = text.toLowerCase();
|
|
3705
|
+
const indices = [];
|
|
3706
|
+
let from = 0;
|
|
3707
|
+
let score = 0;
|
|
3708
|
+
let previous = -2;
|
|
3709
|
+
const nameStart = hay.lastIndexOf("/") + 1;
|
|
3710
|
+
for (const char of needle) {
|
|
3711
|
+
const at = hay.indexOf(char, from);
|
|
3712
|
+
if (at < 0) return void 0;
|
|
3713
|
+
indices.push(at);
|
|
3714
|
+
score += 1 + (at === previous + 1 ? 5 : 0) + (at === 0 || "/_-. ".includes(hay[at - 1]) ? 4 : 0) + (at >= nameStart ? 2 : 0);
|
|
3715
|
+
previous = at;
|
|
3716
|
+
from = at + 1;
|
|
3717
|
+
}
|
|
3718
|
+
return {
|
|
3719
|
+
score: score * 10 - Math.floor(Math.min(text.length, 200) / 10),
|
|
3720
|
+
indices
|
|
3721
|
+
};
|
|
3722
|
+
}
|
|
3723
|
+
const within = (folder, target) => {
|
|
3724
|
+
const path = relative(resolve(folder), resolve(target));
|
|
3725
|
+
return path === "" || !path.startsWith("..") && !/^[A-Za-z]:/u.test(path);
|
|
3726
|
+
};
|
|
3727
|
+
/** Matches for `query` under each root; roots outside `folders` are ignored. */
|
|
3728
|
+
async function searchFiles({ roots, query, folders, now = Date.now }) {
|
|
3729
|
+
const found = [];
|
|
3730
|
+
const started = now();
|
|
3731
|
+
let seen = 0;
|
|
3732
|
+
for (const given of roots) {
|
|
3733
|
+
const root = localFolder(given);
|
|
3734
|
+
if (!root || !folders.some((folder) => within(folder, root))) continue;
|
|
3735
|
+
const queue = [[root, 0]];
|
|
3736
|
+
while (queue.length > 0 && seen < MAX_ENTRIES && now() - started < BUDGET_MS) {
|
|
3737
|
+
const [folder, depth] = queue.shift();
|
|
3738
|
+
let entries;
|
|
3739
|
+
try {
|
|
3740
|
+
entries = await readdir(folder, { withFileTypes: true });
|
|
3741
|
+
} catch {
|
|
3742
|
+
continue;
|
|
3743
|
+
}
|
|
3744
|
+
for (const entry of entries) {
|
|
3745
|
+
if (entry.name.startsWith(".") && entry.name !== ".github") continue;
|
|
3746
|
+
const directory = entry.isDirectory();
|
|
3747
|
+
if (!directory && !entry.isFile()) continue;
|
|
3748
|
+
seen += 1;
|
|
3749
|
+
const path = relative(root, join(folder, entry.name)).split(sep).join("/");
|
|
3750
|
+
const match = query === "" ? {
|
|
3751
|
+
score: 0,
|
|
3752
|
+
indices: []
|
|
3753
|
+
} : fuzzyScore(query, path);
|
|
3754
|
+
if (match) found.push({
|
|
3755
|
+
root,
|
|
3756
|
+
path,
|
|
3757
|
+
match_type: directory ? "directory" : "file",
|
|
3758
|
+
file_name: entry.name,
|
|
3759
|
+
score: match.score,
|
|
3760
|
+
indices: match.indices
|
|
3761
|
+
});
|
|
3762
|
+
if (directory && !SKIP.has(entry.name) && depth < MAX_DEPTH) queue.push([join(folder, entry.name), depth + 1]);
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
return found.sort((a, b) => b.score - a.score || a.path.length - b.path.length).slice(0, RESULTS);
|
|
3767
|
+
}
|
|
3768
|
+
//#endregion
|
|
3337
3769
|
//#region src/remote-control-dsh.js
|
|
3338
3770
|
/** A folder the phone named, only when it is an existing absolute folder on this machine. */
|
|
3339
3771
|
function localFolder(value) {
|
|
@@ -3345,6 +3777,84 @@ function localFolder(value) {
|
|
|
3345
3777
|
return;
|
|
3346
3778
|
}
|
|
3347
3779
|
}
|
|
3780
|
+
/** One image the phone attached, sent inline as a data URL (its own file paths mean nothing on this machine). */
|
|
3781
|
+
function dataImage(url) {
|
|
3782
|
+
const match = typeof url === "string" ? /^data:(image\/[A-Za-z0-9.+-]+);base64,([A-Za-z0-9+/=\s]+)$/u.exec(url) : null;
|
|
3783
|
+
return match ? {
|
|
3784
|
+
mediaType: match[1].toLowerCase(),
|
|
3785
|
+
data: new Uint8Array(Buffer.from(match[2], "base64"))
|
|
3786
|
+
} : void 0;
|
|
3787
|
+
}
|
|
3788
|
+
const inputImages = (input) => (Array.isArray(input) ? input : []).filter((part) => part?.type === "image" && typeof part.url === "string");
|
|
3789
|
+
const MAX_PHONE_IMAGES = 8;
|
|
3790
|
+
/** Facts about a real local file or folder the phone asks about, so its folder picker shows what is on this machine. */
|
|
3791
|
+
function realMetadata(value) {
|
|
3792
|
+
if (typeof value !== "string" || !isAbsolute(value)) return void 0;
|
|
3793
|
+
try {
|
|
3794
|
+
const stat = statSync(value);
|
|
3795
|
+
return {
|
|
3796
|
+
isDirectory: stat.isDirectory(),
|
|
3797
|
+
isFile: stat.isFile(),
|
|
3798
|
+
isSymlink: false,
|
|
3799
|
+
createdAtMs: Math.floor(stat.birthtimeMs),
|
|
3800
|
+
modifiedAtMs: Math.floor(stat.mtimeMs)
|
|
3801
|
+
};
|
|
3802
|
+
} catch {
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
/** Names inside a real local folder (folders first, hidden ones left out, bounded); nothing for anything else. */
|
|
3807
|
+
function readFolder(value) {
|
|
3808
|
+
const folder = localFolder(value);
|
|
3809
|
+
if (!folder) return [];
|
|
3810
|
+
try {
|
|
3811
|
+
return readdirSync(folder, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".") && (entry.isDirectory() || entry.isFile())).map((entry) => ({
|
|
3812
|
+
fileName: entry.name,
|
|
3813
|
+
isDirectory: entry.isDirectory(),
|
|
3814
|
+
isFile: entry.isFile()
|
|
3815
|
+
})).sort((a, b) => Number(b.isDirectory) - Number(a.isDirectory) || a.fileName.localeCompare(b.fileName)).slice(0, 500);
|
|
3816
|
+
} catch {
|
|
3817
|
+
return [];
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
/** Offset paging over a full list, the way the phone asks for it (cursor is the next offset). */
|
|
3821
|
+
function page(list, params) {
|
|
3822
|
+
const start = Number.isInteger(Number(params?.cursor)) ? Math.max(0, Number(params.cursor)) : 0;
|
|
3823
|
+
const limit = Number.isInteger(params?.limit) && params.limit > 0 ? params.limit : list.length;
|
|
3824
|
+
return {
|
|
3825
|
+
data: list.slice(start, start + limit),
|
|
3826
|
+
nextCursor: start + limit < list.length ? String(start + limit) : null,
|
|
3827
|
+
backwardsCursor: null
|
|
3828
|
+
};
|
|
3829
|
+
}
|
|
3830
|
+
/** Codex usage as the phone's rate-limit snapshot (primary and secondary windows of the main limit). */
|
|
3831
|
+
function rateLimits(read) {
|
|
3832
|
+
const toWindow = (entry) => entry && {
|
|
3833
|
+
usedPercent: Math.round(entry.usedPercent),
|
|
3834
|
+
windowDurationMins: Math.round(entry.windowSeconds / 60),
|
|
3835
|
+
resetsAt: entry.resetsAt ?? null
|
|
3836
|
+
};
|
|
3837
|
+
const snapshot = (limit) => ({
|
|
3838
|
+
limitId: limit?.id ?? "codex",
|
|
3839
|
+
limitName: limit?.name ?? null,
|
|
3840
|
+
normalModelSlug: null,
|
|
3841
|
+
primary: toWindow(limit?.windows?.[0]) ?? null,
|
|
3842
|
+
secondary: toWindow(limit?.windows?.[1]) ?? null,
|
|
3843
|
+
credits: null,
|
|
3844
|
+
individualLimit: null,
|
|
3845
|
+
spendControlReached: read?.spendControlReached ?? null,
|
|
3846
|
+
planType: read?.planType ?? null,
|
|
3847
|
+
rateLimitReachedType: read?.rateLimitReachedType ?? null
|
|
3848
|
+
});
|
|
3849
|
+
return {
|
|
3850
|
+
ordinaryUsageAllowed: null,
|
|
3851
|
+
rateLimits: snapshot(read?.rateLimits?.find((limit) => limit.id === "codex") ?? read?.rateLimits?.[0]),
|
|
3852
|
+
rateLimitsByLimitId: read ? Object.fromEntries((read.rateLimits ?? []).map((limit) => [limit.id, snapshot(limit)])) : null,
|
|
3853
|
+
rateLimitResetCredits: null,
|
|
3854
|
+
accountId: null,
|
|
3855
|
+
rateLimitUpsell: null
|
|
3856
|
+
};
|
|
3857
|
+
}
|
|
3348
3858
|
const seconds = (value) => Math.floor((Number.isFinite(value) ? value : Date.now()) / 1e3);
|
|
3349
3859
|
const emptyTurn = () => ({
|
|
3350
3860
|
id: randomUUID(),
|
|
@@ -3358,6 +3868,8 @@ const emptyTurn = () => ({
|
|
|
3358
3868
|
});
|
|
3359
3869
|
const textOf = (content) => (content ?? []).map((block) => block?.type === "text" ? block.text : "").filter(Boolean).join("\n");
|
|
3360
3870
|
const inputText = (input) => (Array.isArray(input) ? input : []).map((part) => part?.type === "text" ? part.text : "").filter(Boolean).join("\n");
|
|
3871
|
+
/** Skills the phone picked, written the way DSH invokes a skill: a /name gesture ahead of the text. */
|
|
3872
|
+
const skillGestures = (input) => (Array.isArray(input) ? input : []).filter((part) => part?.type === "skill" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(String(part.name))).map((part) => `/${part.name}`);
|
|
3361
3873
|
const notFound = (id) => new RpcError(-32602, `Thread not found: ${String(id).slice(0, 80)}`);
|
|
3362
3874
|
const modelKey = (selection) => `${encodeURIComponent(selection.provider)}/${encodeURIComponent(selection.model)}`;
|
|
3363
3875
|
function parseModelKey(value) {
|
|
@@ -3384,8 +3896,98 @@ function parseArguments(value) {
|
|
|
3384
3896
|
const visibleText = (content) => textOf(content).replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gu, "").trim();
|
|
3385
3897
|
/** Text of a user-role message the person wrote; DSH's own injected ones (runtime context, notices) carry another source kind. */
|
|
3386
3898
|
const typedText = (message) => message.role === "user" && (message.source === void 0 || message.source?.kind === "user") ? visibleText(message.content) : "";
|
|
3899
|
+
const lines = (text) => String(text ?? "").split("\n");
|
|
3900
|
+
const added = (text) => lines(text).map((line) => `+${line}`).join("\n");
|
|
3901
|
+
const removed = (text) => lines(text).map((line) => `-${line}`).join("\n");
|
|
3902
|
+
/**
|
|
3903
|
+
* How a DSH tool call looks on the phone: shell commands and file edits use the phone's own
|
|
3904
|
+
* command and diff views; everything else stays a generic tool call.
|
|
3905
|
+
*/
|
|
3906
|
+
function toolItem(block, cwd) {
|
|
3907
|
+
const args = parseArguments(block.arguments);
|
|
3908
|
+
const base = {
|
|
3909
|
+
id: block.id,
|
|
3910
|
+
status: "inProgress"
|
|
3911
|
+
};
|
|
3912
|
+
if (/^(bash|pwsh)(_persistent)?$/u.test(block.name) && typeof args?.command === "string") return {
|
|
3913
|
+
type: "commandExecution",
|
|
3914
|
+
...base,
|
|
3915
|
+
command: args.command,
|
|
3916
|
+
cwd: typeof args.workdir === "string" ? args.workdir : cwd ?? "",
|
|
3917
|
+
processId: null,
|
|
3918
|
+
source: "agent",
|
|
3919
|
+
commandActions: [],
|
|
3920
|
+
aggregatedOutput: null,
|
|
3921
|
+
exitCode: null,
|
|
3922
|
+
durationMs: null
|
|
3923
|
+
};
|
|
3924
|
+
if (block.name === "edit" && typeof args?.file_path === "string") return {
|
|
3925
|
+
type: "fileChange",
|
|
3926
|
+
...base,
|
|
3927
|
+
changes: [{
|
|
3928
|
+
path: args.file_path,
|
|
3929
|
+
kind: {
|
|
3930
|
+
type: "update",
|
|
3931
|
+
move_path: null
|
|
3932
|
+
},
|
|
3933
|
+
diff: `@@\n${removed(args.old_string)}\n${added(args.new_string)}`
|
|
3934
|
+
}]
|
|
3935
|
+
};
|
|
3936
|
+
if (block.name === "write" && typeof args?.file_path === "string") return {
|
|
3937
|
+
type: "fileChange",
|
|
3938
|
+
...base,
|
|
3939
|
+
changes: [{
|
|
3940
|
+
path: args.file_path,
|
|
3941
|
+
kind: { type: "add" },
|
|
3942
|
+
diff: added(args.content)
|
|
3943
|
+
}]
|
|
3944
|
+
};
|
|
3945
|
+
if (block.name === "codex_image_generate") return {
|
|
3946
|
+
type: "imageGeneration",
|
|
3947
|
+
...base,
|
|
3948
|
+
revisedPrompt: typeof args?.prompt === "string" ? args.prompt : null,
|
|
3949
|
+
result: ""
|
|
3950
|
+
};
|
|
3951
|
+
return {
|
|
3952
|
+
type: "dynamicToolCall",
|
|
3953
|
+
...base,
|
|
3954
|
+
namespace: null,
|
|
3955
|
+
tool: block.name,
|
|
3956
|
+
arguments: args,
|
|
3957
|
+
contentItems: null,
|
|
3958
|
+
success: null,
|
|
3959
|
+
durationMs: null
|
|
3960
|
+
};
|
|
3961
|
+
}
|
|
3962
|
+
/** Record a tool's result on the item that showed the call. */
|
|
3963
|
+
function finishTool(item, message, imageUrls) {
|
|
3964
|
+
const failed = Boolean(message.isError);
|
|
3965
|
+
item.status = failed ? "failed" : "completed";
|
|
3966
|
+
if (item.type === "commandExecution") {
|
|
3967
|
+
item.aggregatedOutput = textOf(message.content);
|
|
3968
|
+
item.exitCode = failed ? 1 : 0;
|
|
3969
|
+
} else if (item.type === "imageGeneration") {
|
|
3970
|
+
const url = imageBlocks(message).map((block) => imageUrls.get(block.attachment.attachmentId)).find(Boolean);
|
|
3971
|
+
item.result = url ? url.slice(url.indexOf(",") + 1) : "";
|
|
3972
|
+
if (!url) item.status = "failed";
|
|
3973
|
+
} else if (item.type === "dynamicToolCall") {
|
|
3974
|
+
item.contentItems = toolOutput(message, imageUrls);
|
|
3975
|
+
item.success = !failed;
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
const imageBlocks = (message) => (message.content ?? []).filter((block) => block?.type === "image" && block.attachment);
|
|
3979
|
+
const toolOutput = (message, imageUrls) => [{
|
|
3980
|
+
type: "inputText",
|
|
3981
|
+
text: textOf(message.content)
|
|
3982
|
+
}, ...imageBlocks(message).map((block) => imageUrls.get(block.attachment.attachmentId)).filter(Boolean).map((imageUrl) => ({
|
|
3983
|
+
type: "inputImage",
|
|
3984
|
+
imageUrl
|
|
3985
|
+
}))];
|
|
3387
3986
|
/** One user message starts a turn; assistant blocks and tool results fill it. */
|
|
3388
|
-
|
|
3987
|
+
/** Images the person attached to a message of their own. */
|
|
3988
|
+
const typedImages = (message) => message.role === "user" && (message.source === void 0 || message.source?.kind === "user") ? (message.content ?? []).filter((block) => block?.type === "image" && block.attachment) : [];
|
|
3989
|
+
/** `imageUrls` maps an attachment id to a data URL the phone can show; images without one are left out. */
|
|
3990
|
+
function projectTurns(messages, at = Date.now(), imageUrls = /* @__PURE__ */ new Map(), cwd = void 0) {
|
|
3389
3991
|
const turns = [];
|
|
3390
3992
|
let turn;
|
|
3391
3993
|
const calls = /* @__PURE__ */ new Map();
|
|
@@ -3399,7 +4001,8 @@ function projectTurns(messages, at = Date.now()) {
|
|
|
3399
4001
|
};
|
|
3400
4002
|
for (const message of messages) if (message.role === "user") {
|
|
3401
4003
|
const text = typedText(message);
|
|
3402
|
-
|
|
4004
|
+
const pictures = typedImages(message).map((block) => imageUrls.get(block.attachment.attachmentId)).filter(Boolean);
|
|
4005
|
+
if (text === "" && pictures.length === 0) continue;
|
|
3403
4006
|
close();
|
|
3404
4007
|
turn = {
|
|
3405
4008
|
...emptyTurn(),
|
|
@@ -3410,11 +4013,14 @@ function projectTurns(messages, at = Date.now()) {
|
|
|
3410
4013
|
type: "userMessage",
|
|
3411
4014
|
id: message.id,
|
|
3412
4015
|
clientId: null,
|
|
3413
|
-
content: [{
|
|
4016
|
+
content: [...text === "" ? [] : [{
|
|
3414
4017
|
type: "text",
|
|
3415
4018
|
text,
|
|
3416
4019
|
text_elements: []
|
|
3417
|
-
}]
|
|
4020
|
+
}], ...pictures.map((url) => ({
|
|
4021
|
+
type: "image",
|
|
4022
|
+
url
|
|
4023
|
+
}))]
|
|
3418
4024
|
});
|
|
3419
4025
|
} else if (message.role === "assistant") {
|
|
3420
4026
|
turn ??= {
|
|
@@ -3438,36 +4044,19 @@ function projectTurns(messages, at = Date.now()) {
|
|
|
3438
4044
|
content: [block.text]
|
|
3439
4045
|
});
|
|
3440
4046
|
else if (block.type === "tool-call") {
|
|
3441
|
-
const item =
|
|
3442
|
-
type: "dynamicToolCall",
|
|
3443
|
-
id: block.id,
|
|
3444
|
-
namespace: null,
|
|
3445
|
-
tool: block.name,
|
|
3446
|
-
arguments: parseArguments(block.arguments),
|
|
3447
|
-
status: "inProgress",
|
|
3448
|
-
contentItems: null,
|
|
3449
|
-
success: null,
|
|
3450
|
-
durationMs: null
|
|
3451
|
-
};
|
|
4047
|
+
const item = toolItem(block, cwd);
|
|
3452
4048
|
calls.set(block.id, item);
|
|
3453
4049
|
turn.items.push(item);
|
|
3454
4050
|
}
|
|
3455
4051
|
}
|
|
3456
4052
|
} else if (message.role === "tool") {
|
|
3457
4053
|
const item = calls.get(message.toolCallId);
|
|
3458
|
-
if (item)
|
|
3459
|
-
item.contentItems = [{
|
|
3460
|
-
type: "inputText",
|
|
3461
|
-
text: textOf(message.content)
|
|
3462
|
-
}];
|
|
3463
|
-
item.status = message.isError ? "failed" : "completed";
|
|
3464
|
-
item.success = !message.isError;
|
|
3465
|
-
}
|
|
4054
|
+
if (item) finishTool(item, message, imageUrls);
|
|
3466
4055
|
}
|
|
3467
4056
|
close();
|
|
3468
4057
|
return turns;
|
|
3469
4058
|
}
|
|
3470
|
-
function thread({ id, title, preview, cwd, createdAt, updatedAt, running, turns = [] }) {
|
|
4059
|
+
function thread({ id, title, preview, cwd, createdAt, updatedAt, running, turns = [], projectId = null }) {
|
|
3471
4060
|
return {
|
|
3472
4061
|
id,
|
|
3473
4062
|
sessionId: id,
|
|
@@ -3496,7 +4085,7 @@ function thread({ id, title, preview, cwd, createdAt, updatedAt, running, turns
|
|
|
3496
4085
|
historyMode: "legacy",
|
|
3497
4086
|
section: null,
|
|
3498
4087
|
sectionEnteredAt: null,
|
|
3499
|
-
projectId
|
|
4088
|
+
projectId,
|
|
3500
4089
|
canAcceptDirectInput: true,
|
|
3501
4090
|
extra: null
|
|
3502
4091
|
};
|
|
@@ -3505,7 +4094,7 @@ function thread({ id, title, preview, cwd, createdAt, updatedAt, running, turns
|
|
|
3505
4094
|
* @param {{ controller: () => any, agents: () => any, permissions?: () => any, userAgent: string }} options
|
|
3506
4095
|
*/
|
|
3507
4096
|
const PHONE_RETURN_MS = 12e4;
|
|
3508
|
-
function createDshRemoteControl({ controller, agents, permissions = () => void 0, workspaces = () => void 0, trace = () => {}, phoneReturnMs = PHONE_RETURN_MS, userAgent }) {
|
|
4097
|
+
function createDshRemoteControl({ controller, agents, permissions = () => void 0, workspaces = () => void 0, attachments = () => void 0, usage = () => void 0, projections = () => void 0, skills = () => void 0, llm = () => void 0, trace = () => {}, phoneReturnMs = PHONE_RETURN_MS, userAgent }) {
|
|
3509
4098
|
const subscribers = /* @__PURE__ */ new Map();
|
|
3510
4099
|
const active = /* @__PURE__ */ new Map();
|
|
3511
4100
|
const queues = /* @__PURE__ */ new Map();
|
|
@@ -3577,6 +4166,69 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
3577
4166
|
turnsBackwardsCursor: null
|
|
3578
4167
|
};
|
|
3579
4168
|
};
|
|
4169
|
+
const searches = /* @__PURE__ */ new Map();
|
|
4170
|
+
/** Folders of DSH workspaces and conversations: the only places the phone may search or run git. */
|
|
4171
|
+
const conversationFolders = async () => {
|
|
4172
|
+
const items = (await service().list({}).catch(() => ({ items: [] }))).items ?? [];
|
|
4173
|
+
return [...workspaceList().map((entry) => entry.path), ...items.map((item) => item.cwd)].filter((folder) => typeof folder === "string" && folder !== "");
|
|
4174
|
+
};
|
|
4175
|
+
/** DSH's token and context-window projections as the phone's token-usage ring. */
|
|
4176
|
+
const tokenUsage = (session) => {
|
|
4177
|
+
try {
|
|
4178
|
+
const values = projections()?.snapshot?.(session, ["tokenUsage", "contextPressure"])?.values;
|
|
4179
|
+
const used = values?.tokenUsage;
|
|
4180
|
+
if (!used) return void 0;
|
|
4181
|
+
const breakdown = (total, input, cached, output) => ({
|
|
4182
|
+
totalTokens: total,
|
|
4183
|
+
inputTokens: input,
|
|
4184
|
+
cachedInputTokens: cached,
|
|
4185
|
+
cacheWriteInputTokens: 0,
|
|
4186
|
+
outputTokens: output,
|
|
4187
|
+
reasoningOutputTokens: 0
|
|
4188
|
+
});
|
|
4189
|
+
const input = (used.uncachedInputTokens ?? 0) + (used.cacheReadTokens ?? 0) + (used.cacheWriteTokens ?? 0);
|
|
4190
|
+
const pressure = values.contextPressure?.pressureTokens ?? 0;
|
|
4191
|
+
return {
|
|
4192
|
+
total: breakdown(input + (used.outputTokens ?? 0), input, used.cacheReadTokens ?? 0, used.outputTokens ?? 0),
|
|
4193
|
+
last: breakdown(pressure, pressure, 0, 0),
|
|
4194
|
+
modelContextWindow: values.contextPressure?.contextWindow ?? null
|
|
4195
|
+
};
|
|
4196
|
+
} catch {
|
|
4197
|
+
return;
|
|
4198
|
+
}
|
|
4199
|
+
};
|
|
4200
|
+
const workspaceList = () => {
|
|
4201
|
+
try {
|
|
4202
|
+
return workspaces()?.list?.() ?? [];
|
|
4203
|
+
} catch {
|
|
4204
|
+
return [];
|
|
4205
|
+
}
|
|
4206
|
+
};
|
|
4207
|
+
const projectIdOf = (sessionId) => workspaceList().find((entry) => entry.sessionIds?.includes(sessionId))?.id ?? null;
|
|
4208
|
+
const projectOf = (entry, position) => ({
|
|
4209
|
+
id: String(entry.id),
|
|
4210
|
+
name: entry.title || String(entry.path).split(/[\/]/u).filter(Boolean).at(-1) || "Workspace",
|
|
4211
|
+
roots: [{ path: entry.path }],
|
|
4212
|
+
metadata: {},
|
|
4213
|
+
position,
|
|
4214
|
+
createdAt: seconds(entry.createdAt),
|
|
4215
|
+
updatedAt: seconds(entry.updatedAt),
|
|
4216
|
+
recencyAt: entry.sessionIds?.length ? seconds(entry.updatedAt) : null
|
|
4217
|
+
});
|
|
4218
|
+
/** Data URLs for the images in a conversation (newest first, bounded), so the phone can show them. */
|
|
4219
|
+
const imageUrlsOf = async (messages) => {
|
|
4220
|
+
const urls = /* @__PURE__ */ new Map();
|
|
4221
|
+
const store = attachments();
|
|
4222
|
+
if (!store) return urls;
|
|
4223
|
+
const wanted = messages.flatMap((message) => message.role === "tool" || typedImages(message).length > 0 ? imageBlocks(message) : []).toReversed().slice(0, 12);
|
|
4224
|
+
await Promise.all(wanted.map(async (block) => {
|
|
4225
|
+
try {
|
|
4226
|
+
const stored = await store.readImage(block.attachment, AbortSignal.timeout(1e4));
|
|
4227
|
+
urls.set(block.attachment.attachmentId, `data:${stored.ref.mediaType};base64,${Buffer.from(stored.data).toString("base64")}`);
|
|
4228
|
+
} catch {}
|
|
4229
|
+
}));
|
|
4230
|
+
return urls;
|
|
4231
|
+
};
|
|
3580
4232
|
const openThread = async (id, notify) => {
|
|
3581
4233
|
const agent = await agentFor(id);
|
|
3582
4234
|
if (notify) subscribe(id, notify);
|
|
@@ -3584,6 +4236,7 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
3584
4236
|
const session = agent.session;
|
|
3585
4237
|
const messages = session.deriveMessages();
|
|
3586
4238
|
const first = messages.find((message) => typedText(message) !== "");
|
|
4239
|
+
const imageUrls = await imageUrlsOf(messages);
|
|
3587
4240
|
return {
|
|
3588
4241
|
agent,
|
|
3589
4242
|
thread: thread({
|
|
@@ -3591,8 +4244,9 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
3591
4244
|
cwd: session.meta?.cwd,
|
|
3592
4245
|
createdAt: session.meta?.createdAt,
|
|
3593
4246
|
running: active.has(id),
|
|
4247
|
+
projectId: projectIdOf(id),
|
|
3594
4248
|
preview: first ? visibleText(first.content).slice(0, 200) : "",
|
|
3595
|
-
turns: projectTurns(messages, session.meta?.createdAt)
|
|
4249
|
+
turns: projectTurns(messages, session.meta?.createdAt, imageUrls, session.meta?.cwd)
|
|
3596
4250
|
})
|
|
3597
4251
|
};
|
|
3598
4252
|
};
|
|
@@ -3699,9 +4353,33 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
3699
4353
|
completedAt: null
|
|
3700
4354
|
} };
|
|
3701
4355
|
};
|
|
4356
|
+
/** Message content for DSH from what the phone sent: its text plus any inline images (DSH admits and stores them itself). */
|
|
4357
|
+
const contentOf = (input) => {
|
|
4358
|
+
const text = [...skillGestures(input), inputText(input)].filter(Boolean).join(" ");
|
|
4359
|
+
const pictures = inputImages(input);
|
|
4360
|
+
if (text === "" && pictures.length === 0) throw invalidParams("Send some text or an image");
|
|
4361
|
+
if (pictures.length > MAX_PHONE_IMAGES) throw invalidParams(`At most ${MAX_PHONE_IMAGES} images per message`);
|
|
4362
|
+
const images = pictures.map((picture) => {
|
|
4363
|
+
const image = dataImage(picture.url);
|
|
4364
|
+
if (!image) throw invalidParams("Only images sent inline are supported");
|
|
4365
|
+
return {
|
|
4366
|
+
type: "image",
|
|
4367
|
+
data: Buffer.from(image.data).toString("base64"),
|
|
4368
|
+
mediaType: image.mediaType,
|
|
4369
|
+
name: "phone-image"
|
|
4370
|
+
};
|
|
4371
|
+
});
|
|
4372
|
+
return {
|
|
4373
|
+
text,
|
|
4374
|
+
pictures,
|
|
4375
|
+
content: [...text === "" ? [] : [{
|
|
4376
|
+
type: "text",
|
|
4377
|
+
text
|
|
4378
|
+
}], ...images]
|
|
4379
|
+
};
|
|
4380
|
+
};
|
|
3702
4381
|
async function startTurn(params, notify) {
|
|
3703
|
-
const text =
|
|
3704
|
-
if (text === "") throw invalidParams("Only text input is supported");
|
|
4382
|
+
const { text, pictures, content } = contentOf(params?.input);
|
|
3705
4383
|
lastPrompt.text = text;
|
|
3706
4384
|
const id = params?.threadId;
|
|
3707
4385
|
const agent = await agentFor(id);
|
|
@@ -3718,16 +4396,26 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
3718
4396
|
type: "userMessage",
|
|
3719
4397
|
id: randomUUID(),
|
|
3720
4398
|
clientId: params.clientUserMessageId ?? null,
|
|
3721
|
-
content: [{
|
|
4399
|
+
content: [...text === "" ? [] : [{
|
|
3722
4400
|
type: "text",
|
|
3723
4401
|
text,
|
|
3724
4402
|
text_elements: []
|
|
3725
|
-
}]
|
|
4403
|
+
}], ...pictures.map((picture) => ({
|
|
4404
|
+
type: "image",
|
|
4405
|
+
url: picture.url
|
|
4406
|
+
}))]
|
|
3726
4407
|
};
|
|
3727
4408
|
emit(id, "turn/started", {
|
|
3728
4409
|
threadId: id,
|
|
3729
4410
|
turn
|
|
3730
4411
|
});
|
|
4412
|
+
emit(id, "thread/status/changed", {
|
|
4413
|
+
threadId: id,
|
|
4414
|
+
status: {
|
|
4415
|
+
type: "active",
|
|
4416
|
+
activeFlags: []
|
|
4417
|
+
}
|
|
4418
|
+
});
|
|
3731
4419
|
emit(id, "item/started", {
|
|
3732
4420
|
threadId: id,
|
|
3733
4421
|
turnId: turn.id,
|
|
@@ -3745,10 +4433,7 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
3745
4433
|
requestId: randomUUID(),
|
|
3746
4434
|
sessionId: id,
|
|
3747
4435
|
mode: "queue",
|
|
3748
|
-
content
|
|
3749
|
-
type: "text",
|
|
3750
|
-
text
|
|
3751
|
-
}]
|
|
4436
|
+
content
|
|
3752
4437
|
}, AbortSignal.timeout(3e4));
|
|
3753
4438
|
} catch (error) {
|
|
3754
4439
|
active.delete(id);
|
|
@@ -3773,10 +4458,20 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
3773
4458
|
}
|
|
3774
4459
|
turn.completedAt = seconds(Date.now());
|
|
3775
4460
|
active.delete(id);
|
|
4461
|
+
const usageNow = tokenUsage(agent.session);
|
|
4462
|
+
if (usageNow) emit(id, "thread/tokenUsage/updated", {
|
|
4463
|
+
threadId: id,
|
|
4464
|
+
turnId: turn.id,
|
|
4465
|
+
tokenUsage: usageNow
|
|
4466
|
+
});
|
|
3776
4467
|
emit(id, "turn/completed", {
|
|
3777
4468
|
threadId: id,
|
|
3778
4469
|
turn
|
|
3779
4470
|
});
|
|
4471
|
+
emit(id, "thread/status/changed", {
|
|
4472
|
+
threadId: id,
|
|
4473
|
+
status: { type: "idle" }
|
|
4474
|
+
});
|
|
3780
4475
|
const [next, ...rest] = queues.get(id) ?? [];
|
|
3781
4476
|
if (next) {
|
|
3782
4477
|
queues.set(id, rest);
|
|
@@ -3800,11 +4495,11 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
3800
4495
|
}[process.platform] ?? process.platform,
|
|
3801
4496
|
userAgent
|
|
3802
4497
|
}),
|
|
3803
|
-
"account/read": () => ({
|
|
4498
|
+
"account/read": async () => ({
|
|
3804
4499
|
account: {
|
|
3805
4500
|
type: "chatgpt",
|
|
3806
4501
|
email: null,
|
|
3807
|
-
planType: "unknown"
|
|
4502
|
+
planType: (await usage()?.read?.().catch(() => void 0))?.planType ?? "unknown"
|
|
3808
4503
|
},
|
|
3809
4504
|
requiresOpenaiAuth: false
|
|
3810
4505
|
}),
|
|
@@ -3817,36 +4512,42 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
3817
4512
|
const value = await loadCatalog();
|
|
3818
4513
|
const defaultKey = value.default ? modelKey(value.default) : void 0;
|
|
3819
4514
|
const data = [];
|
|
3820
|
-
for (const group of value.groups ?? [])
|
|
3821
|
-
const
|
|
3822
|
-
|
|
3823
|
-
model:
|
|
3824
|
-
});
|
|
3825
|
-
const
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
4515
|
+
for (const group of value.groups ?? []) {
|
|
4516
|
+
const modalities = new Map(await Promise.all((group.models ?? []).map(async (model) => {
|
|
4517
|
+
const info = await Promise.resolve(llm()?.resolveModelInfo?.(group.id, model.id)).catch(() => void 0);
|
|
4518
|
+
return [model.id, (Array.isArray(info?.inputModalities) ? info.inputModalities : ["text"]).filter((kind) => kind === "text" || kind === "image")];
|
|
4519
|
+
})));
|
|
4520
|
+
for (const model of group.models ?? []) {
|
|
4521
|
+
const key = modelKey({
|
|
4522
|
+
provider: group.id,
|
|
4523
|
+
model: model.id
|
|
4524
|
+
});
|
|
4525
|
+
const efforts = (model.reasoning?.efforts ?? []).map((effort) => ({
|
|
4526
|
+
reasoningEffort: effort.id,
|
|
4527
|
+
description: effort.name ?? effort.id
|
|
4528
|
+
}));
|
|
4529
|
+
const supported = efforts.length > 0 ? efforts : [{
|
|
4530
|
+
reasoningEffort: "medium",
|
|
4531
|
+
description: "Default"
|
|
4532
|
+
}];
|
|
4533
|
+
data.push({
|
|
4534
|
+
id: key,
|
|
4535
|
+
model: key,
|
|
4536
|
+
displayName: model.name ?? model.id,
|
|
4537
|
+
description: group.name ?? "",
|
|
4538
|
+
hidden: false,
|
|
4539
|
+
isDefault: key === defaultKey,
|
|
4540
|
+
supportedReasoningEfforts: supported,
|
|
4541
|
+
defaultReasoningEffort: model.reasoning?.defaultEffort ?? supported[0].reasoningEffort,
|
|
4542
|
+
inputModalities: modalities.get(model.id)?.length ? modalities.get(model.id) : ["text"],
|
|
4543
|
+
supportsPersonality: false,
|
|
4544
|
+
additionalSpeedTiers: [],
|
|
4545
|
+
upgrade: null,
|
|
4546
|
+
upgradeInfo: null,
|
|
4547
|
+
availabilityNux: null,
|
|
4548
|
+
serviceTiers: []
|
|
4549
|
+
});
|
|
4550
|
+
}
|
|
3850
4551
|
}
|
|
3851
4552
|
return {
|
|
3852
4553
|
data,
|
|
@@ -3874,22 +4575,32 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
3874
4575
|
filePath: process.platform === "win32" ? "C:dshconfig.toml" : "/dsh/config.toml",
|
|
3875
4576
|
overriddenMetadata: null
|
|
3876
4577
|
}),
|
|
3877
|
-
"command/exec": () => {
|
|
3878
|
-
|
|
4578
|
+
"command/exec": async (params) => {
|
|
4579
|
+
const folders = await conversationFolders();
|
|
4580
|
+
try {
|
|
4581
|
+
return await runGit(params, { folders });
|
|
4582
|
+
} catch (error) {
|
|
4583
|
+
trace({
|
|
4584
|
+
method: "command/exec:refused",
|
|
4585
|
+
reason: String(error?.message).slice(0, 80),
|
|
4586
|
+
command: Array.isArray(params?.command) ? params.command.slice(0, 3).map((part) => String(part).slice(0, 40)) : null
|
|
4587
|
+
});
|
|
4588
|
+
throw new RpcError(-32600, error?.code === "not-allowed" ? `Not available on a DSH host: ${error.message}` : "Could not run the command");
|
|
4589
|
+
}
|
|
3879
4590
|
},
|
|
3880
4591
|
"plugin/installed": () => ({
|
|
3881
4592
|
marketplaceLoadErrors: [],
|
|
3882
4593
|
marketplaces: []
|
|
3883
4594
|
}),
|
|
3884
4595
|
"fs/createDirectory": () => ({}),
|
|
3885
|
-
"fs/getMetadata": () => ({
|
|
4596
|
+
"fs/getMetadata": (params) => realMetadata(params?.path) ?? {
|
|
3886
4597
|
isDirectory: true,
|
|
3887
4598
|
isFile: false,
|
|
3888
4599
|
isSymlink: false,
|
|
3889
4600
|
createdAtMs: Date.now(),
|
|
3890
4601
|
modifiedAtMs: Date.now()
|
|
3891
|
-
}
|
|
3892
|
-
"fs/readDirectory": () => ({ entries:
|
|
4602
|
+
},
|
|
4603
|
+
"fs/readDirectory": (params) => ({ entries: readFolder(params?.path) }),
|
|
3893
4604
|
"collaborationMode/list": () => ({ data: [] }),
|
|
3894
4605
|
"permissionProfile/list": () => ({
|
|
3895
4606
|
data: [],
|
|
@@ -3904,7 +4615,6 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
3904
4615
|
data: [],
|
|
3905
4616
|
nextCursor: null
|
|
3906
4617
|
}),
|
|
3907
|
-
"skills/list": () => ({ data: [] }),
|
|
3908
4618
|
"config/read": () => ({
|
|
3909
4619
|
config: {},
|
|
3910
4620
|
origins: {},
|
|
@@ -3919,19 +4629,114 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
3919
4629
|
const value = await service().list({}, signal);
|
|
3920
4630
|
const archived = new Set(workspaces()?.archivedSessionIds ?? []);
|
|
3921
4631
|
const wantArchived = params?.archived === true;
|
|
4632
|
+
const excluded = new Set(Array.isArray(params?.excludedThreadIds) ? params.excludedThreadIds : []);
|
|
4633
|
+
const folders = [params?.cwd].flat().filter((entry) => typeof entry === "string");
|
|
4634
|
+
const members = typeof params?.projectId === "string" ? new Set(workspaceList().find((entry) => String(entry.id) === params.projectId)?.sessionIds ?? []) : void 0;
|
|
4635
|
+
const needle = params?.searchTerm ? String(params.searchTerm).toLocaleLowerCase() : "";
|
|
4636
|
+
return page((value.items ?? []).filter((item) => item.origin !== "subagent" && !item.blank && archived.has(item.sessionId) === wantArchived && !excluded.has(item.sessionId) && (folders.length === 0 || folders.includes(item.cwd)) && (members === void 0 || members.has(item.sessionId))).map((item) => thread({
|
|
4637
|
+
id: item.sessionId,
|
|
4638
|
+
cwd: item.cwd,
|
|
4639
|
+
createdAt: item.updatedAt,
|
|
4640
|
+
updatedAt: item.updatedAt,
|
|
4641
|
+
projectId: projectIdOf(item.sessionId),
|
|
4642
|
+
running: item.running,
|
|
4643
|
+
title: item.projections?.values?.title ?? null
|
|
4644
|
+
})).filter((entry) => !needle || `${entry.name ?? ""} ${entry.preview}`.toLocaleLowerCase().includes(needle)), params);
|
|
4645
|
+
},
|
|
4646
|
+
"project/list": (params) => page(workspaceList().map(projectOf), params),
|
|
4647
|
+
"project/read": async (params) => {
|
|
4648
|
+
const list = workspaceList();
|
|
4649
|
+
const index = list.findIndex((entry) => String(entry.id) === params?.projectId);
|
|
4650
|
+
if (index < 0) throw new RpcError(-32602, `Project not found: ${String(params?.projectId).slice(0, 80)}`);
|
|
4651
|
+
return { project: projectOf(list[index], index) };
|
|
4652
|
+
},
|
|
4653
|
+
"account/rateLimits/read": async () => rateLimits(await usage()?.read?.().catch(() => void 0)),
|
|
4654
|
+
"thread/turns/list": async (params) => {
|
|
4655
|
+
const { thread: value } = await openThread(params?.threadId);
|
|
4656
|
+
const ordered = params?.sortDirection === "asc" ? value.turns : value.turns.toReversed();
|
|
4657
|
+
return {
|
|
4658
|
+
...page(ordered, params),
|
|
4659
|
+
backwardsCursor: ordered.length ? String(0) : null
|
|
4660
|
+
};
|
|
4661
|
+
},
|
|
4662
|
+
"thread/fork": async (params, { notify }) => {
|
|
4663
|
+
const forked = await service().fork({ sessionId: params?.threadId });
|
|
4664
|
+
const agent = await agentFor(forked.sessionId);
|
|
4665
|
+
await applySelection(agent, forked.sessionId, params);
|
|
4666
|
+
const { thread: value } = await openThread(forked.sessionId, notify);
|
|
4667
|
+
emit(forked.sessionId, "thread/started", { thread: value });
|
|
3922
4668
|
return {
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
cwd: item.cwd,
|
|
3926
|
-
createdAt: item.updatedAt,
|
|
3927
|
-
updatedAt: item.updatedAt,
|
|
3928
|
-
running: item.running,
|
|
3929
|
-
title: item.projections?.values?.title ?? null
|
|
3930
|
-
})).filter((entry) => !params?.searchTerm || `${entry.name ?? ""} ${entry.preview}`.toLocaleLowerCase().includes(String(params.searchTerm).toLocaleLowerCase())),
|
|
3931
|
-
nextCursor: null,
|
|
3932
|
-
backwardsCursor: null
|
|
4669
|
+
...settings(agent, value.cwd),
|
|
4670
|
+
thread: value
|
|
3933
4671
|
};
|
|
3934
4672
|
},
|
|
4673
|
+
"thread/readState/update": () => ({}),
|
|
4674
|
+
fuzzyFileSearch: async (params) => ({ files: await searchFiles({
|
|
4675
|
+
roots: params?.roots ?? [],
|
|
4676
|
+
query: String(params?.query ?? ""),
|
|
4677
|
+
folders: await conversationFolders()
|
|
4678
|
+
}) }),
|
|
4679
|
+
"fuzzyFileSearch/sessionStart": (params, { notify }) => {
|
|
4680
|
+
searches.set(params?.sessionId, {
|
|
4681
|
+
roots: params?.roots ?? [],
|
|
4682
|
+
notify
|
|
4683
|
+
});
|
|
4684
|
+
return {};
|
|
4685
|
+
},
|
|
4686
|
+
"fuzzyFileSearch/sessionUpdate": async (params) => {
|
|
4687
|
+
const search = searches.get(params?.sessionId);
|
|
4688
|
+
if (search) {
|
|
4689
|
+
const query = String(params?.query ?? "");
|
|
4690
|
+
const files = await searchFiles({
|
|
4691
|
+
roots: search.roots,
|
|
4692
|
+
query,
|
|
4693
|
+
folders: await conversationFolders()
|
|
4694
|
+
});
|
|
4695
|
+
search.notify("fuzzyFileSearch/sessionUpdated", {
|
|
4696
|
+
sessionId: params.sessionId,
|
|
4697
|
+
query,
|
|
4698
|
+
files
|
|
4699
|
+
}).catch(() => {});
|
|
4700
|
+
search.notify("fuzzyFileSearch/sessionCompleted", { sessionId: params.sessionId }).catch(() => {});
|
|
4701
|
+
}
|
|
4702
|
+
return {};
|
|
4703
|
+
},
|
|
4704
|
+
"fuzzyFileSearch/sessionStop": (params) => {
|
|
4705
|
+
searches.delete(params?.sessionId);
|
|
4706
|
+
return {};
|
|
4707
|
+
},
|
|
4708
|
+
"skills/list": async (params, { signal }) => {
|
|
4709
|
+
const registry = skills();
|
|
4710
|
+
const folders = Array.isArray(params?.cwds) && params.cwds.length > 0 ? params.cwds : [workspaceList()[0]?.path ?? ""];
|
|
4711
|
+
return { data: await Promise.all(folders.map(async (cwd) => {
|
|
4712
|
+
try {
|
|
4713
|
+
return {
|
|
4714
|
+
cwd,
|
|
4715
|
+
errors: [],
|
|
4716
|
+
skills: (await registry?.list?.({
|
|
4717
|
+
cwd: cwd || void 0,
|
|
4718
|
+
signal
|
|
4719
|
+
}) ?? []).filter((skill) => skill?.invocation?.userInvocable !== false).map((skill) => ({
|
|
4720
|
+
name: skill.name,
|
|
4721
|
+
description: skill.description ?? "",
|
|
4722
|
+
shortDescription: null,
|
|
4723
|
+
interface: null,
|
|
4724
|
+
dependencies: null,
|
|
4725
|
+
path: skill.path ?? skill.name,
|
|
4726
|
+
scope: "user",
|
|
4727
|
+
enabled: true,
|
|
4728
|
+
pluginId: null
|
|
4729
|
+
}))
|
|
4730
|
+
};
|
|
4731
|
+
} catch {
|
|
4732
|
+
return {
|
|
4733
|
+
cwd,
|
|
4734
|
+
skills: [],
|
|
4735
|
+
errors: []
|
|
4736
|
+
};
|
|
4737
|
+
}
|
|
4738
|
+
})) };
|
|
4739
|
+
},
|
|
3935
4740
|
"thread/archive": async (params) => {
|
|
3936
4741
|
const registry = workspaces();
|
|
3937
4742
|
if (!registry) throw new RpcError(-32603, "DSH archive is unavailable in this profile");
|
|
@@ -3960,7 +4765,8 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
3960
4765
|
"thread/start": async (params, { notify }) => {
|
|
3961
4766
|
if (params?.ephemeral === true) return titleThread(params);
|
|
3962
4767
|
const cwd = localFolder(params?.cwd);
|
|
3963
|
-
const
|
|
4768
|
+
const project = typeof params?.projectId === "string" ? workspaceList().find((entry) => String(entry.id) === params.projectId) : void 0;
|
|
4769
|
+
const created = await service().create(project ? { workspaceId: project.id } : cwd ? { cwd } : {});
|
|
3964
4770
|
const agent = await agentFor(created.sessionId);
|
|
3965
4771
|
await applySelection(agent, created.sessionId, params);
|
|
3966
4772
|
const { thread: value } = await openThread(created.sessionId, notify);
|
|
@@ -3976,16 +4782,12 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
3976
4782
|
return {};
|
|
3977
4783
|
},
|
|
3978
4784
|
"turn/steer": async (params) => {
|
|
3979
|
-
const
|
|
3980
|
-
if (text === "") throw invalidParams("Only text input is supported");
|
|
4785
|
+
const { content } = contentOf(params?.input);
|
|
3981
4786
|
await service().prompt({
|
|
3982
4787
|
requestId: randomUUID(),
|
|
3983
4788
|
sessionId: params?.threadId,
|
|
3984
4789
|
mode: "steer",
|
|
3985
|
-
content
|
|
3986
|
-
type: "text",
|
|
3987
|
-
text
|
|
3988
|
-
}]
|
|
4790
|
+
content
|
|
3989
4791
|
}, AbortSignal.timeout(3e4));
|
|
3990
4792
|
return { turnId: params?.expectedTurnId ?? active.get(params?.threadId)?.turn.id ?? "" };
|
|
3991
4793
|
},
|
|
@@ -4083,6 +4885,26 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4083
4885
|
*/
|
|
4084
4886
|
const askPhone = async (request, method, params) => {
|
|
4085
4887
|
const sessionId = request.agent.session.id;
|
|
4888
|
+
emit(sessionId, "thread/status/changed", {
|
|
4889
|
+
threadId: sessionId,
|
|
4890
|
+
status: {
|
|
4891
|
+
type: "active",
|
|
4892
|
+
activeFlags: [method === "item/tool/requestUserInput" ? "waitingOnUserInput" : "waitingOnApproval"]
|
|
4893
|
+
}
|
|
4894
|
+
});
|
|
4895
|
+
try {
|
|
4896
|
+
return await asked(request, method, params, sessionId);
|
|
4897
|
+
} finally {
|
|
4898
|
+
if (active.has(sessionId)) emit(sessionId, "thread/status/changed", {
|
|
4899
|
+
threadId: sessionId,
|
|
4900
|
+
status: {
|
|
4901
|
+
type: "active",
|
|
4902
|
+
activeFlags: []
|
|
4903
|
+
}
|
|
4904
|
+
});
|
|
4905
|
+
}
|
|
4906
|
+
};
|
|
4907
|
+
const asked = async (request, method, params, sessionId) => {
|
|
4086
4908
|
for (;;) {
|
|
4087
4909
|
const phone = phoneFor(request)?.phone;
|
|
4088
4910
|
if (!phone) throw new RpcError(-32800, "No phone");
|
|
@@ -4113,6 +4935,7 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4113
4935
|
threadId: session.id,
|
|
4114
4936
|
turnId: running.turn.id,
|
|
4115
4937
|
itemId: request.wait?.callId ?? randomUUID(),
|
|
4938
|
+
isBlocking: true,
|
|
4116
4939
|
questions: request.questions.map((question) => ({
|
|
4117
4940
|
id: question.id,
|
|
4118
4941
|
header: question.header ?? "",
|
|
@@ -4145,27 +4968,95 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4145
4968
|
});
|
|
4146
4969
|
if (!found) return next();
|
|
4147
4970
|
const { session, running } = found;
|
|
4148
|
-
|
|
4971
|
+
const call = request.callId ? callOf(session, request.callId) : void 0;
|
|
4972
|
+
const reason = request.displayReason?.en ?? request.reason ?? null;
|
|
4973
|
+
const common = {
|
|
4149
4974
|
threadId: session.id,
|
|
4150
4975
|
turnId: running.turn.id,
|
|
4151
4976
|
itemId: request.callId ?? randomUUID(),
|
|
4152
|
-
startedAtMs: Date.now()
|
|
4977
|
+
startedAtMs: Date.now()
|
|
4978
|
+
};
|
|
4979
|
+
return (call && (call.name === "edit" || call.name === "write") ? askPhone(request, "item/fileChange/requestApproval", {
|
|
4980
|
+
...common,
|
|
4981
|
+
reason,
|
|
4982
|
+
grantRoot: null
|
|
4983
|
+
}) : askPhone(request, "item/commandExecution/requestApproval", {
|
|
4984
|
+
...common,
|
|
4153
4985
|
command: commandText(session, request),
|
|
4154
4986
|
cwd: session.meta?.cwd ?? null,
|
|
4155
|
-
reason
|
|
4987
|
+
reason,
|
|
4156
4988
|
commandActions: [],
|
|
4157
4989
|
availableDecisions: [
|
|
4158
4990
|
"accept",
|
|
4159
4991
|
"decline",
|
|
4160
4992
|
"cancel"
|
|
4161
4993
|
]
|
|
4162
|
-
}).then((answer) => {
|
|
4994
|
+
})).then((answer) => {
|
|
4163
4995
|
const decision = typeof answer?.decision === "string" ? answer.decision : "";
|
|
4164
4996
|
return decision === "accept" || decision === "acceptForSession" ? "allowed-once" : decision === "cancel" ? "cancelled" : "rejected";
|
|
4165
4997
|
}, () => next());
|
|
4166
4998
|
};
|
|
4999
|
+
const streams = /* @__PURE__ */ new Map();
|
|
5000
|
+
const onStream = (session, frame) => {
|
|
5001
|
+
const running = session ? active.get(session.id) : void 0;
|
|
5002
|
+
if (!running || !subscribers.has(session.id)) return;
|
|
5003
|
+
const id = session.id;
|
|
5004
|
+
if (frame.type === "start") {
|
|
5005
|
+
streams.delete(id);
|
|
5006
|
+
return;
|
|
5007
|
+
}
|
|
5008
|
+
if (frame.type === "end") {
|
|
5009
|
+
const stream = streams.get(id);
|
|
5010
|
+
if (stream && frame.outcome?.eventType !== "assistant/message") {
|
|
5011
|
+
streams.delete(id);
|
|
5012
|
+
stream.item.text = stream.text;
|
|
5013
|
+
emit(id, "item/completed", {
|
|
5014
|
+
threadId: id,
|
|
5015
|
+
turnId: running.turn.id,
|
|
5016
|
+
item: stream.item,
|
|
5017
|
+
completedAtMs: Date.now()
|
|
5018
|
+
});
|
|
5019
|
+
}
|
|
5020
|
+
return;
|
|
5021
|
+
}
|
|
5022
|
+
const chunk = frame.chunk;
|
|
5023
|
+
if (chunk?.type !== "text-delta" || typeof chunk.text !== "string" || chunk.text === "") return;
|
|
5024
|
+
let stream = streams.get(id);
|
|
5025
|
+
if (!stream) {
|
|
5026
|
+
stream = {
|
|
5027
|
+
item: {
|
|
5028
|
+
type: "agentMessage",
|
|
5029
|
+
id: randomUUID(),
|
|
5030
|
+
text: "",
|
|
5031
|
+
phase: null,
|
|
5032
|
+
memoryCitation: null,
|
|
5033
|
+
delivery: null
|
|
5034
|
+
},
|
|
5035
|
+
text: ""
|
|
5036
|
+
};
|
|
5037
|
+
streams.set(id, stream);
|
|
5038
|
+
running.turn.items.push(stream.item);
|
|
5039
|
+
emit(id, "item/started", {
|
|
5040
|
+
threadId: id,
|
|
5041
|
+
turnId: running.turn.id,
|
|
5042
|
+
item: stream.item,
|
|
5043
|
+
startedAtMs: Date.now()
|
|
5044
|
+
});
|
|
5045
|
+
}
|
|
5046
|
+
stream.text += chunk.text;
|
|
5047
|
+
emit(id, "item/agentMessage/delta", {
|
|
5048
|
+
threadId: id,
|
|
5049
|
+
turnId: running.turn.id,
|
|
5050
|
+
itemId: stream.item.id,
|
|
5051
|
+
delta: chunk.text
|
|
5052
|
+
});
|
|
5053
|
+
};
|
|
4167
5054
|
/** Forward committed assistant and tool events of a phone-started turn as items. */
|
|
4168
5055
|
const onSessionEvent = (session, event) => {
|
|
5056
|
+
if (event.type === "session/title" && typeof event.data?.title === "string" && subscribers.has(session.id)) emit(session.id, "thread/name/updated", {
|
|
5057
|
+
threadId: session.id,
|
|
5058
|
+
threadName: event.data.title
|
|
5059
|
+
});
|
|
4169
5060
|
const running = active.get(session.id);
|
|
4170
5061
|
if (!running || !subscribers.has(session.id)) return;
|
|
4171
5062
|
if (event.type === "turn/end") {
|
|
@@ -4175,20 +5066,19 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4175
5066
|
if (event.type !== "assistant/message" && event.type !== "tool/result") return;
|
|
4176
5067
|
const message = session.deriveMessages().at(-1);
|
|
4177
5068
|
if (message?.role === "tool") {
|
|
4178
|
-
const item = running.turn.items.find((entry) => entry.
|
|
5069
|
+
const item = running.turn.items.find((entry) => entry.id === message.toolCallId && entry.type !== "agentMessage");
|
|
4179
5070
|
if (item) {
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
});
|
|
5071
|
+
const finish = (imageUrls) => {
|
|
5072
|
+
finishTool(item, message, imageUrls);
|
|
5073
|
+
emit(session.id, "item/completed", {
|
|
5074
|
+
threadId: session.id,
|
|
5075
|
+
turnId: running.turn.id,
|
|
5076
|
+
item,
|
|
5077
|
+
completedAtMs: Date.now()
|
|
5078
|
+
});
|
|
5079
|
+
};
|
|
5080
|
+
if (imageBlocks(message).length > 0) imageUrlsOf([message]).then(finish, () => finish(/* @__PURE__ */ new Map()));
|
|
5081
|
+
else finish(/* @__PURE__ */ new Map());
|
|
4192
5082
|
}
|
|
4193
5083
|
return;
|
|
4194
5084
|
}
|
|
@@ -4196,9 +5086,46 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4196
5086
|
const items = projectTurns([{
|
|
4197
5087
|
role: "user",
|
|
4198
5088
|
id: "x",
|
|
4199
|
-
content: [
|
|
4200
|
-
|
|
5089
|
+
content: [{
|
|
5090
|
+
type: "text",
|
|
5091
|
+
text: "x"
|
|
5092
|
+
}]
|
|
5093
|
+
}, message], void 0, void 0, session.meta?.cwd).flatMap((turn) => turn.items.slice(1));
|
|
5094
|
+
const stream = streams.get(session.id);
|
|
5095
|
+
streams.delete(session.id);
|
|
5096
|
+
let reuse = stream?.item;
|
|
5097
|
+
for (const block of message.content) {
|
|
5098
|
+
if (block.type !== "tool-call" || block.name !== "todo_write") continue;
|
|
5099
|
+
const todos = parseArguments(block.arguments)?.todos;
|
|
5100
|
+
if (!Array.isArray(todos)) continue;
|
|
5101
|
+
const status = {
|
|
5102
|
+
pending: "pending",
|
|
5103
|
+
in_progress: "inProgress",
|
|
5104
|
+
completed: "completed"
|
|
5105
|
+
};
|
|
5106
|
+
emit(session.id, "turn/plan/updated", {
|
|
5107
|
+
threadId: session.id,
|
|
5108
|
+
turnId: running.turn.id,
|
|
5109
|
+
explanation: null,
|
|
5110
|
+
plan: todos.filter((todo) => typeof todo?.content === "string").map((todo) => ({
|
|
5111
|
+
step: todo.content,
|
|
5112
|
+
status: status[todo.status] ?? "pending"
|
|
5113
|
+
}))
|
|
5114
|
+
});
|
|
5115
|
+
}
|
|
4201
5116
|
for (const item of items) {
|
|
5117
|
+
if (reuse && item.type === "agentMessage") {
|
|
5118
|
+
const shown = reuse;
|
|
5119
|
+
reuse = void 0;
|
|
5120
|
+
shown.text = item.text;
|
|
5121
|
+
emit(session.id, "item/completed", {
|
|
5122
|
+
threadId: session.id,
|
|
5123
|
+
turnId: running.turn.id,
|
|
5124
|
+
item: shown,
|
|
5125
|
+
completedAtMs: Date.now()
|
|
5126
|
+
});
|
|
5127
|
+
continue;
|
|
5128
|
+
}
|
|
4202
5129
|
running.turn.items.push(item);
|
|
4203
5130
|
emit(session.id, "item/started", {
|
|
4204
5131
|
threadId: session.id,
|
|
@@ -4217,6 +5144,7 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4217
5144
|
return {
|
|
4218
5145
|
methods,
|
|
4219
5146
|
onSessionEvent,
|
|
5147
|
+
onStream,
|
|
4220
5148
|
onApproval,
|
|
4221
5149
|
onQuestion,
|
|
4222
5150
|
forget: (notify) => {
|
|
@@ -6492,21 +7420,35 @@ function creditExpiry({ planType, balance, apiExpiresAt }, now = Date.now()) {
|
|
|
6492
7420
|
};
|
|
6493
7421
|
}
|
|
6494
7422
|
//#endregion
|
|
7423
|
+
//#region src/quota-cadence.js
|
|
7424
|
+
/** Milliseconds until the next quota read, from the highest used percentage of the windows that matter. */
|
|
7425
|
+
function quotaRefreshMs(percent) {
|
|
7426
|
+
if (!Number.isFinite(percent)) return 6e4;
|
|
7427
|
+
if (percent >= 99) return 5e3;
|
|
7428
|
+
if (percent >= 90) return 15e3;
|
|
7429
|
+
if (percent >= 75) return 3e4;
|
|
7430
|
+
return 6e4;
|
|
7431
|
+
}
|
|
7432
|
+
/** Highest used percentage among windows (objects with usedPercent), or undefined when there are none. */
|
|
7433
|
+
function highestUsed(windows) {
|
|
7434
|
+
const values = (Array.isArray(windows) ? windows : []).map((window) => window?.usedPercent).filter(Number.isFinite);
|
|
7435
|
+
return values.length === 0 ? void 0 : Math.max(...values);
|
|
7436
|
+
}
|
|
7437
|
+
//#endregion
|
|
6495
7438
|
//#region src/usage.js
|
|
6496
7439
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
6497
|
-
const DEFAULT_TTL_MS = 6e4;
|
|
6498
7440
|
const DEFAULT_TIMEOUT_MS$1 = 15e3;
|
|
6499
7441
|
const DEFAULT_FAILURE_TTL_MS = 5e3;
|
|
6500
7442
|
const DEFAULT_MAX_RETRY_AFTER_MS = 5 * 6e4;
|
|
6501
7443
|
const record$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6502
|
-
function windowOf(value) {
|
|
7444
|
+
function windowOf(value, nowSeconds) {
|
|
6503
7445
|
if (value === void 0 || value === null) return void 0;
|
|
6504
7446
|
if (!record$1(value)) throw new Error("Codex returned a malformed rate-limit window");
|
|
6505
7447
|
const used = value.used_percent;
|
|
6506
7448
|
const seconds = value.limit_window_seconds;
|
|
6507
7449
|
if (!Number.isFinite(used) || used < 0 || used > 100) throw new Error("Codex returned an invalid used percentage");
|
|
6508
7450
|
if (!Number.isInteger(seconds) || seconds <= 0) throw new Error("Codex returned an invalid window duration");
|
|
6509
|
-
const resetsAt = epochSeconds(value.reset_at, "rate-limit reset time");
|
|
7451
|
+
const resetsAt = epochSeconds(value.reset_at, "rate-limit reset time") ?? (Number.isInteger(value.reset_after_seconds) && value.reset_after_seconds > 0 && Number.isFinite(nowSeconds) ? nowSeconds + value.reset_after_seconds : void 0);
|
|
6510
7452
|
return {
|
|
6511
7453
|
usedPercent: used,
|
|
6512
7454
|
remainingPercent: 100 - used,
|
|
@@ -6514,10 +7456,10 @@ function windowOf(value) {
|
|
|
6514
7456
|
...resetsAt === void 0 ? {} : { resetsAt }
|
|
6515
7457
|
};
|
|
6516
7458
|
}
|
|
6517
|
-
function limitOf(id, name, value) {
|
|
7459
|
+
function limitOf(id, name, value, nowSeconds) {
|
|
6518
7460
|
if (value === void 0 || value === null) return void 0;
|
|
6519
7461
|
if (!record$1(value)) throw new Error("Codex returned malformed rate-limit details");
|
|
6520
|
-
const windows = [windowOf(value.primary_window), windowOf(value.secondary_window)].filter(Boolean);
|
|
7462
|
+
const windows = [windowOf(value.primary_window, nowSeconds), windowOf(value.secondary_window, nowSeconds)].filter(Boolean);
|
|
6521
7463
|
return windows.length === 0 ? void 0 : {
|
|
6522
7464
|
id,
|
|
6523
7465
|
...name ? { name } : {},
|
|
@@ -6617,7 +7559,8 @@ function resetCreditsOf(value) {
|
|
|
6617
7559
|
};
|
|
6618
7560
|
}
|
|
6619
7561
|
/** Reduce the provider payload to a browser-safe quota projection. */
|
|
6620
|
-
function parseCodexUsage(value) {
|
|
7562
|
+
function parseCodexUsage(value, nowMs = Date.now()) {
|
|
7563
|
+
const nowSeconds = Math.floor(nowMs / 1e3);
|
|
6621
7564
|
if (!record$1(value)) throw new Error("Codex returned a malformed usage response");
|
|
6622
7565
|
const rateLimits = [];
|
|
6623
7566
|
const seenLimitIds = /* @__PURE__ */ new Set();
|
|
@@ -6626,21 +7569,27 @@ function parseCodexUsage(value) {
|
|
|
6626
7569
|
seenLimitIds.add(limit.id);
|
|
6627
7570
|
rateLimits.push(limit);
|
|
6628
7571
|
};
|
|
6629
|
-
addLimit(limitOf("codex", "Codex", value.rate_limit));
|
|
7572
|
+
addLimit(limitOf("codex", "Codex", value.rate_limit, nowSeconds));
|
|
6630
7573
|
if (value.additional_rate_limits !== void 0 && value.additional_rate_limits !== null && !Array.isArray(value.additional_rate_limits)) throw new Error("Codex returned malformed additional rate limits");
|
|
6631
7574
|
for (const entry of value.additional_rate_limits ?? []) {
|
|
6632
7575
|
if (!record$1(entry) || typeof entry.metered_feature !== "string" || entry.metered_feature.length === 0) throw new Error("Codex returned a malformed additional rate limit");
|
|
6633
7576
|
if (entry.limit_name !== void 0 && entry.limit_name !== null && typeof entry.limit_name !== "string") throw new Error("Codex returned an invalid additional rate-limit name");
|
|
6634
|
-
addLimit(limitOf(entry.metered_feature, entry.limit_name || void 0, entry.rate_limit));
|
|
7577
|
+
addLimit(limitOf(entry.metered_feature, entry.limit_name || void 0, entry.rate_limit, nowSeconds));
|
|
6635
7578
|
}
|
|
6636
|
-
addLimit(limitOf("code_review", "Code review", value.code_review_rate_limit));
|
|
7579
|
+
addLimit(limitOf("code_review", "Code review", value.code_review_rate_limit, nowSeconds));
|
|
6637
7580
|
const chatPass = chatPassOf(value.chatpass);
|
|
6638
7581
|
const credits = creditsOf(value.credits, typeof value.plan_type === "string" ? value.plan_type : void 0);
|
|
6639
7582
|
const individualLimit = individualOf(value.spend_control);
|
|
6640
7583
|
const spendControlReached = spendControlReachedOf(value.spend_control);
|
|
6641
7584
|
const resetCredits = resetCreditsOf(value.rate_limit_reset_credits);
|
|
7585
|
+
const planType = typeof value.plan_type === "string" && /^[a-z][a-z_]{0,31}$/u.test(value.plan_type) ? value.plan_type : void 0;
|
|
7586
|
+
const reached = value.rate_limit_reached_type;
|
|
7587
|
+
const reachedRaw = typeof reached === "string" ? reached : record$1(reached) ? reached.type : void 0;
|
|
7588
|
+
const rateLimitReachedType = typeof reachedRaw === "string" && /^[a-z][a-z_]{0,63}$/u.test(reachedRaw) ? reachedRaw : void 0;
|
|
6642
7589
|
return {
|
|
6643
7590
|
rateLimits,
|
|
7591
|
+
...rateLimitReachedType === void 0 ? {} : { rateLimitReachedType },
|
|
7592
|
+
...planType === void 0 ? {} : { planType },
|
|
6644
7593
|
...chatPass === void 0 ? {} : { chatPass },
|
|
6645
7594
|
...credits === void 0 ? {} : { credits },
|
|
6646
7595
|
...individualLimit === void 0 ? {} : { individualLimit },
|
|
@@ -6671,7 +7620,7 @@ function createCodexUsageReader(options) {
|
|
|
6671
7620
|
const readCredential = options.readCredential;
|
|
6672
7621
|
const fetchUsage = options.fetch ?? fetch;
|
|
6673
7622
|
const now = options.now ?? Date.now;
|
|
6674
|
-
const
|
|
7623
|
+
const ttlFor = (value) => options.ttlMs ?? quotaRefreshMs(highestUsed(value?.rateLimits?.find((limit) => limit.id === "codex")?.windows));
|
|
6675
7624
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
|
|
6676
7625
|
const failureTtlMs = options.failureTtlMs ?? DEFAULT_FAILURE_TTL_MS;
|
|
6677
7626
|
const maxRetryAfterMs = options.maxRetryAfterMs ?? DEFAULT_MAX_RETRY_AFTER_MS;
|
|
@@ -6706,14 +7655,14 @@ function createCodexUsageReader(options) {
|
|
|
6706
7655
|
throw new Error("ChatGPT returned an unreadable usage response");
|
|
6707
7656
|
}
|
|
6708
7657
|
return {
|
|
6709
|
-
...parseCodexUsage(value),
|
|
7658
|
+
...parseCodexUsage(value, now()),
|
|
6710
7659
|
fetchedAt: now()
|
|
6711
7660
|
};
|
|
6712
7661
|
};
|
|
6713
7662
|
return Object.freeze({
|
|
6714
7663
|
read({ force = false, signal } = {}) {
|
|
6715
7664
|
if (failed !== void 0 && now() < failed.retryAt) return Promise.reject(new Error(failed.message));
|
|
6716
|
-
if (!force && cached !== void 0 && now() - cached.fetchedAt <
|
|
7665
|
+
if (!force && cached !== void 0 && now() - cached.fetchedAt < ttlFor(cached)) return Promise.resolve(structuredClone(cached));
|
|
6717
7666
|
if (inFlight !== void 0) return inFlight.then(structuredClone);
|
|
6718
7667
|
const currentGeneration = generation;
|
|
6719
7668
|
const current = load(signal).then((value) => {
|
|
@@ -6797,6 +7746,7 @@ function createWakeSignal() {
|
|
|
6797
7746
|
};
|
|
6798
7747
|
}
|
|
6799
7748
|
function shortResetDelay(usage, nowMs, maxWaitMs, resetMarginMs) {
|
|
7749
|
+
if (typeof usage?.rateLimitReachedType === "string" && /^(?:workspace_|.*credits_depleted)/u.test(usage.rateLimitReachedType)) return void 0;
|
|
6800
7750
|
let latestDelayMs;
|
|
6801
7751
|
for (const limit of usage?.rateLimits ?? []) {
|
|
6802
7752
|
if (limit?.id !== "codex") continue;
|
|
@@ -7631,14 +8581,16 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
|
|
|
7631
8581
|
"remote/status",
|
|
7632
8582
|
"remote/enable",
|
|
7633
8583
|
"remote/disable",
|
|
7634
|
-
"remote/pair"
|
|
8584
|
+
"remote/pair",
|
|
8585
|
+
"remote/clients",
|
|
8586
|
+
"remote/revoke"
|
|
7635
8587
|
].includes(endpoint)) try {
|
|
7636
8588
|
signal.throwIfAborted();
|
|
7637
8589
|
if (!remoteControl) return publicError("unavailable", "Remote Control is unavailable");
|
|
7638
8590
|
const action = endpoint.split("/")[1];
|
|
7639
8591
|
return {
|
|
7640
8592
|
ok: true,
|
|
7641
|
-
value: action === "status" ? remoteControl.status() : action === "enable" ? await remoteControl.enable() : action === "disable" ? await remoteControl.disable() : await remoteControl.pair(signal)
|
|
8593
|
+
value: action === "status" ? remoteControl.status() : action === "enable" ? await remoteControl.enable() : action === "disable" ? await remoteControl.disable() : action === "clients" ? await remoteControl.clients(signal) : action === "revoke" ? await remoteControl.revoke(payload?.clientId, signal) : await remoteControl.pair(signal)
|
|
7642
8594
|
};
|
|
7643
8595
|
} catch (error) {
|
|
7644
8596
|
if (signal.aborted) throw error;
|
|
@@ -8171,6 +9123,11 @@ function apply(ctx, config = {}) {
|
|
|
8171
9123
|
agents: () => ctx.get?.("agents"),
|
|
8172
9124
|
permissions: () => ctx.get?.("permissionPresets"),
|
|
8173
9125
|
workspaces: () => ctx.get?.("workspaceRegistry"),
|
|
9126
|
+
attachments: () => ctx.attachments,
|
|
9127
|
+
usage: () => usageReader,
|
|
9128
|
+
projections: () => ctx.get?.("sessionProjections"),
|
|
9129
|
+
skills: () => ctx.get?.("skills"),
|
|
9130
|
+
llm: () => ctx.llm,
|
|
8174
9131
|
trace: (entry) => {
|
|
8175
9132
|
try {
|
|
8176
9133
|
remoteControl.host.note(entry);
|
|
@@ -8188,9 +9145,15 @@ function apply(ctx, config = {}) {
|
|
|
8188
9145
|
stateFile: dshHomePath("state", "codex-subscription", "remote-control.json"),
|
|
8189
9146
|
hostName: `DSH (${hostname()})`,
|
|
8190
9147
|
userAgent: USER_AGENT,
|
|
9148
|
+
base: process.env.DSH_CODEX_REMOTE_BASE || void 0,
|
|
8191
9149
|
methods: remoteBridge.methods,
|
|
8192
9150
|
onClose: (_client, notify) => remoteBridge.forget(notify)
|
|
8193
9151
|
});
|
|
9152
|
+
ctx.on("agent/assistant-stream", ({ agent, frame }) => {
|
|
9153
|
+
try {
|
|
9154
|
+
remoteBridge.onStream(agent?.session, frame);
|
|
9155
|
+
} catch {}
|
|
9156
|
+
});
|
|
8194
9157
|
ctx.on("session/event", (session, event) => {
|
|
8195
9158
|
try {
|
|
8196
9159
|
remoteBridge.onSessionEvent(session, event);
|