dsh-codex-subscription 2.5.9 → 3.0.0-beta.2
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 +29 -6
- package/README.md +29 -6
- package/compatibility.json +2 -12
- package/lib/client.js +2447 -89
- package/lib/index.js +1651 -22
- package/package.json +23 -22
package/lib/index.js
CHANGED
|
@@ -9,13 +9,14 @@ 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 } from "node:fs/promises";
|
|
13
|
-
import { constants, readFileSync } from "node:fs";
|
|
14
|
-
import { dirname, join, resolve } from "node:path";
|
|
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";
|
|
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";
|
|
18
18
|
import * as dshCredentials from "@deepseek-ai/dsh-credentials";
|
|
19
|
+
import { hostname } from "node:os";
|
|
19
20
|
import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
20
21
|
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
21
22
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -23,8 +24,7 @@ import { openaiCodexProvider as createOpenAICodexProvider } from "@earendil-work
|
|
|
23
24
|
import { createModels } from "@earendil-works/pi-ai";
|
|
24
25
|
import { WebError } from "@deepseek-ai/dsh-web";
|
|
25
26
|
//#region src/settings-adapter.js
|
|
26
|
-
function createSettingsAdapter(ctx,
|
|
27
|
-
if (typeof ctx.settings.register === "function") return ctx.settings.register(namespace, schema);
|
|
27
|
+
function createSettingsAdapter(ctx, config) {
|
|
28
28
|
if (typeof ctx.settings.configure !== "function" || typeof ctx.settings.update !== "function") throw new Error("DSH settings API is unavailable");
|
|
29
29
|
ctx.effect(() => ctx.settings.configure({ auto: false }));
|
|
30
30
|
const entry = ctx.fiber.entry;
|
|
@@ -35,7 +35,13 @@ function createSettingsAdapter(ctx, schema, config, namespace) {
|
|
|
35
35
|
get,
|
|
36
36
|
update: (patch) => ctx.settings.update(id, patch),
|
|
37
37
|
watch(listener) {
|
|
38
|
-
|
|
38
|
+
let previous = get();
|
|
39
|
+
return ctx.on("loader/volatile-update", () => {
|
|
40
|
+
const value = get();
|
|
41
|
+
const before = previous;
|
|
42
|
+
previous = value;
|
|
43
|
+
listener(value, before);
|
|
44
|
+
});
|
|
39
45
|
}
|
|
40
46
|
};
|
|
41
47
|
}
|
|
@@ -275,7 +281,6 @@ function capabilityPatch(payload) {
|
|
|
275
281
|
}
|
|
276
282
|
//#endregion
|
|
277
283
|
//#region src/settings-contract.js
|
|
278
|
-
const SETTINGS_NAMESPACE = "codex-subscription";
|
|
279
284
|
const DISABLED_MODELS_FIELD = "disabledModels";
|
|
280
285
|
const AUTO_QUOTA_RETRY_FIELD = "autoQuotaRetry";
|
|
281
286
|
const QUICK_QUOTA_MODE_FIELD = "quickQuotaMode";
|
|
@@ -429,6 +434,11 @@ const PREFERENCE_FIELDS = Object.freeze({
|
|
|
429
434
|
default: "session",
|
|
430
435
|
error: "Invalid review model"
|
|
431
436
|
},
|
|
437
|
+
remoteControl: {
|
|
438
|
+
choices: ["off", "on"],
|
|
439
|
+
default: "off",
|
|
440
|
+
error: "Invalid Remote Control setting"
|
|
441
|
+
},
|
|
432
442
|
subagentBackend: {
|
|
433
443
|
choices: ["dsh", "codex"],
|
|
434
444
|
default: "dsh",
|
|
@@ -1110,6 +1120,10 @@ const RPC_ENDPOINTS = Object.freeze([
|
|
|
1110
1120
|
"runtime/cancel",
|
|
1111
1121
|
"storage/status",
|
|
1112
1122
|
"storage/clear-forecast",
|
|
1123
|
+
"remote/status",
|
|
1124
|
+
"remote/enable",
|
|
1125
|
+
"remote/disable",
|
|
1126
|
+
"remote/pair",
|
|
1113
1127
|
"reset-credit/inspect",
|
|
1114
1128
|
"reset-credit/prepare",
|
|
1115
1129
|
"reset-credit/consume",
|
|
@@ -1286,13 +1300,11 @@ function authenticatedSubagentChild(child, { Transport, getTokens, thread, signa
|
|
|
1286
1300
|
//#region src/subagent-runtime.js
|
|
1287
1301
|
const SUBAGENT_RUNTIME_PACKAGE = "@deepseek-ai/dsh-subagent-codex";
|
|
1288
1302
|
const SUPPORTED_RUNTIME_VERSIONS = Object.freeze([
|
|
1289
|
-
"0.1.5-rc.2",
|
|
1290
|
-
"0.1.5-rc.3",
|
|
1291
|
-
"0.1.7-rc.1",
|
|
1292
1303
|
"0.1.7-rc.2",
|
|
1293
1304
|
"0.2.0-rc.1",
|
|
1294
1305
|
"0.2.0-rc.2",
|
|
1295
|
-
"0.2.1-alpha.1"
|
|
1306
|
+
"0.2.1-alpha.1",
|
|
1307
|
+
"0.2.1-alpha.2"
|
|
1296
1308
|
]);
|
|
1297
1309
|
const require = createRequire(import.meta.url);
|
|
1298
1310
|
const execute = promisify(execFile);
|
|
@@ -2032,10 +2044,11 @@ function originalImageRefsEqual(left, right) {
|
|
|
2032
2044
|
/** Resolve only an exact original reference copied into a DSH fork prefix. */
|
|
2033
2045
|
function inheritedOriginalImageRef(session, assetId) {
|
|
2034
2046
|
const parentSession = session?.header?.parentSession;
|
|
2035
|
-
const seedLength =
|
|
2047
|
+
const seedLength = session?.inheritedEventCount;
|
|
2036
2048
|
if (typeof parentSession !== "string" || parentSession.length === 0 || !Number.isSafeInteger(seedLength) || seedLength < 0 || !ORIGINAL_IMAGE_ID_PATTERN.test(assetId)) return void 0;
|
|
2037
|
-
|
|
2038
|
-
|
|
2049
|
+
if (typeof session?.snapshotEvents !== "function") return void 0;
|
|
2050
|
+
let events;
|
|
2051
|
+
try {
|
|
2039
2052
|
events = session.snapshotEvents();
|
|
2040
2053
|
} catch {
|
|
2041
2054
|
return;
|
|
@@ -2049,7 +2062,7 @@ function inheritedOriginalImageRef(session, assetId) {
|
|
|
2049
2062
|
}
|
|
2050
2063
|
//#endregion
|
|
2051
2064
|
//#region src/version.js
|
|
2052
|
-
const PACKAGE_VERSION = "
|
|
2065
|
+
const PACKAGE_VERSION = "3.0.0-beta.2";
|
|
2053
2066
|
const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
|
|
2054
2067
|
//#endregion
|
|
2055
2068
|
//#region src/codex-images.js
|
|
@@ -2677,6 +2690,1541 @@ function createSketchAgentTool(bridge, attachments, getSessionMessages = () => [
|
|
|
2677
2690
|
});
|
|
2678
2691
|
}
|
|
2679
2692
|
//#endregion
|
|
2693
|
+
//#region src/remote-control-host.js
|
|
2694
|
+
const METHOD_NOT_FOUND = -32601;
|
|
2695
|
+
const INVALID_PARAMS = -32602;
|
|
2696
|
+
const INTERNAL_ERROR = -32603;
|
|
2697
|
+
const MAX_SEEN = 128;
|
|
2698
|
+
const QUIET = /* @__PURE__ */ new Set([
|
|
2699
|
+
"command/exec",
|
|
2700
|
+
"thread/attachment/list",
|
|
2701
|
+
"thread/list",
|
|
2702
|
+
"thread/queue/list",
|
|
2703
|
+
"threadSection/list",
|
|
2704
|
+
"config/read",
|
|
2705
|
+
"configRequirements/read"
|
|
2706
|
+
]);
|
|
2707
|
+
var RpcError = class extends Error {
|
|
2708
|
+
constructor(code, message) {
|
|
2709
|
+
super(message);
|
|
2710
|
+
this.code = code;
|
|
2711
|
+
}
|
|
2712
|
+
};
|
|
2713
|
+
const invalidParams = (message) => new RpcError(INVALID_PARAMS, message);
|
|
2714
|
+
/**
|
|
2715
|
+
* One host per relay; each remote client gets its own connection.
|
|
2716
|
+
* @param {{ methods: Record<string, (params: any, context: { client: object, signal: AbortSignal, notify: (method: string, params?: object) => Promise<void> }) => any>, notifications?: Record<string, Function>, onConnection?: (client: object) => void, onClose?: (client: object) => void }} options
|
|
2717
|
+
*/
|
|
2718
|
+
function createRemoteControlHost({ methods, notifications = {}, onConnection, onActive, onClose, debug = false }) {
|
|
2719
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2720
|
+
const trace = [];
|
|
2721
|
+
const record = (entry) => {
|
|
2722
|
+
if (!debug) return;
|
|
2723
|
+
trace.push({
|
|
2724
|
+
at: Date.now(),
|
|
2725
|
+
...entry
|
|
2726
|
+
});
|
|
2727
|
+
if (trace.length > 60) trace.shift();
|
|
2728
|
+
};
|
|
2729
|
+
const note = (kind, method) => {
|
|
2730
|
+
const key = `${kind}:${method}`;
|
|
2731
|
+
if (!seen.has(key) && seen.size >= MAX_SEEN) return;
|
|
2732
|
+
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
2733
|
+
};
|
|
2734
|
+
return Object.freeze({
|
|
2735
|
+
/** Methods remote clients called, for diagnostics; never params or content. */
|
|
2736
|
+
seen: () => Object.fromEntries(seen),
|
|
2737
|
+
/** Add a development trace entry from the bridge; ignored unless debug is on. */
|
|
2738
|
+
note: (entry) => record(entry),
|
|
2739
|
+
/** Development trace of recent calls; only filled when debug is on. */
|
|
2740
|
+
trace: () => [...trace],
|
|
2741
|
+
serve(client) {
|
|
2742
|
+
const aborts = /* @__PURE__ */ new Set();
|
|
2743
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2744
|
+
let initialized = false;
|
|
2745
|
+
let active = false;
|
|
2746
|
+
const notify = async (method, params) => {
|
|
2747
|
+
await client.send(params === void 0 ? { method } : {
|
|
2748
|
+
method,
|
|
2749
|
+
params
|
|
2750
|
+
});
|
|
2751
|
+
};
|
|
2752
|
+
/** Ask the phone something (approvals); settles with its result or rejects. */
|
|
2753
|
+
const ask = (method, params, signal) => new Promise((resolve, reject) => {
|
|
2754
|
+
const id = `s${randomUUID()}`;
|
|
2755
|
+
const done = () => {
|
|
2756
|
+
pending.delete(id);
|
|
2757
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2758
|
+
};
|
|
2759
|
+
const onAbort = () => {
|
|
2760
|
+
done();
|
|
2761
|
+
reject(new RpcError(-32800, "Request cancelled"));
|
|
2762
|
+
};
|
|
2763
|
+
if (signal?.aborted) return onAbort();
|
|
2764
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2765
|
+
record({
|
|
2766
|
+
method: `ask:${method}`,
|
|
2767
|
+
params: JSON.stringify(params ?? null).slice(0, 400)
|
|
2768
|
+
});
|
|
2769
|
+
pending.set(id, {
|
|
2770
|
+
resolve: (value) => {
|
|
2771
|
+
record({
|
|
2772
|
+
method: `answer:${method}`,
|
|
2773
|
+
params: JSON.stringify(value ?? null).slice(0, 400)
|
|
2774
|
+
});
|
|
2775
|
+
done();
|
|
2776
|
+
resolve(value);
|
|
2777
|
+
},
|
|
2778
|
+
reject: (error) => {
|
|
2779
|
+
record({
|
|
2780
|
+
method: `answer:${method}`,
|
|
2781
|
+
error: String(error?.message ?? error).slice(0, 200)
|
|
2782
|
+
});
|
|
2783
|
+
done();
|
|
2784
|
+
reject(error);
|
|
2785
|
+
}
|
|
2786
|
+
});
|
|
2787
|
+
client.send({
|
|
2788
|
+
id,
|
|
2789
|
+
method,
|
|
2790
|
+
params
|
|
2791
|
+
}).catch((error) => {
|
|
2792
|
+
done();
|
|
2793
|
+
reject(error);
|
|
2794
|
+
});
|
|
2795
|
+
});
|
|
2796
|
+
notify.ask = ask;
|
|
2797
|
+
const context = {
|
|
2798
|
+
client,
|
|
2799
|
+
notify,
|
|
2800
|
+
ask
|
|
2801
|
+
};
|
|
2802
|
+
onConnection?.(client);
|
|
2803
|
+
const reply = async (id, result) => {
|
|
2804
|
+
await client.send({
|
|
2805
|
+
id,
|
|
2806
|
+
result
|
|
2807
|
+
});
|
|
2808
|
+
};
|
|
2809
|
+
const fail = async (id, error) => {
|
|
2810
|
+
const known = error instanceof RpcError;
|
|
2811
|
+
await client.send({
|
|
2812
|
+
id,
|
|
2813
|
+
error: {
|
|
2814
|
+
code: known ? error.code : INTERNAL_ERROR,
|
|
2815
|
+
message: known ? error.message : "Request failed"
|
|
2816
|
+
}
|
|
2817
|
+
});
|
|
2818
|
+
};
|
|
2819
|
+
return {
|
|
2820
|
+
receive(message) {
|
|
2821
|
+
if (message !== null && typeof message === "object" && typeof message.method !== "string" && message.id !== void 0) {
|
|
2822
|
+
const waiting = pending.get(message.id);
|
|
2823
|
+
if (waiting) if (message.error) waiting.reject(new RpcError(message.error.code ?? INTERNAL_ERROR, String(message.error.message ?? "Request failed")));
|
|
2824
|
+
else waiting.resolve(message.result);
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
if (message === null || typeof message !== "object" || typeof message.method !== "string") return;
|
|
2828
|
+
const { id, method, params } = message;
|
|
2829
|
+
if (id === void 0) {
|
|
2830
|
+
note("notify", method);
|
|
2831
|
+
if (method === "initialized") initialized = true;
|
|
2832
|
+
notifications[method]?.(params, {
|
|
2833
|
+
...context,
|
|
2834
|
+
signal: new AbortController().signal
|
|
2835
|
+
});
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
note("request", method);
|
|
2839
|
+
if (!active && method !== "initialize") {
|
|
2840
|
+
active = true;
|
|
2841
|
+
onActive?.(client);
|
|
2842
|
+
}
|
|
2843
|
+
if (debug && !QUIET.has(method)) record({
|
|
2844
|
+
method: `request:${method}`,
|
|
2845
|
+
params: JSON.stringify(params ?? null).slice(0, 700)
|
|
2846
|
+
});
|
|
2847
|
+
const handler = methods[method];
|
|
2848
|
+
if (!handler) {
|
|
2849
|
+
record({
|
|
2850
|
+
method,
|
|
2851
|
+
missing: true,
|
|
2852
|
+
params: JSON.stringify(params ?? null).slice(0, 600)
|
|
2853
|
+
});
|
|
2854
|
+
fail(id, new RpcError(METHOD_NOT_FOUND, `Method not found: ${method}`)).catch(() => {});
|
|
2855
|
+
return;
|
|
2856
|
+
}
|
|
2857
|
+
if (method !== "initialize" && !initialized && method !== "ping") initialized = true;
|
|
2858
|
+
const controller = new AbortController();
|
|
2859
|
+
aborts.add(controller);
|
|
2860
|
+
Promise.resolve().then(() => handler(params, {
|
|
2861
|
+
...context,
|
|
2862
|
+
signal: controller.signal
|
|
2863
|
+
})).then((result) => reply(id, result ?? {}), (error) => {
|
|
2864
|
+
if (!QUIET.has(method)) record({
|
|
2865
|
+
method,
|
|
2866
|
+
error: String(error?.message ?? error).slice(0, 300),
|
|
2867
|
+
params: JSON.stringify(params ?? null).slice(0, 600)
|
|
2868
|
+
});
|
|
2869
|
+
return fail(id, error);
|
|
2870
|
+
}).catch(() => {}).finally(() => aborts.delete(controller));
|
|
2871
|
+
},
|
|
2872
|
+
close() {
|
|
2873
|
+
for (const waiting of [...pending.values()]) waiting.reject(new RpcError(-32800, "Client disconnected"));
|
|
2874
|
+
for (const controller of aborts) controller.abort();
|
|
2875
|
+
aborts.clear();
|
|
2876
|
+
onClose?.(client, notify);
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
//#endregion
|
|
2883
|
+
//#region src/remote-control-relay.js
|
|
2884
|
+
const REMOTE_CONTROL_BASE_URL = "https://chatgpt.com/backend-api/";
|
|
2885
|
+
const PROTOCOL_VERSION = "3";
|
|
2886
|
+
const MAX_SEGMENT_BYTES = 150 * 1024;
|
|
2887
|
+
const TARGET_SEGMENT_BYTES = 100 * 1024;
|
|
2888
|
+
const MAX_MESSAGE_BYTES = 100 * 1024 * 1024;
|
|
2889
|
+
const MAX_CHUNKS = 1024;
|
|
2890
|
+
const INITIAL_RECONNECT_MS = 1e3;
|
|
2891
|
+
const CONFLICT_RENEW_MS = 15 * 6e4;
|
|
2892
|
+
const MAX_RECONNECT_MS = 3e4;
|
|
2893
|
+
const REQUEST_TIMEOUT_MS = 3e4;
|
|
2894
|
+
const REFRESH_MARGIN_MS = 5 * 6e4;
|
|
2895
|
+
var RemoteControlError = class extends Error {
|
|
2896
|
+
constructor(code, message) {
|
|
2897
|
+
super(message);
|
|
2898
|
+
this.name = "RemoteControlError";
|
|
2899
|
+
this.code = code;
|
|
2900
|
+
}
|
|
2901
|
+
};
|
|
2902
|
+
function remoteControlEndpoints(base = REMOTE_CONTROL_BASE_URL) {
|
|
2903
|
+
const root = new URL(base);
|
|
2904
|
+
const local = [
|
|
2905
|
+
"localhost",
|
|
2906
|
+
"127.0.0.1",
|
|
2907
|
+
"[::1]"
|
|
2908
|
+
].includes(root.hostname);
|
|
2909
|
+
if (!(root.protocol === "https:" && (root.hostname === "chatgpt.com" || root.hostname.endsWith(".chatgpt.com"))) && !(local && root.protocol === "http:")) throw new RemoteControlError("unsupported-host", "Remote Control only talks to chatgpt.com");
|
|
2910
|
+
if (!root.pathname.endsWith("/")) root.pathname += "/";
|
|
2911
|
+
const at = (suffix) => new URL(suffix, root);
|
|
2912
|
+
const websocket = at("wham/remote/control/server");
|
|
2913
|
+
websocket.protocol = root.protocol === "https:" ? "wss:" : "ws:";
|
|
2914
|
+
return Object.freeze({
|
|
2915
|
+
enroll: at("wham/remote/control/server/enroll"),
|
|
2916
|
+
refresh: at("wham/remote/control/server/refresh"),
|
|
2917
|
+
pair: at("wham/remote/control/server/pair"),
|
|
2918
|
+
websocket
|
|
2919
|
+
});
|
|
2920
|
+
}
|
|
2921
|
+
const text = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2922
|
+
function enrollment(value) {
|
|
2923
|
+
const environmentId = text(value?.environment_id);
|
|
2924
|
+
const serverId = text(value?.server_id);
|
|
2925
|
+
const token = text(value?.remote_control_token);
|
|
2926
|
+
const expiresAt = Date.parse(value?.expires_at);
|
|
2927
|
+
if (!environmentId || !serverId || !token || !Number.isFinite(expiresAt)) throw new RemoteControlError("bad-response", "Remote Control returned an unexpected enrollment");
|
|
2928
|
+
return {
|
|
2929
|
+
environmentId,
|
|
2930
|
+
serverId,
|
|
2931
|
+
token,
|
|
2932
|
+
expiresAt
|
|
2933
|
+
};
|
|
2934
|
+
}
|
|
2935
|
+
/**
|
|
2936
|
+
* @param {{
|
|
2937
|
+
* credentials: (signal?: AbortSignal) => Promise<{ access?: string, accountId?: string }>,
|
|
2938
|
+
* fetch?: typeof fetch,
|
|
2939
|
+
* WebSocket: new (url: string, options: object) => any,
|
|
2940
|
+
* installationId: string,
|
|
2941
|
+
* hostName: string,
|
|
2942
|
+
* userAgent: string,
|
|
2943
|
+
* serve: (client: { send: (message: object) => Promise<void>, close: () => void, id: string }) => { receive: (message: object) => void, close: () => void },
|
|
2944
|
+
* base?: string,
|
|
2945
|
+
* now?: () => number,
|
|
2946
|
+
* wait?: (ms: number, signal: AbortSignal) => Promise<void>,
|
|
2947
|
+
* }} options
|
|
2948
|
+
*/
|
|
2949
|
+
function createRemoteControlRelay(options) {
|
|
2950
|
+
let installation = options.installationId;
|
|
2951
|
+
const endpoints = remoteControlEndpoints(options.base);
|
|
2952
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
2953
|
+
const now = options.now ?? Date.now;
|
|
2954
|
+
const wait = options.wait ?? ((ms, signal) => new Promise((resolve) => {
|
|
2955
|
+
const timer = setTimeout(resolve, ms);
|
|
2956
|
+
signal.addEventListener("abort", () => {
|
|
2957
|
+
clearTimeout(timer);
|
|
2958
|
+
resolve();
|
|
2959
|
+
}, { once: true });
|
|
2960
|
+
}));
|
|
2961
|
+
let enrolled;
|
|
2962
|
+
let stop;
|
|
2963
|
+
let running;
|
|
2964
|
+
let socket;
|
|
2965
|
+
const clients = /* @__PURE__ */ new Map();
|
|
2966
|
+
const assemblies = /* @__PURE__ */ new Map();
|
|
2967
|
+
const sequences = /* @__PURE__ */ new Map();
|
|
2968
|
+
const state = {
|
|
2969
|
+
status: "stopped",
|
|
2970
|
+
connectedAt: void 0,
|
|
2971
|
+
lastError: void 0,
|
|
2972
|
+
reconnects: 0
|
|
2973
|
+
};
|
|
2974
|
+
const request = async (url, init, signal) => {
|
|
2975
|
+
const response = await doFetch(url, {
|
|
2976
|
+
...init,
|
|
2977
|
+
redirect: "error",
|
|
2978
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]) : AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
2979
|
+
});
|
|
2980
|
+
if (!response.ok) throw new RemoteControlError(response.status === 401 || response.status === 403 ? "unauthorized" : "http-error", `Remote Control request failed (HTTP ${response.status})`);
|
|
2981
|
+
return response.json();
|
|
2982
|
+
};
|
|
2983
|
+
const ensureEnrollment = async (signal) => {
|
|
2984
|
+
if (enrolled && enrolled.expiresAt > now() + REFRESH_MARGIN_MS) return enrolled;
|
|
2985
|
+
const { access, accountId } = await options.credentials(signal);
|
|
2986
|
+
if (!access || !accountId) throw new RemoteControlError("not-signed-in", "ChatGPT subscription is not signed in");
|
|
2987
|
+
const headers = {
|
|
2988
|
+
authorization: `Bearer ${access}`,
|
|
2989
|
+
"chatgpt-account-id": accountId,
|
|
2990
|
+
"content-type": "application/json",
|
|
2991
|
+
"user-agent": options.userAgent,
|
|
2992
|
+
"x-codex-installation-id": installation
|
|
2993
|
+
};
|
|
2994
|
+
const refresh = enrolled !== void 0;
|
|
2995
|
+
const body = refresh ? {
|
|
2996
|
+
installation_id: installation,
|
|
2997
|
+
server_id: enrolled.serverId
|
|
2998
|
+
} : {
|
|
2999
|
+
app_server_version: options.userAgent.split("/").at(-1),
|
|
3000
|
+
arch: process.arch,
|
|
3001
|
+
installation_id: installation,
|
|
3002
|
+
name: options.hostName,
|
|
3003
|
+
os: process.platform
|
|
3004
|
+
};
|
|
3005
|
+
const next = enrollment(await request(refresh ? endpoints.refresh : endpoints.enroll, {
|
|
3006
|
+
method: "POST",
|
|
3007
|
+
headers,
|
|
3008
|
+
body: JSON.stringify(body)
|
|
3009
|
+
}, signal));
|
|
3010
|
+
if (refresh && next.serverId !== enrolled.serverId) throw new RemoteControlError("bad-response", "Remote Control returned a mismatched enrollment");
|
|
3011
|
+
enrolled = next;
|
|
3012
|
+
return enrolled;
|
|
3013
|
+
};
|
|
3014
|
+
/** Start a pairing: the code goes into a QR; the manual code can be typed in the app. */
|
|
3015
|
+
const pair = async (signal) => {
|
|
3016
|
+
const current = await ensureEnrollment(signal);
|
|
3017
|
+
const value = await request(endpoints.pair, {
|
|
3018
|
+
method: "POST",
|
|
3019
|
+
headers: {
|
|
3020
|
+
authorization: `Bearer ${current.token}`,
|
|
3021
|
+
"content-type": "application/json",
|
|
3022
|
+
"user-agent": options.userAgent
|
|
3023
|
+
},
|
|
3024
|
+
body: JSON.stringify({ manual_code: true })
|
|
3025
|
+
}, signal);
|
|
3026
|
+
const pairingCode = text(value?.pairing_code);
|
|
3027
|
+
const expiresAt = Date.parse(value?.expires_at);
|
|
3028
|
+
if (!pairingCode || value?.server_id !== current.serverId || !Number.isFinite(expiresAt)) throw new RemoteControlError("bad-response", "Remote Control returned an unexpected pairing");
|
|
3029
|
+
return {
|
|
3030
|
+
pairingCode,
|
|
3031
|
+
manualCode: text(value?.manual_pairing_code) ?? null,
|
|
3032
|
+
expiresAt
|
|
3033
|
+
};
|
|
3034
|
+
};
|
|
3035
|
+
const sendEnvelope = async (envelope) => {
|
|
3036
|
+
if (socket?.readyState !== 1) throw new RemoteControlError("not-connected", "Remote Control is not connected");
|
|
3037
|
+
await new Promise((resolve, reject) => socket.send(JSON.stringify(envelope), (error) => error ? reject(error) : resolve()));
|
|
3038
|
+
};
|
|
3039
|
+
const sendServerMessage = async (clientId, streamId, message) => {
|
|
3040
|
+
const key = `${clientId}\u0000${streamId}`;
|
|
3041
|
+
const sequence = sequences.get(key) ?? 1;
|
|
3042
|
+
sequences.set(key, sequence + 1);
|
|
3043
|
+
const envelope = {
|
|
3044
|
+
client_id: clientId,
|
|
3045
|
+
message,
|
|
3046
|
+
seq_id: sequence,
|
|
3047
|
+
stream_id: streamId,
|
|
3048
|
+
type: "server_message"
|
|
3049
|
+
};
|
|
3050
|
+
const encoded = JSON.stringify(envelope);
|
|
3051
|
+
if (Buffer.byteLength(encoded) <= MAX_SEGMENT_BYTES) return sendEnvelope(envelope);
|
|
3052
|
+
const bytes = Buffer.from(JSON.stringify(message));
|
|
3053
|
+
if (bytes.byteLength > MAX_MESSAGE_BYTES) throw new RemoteControlError("too-large", "Remote Control message is too large");
|
|
3054
|
+
const chunks = [];
|
|
3055
|
+
for (let offset = 0; offset < bytes.byteLength; offset += TARGET_SEGMENT_BYTES) chunks.push(bytes.subarray(offset, offset + TARGET_SEGMENT_BYTES).toString("base64"));
|
|
3056
|
+
await Promise.all(chunks.map((chunk, segmentId) => sendEnvelope({
|
|
3057
|
+
client_id: clientId,
|
|
3058
|
+
message_chunk_base64: chunk,
|
|
3059
|
+
message_size_bytes: bytes.byteLength,
|
|
3060
|
+
segment_count: chunks.length,
|
|
3061
|
+
segment_id: segmentId,
|
|
3062
|
+
seq_id: sequence,
|
|
3063
|
+
stream_id: streamId,
|
|
3064
|
+
type: "server_message_chunk"
|
|
3065
|
+
})));
|
|
3066
|
+
};
|
|
3067
|
+
const closeClient = (clientId, streamId) => {
|
|
3068
|
+
for (const [key, client] of clients) if (key.startsWith(`${clientId}\u0000`) && (streamId === void 0 || key === `${clientId}\u0000${streamId}`)) {
|
|
3069
|
+
clients.delete(key);
|
|
3070
|
+
sequences.delete(key);
|
|
3071
|
+
client.close();
|
|
3072
|
+
}
|
|
3073
|
+
};
|
|
3074
|
+
const deliver = async (envelope) => {
|
|
3075
|
+
const streamId = envelope.stream_id ?? randomUUID();
|
|
3076
|
+
const key = `${envelope.client_id}\u0000${streamId}`;
|
|
3077
|
+
let client = clients.get(key);
|
|
3078
|
+
if (!client) {
|
|
3079
|
+
let closed = false;
|
|
3080
|
+
const connection = options.serve({
|
|
3081
|
+
id: `${envelope.client_id}/${streamId}`,
|
|
3082
|
+
send: async (message) => {
|
|
3083
|
+
if (!closed) await sendServerMessage(envelope.client_id, streamId, message);
|
|
3084
|
+
},
|
|
3085
|
+
close: () => {
|
|
3086
|
+
closed = true;
|
|
3087
|
+
}
|
|
3088
|
+
});
|
|
3089
|
+
client = {
|
|
3090
|
+
receive: connection.receive,
|
|
3091
|
+
close: () => {
|
|
3092
|
+
closed = true;
|
|
3093
|
+
connection.close();
|
|
3094
|
+
}
|
|
3095
|
+
};
|
|
3096
|
+
clients.set(key, client);
|
|
3097
|
+
}
|
|
3098
|
+
client.receive(envelope.message);
|
|
3099
|
+
if (Number.isInteger(envelope.seq_id)) await sendEnvelope({
|
|
3100
|
+
client_id: envelope.client_id,
|
|
3101
|
+
seq_id: envelope.seq_id,
|
|
3102
|
+
stream_id: streamId,
|
|
3103
|
+
type: "ack"
|
|
3104
|
+
});
|
|
3105
|
+
};
|
|
3106
|
+
const receiveChunk = (envelope) => {
|
|
3107
|
+
const streamId = envelope.stream_id ?? randomUUID();
|
|
3108
|
+
const key = `${envelope.client_id}\u0000${streamId}\u0000${envelope.seq_id ?? 0}`;
|
|
3109
|
+
const count = envelope.segment_count;
|
|
3110
|
+
if (!Number.isInteger(count) || count < 1 || count > MAX_CHUNKS || !Number.isInteger(envelope.segment_id) || envelope.segment_id < 0 || envelope.segment_id >= count || !Number.isInteger(envelope.message_size_bytes) || envelope.message_size_bytes < 1 || envelope.message_size_bytes > MAX_MESSAGE_BYTES) {
|
|
3111
|
+
assemblies.delete(key);
|
|
3112
|
+
return;
|
|
3113
|
+
}
|
|
3114
|
+
const assembly = assemblies.get(key) ?? {
|
|
3115
|
+
chunks: new Array(count),
|
|
3116
|
+
size: envelope.message_size_bytes
|
|
3117
|
+
};
|
|
3118
|
+
if (assembly.chunks.length !== count || assembly.size !== envelope.message_size_bytes) {
|
|
3119
|
+
assemblies.delete(key);
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
assembly.chunks[envelope.segment_id] = envelope.message_chunk_base64;
|
|
3123
|
+
assemblies.set(key, assembly);
|
|
3124
|
+
if (assembly.chunks.some((chunk) => typeof chunk !== "string")) return void 0;
|
|
3125
|
+
assemblies.delete(key);
|
|
3126
|
+
const bytes = Buffer.concat(assembly.chunks.map((chunk) => Buffer.from(chunk, "base64")));
|
|
3127
|
+
if (bytes.byteLength !== assembly.size) return void 0;
|
|
3128
|
+
return {
|
|
3129
|
+
client_id: envelope.client_id,
|
|
3130
|
+
message: JSON.parse(bytes.toString("utf8")),
|
|
3131
|
+
seq_id: envelope.seq_id,
|
|
3132
|
+
stream_id: streamId,
|
|
3133
|
+
type: "client_message"
|
|
3134
|
+
};
|
|
3135
|
+
};
|
|
3136
|
+
const receive = async (raw) => {
|
|
3137
|
+
const envelope = JSON.parse(raw);
|
|
3138
|
+
if (typeof envelope?.client_id !== "string" || envelope.client_id.length === 0) return;
|
|
3139
|
+
if (envelope.type === "client_message") await deliver(envelope);
|
|
3140
|
+
else if (envelope.type === "client_message_chunk") {
|
|
3141
|
+
const message = receiveChunk(envelope);
|
|
3142
|
+
if (message) await deliver(message);
|
|
3143
|
+
} else if (envelope.type === "client_closed") closeClient(envelope.client_id, envelope.stream_id);
|
|
3144
|
+
else if (envelope.type === "ping") await sendEnvelope({
|
|
3145
|
+
client_id: envelope.client_id,
|
|
3146
|
+
seq_id: envelope.seq_id ?? 0,
|
|
3147
|
+
status: "active",
|
|
3148
|
+
stream_id: envelope.stream_id ?? randomUUID(),
|
|
3149
|
+
type: "pong"
|
|
3150
|
+
});
|
|
3151
|
+
};
|
|
3152
|
+
const connectOnce = async (signal) => {
|
|
3153
|
+
const current = await ensureEnrollment(signal);
|
|
3154
|
+
await new Promise((resolve, reject) => {
|
|
3155
|
+
const ws = new options.WebSocket(endpoints.websocket.toString(), { headers: {
|
|
3156
|
+
authorization: `Bearer ${current.token}`,
|
|
3157
|
+
"user-agent": options.userAgent,
|
|
3158
|
+
"x-codex-installation-id": installation,
|
|
3159
|
+
"x-codex-name": Buffer.from(options.hostName).toString("base64"),
|
|
3160
|
+
"x-codex-protocol-version": PROTOCOL_VERSION,
|
|
3161
|
+
"x-codex-server-id": current.serverId
|
|
3162
|
+
} });
|
|
3163
|
+
socket = ws;
|
|
3164
|
+
let queue = Promise.resolve();
|
|
3165
|
+
const onAbort = () => ws.close();
|
|
3166
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3167
|
+
ws.on("open", () => {
|
|
3168
|
+
state.status = "connected";
|
|
3169
|
+
state.connectedAt = now();
|
|
3170
|
+
state.lastError = void 0;
|
|
3171
|
+
});
|
|
3172
|
+
ws.on("message", (data) => {
|
|
3173
|
+
queue = queue.then(() => receive(String(data))).catch((error) => {
|
|
3174
|
+
state.lastError = error?.code ?? "protocol";
|
|
3175
|
+
});
|
|
3176
|
+
});
|
|
3177
|
+
ws.on("error", (error) => reject(new RemoteControlError("socket", error?.message ?? "socket error")));
|
|
3178
|
+
ws.on("close", () => {
|
|
3179
|
+
signal.removeEventListener("abort", onAbort);
|
|
3180
|
+
resolve();
|
|
3181
|
+
});
|
|
3182
|
+
});
|
|
3183
|
+
for (const [, client] of clients) client.close();
|
|
3184
|
+
clients.clear();
|
|
3185
|
+
sequences.clear();
|
|
3186
|
+
assemblies.clear();
|
|
3187
|
+
socket = void 0;
|
|
3188
|
+
};
|
|
3189
|
+
const loop = async (signal) => {
|
|
3190
|
+
let delay = INITIAL_RECONNECT_MS;
|
|
3191
|
+
let conflicts = 0;
|
|
3192
|
+
let conflictSince = 0;
|
|
3193
|
+
while (!signal.aborted) {
|
|
3194
|
+
state.status = "connecting";
|
|
3195
|
+
try {
|
|
3196
|
+
await connectOnce(signal);
|
|
3197
|
+
delay = INITIAL_RECONNECT_MS;
|
|
3198
|
+
} catch (error) {
|
|
3199
|
+
state.lastError = error?.code ?? "network";
|
|
3200
|
+
if (options.debug) state.detail = String(error?.message ?? error).slice(0, 160);
|
|
3201
|
+
state.status = "error";
|
|
3202
|
+
conflicts = /409/u.test(String(error?.message)) ? conflicts + 1 : 0;
|
|
3203
|
+
if (conflicts === 1) conflictSince = now();
|
|
3204
|
+
if (error?.code === "unauthorized" || conflicts >= 3) enrolled = void 0;
|
|
3205
|
+
if (conflicts >= 3 && now() - conflictSince >= (options.conflictRenewMs ?? CONFLICT_RENEW_MS) && options.renewInstallation) {
|
|
3206
|
+
installation = await options.renewInstallation();
|
|
3207
|
+
enrolled = void 0;
|
|
3208
|
+
conflicts = 0;
|
|
3209
|
+
}
|
|
3210
|
+
if (error?.code === "not-signed-in") {
|
|
3211
|
+
state.status = "stopped";
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
if (signal.aborted) break;
|
|
3216
|
+
state.reconnects += 1;
|
|
3217
|
+
await wait(delay, signal);
|
|
3218
|
+
delay = Math.min(delay * 2, MAX_RECONNECT_MS);
|
|
3219
|
+
}
|
|
3220
|
+
};
|
|
3221
|
+
return Object.freeze({
|
|
3222
|
+
pair,
|
|
3223
|
+
status: () => ({
|
|
3224
|
+
...state,
|
|
3225
|
+
enrolled: enrolled !== void 0,
|
|
3226
|
+
server: enrolled?.serverId.slice(-6),
|
|
3227
|
+
clients: clients.size
|
|
3228
|
+
}),
|
|
3229
|
+
start() {
|
|
3230
|
+
if (running) return running;
|
|
3231
|
+
stop = new AbortController();
|
|
3232
|
+
state.status = "connecting";
|
|
3233
|
+
running = loop(stop.signal).finally(() => {
|
|
3234
|
+
running = void 0;
|
|
3235
|
+
if (state.status !== "error") state.status = "stopped";
|
|
3236
|
+
});
|
|
3237
|
+
return running;
|
|
3238
|
+
},
|
|
3239
|
+
async stop() {
|
|
3240
|
+
stop?.abort();
|
|
3241
|
+
socket?.close();
|
|
3242
|
+
await running;
|
|
3243
|
+
state.status = "stopped";
|
|
3244
|
+
}
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
3247
|
+
//#endregion
|
|
3248
|
+
//#region src/remote-control.js
|
|
3249
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
3250
|
+
/** The installation id only tells ChatGPT "this is the same machine"; it is not a secret. */
|
|
3251
|
+
async function installationId(filename, fresh = false) {
|
|
3252
|
+
try {
|
|
3253
|
+
if (fresh) throw Object.assign(/* @__PURE__ */ new Error("rotate"), { code: "ENOENT" });
|
|
3254
|
+
const value = JSON.parse(await readFile(filename, "utf8"))?.installationId;
|
|
3255
|
+
if (typeof value === "string" && UUID.test(value)) return value;
|
|
3256
|
+
} catch (error) {
|
|
3257
|
+
if (error?.code !== "ENOENT" && !(error instanceof SyntaxError)) throw error;
|
|
3258
|
+
}
|
|
3259
|
+
const created = randomUUID();
|
|
3260
|
+
await mkdir(dirname(filename), { recursive: true });
|
|
3261
|
+
const temporary = `${filename}.${process.pid}.tmp`;
|
|
3262
|
+
await writeFile(temporary, `${JSON.stringify({ installationId: created })}\n`, { mode: 384 });
|
|
3263
|
+
await rename(temporary, filename);
|
|
3264
|
+
return created;
|
|
3265
|
+
}
|
|
3266
|
+
/**
|
|
3267
|
+
* Optional ChatGPT Remote Control host. Nothing is contacted until enable().
|
|
3268
|
+
* @param {{ credentials: Function, WebSocket: Function, fetch?: Function, stateFile: string, hostName: string, userAgent: string, methods: object, notifications?: object, onConnection?: Function, onClose?: Function, base?: string }} options
|
|
3269
|
+
*/
|
|
3270
|
+
function createRemoteControl(options) {
|
|
3271
|
+
const phones = /* @__PURE__ */ new Set();
|
|
3272
|
+
const host = createRemoteControlHost({
|
|
3273
|
+
methods: options.methods,
|
|
3274
|
+
notifications: options.notifications,
|
|
3275
|
+
onConnection: options.onConnection,
|
|
3276
|
+
onActive: (client) => {
|
|
3277
|
+
phones.add(client.id);
|
|
3278
|
+
},
|
|
3279
|
+
onClose: (client, notify) => {
|
|
3280
|
+
phones.delete(client.id);
|
|
3281
|
+
options.onClose?.(client, notify);
|
|
3282
|
+
},
|
|
3283
|
+
debug: process.env.DSH_CODEX_REMOTE_DEBUG === "1"
|
|
3284
|
+
});
|
|
3285
|
+
let relay;
|
|
3286
|
+
let starting;
|
|
3287
|
+
const ensureRelay = async () => {
|
|
3288
|
+
relay ??= createRemoteControlRelay({
|
|
3289
|
+
debug: process.env.DSH_CODEX_REMOTE_DEBUG === "1",
|
|
3290
|
+
credentials: options.credentials,
|
|
3291
|
+
fetch: options.fetch,
|
|
3292
|
+
WebSocket: options.WebSocket,
|
|
3293
|
+
installationId: await installationId(options.stateFile),
|
|
3294
|
+
renewInstallation: () => installationId(options.stateFile, true),
|
|
3295
|
+
hostName: options.hostName,
|
|
3296
|
+
userAgent: options.userAgent,
|
|
3297
|
+
base: options.base,
|
|
3298
|
+
serve: (client) => host.serve(client)
|
|
3299
|
+
});
|
|
3300
|
+
return relay;
|
|
3301
|
+
};
|
|
3302
|
+
return Object.freeze({
|
|
3303
|
+
host,
|
|
3304
|
+
async enable() {
|
|
3305
|
+
starting ??= ensureRelay().then((value) => {
|
|
3306
|
+
value.start();
|
|
3307
|
+
return value;
|
|
3308
|
+
}).finally(() => {
|
|
3309
|
+
starting = void 0;
|
|
3310
|
+
});
|
|
3311
|
+
await starting;
|
|
3312
|
+
return this.status();
|
|
3313
|
+
},
|
|
3314
|
+
async disable() {
|
|
3315
|
+
if (relay) await relay.stop();
|
|
3316
|
+
return this.status();
|
|
3317
|
+
},
|
|
3318
|
+
async pair(signal) {
|
|
3319
|
+
return (await ensureRelay()).pair(signal);
|
|
3320
|
+
},
|
|
3321
|
+
status() {
|
|
3322
|
+
return {
|
|
3323
|
+
...relay?.status() ?? {
|
|
3324
|
+
status: "stopped",
|
|
3325
|
+
enrolled: false,
|
|
3326
|
+
clients: 0,
|
|
3327
|
+
reconnects: 0
|
|
3328
|
+
},
|
|
3329
|
+
phones: phones.size,
|
|
3330
|
+
methods: host.seen(),
|
|
3331
|
+
trace: host.trace()
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
});
|
|
3335
|
+
}
|
|
3336
|
+
//#endregion
|
|
3337
|
+
//#region src/remote-control-dsh.js
|
|
3338
|
+
/** A folder the phone named, only when it is an existing absolute folder on this machine. */
|
|
3339
|
+
function localFolder(value) {
|
|
3340
|
+
if (typeof value !== "string" || value === "" || !isAbsolute(value)) return void 0;
|
|
3341
|
+
if (process.platform === "win32" && !/^[A-Za-z]:[\\/]/u.test(value)) return void 0;
|
|
3342
|
+
try {
|
|
3343
|
+
return statSync(value).isDirectory() ? value : void 0;
|
|
3344
|
+
} catch {
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
const seconds = (value) => Math.floor((Number.isFinite(value) ? value : Date.now()) / 1e3);
|
|
3349
|
+
const emptyTurn = () => ({
|
|
3350
|
+
id: randomUUID(),
|
|
3351
|
+
items: [],
|
|
3352
|
+
itemsView: "full",
|
|
3353
|
+
status: "inProgress",
|
|
3354
|
+
error: null,
|
|
3355
|
+
startedAt: seconds(Date.now()),
|
|
3356
|
+
completedAt: null,
|
|
3357
|
+
durationMs: null
|
|
3358
|
+
});
|
|
3359
|
+
const textOf = (content) => (content ?? []).map((block) => block?.type === "text" ? block.text : "").filter(Boolean).join("\n");
|
|
3360
|
+
const inputText = (input) => (Array.isArray(input) ? input : []).map((part) => part?.type === "text" ? part.text : "").filter(Boolean).join("\n");
|
|
3361
|
+
const notFound = (id) => new RpcError(-32602, `Thread not found: ${String(id).slice(0, 80)}`);
|
|
3362
|
+
const modelKey = (selection) => `${encodeURIComponent(selection.provider)}/${encodeURIComponent(selection.model)}`;
|
|
3363
|
+
function parseModelKey(value) {
|
|
3364
|
+
const at = typeof value === "string" ? value.indexOf("/") : -1;
|
|
3365
|
+
if (at <= 0) return void 0;
|
|
3366
|
+
try {
|
|
3367
|
+
return {
|
|
3368
|
+
provider: decodeURIComponent(value.slice(0, at)),
|
|
3369
|
+
model: decodeURIComponent(value.slice(at + 1))
|
|
3370
|
+
};
|
|
3371
|
+
} catch {
|
|
3372
|
+
return;
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
function parseArguments(value) {
|
|
3376
|
+
if (typeof value !== "string") return value ?? null;
|
|
3377
|
+
try {
|
|
3378
|
+
return JSON.parse(value);
|
|
3379
|
+
} catch {
|
|
3380
|
+
return { raw: value };
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
/** What the person typed: DSH appends its own system reminders (skills, context) to user messages. */
|
|
3384
|
+
const visibleText = (content) => textOf(content).replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gu, "").trim();
|
|
3385
|
+
/** Text of a user-role message the person wrote; DSH's own injected ones (runtime context, notices) carry another source kind. */
|
|
3386
|
+
const typedText = (message) => message.role === "user" && (message.source === void 0 || message.source?.kind === "user") ? visibleText(message.content) : "";
|
|
3387
|
+
/** One user message starts a turn; assistant blocks and tool results fill it. */
|
|
3388
|
+
function projectTurns(messages, at = Date.now()) {
|
|
3389
|
+
const turns = [];
|
|
3390
|
+
let turn;
|
|
3391
|
+
const calls = /* @__PURE__ */ new Map();
|
|
3392
|
+
const close = () => {
|
|
3393
|
+
if (turn && turn.items.length > 0) {
|
|
3394
|
+
turn.status = "completed";
|
|
3395
|
+
turn.completedAt = turn.startedAt;
|
|
3396
|
+
turns.push(turn);
|
|
3397
|
+
}
|
|
3398
|
+
turn = void 0;
|
|
3399
|
+
};
|
|
3400
|
+
for (const message of messages) if (message.role === "user") {
|
|
3401
|
+
const text = typedText(message);
|
|
3402
|
+
if (text === "") continue;
|
|
3403
|
+
close();
|
|
3404
|
+
turn = {
|
|
3405
|
+
...emptyTurn(),
|
|
3406
|
+
id: message.id,
|
|
3407
|
+
startedAt: seconds(at)
|
|
3408
|
+
};
|
|
3409
|
+
turn.items.push({
|
|
3410
|
+
type: "userMessage",
|
|
3411
|
+
id: message.id,
|
|
3412
|
+
clientId: null,
|
|
3413
|
+
content: [{
|
|
3414
|
+
type: "text",
|
|
3415
|
+
text,
|
|
3416
|
+
text_elements: []
|
|
3417
|
+
}]
|
|
3418
|
+
});
|
|
3419
|
+
} else if (message.role === "assistant") {
|
|
3420
|
+
turn ??= {
|
|
3421
|
+
...emptyTurn(),
|
|
3422
|
+
startedAt: seconds(at)
|
|
3423
|
+
};
|
|
3424
|
+
for (const [index, block] of message.content.entries()) {
|
|
3425
|
+
const id = `${message.id}:${index}`;
|
|
3426
|
+
if (block.type === "text") turn.items.push({
|
|
3427
|
+
type: "agentMessage",
|
|
3428
|
+
id,
|
|
3429
|
+
text: block.text,
|
|
3430
|
+
phase: null,
|
|
3431
|
+
memoryCitation: null,
|
|
3432
|
+
delivery: null
|
|
3433
|
+
});
|
|
3434
|
+
else if (block.type === "reasoning") turn.items.push({
|
|
3435
|
+
type: "reasoning",
|
|
3436
|
+
id,
|
|
3437
|
+
summary: [],
|
|
3438
|
+
content: [block.text]
|
|
3439
|
+
});
|
|
3440
|
+
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
|
+
};
|
|
3452
|
+
calls.set(block.id, item);
|
|
3453
|
+
turn.items.push(item);
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
} else if (message.role === "tool") {
|
|
3457
|
+
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
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
close();
|
|
3468
|
+
return turns;
|
|
3469
|
+
}
|
|
3470
|
+
function thread({ id, title, preview, cwd, createdAt, updatedAt, running, turns = [] }) {
|
|
3471
|
+
return {
|
|
3472
|
+
id,
|
|
3473
|
+
sessionId: id,
|
|
3474
|
+
forkedFromId: null,
|
|
3475
|
+
parentThreadId: null,
|
|
3476
|
+
preview: preview ?? title ?? "",
|
|
3477
|
+
ephemeral: false,
|
|
3478
|
+
modelProvider: "dsh",
|
|
3479
|
+
createdAt: seconds(createdAt),
|
|
3480
|
+
updatedAt: seconds(updatedAt ?? createdAt),
|
|
3481
|
+
recencyAt: seconds(updatedAt ?? createdAt),
|
|
3482
|
+
status: running ? {
|
|
3483
|
+
type: "active",
|
|
3484
|
+
activeFlags: []
|
|
3485
|
+
} : { type: "idle" },
|
|
3486
|
+
path: null,
|
|
3487
|
+
cwd: cwd ?? "",
|
|
3488
|
+
cliVersion: "dsh",
|
|
3489
|
+
source: { custom: "dsh" },
|
|
3490
|
+
threadSource: "dsh",
|
|
3491
|
+
agentNickname: null,
|
|
3492
|
+
agentRole: null,
|
|
3493
|
+
gitInfo: null,
|
|
3494
|
+
name: title ?? null,
|
|
3495
|
+
turns,
|
|
3496
|
+
historyMode: "legacy",
|
|
3497
|
+
section: null,
|
|
3498
|
+
sectionEnteredAt: null,
|
|
3499
|
+
projectId: null,
|
|
3500
|
+
canAcceptDirectInput: true,
|
|
3501
|
+
extra: null
|
|
3502
|
+
};
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* @param {{ controller: () => any, agents: () => any, permissions?: () => any, userAgent: string }} options
|
|
3506
|
+
*/
|
|
3507
|
+
const PHONE_RETURN_MS = 12e4;
|
|
3508
|
+
function createDshRemoteControl({ controller, agents, permissions = () => void 0, workspaces = () => void 0, trace = () => {}, phoneReturnMs = PHONE_RETURN_MS, userAgent }) {
|
|
3509
|
+
const subscribers = /* @__PURE__ */ new Map();
|
|
3510
|
+
const active = /* @__PURE__ */ new Map();
|
|
3511
|
+
const queues = /* @__PURE__ */ new Map();
|
|
3512
|
+
let catalog;
|
|
3513
|
+
const service = () => {
|
|
3514
|
+
const value = controller();
|
|
3515
|
+
if (!value) throw new RpcError(-32603, "DSH session service is unavailable in this profile");
|
|
3516
|
+
return value;
|
|
3517
|
+
};
|
|
3518
|
+
const arrivals = /* @__PURE__ */ new Map();
|
|
3519
|
+
const subscribe = (threadId, notify) => {
|
|
3520
|
+
const set = subscribers.get(threadId) ?? /* @__PURE__ */ new Set();
|
|
3521
|
+
set.add(notify);
|
|
3522
|
+
subscribers.set(threadId, set);
|
|
3523
|
+
for (const wake of arrivals.get(threadId) ?? []) wake();
|
|
3524
|
+
};
|
|
3525
|
+
/** Wait until a phone opens the thread again, the signal aborts, or the time runs out. */
|
|
3526
|
+
const phoneReturns = (threadId, ms, signal) => new Promise((resolve) => {
|
|
3527
|
+
const set = arrivals.get(threadId) ?? /* @__PURE__ */ new Set();
|
|
3528
|
+
arrivals.set(threadId, set);
|
|
3529
|
+
const done = (value) => {
|
|
3530
|
+
clearTimeout(timer);
|
|
3531
|
+
set.delete(wake);
|
|
3532
|
+
signal?.removeEventListener("abort", stop);
|
|
3533
|
+
if (set.size === 0) arrivals.delete(threadId);
|
|
3534
|
+
resolve(value);
|
|
3535
|
+
};
|
|
3536
|
+
const wake = () => done(true);
|
|
3537
|
+
const stop = () => done(false);
|
|
3538
|
+
const timer = setTimeout(stop, ms);
|
|
3539
|
+
set.add(wake);
|
|
3540
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
3541
|
+
});
|
|
3542
|
+
const emit = (threadId, method, params) => {
|
|
3543
|
+
for (const notify of subscribers.get(threadId) ?? []) notify(method, params).catch(() => {});
|
|
3544
|
+
};
|
|
3545
|
+
const agentFor = async (id) => {
|
|
3546
|
+
const result = await service().resolveAgent(id);
|
|
3547
|
+
if (!result?.agent) throw notFound(id);
|
|
3548
|
+
return result.agent;
|
|
3549
|
+
};
|
|
3550
|
+
const loadCatalog = async () => {
|
|
3551
|
+
catalog = await service().modelCatalog();
|
|
3552
|
+
return catalog;
|
|
3553
|
+
};
|
|
3554
|
+
const selection = (agent) => agent?.options?.provider && agent?.options?.model ? {
|
|
3555
|
+
provider: agent.options.provider,
|
|
3556
|
+
model: agent.options.model,
|
|
3557
|
+
reasoningEffort: agent.options.reasoningEffort
|
|
3558
|
+
} : catalog?.default;
|
|
3559
|
+
const settings = (agent, cwd) => {
|
|
3560
|
+
const current = selection(agent);
|
|
3561
|
+
const preset = agent?.session ? presetOf(agent.session) : void 0;
|
|
3562
|
+
return {
|
|
3563
|
+
activePermissionProfile: null,
|
|
3564
|
+
approvalPolicy: preset === "danger-full-access" ? "never" : "on-request",
|
|
3565
|
+
approvalsReviewer: "user",
|
|
3566
|
+
cwd: cwd ?? "",
|
|
3567
|
+
initialTurnsPage: null,
|
|
3568
|
+
instructionSources: [],
|
|
3569
|
+
itemsBackwardsCursor: null,
|
|
3570
|
+
model: current ? modelKey(current) : "dsh/default",
|
|
3571
|
+
modelProvider: current?.provider ?? "dsh",
|
|
3572
|
+
multiAgentMode: "explicitRequestOnly",
|
|
3573
|
+
reasoningEffort: current?.reasoningEffort ?? null,
|
|
3574
|
+
runtimeWorkspaceRoots: cwd ? [cwd] : [],
|
|
3575
|
+
sandbox: sandboxFor(preset, cwd),
|
|
3576
|
+
serviceTier: null,
|
|
3577
|
+
turnsBackwardsCursor: null
|
|
3578
|
+
};
|
|
3579
|
+
};
|
|
3580
|
+
const openThread = async (id, notify) => {
|
|
3581
|
+
const agent = await agentFor(id);
|
|
3582
|
+
if (notify) subscribe(id, notify);
|
|
3583
|
+
await (catalog ? void 0 : loadCatalog().catch(() => void 0));
|
|
3584
|
+
const session = agent.session;
|
|
3585
|
+
const messages = session.deriveMessages();
|
|
3586
|
+
const first = messages.find((message) => typedText(message) !== "");
|
|
3587
|
+
return {
|
|
3588
|
+
agent,
|
|
3589
|
+
thread: thread({
|
|
3590
|
+
id,
|
|
3591
|
+
cwd: session.meta?.cwd,
|
|
3592
|
+
createdAt: session.meta?.createdAt,
|
|
3593
|
+
running: active.has(id),
|
|
3594
|
+
preview: first ? visibleText(first.content).slice(0, 200) : "",
|
|
3595
|
+
turns: projectTurns(messages, session.meta?.createdAt)
|
|
3596
|
+
})
|
|
3597
|
+
};
|
|
3598
|
+
};
|
|
3599
|
+
/** Apply a model or effort the phone picked; DSH validates it against its own catalog. */
|
|
3600
|
+
const applySelection = async (agent, sessionId, params) => {
|
|
3601
|
+
const picked = parseModelKey(params?.model);
|
|
3602
|
+
const effort = typeof params?.effort === "string" ? params.effort : void 0;
|
|
3603
|
+
if (!picked && effort === void 0) return;
|
|
3604
|
+
const current = selection(agent) ?? {};
|
|
3605
|
+
const next = {
|
|
3606
|
+
provider: picked?.provider ?? current.provider,
|
|
3607
|
+
model: picked?.model ?? current.model,
|
|
3608
|
+
...effort ?? current.reasoningEffort ? { reasoningEffort: effort ?? current.reasoningEffort } : {}
|
|
3609
|
+
};
|
|
3610
|
+
if (!next.provider || !next.model) return;
|
|
3611
|
+
if (next.provider === current.provider && next.model === current.model && next.reasoningEffort === current.reasoningEffort) return;
|
|
3612
|
+
await service().selectModel({
|
|
3613
|
+
sessionId,
|
|
3614
|
+
...next
|
|
3615
|
+
});
|
|
3616
|
+
};
|
|
3617
|
+
const presetOf = (session) => {
|
|
3618
|
+
try {
|
|
3619
|
+
return permissions()?.current?.(session);
|
|
3620
|
+
} catch {
|
|
3621
|
+
return;
|
|
3622
|
+
}
|
|
3623
|
+
};
|
|
3624
|
+
const sandboxFor = (preset, cwd) => preset === "danger-full-access" || preset === "auto" ? { type: "dangerFullAccess" } : {
|
|
3625
|
+
type: "workspaceWrite",
|
|
3626
|
+
writableRoots: cwd ? [cwd] : [],
|
|
3627
|
+
networkAccess: false,
|
|
3628
|
+
excludeTmpdirEnvVar: false,
|
|
3629
|
+
excludeSlashTmp: false
|
|
3630
|
+
};
|
|
3631
|
+
const applyPermissions = (agent, params) => {
|
|
3632
|
+
const type = params?.sandboxPolicy?.type ?? (typeof params?.sandbox === "string" ? params.sandbox : void 0);
|
|
3633
|
+
const preset = type === "dangerFullAccess" || type === "danger-full-access" ? "danger-full-access" : type === "workspaceWrite" || type === "workspace-write" ? "workspace-write" : void 0;
|
|
3634
|
+
if (!preset || !agent?.session || presetOf(agent.session) === preset) return;
|
|
3635
|
+
permissions()?.set?.(agent.session, preset);
|
|
3636
|
+
};
|
|
3637
|
+
const titles = /* @__PURE__ */ new Map();
|
|
3638
|
+
const lastPrompt = { text: "" };
|
|
3639
|
+
const titleThread = (params) => {
|
|
3640
|
+
const id = `ephemeral-${randomUUID()}`;
|
|
3641
|
+
titles.set(id, Date.now());
|
|
3642
|
+
for (const [key, at] of titles) if (Date.now() - at > 6e5) titles.delete(key);
|
|
3643
|
+
return {
|
|
3644
|
+
...settings(void 0, ""),
|
|
3645
|
+
thread: {
|
|
3646
|
+
...thread({
|
|
3647
|
+
id,
|
|
3648
|
+
cwd: "",
|
|
3649
|
+
createdAt: Date.now()
|
|
3650
|
+
}),
|
|
3651
|
+
ephemeral: true,
|
|
3652
|
+
modelProvider: "dsh"
|
|
3653
|
+
}
|
|
3654
|
+
};
|
|
3655
|
+
};
|
|
3656
|
+
const titleTurn = async (params, notify) => {
|
|
3657
|
+
const title = (lastPrompt.text || inputText(params?.input).split(/\n\s*\n/u).map((part) => part.trim()).filter(Boolean).at(-1) || "DSH").split("\n")[0].trim().slice(0, 36);
|
|
3658
|
+
const turn = {
|
|
3659
|
+
...emptyTurn(),
|
|
3660
|
+
status: "completed",
|
|
3661
|
+
completedAt: seconds(Date.now())
|
|
3662
|
+
};
|
|
3663
|
+
const item = {
|
|
3664
|
+
type: "agentMessage",
|
|
3665
|
+
id: randomUUID(),
|
|
3666
|
+
text: title,
|
|
3667
|
+
phase: null,
|
|
3668
|
+
memoryCitation: null,
|
|
3669
|
+
delivery: null
|
|
3670
|
+
};
|
|
3671
|
+
turn.items.push(item);
|
|
3672
|
+
const threadId = params.threadId;
|
|
3673
|
+
titles.delete(threadId);
|
|
3674
|
+
queueMicrotask(() => {
|
|
3675
|
+
notify("turn/started", {
|
|
3676
|
+
threadId,
|
|
3677
|
+
turn: {
|
|
3678
|
+
...turn,
|
|
3679
|
+
items: [],
|
|
3680
|
+
status: "inProgress",
|
|
3681
|
+
completedAt: null
|
|
3682
|
+
}
|
|
3683
|
+
}).catch(() => {});
|
|
3684
|
+
notify("item/completed", {
|
|
3685
|
+
threadId,
|
|
3686
|
+
turnId: turn.id,
|
|
3687
|
+
item,
|
|
3688
|
+
completedAtMs: Date.now()
|
|
3689
|
+
}).catch(() => {});
|
|
3690
|
+
notify("turn/completed", {
|
|
3691
|
+
threadId,
|
|
3692
|
+
turn
|
|
3693
|
+
}).catch(() => {});
|
|
3694
|
+
});
|
|
3695
|
+
return { turn: {
|
|
3696
|
+
...turn,
|
|
3697
|
+
items: [],
|
|
3698
|
+
status: "inProgress",
|
|
3699
|
+
completedAt: null
|
|
3700
|
+
} };
|
|
3701
|
+
};
|
|
3702
|
+
async function startTurn(params, notify) {
|
|
3703
|
+
const text = inputText(params?.input);
|
|
3704
|
+
if (text === "") throw invalidParams("Only text input is supported");
|
|
3705
|
+
lastPrompt.text = text;
|
|
3706
|
+
const id = params?.threadId;
|
|
3707
|
+
const agent = await agentFor(id);
|
|
3708
|
+
if (active.has(id)) throw new RpcError(-32602, "A turn is already running in this conversation");
|
|
3709
|
+
subscribe(id, notify);
|
|
3710
|
+
await applySelection(agent, id, params);
|
|
3711
|
+
applyPermissions(agent, params);
|
|
3712
|
+
const turn = emptyTurn();
|
|
3713
|
+
active.set(id, {
|
|
3714
|
+
turn,
|
|
3715
|
+
ended: void 0
|
|
3716
|
+
});
|
|
3717
|
+
const userItem = {
|
|
3718
|
+
type: "userMessage",
|
|
3719
|
+
id: randomUUID(),
|
|
3720
|
+
clientId: params.clientUserMessageId ?? null,
|
|
3721
|
+
content: [{
|
|
3722
|
+
type: "text",
|
|
3723
|
+
text,
|
|
3724
|
+
text_elements: []
|
|
3725
|
+
}]
|
|
3726
|
+
};
|
|
3727
|
+
emit(id, "turn/started", {
|
|
3728
|
+
threadId: id,
|
|
3729
|
+
turn
|
|
3730
|
+
});
|
|
3731
|
+
emit(id, "item/started", {
|
|
3732
|
+
threadId: id,
|
|
3733
|
+
turnId: turn.id,
|
|
3734
|
+
item: userItem,
|
|
3735
|
+
startedAtMs: Date.now()
|
|
3736
|
+
});
|
|
3737
|
+
emit(id, "item/completed", {
|
|
3738
|
+
threadId: id,
|
|
3739
|
+
turnId: turn.id,
|
|
3740
|
+
item: userItem,
|
|
3741
|
+
completedAtMs: Date.now()
|
|
3742
|
+
});
|
|
3743
|
+
try {
|
|
3744
|
+
await service().prompt({
|
|
3745
|
+
requestId: randomUUID(),
|
|
3746
|
+
sessionId: id,
|
|
3747
|
+
mode: "queue",
|
|
3748
|
+
content: [{
|
|
3749
|
+
type: "text",
|
|
3750
|
+
text
|
|
3751
|
+
}]
|
|
3752
|
+
}, AbortSignal.timeout(3e4));
|
|
3753
|
+
} catch (error) {
|
|
3754
|
+
active.delete(id);
|
|
3755
|
+
throw new RpcError(-32603, error?.message ? `DSH did not accept the message: ${String(error.message).slice(0, 200)}` : "DSH did not accept the message");
|
|
3756
|
+
}
|
|
3757
|
+
Promise.resolve(agent.whenIdle?.()).catch(() => {}).then(() => {
|
|
3758
|
+
const reason = active.get(id)?.ended;
|
|
3759
|
+
turn.status = reason?.kind === "error" ? "failed" : reason?.kind === "aborted" || reason?.kind === "interrupted" ? "interrupted" : "completed";
|
|
3760
|
+
if (reason?.kind === "error") {
|
|
3761
|
+
const message = String(reason.error?.message ?? reason.error?.code ?? "The model request failed").slice(0, 500);
|
|
3762
|
+
turn.error = {
|
|
3763
|
+
message,
|
|
3764
|
+
codexErrorInfo: null,
|
|
3765
|
+
additionalDetails: null
|
|
3766
|
+
};
|
|
3767
|
+
emit(id, "error", {
|
|
3768
|
+
threadId: id,
|
|
3769
|
+
turnId: turn.id,
|
|
3770
|
+
willRetry: false,
|
|
3771
|
+
error: turn.error
|
|
3772
|
+
});
|
|
3773
|
+
}
|
|
3774
|
+
turn.completedAt = seconds(Date.now());
|
|
3775
|
+
active.delete(id);
|
|
3776
|
+
emit(id, "turn/completed", {
|
|
3777
|
+
threadId: id,
|
|
3778
|
+
turn
|
|
3779
|
+
});
|
|
3780
|
+
const [next, ...rest] = queues.get(id) ?? [];
|
|
3781
|
+
if (next) {
|
|
3782
|
+
queues.set(id, rest);
|
|
3783
|
+
emit(id, "thread/queue/changed", { threadId: id });
|
|
3784
|
+
startTurn({
|
|
3785
|
+
threadId: id,
|
|
3786
|
+
input: next.input,
|
|
3787
|
+
clientUserMessageId: next.clientUserMessageId
|
|
3788
|
+
}, notify).catch(() => {});
|
|
3789
|
+
}
|
|
3790
|
+
});
|
|
3791
|
+
return { turn };
|
|
3792
|
+
}
|
|
3793
|
+
const methods = {
|
|
3794
|
+
initialize: () => ({
|
|
3795
|
+
codexHome: "",
|
|
3796
|
+
platformFamily: process.platform === "win32" ? "windows" : "unix",
|
|
3797
|
+
platformOs: {
|
|
3798
|
+
darwin: "macos",
|
|
3799
|
+
win32: "windows"
|
|
3800
|
+
}[process.platform] ?? process.platform,
|
|
3801
|
+
userAgent
|
|
3802
|
+
}),
|
|
3803
|
+
"account/read": () => ({
|
|
3804
|
+
account: {
|
|
3805
|
+
type: "chatgpt",
|
|
3806
|
+
email: null,
|
|
3807
|
+
planType: "unknown"
|
|
3808
|
+
},
|
|
3809
|
+
requiresOpenaiAuth: false
|
|
3810
|
+
}),
|
|
3811
|
+
"modelProvider/capabilities/read": () => ({
|
|
3812
|
+
imageGeneration: false,
|
|
3813
|
+
namespaceTools: false,
|
|
3814
|
+
webSearch: false
|
|
3815
|
+
}),
|
|
3816
|
+
"model/list": async () => {
|
|
3817
|
+
const value = await loadCatalog();
|
|
3818
|
+
const defaultKey = value.default ? modelKey(value.default) : void 0;
|
|
3819
|
+
const data = [];
|
|
3820
|
+
for (const group of value.groups ?? []) for (const model of group.models ?? []) {
|
|
3821
|
+
const key = modelKey({
|
|
3822
|
+
provider: group.id,
|
|
3823
|
+
model: model.id
|
|
3824
|
+
});
|
|
3825
|
+
const efforts = (model.reasoning?.efforts ?? []).map((effort) => ({
|
|
3826
|
+
reasoningEffort: effort.id,
|
|
3827
|
+
description: effort.name ?? effort.id
|
|
3828
|
+
}));
|
|
3829
|
+
const supported = efforts.length > 0 ? efforts : [{
|
|
3830
|
+
reasoningEffort: "medium",
|
|
3831
|
+
description: "Default"
|
|
3832
|
+
}];
|
|
3833
|
+
data.push({
|
|
3834
|
+
id: key,
|
|
3835
|
+
model: key,
|
|
3836
|
+
displayName: model.name ?? model.id,
|
|
3837
|
+
description: group.name ?? "",
|
|
3838
|
+
hidden: false,
|
|
3839
|
+
isDefault: key === defaultKey,
|
|
3840
|
+
supportedReasoningEfforts: supported,
|
|
3841
|
+
defaultReasoningEffort: model.reasoning?.defaultEffort ?? supported[0].reasoningEffort,
|
|
3842
|
+
inputModalities: ["text"],
|
|
3843
|
+
supportsPersonality: false,
|
|
3844
|
+
additionalSpeedTiers: [],
|
|
3845
|
+
upgrade: null,
|
|
3846
|
+
upgradeInfo: null,
|
|
3847
|
+
availabilityNux: null,
|
|
3848
|
+
serviceTiers: []
|
|
3849
|
+
});
|
|
3850
|
+
}
|
|
3851
|
+
return {
|
|
3852
|
+
data,
|
|
3853
|
+
nextCursor: null
|
|
3854
|
+
};
|
|
3855
|
+
},
|
|
3856
|
+
"configRequirements/read": () => ({ requirements: {
|
|
3857
|
+
allowedSandboxModes: ["workspace-write", "danger-full-access"],
|
|
3858
|
+
allowedApprovalPolicies: ["on-request", "never"],
|
|
3859
|
+
allowRemoteControl: true
|
|
3860
|
+
} }),
|
|
3861
|
+
"thread/settings/update": async (params) => {
|
|
3862
|
+
const agent = await agentFor(params?.threadId);
|
|
3863
|
+
await applySelection(agent, params.threadId, params);
|
|
3864
|
+
applyPermissions(agent, params);
|
|
3865
|
+
return {};
|
|
3866
|
+
},
|
|
3867
|
+
"thread/attachment/list": () => ({
|
|
3868
|
+
data: [],
|
|
3869
|
+
nextCursor: null
|
|
3870
|
+
}),
|
|
3871
|
+
"config/batchWrite": () => ({
|
|
3872
|
+
status: "ok",
|
|
3873
|
+
version: "1",
|
|
3874
|
+
filePath: process.platform === "win32" ? "C:dshconfig.toml" : "/dsh/config.toml",
|
|
3875
|
+
overriddenMetadata: null
|
|
3876
|
+
}),
|
|
3877
|
+
"command/exec": () => {
|
|
3878
|
+
throw new RpcError(-32600, "Running commands directly is not available on a DSH host; ask in the conversation instead");
|
|
3879
|
+
},
|
|
3880
|
+
"plugin/installed": () => ({
|
|
3881
|
+
marketplaceLoadErrors: [],
|
|
3882
|
+
marketplaces: []
|
|
3883
|
+
}),
|
|
3884
|
+
"fs/createDirectory": () => ({}),
|
|
3885
|
+
"fs/getMetadata": () => ({
|
|
3886
|
+
isDirectory: true,
|
|
3887
|
+
isFile: false,
|
|
3888
|
+
isSymlink: false,
|
|
3889
|
+
createdAtMs: Date.now(),
|
|
3890
|
+
modifiedAtMs: Date.now()
|
|
3891
|
+
}),
|
|
3892
|
+
"fs/readDirectory": () => ({ entries: [] }),
|
|
3893
|
+
"collaborationMode/list": () => ({ data: [] }),
|
|
3894
|
+
"permissionProfile/list": () => ({
|
|
3895
|
+
data: [],
|
|
3896
|
+
nextCursor: null
|
|
3897
|
+
}),
|
|
3898
|
+
"threadSection/list": () => ({
|
|
3899
|
+
data: [],
|
|
3900
|
+
nextCursor: null
|
|
3901
|
+
}),
|
|
3902
|
+
"thread/goal/get": () => ({ goal: null }),
|
|
3903
|
+
"experimentalFeature/list": () => ({
|
|
3904
|
+
data: [],
|
|
3905
|
+
nextCursor: null
|
|
3906
|
+
}),
|
|
3907
|
+
"skills/list": () => ({ data: [] }),
|
|
3908
|
+
"config/read": () => ({
|
|
3909
|
+
config: {},
|
|
3910
|
+
origins: {},
|
|
3911
|
+
layers: null
|
|
3912
|
+
}),
|
|
3913
|
+
"thread/loaded/list": () => ({
|
|
3914
|
+
data: [...subscribers.keys()],
|
|
3915
|
+
nextCursor: null
|
|
3916
|
+
}),
|
|
3917
|
+
"thread/unsubscribe": () => ({ status: "notSubscribed" }),
|
|
3918
|
+
"thread/list": async (params, { signal }) => {
|
|
3919
|
+
const value = await service().list({}, signal);
|
|
3920
|
+
const archived = new Set(workspaces()?.archivedSessionIds ?? []);
|
|
3921
|
+
const wantArchived = params?.archived === true;
|
|
3922
|
+
return {
|
|
3923
|
+
data: (value.items ?? []).filter((item) => item.origin !== "subagent" && !item.blank && archived.has(item.sessionId) === wantArchived).map((item) => thread({
|
|
3924
|
+
id: item.sessionId,
|
|
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
|
|
3933
|
+
};
|
|
3934
|
+
},
|
|
3935
|
+
"thread/archive": async (params) => {
|
|
3936
|
+
const registry = workspaces();
|
|
3937
|
+
if (!registry) throw new RpcError(-32603, "DSH archive is unavailable in this profile");
|
|
3938
|
+
try {
|
|
3939
|
+
await registry.archiveSession(params?.threadId, { stopActivity: true });
|
|
3940
|
+
} catch {
|
|
3941
|
+
throw notFound(params?.threadId);
|
|
3942
|
+
}
|
|
3943
|
+
active.delete(params?.threadId);
|
|
3944
|
+
return {};
|
|
3945
|
+
},
|
|
3946
|
+
"thread/unarchive": async (params) => {
|
|
3947
|
+
const registry = workspaces();
|
|
3948
|
+
if (!registry) throw new RpcError(-32603, "DSH archive is unavailable in this profile");
|
|
3949
|
+
await registry.unarchiveSession(params?.threadId);
|
|
3950
|
+
return { thread: (await openThread(params?.threadId)).thread };
|
|
3951
|
+
},
|
|
3952
|
+
"thread/read": async (params) => ({ thread: (await openThread(params?.threadId)).thread }),
|
|
3953
|
+
"thread/resume": async (params, { notify }) => {
|
|
3954
|
+
const { agent, thread: value } = await openThread(params?.threadId, notify);
|
|
3955
|
+
return {
|
|
3956
|
+
...settings(agent, value.cwd),
|
|
3957
|
+
thread: value
|
|
3958
|
+
};
|
|
3959
|
+
},
|
|
3960
|
+
"thread/start": async (params, { notify }) => {
|
|
3961
|
+
if (params?.ephemeral === true) return titleThread(params);
|
|
3962
|
+
const cwd = localFolder(params?.cwd);
|
|
3963
|
+
const created = await service().create(cwd ? { cwd } : {});
|
|
3964
|
+
const agent = await agentFor(created.sessionId);
|
|
3965
|
+
await applySelection(agent, created.sessionId, params);
|
|
3966
|
+
const { thread: value } = await openThread(created.sessionId, notify);
|
|
3967
|
+
emit(created.sessionId, "thread/started", { thread: value });
|
|
3968
|
+
return {
|
|
3969
|
+
...settings(agent, value.cwd),
|
|
3970
|
+
thread: value
|
|
3971
|
+
};
|
|
3972
|
+
},
|
|
3973
|
+
"turn/start": (params, { notify }) => titles.has(params?.threadId) ? titleTurn(params, notify) : startTurn(params, notify),
|
|
3974
|
+
"turn/interrupt": (params) => {
|
|
3975
|
+
service().cancel({ sessionId: params?.threadId });
|
|
3976
|
+
return {};
|
|
3977
|
+
},
|
|
3978
|
+
"turn/steer": async (params) => {
|
|
3979
|
+
const text = inputText(params?.input);
|
|
3980
|
+
if (text === "") throw invalidParams("Only text input is supported");
|
|
3981
|
+
await service().prompt({
|
|
3982
|
+
requestId: randomUUID(),
|
|
3983
|
+
sessionId: params?.threadId,
|
|
3984
|
+
mode: "steer",
|
|
3985
|
+
content: [{
|
|
3986
|
+
type: "text",
|
|
3987
|
+
text
|
|
3988
|
+
}]
|
|
3989
|
+
}, AbortSignal.timeout(3e4));
|
|
3990
|
+
return { turnId: params?.expectedTurnId ?? active.get(params?.threadId)?.turn.id ?? "" };
|
|
3991
|
+
},
|
|
3992
|
+
"thread/name/set": async (params) => {
|
|
3993
|
+
await service().rename({
|
|
3994
|
+
sessionId: params?.threadId,
|
|
3995
|
+
title: String(params?.name ?? "")
|
|
3996
|
+
});
|
|
3997
|
+
return {};
|
|
3998
|
+
},
|
|
3999
|
+
"thread/queue/list": (params) => ({
|
|
4000
|
+
data: queues.get(params?.threadId) ?? [],
|
|
4001
|
+
nextCursor: null
|
|
4002
|
+
}),
|
|
4003
|
+
"thread/queue/add": async (params, { notify }) => {
|
|
4004
|
+
if (!Array.isArray(params?.input)) throw invalidParams("input is required");
|
|
4005
|
+
const id = params?.threadId;
|
|
4006
|
+
const queued = {
|
|
4007
|
+
id: randomUUID(),
|
|
4008
|
+
clientUserMessageId: String(params.clientUserMessageId ?? randomUUID()),
|
|
4009
|
+
input: params.input
|
|
4010
|
+
};
|
|
4011
|
+
subscribe(id, notify);
|
|
4012
|
+
if (!active.has(id)) await startTurn({
|
|
4013
|
+
threadId: id,
|
|
4014
|
+
input: queued.input,
|
|
4015
|
+
clientUserMessageId: queued.clientUserMessageId,
|
|
4016
|
+
model: params.model,
|
|
4017
|
+
effort: params.effort
|
|
4018
|
+
}, notify);
|
|
4019
|
+
else {
|
|
4020
|
+
queues.set(id, [...queues.get(id) ?? [], queued]);
|
|
4021
|
+
emit(id, "thread/queue/changed", { threadId: id });
|
|
4022
|
+
}
|
|
4023
|
+
return { queuedSubmission: queued };
|
|
4024
|
+
},
|
|
4025
|
+
"thread/queue/delete": (params) => {
|
|
4026
|
+
const list = queues.get(params?.threadId) ?? [];
|
|
4027
|
+
const next = list.filter((entry) => entry.id !== params?.queuedSubmissionId);
|
|
4028
|
+
queues.set(params?.threadId, next);
|
|
4029
|
+
if (next.length !== list.length) emit(params?.threadId, "thread/queue/changed", { threadId: params?.threadId });
|
|
4030
|
+
return { deleted: next.length !== list.length };
|
|
4031
|
+
},
|
|
4032
|
+
"thread/queue/reorder": (params) => {
|
|
4033
|
+
const order = Array.isArray(params?.queuedSubmissionIds) ? params.queuedSubmissionIds : [];
|
|
4034
|
+
const list = queues.get(params?.threadId) ?? [];
|
|
4035
|
+
queues.set(params?.threadId, [...order.map((id) => list.find((entry) => entry.id === id)).filter(Boolean), ...list.filter((entry) => !order.includes(entry.id))]);
|
|
4036
|
+
emit(params?.threadId, "thread/queue/changed", { threadId: params?.threadId });
|
|
4037
|
+
return {};
|
|
4038
|
+
},
|
|
4039
|
+
"thread/queue/start": async (params, { notify }) => {
|
|
4040
|
+
const list = queues.get(params?.threadId) ?? [];
|
|
4041
|
+
const queued = list.find((entry) => params?.queuedSubmissionId == null || entry.id === params.queuedSubmissionId);
|
|
4042
|
+
if (!queued) throw invalidParams("Nothing queued");
|
|
4043
|
+
queues.set(params.threadId, list.filter((entry) => entry !== queued));
|
|
4044
|
+
emit(params.threadId, "thread/queue/changed", { threadId: params.threadId });
|
|
4045
|
+
return startTurn({
|
|
4046
|
+
threadId: params.threadId,
|
|
4047
|
+
input: queued.input,
|
|
4048
|
+
clientUserMessageId: queued.clientUserMessageId
|
|
4049
|
+
}, notify);
|
|
4050
|
+
}
|
|
4051
|
+
};
|
|
4052
|
+
const callOf = (session, callId) => {
|
|
4053
|
+
for (const message of session.deriveMessages().toReversed()) {
|
|
4054
|
+
if (message.role !== "assistant") continue;
|
|
4055
|
+
const block = message.content.find((entry) => entry.type === "tool-call" && entry.id === callId);
|
|
4056
|
+
if (block) return block;
|
|
4057
|
+
}
|
|
4058
|
+
};
|
|
4059
|
+
const commandText = (session, request) => {
|
|
4060
|
+
const call = request.callId ? callOf(session, request.callId) : void 0;
|
|
4061
|
+
const args = call ? parseArguments(call.arguments) : void 0;
|
|
4062
|
+
const shown = typeof args?.command === "string" ? args.command : typeof args?.cmd === "string" ? args.cmd : call ? `${call.name} ${JSON.stringify(args)}` : request.toolName;
|
|
4063
|
+
return String(shown).slice(0, 2e3);
|
|
4064
|
+
};
|
|
4065
|
+
/**
|
|
4066
|
+
* DSH approval answerer: while a phone drives a turn, it answers that turn's approvals.
|
|
4067
|
+
* Without a phone (or if it drops) the request goes on to DSH's own prompt.
|
|
4068
|
+
*/
|
|
4069
|
+
/** The phone driving this agent's running turn, when there is one. */
|
|
4070
|
+
const phoneFor = (request) => {
|
|
4071
|
+
const session = request.agent?.session;
|
|
4072
|
+
const running = session ? active.get(session.id) : void 0;
|
|
4073
|
+
const phone = session ? [...subscribers.get(session.id) ?? []].find((notify) => typeof notify.ask === "function") : void 0;
|
|
4074
|
+
return running && phone ? {
|
|
4075
|
+
session,
|
|
4076
|
+
running,
|
|
4077
|
+
phone
|
|
4078
|
+
} : void 0;
|
|
4079
|
+
};
|
|
4080
|
+
/**
|
|
4081
|
+
* Ask whichever phone drives the turn. A phone that drops (app switched away, network) gets the
|
|
4082
|
+
* same request again when it reopens the thread within PHONE_RETURN_MS; otherwise DSH decides.
|
|
4083
|
+
*/
|
|
4084
|
+
const askPhone = async (request, method, params) => {
|
|
4085
|
+
const sessionId = request.agent.session.id;
|
|
4086
|
+
for (;;) {
|
|
4087
|
+
const phone = phoneFor(request)?.phone;
|
|
4088
|
+
if (!phone) throw new RpcError(-32800, "No phone");
|
|
4089
|
+
try {
|
|
4090
|
+
return await phone.ask(method, params, request.signal);
|
|
4091
|
+
} catch (error) {
|
|
4092
|
+
if (request.signal?.aborted || !active.has(sessionId) || phoneFor(request)?.phone === phone) throw error;
|
|
4093
|
+
trace({
|
|
4094
|
+
method: `wait:${method}`,
|
|
4095
|
+
session: sessionId
|
|
4096
|
+
});
|
|
4097
|
+
if (!phoneFor(request) && !await phoneReturns(sessionId, phoneReturnMs, request.signal)) throw error;
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
};
|
|
4101
|
+
/** DSH's ask-user tool: show the questions on the phone that drives the turn, else leave them to DSH. */
|
|
4102
|
+
const onQuestion = (request, next) => {
|
|
4103
|
+
const found = phoneFor(request);
|
|
4104
|
+
trace({
|
|
4105
|
+
method: "hook:user-questions",
|
|
4106
|
+
phone: Boolean(found),
|
|
4107
|
+
session: request.agent?.session?.id ?? null
|
|
4108
|
+
});
|
|
4109
|
+
if (!found) return next();
|
|
4110
|
+
const { session, running } = found;
|
|
4111
|
+
const labels = new Map(request.questions.map((question) => [question.id, new Set((question.options ?? []).map((option) => option.label))]));
|
|
4112
|
+
return askPhone(request, "item/tool/requestUserInput", {
|
|
4113
|
+
threadId: session.id,
|
|
4114
|
+
turnId: running.turn.id,
|
|
4115
|
+
itemId: request.wait?.callId ?? randomUUID(),
|
|
4116
|
+
questions: request.questions.map((question) => ({
|
|
4117
|
+
id: question.id,
|
|
4118
|
+
header: question.header ?? "",
|
|
4119
|
+
question: question.detail ? `${question.question}\n\n${question.detail}` : question.question,
|
|
4120
|
+
isOther: true,
|
|
4121
|
+
isSecret: false,
|
|
4122
|
+
options: question.options?.length ? question.options.map((option) => ({
|
|
4123
|
+
label: option.label,
|
|
4124
|
+
description: option.description ?? ""
|
|
4125
|
+
})) : null
|
|
4126
|
+
}))
|
|
4127
|
+
}).then((answer) => ({ answers: request.questions.map((question) => {
|
|
4128
|
+
const given = Array.isArray(answer?.answers?.[question.id]?.answers) ? answer.answers[question.id].answers.map(String) : [];
|
|
4129
|
+
const known = labels.get(question.id);
|
|
4130
|
+
const selected = given.filter((value) => known.has(value));
|
|
4131
|
+
const custom = given.filter((value) => !known.has(value)).join("\n");
|
|
4132
|
+
return {
|
|
4133
|
+
id: question.id,
|
|
4134
|
+
selected,
|
|
4135
|
+
...custom === "" ? {} : { custom }
|
|
4136
|
+
};
|
|
4137
|
+
}) }), () => next());
|
|
4138
|
+
};
|
|
4139
|
+
const onApproval = (request, next) => {
|
|
4140
|
+
const found = phoneFor(request);
|
|
4141
|
+
trace({
|
|
4142
|
+
method: "hook:approval",
|
|
4143
|
+
phone: Boolean(found),
|
|
4144
|
+
session: request.agent?.session?.id ?? null
|
|
4145
|
+
});
|
|
4146
|
+
if (!found) return next();
|
|
4147
|
+
const { session, running } = found;
|
|
4148
|
+
return askPhone(request, "item/commandExecution/requestApproval", {
|
|
4149
|
+
threadId: session.id,
|
|
4150
|
+
turnId: running.turn.id,
|
|
4151
|
+
itemId: request.callId ?? randomUUID(),
|
|
4152
|
+
startedAtMs: Date.now(),
|
|
4153
|
+
command: commandText(session, request),
|
|
4154
|
+
cwd: session.meta?.cwd ?? null,
|
|
4155
|
+
reason: request.displayReason?.en ?? request.reason ?? null,
|
|
4156
|
+
commandActions: [],
|
|
4157
|
+
availableDecisions: [
|
|
4158
|
+
"accept",
|
|
4159
|
+
"decline",
|
|
4160
|
+
"cancel"
|
|
4161
|
+
]
|
|
4162
|
+
}).then((answer) => {
|
|
4163
|
+
const decision = typeof answer?.decision === "string" ? answer.decision : "";
|
|
4164
|
+
return decision === "accept" || decision === "acceptForSession" ? "allowed-once" : decision === "cancel" ? "cancelled" : "rejected";
|
|
4165
|
+
}, () => next());
|
|
4166
|
+
};
|
|
4167
|
+
/** Forward committed assistant and tool events of a phone-started turn as items. */
|
|
4168
|
+
const onSessionEvent = (session, event) => {
|
|
4169
|
+
const running = active.get(session.id);
|
|
4170
|
+
if (!running || !subscribers.has(session.id)) return;
|
|
4171
|
+
if (event.type === "turn/end") {
|
|
4172
|
+
running.ended = event.data?.reason;
|
|
4173
|
+
return;
|
|
4174
|
+
}
|
|
4175
|
+
if (event.type !== "assistant/message" && event.type !== "tool/result") return;
|
|
4176
|
+
const message = session.deriveMessages().at(-1);
|
|
4177
|
+
if (message?.role === "tool") {
|
|
4178
|
+
const item = running.turn.items.find((entry) => entry.type === "dynamicToolCall" && entry.id === message.toolCallId);
|
|
4179
|
+
if (item) {
|
|
4180
|
+
item.contentItems = [{
|
|
4181
|
+
type: "inputText",
|
|
4182
|
+
text: textOf(message.content)
|
|
4183
|
+
}];
|
|
4184
|
+
item.status = message.isError ? "failed" : "completed";
|
|
4185
|
+
item.success = !message.isError;
|
|
4186
|
+
emit(session.id, "item/completed", {
|
|
4187
|
+
threadId: session.id,
|
|
4188
|
+
turnId: running.turn.id,
|
|
4189
|
+
item,
|
|
4190
|
+
completedAtMs: Date.now()
|
|
4191
|
+
});
|
|
4192
|
+
}
|
|
4193
|
+
return;
|
|
4194
|
+
}
|
|
4195
|
+
if (!message || message.role !== "assistant") return;
|
|
4196
|
+
const items = projectTurns([{
|
|
4197
|
+
role: "user",
|
|
4198
|
+
id: "x",
|
|
4199
|
+
content: []
|
|
4200
|
+
}, message]).flatMap((turn) => turn.items.slice(1));
|
|
4201
|
+
for (const item of items) {
|
|
4202
|
+
running.turn.items.push(item);
|
|
4203
|
+
emit(session.id, "item/started", {
|
|
4204
|
+
threadId: session.id,
|
|
4205
|
+
turnId: running.turn.id,
|
|
4206
|
+
item,
|
|
4207
|
+
startedAtMs: Date.now()
|
|
4208
|
+
});
|
|
4209
|
+
emit(session.id, "item/completed", {
|
|
4210
|
+
threadId: session.id,
|
|
4211
|
+
turnId: running.turn.id,
|
|
4212
|
+
item,
|
|
4213
|
+
completedAtMs: Date.now()
|
|
4214
|
+
});
|
|
4215
|
+
}
|
|
4216
|
+
};
|
|
4217
|
+
return {
|
|
4218
|
+
methods,
|
|
4219
|
+
onSessionEvent,
|
|
4220
|
+
onApproval,
|
|
4221
|
+
onQuestion,
|
|
4222
|
+
forget: (notify) => {
|
|
4223
|
+
for (const set of subscribers.values()) set.delete(notify);
|
|
4224
|
+
}
|
|
4225
|
+
};
|
|
4226
|
+
}
|
|
4227
|
+
//#endregion
|
|
2680
4228
|
//#region src/account-vault.js
|
|
2681
4229
|
const VERSION = 1;
|
|
2682
4230
|
const DEFAULT_LABEL = "Account 1";
|
|
@@ -3561,6 +5109,7 @@ function normalizeTransportEvent(event, signal) {
|
|
|
3561
5109
|
const FAST_SERVICE_TIER = "priority";
|
|
3562
5110
|
const ULTRAFAST_SERVICE_TIER = "ultrafast";
|
|
3563
5111
|
/** Apply the chosen detail to user and tool-result images after the provider assembles the request. */
|
|
5112
|
+
const withoutUserAgent = (headers) => headers && typeof headers === "object" ? Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== "user-agent")) : headers;
|
|
3564
5113
|
function withInputImageDetail(payload, detail) {
|
|
3565
5114
|
if (!Array.isArray(payload.input)) return payload;
|
|
3566
5115
|
let changed = false;
|
|
@@ -3639,6 +5188,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
|
|
|
3639
5188
|
const { temperature: _temperature, onPayload, ...rest } = options;
|
|
3640
5189
|
return {
|
|
3641
5190
|
...rest,
|
|
5191
|
+
...rest.headers === void 0 ? {} : { headers: withoutUserAgent(rest.headers) },
|
|
3642
5192
|
...textVerbosity === void 0 ? {} : { textVerbosity },
|
|
3643
5193
|
...serviceTier === void 0 ? {} : { serviceTier },
|
|
3644
5194
|
async onPayload(payload, requestModel) {
|
|
@@ -3700,6 +5250,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
|
|
|
3700
5250
|
const { temperature: _temperature, onPayload, ...rest } = options;
|
|
3701
5251
|
return {
|
|
3702
5252
|
...rest,
|
|
5253
|
+
...rest.headers === void 0 ? {} : { headers: withoutUserAgent(rest.headers) },
|
|
3703
5254
|
async onPayload(payload, requestModel) {
|
|
3704
5255
|
const { temperature: _dropped, service_tier: _tier, text: _text, ...accepted } = await onPayload?.(payload, requestModel) ?? payload;
|
|
3705
5256
|
return {
|
|
@@ -3788,9 +5339,9 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
|
|
|
3788
5339
|
});
|
|
3789
5340
|
}
|
|
3790
5341
|
Object.freeze([
|
|
3791
|
-
"0.82.1",
|
|
3792
5342
|
"0.85.1",
|
|
3793
|
-
"0.87.1"
|
|
5343
|
+
"0.87.1",
|
|
5344
|
+
"1.1.0"
|
|
3794
5345
|
]);
|
|
3795
5346
|
//#endregion
|
|
3796
5347
|
//#region src/model-catalog.js
|
|
@@ -6061,7 +7612,7 @@ const publicError = (code, message) => ({
|
|
|
6061
7612
|
details: { issues: [] }
|
|
6062
7613
|
}
|
|
6063
7614
|
});
|
|
6064
|
-
function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, defaultModel, runtimeManagement, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged, onCleanupFailure }) {
|
|
7615
|
+
function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, defaultModel, runtimeManagement, remoteControl, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged, onCleanupFailure }) {
|
|
6065
7616
|
return async (endpoint, payload, signal) => {
|
|
6066
7617
|
if (endpoint === "default-model/status" || endpoint === "default-model/select") try {
|
|
6067
7618
|
signal.throwIfAborted();
|
|
@@ -6076,6 +7627,23 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
|
|
|
6076
7627
|
if (error?.code === "unavailable") return publicError("unavailable", endpoint === "default-model/status" ? "The default model is unavailable" : "Could not save the default model");
|
|
6077
7628
|
return publicError("internal", endpoint === "default-model/status" ? "Could not read the default model" : "Could not save the default model");
|
|
6078
7629
|
}
|
|
7630
|
+
if ([
|
|
7631
|
+
"remote/status",
|
|
7632
|
+
"remote/enable",
|
|
7633
|
+
"remote/disable",
|
|
7634
|
+
"remote/pair"
|
|
7635
|
+
].includes(endpoint)) try {
|
|
7636
|
+
signal.throwIfAborted();
|
|
7637
|
+
if (!remoteControl) return publicError("unavailable", "Remote Control is unavailable");
|
|
7638
|
+
const action = endpoint.split("/")[1];
|
|
7639
|
+
return {
|
|
7640
|
+
ok: true,
|
|
7641
|
+
value: action === "status" ? remoteControl.status() : action === "enable" ? await remoteControl.enable() : action === "disable" ? await remoteControl.disable() : await remoteControl.pair(signal)
|
|
7642
|
+
};
|
|
7643
|
+
} catch (error) {
|
|
7644
|
+
if (signal.aborted) throw error;
|
|
7645
|
+
return publicError(typeof error?.code === "string" ? error.code : "operation-error", "Remote Control request failed");
|
|
7646
|
+
}
|
|
6079
7647
|
if (endpoint === "storage/status" || endpoint === "storage/clear-forecast") try {
|
|
6080
7648
|
signal.throwIfAborted();
|
|
6081
7649
|
if (endpoint === "storage/clear-forecast") await usageReader.clear();
|
|
@@ -6273,7 +7841,7 @@ const PROVIDER = "openai-codex";
|
|
|
6273
7841
|
const OAUTH_EXPIRY_SKEW_MS = 6e4;
|
|
6274
7842
|
const CREDENTIAL_REF = dshCredentials.credentialRef("OPENAI_CODEX_SUBSCRIPTION_OAUTH");
|
|
6275
7843
|
const LEGACY_CREDENTIAL_REF = dshCredentials.credentialRef("WSL043_OPENAI_CODEX_OAUTH");
|
|
6276
|
-
const ACCOUNT_VAULT_KEY =
|
|
7844
|
+
const ACCOUNT_VAULT_KEY = dshCredentials.credentialKey("codex-subscription", "accounts");
|
|
6277
7845
|
const WEB_ENTRY_ID = "web";
|
|
6278
7846
|
const DSH_SEARCH_PROVIDER_FALLBACK = "deepseek-official";
|
|
6279
7847
|
const MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
@@ -6328,17 +7896,17 @@ const settingsFields = {
|
|
|
6328
7896
|
};
|
|
6329
7897
|
const Config = z.object(Object.fromEntries(Object.entries(settingsFields).map(([key, field]) => [key, field.extra("volatile", true)])));
|
|
6330
7898
|
function apply(ctx, config = {}) {
|
|
6331
|
-
const settings = createSettingsAdapter(ctx,
|
|
7899
|
+
const settings = createSettingsAdapter(ctx, config);
|
|
6332
7900
|
const searchProvider = createSearchProviderSwitcher(ctx.loader);
|
|
6333
7901
|
const network = createCodexNetworkTransport();
|
|
6334
7902
|
const diagnosticOperations = createDiagnosticOperations();
|
|
6335
7903
|
ctx.effect(() => ctx.on?.("tools/result", (exec, result) => diagnosticOperations.tool(exec, result)), "codex-subscription: operation evidence");
|
|
6336
7904
|
const originalImages = new OriginalImageStore();
|
|
6337
|
-
const accountVault =
|
|
7905
|
+
const accountVault = new DshOAuthAccountVault(ctx.credentials, {
|
|
6338
7906
|
key: ACCOUNT_VAULT_KEY,
|
|
6339
7907
|
legacyRef: CREDENTIAL_REF,
|
|
6340
7908
|
legacyRefs: [LEGACY_CREDENTIAL_REF]
|
|
6341
|
-
})
|
|
7909
|
+
});
|
|
6342
7910
|
const store = new DshOAuthCredentialStore(ctx.credentials, CREDENTIAL_REF, [LEGACY_CREDENTIAL_REF], {
|
|
6343
7911
|
expirySkewMs: OAUTH_EXPIRY_SKEW_MS,
|
|
6344
7912
|
vault: accountVault
|
|
@@ -6404,6 +7972,7 @@ function apply(ctx, config = {}) {
|
|
|
6404
7972
|
compactionMode: settings.get().compactionMode ?? "dsh",
|
|
6405
7973
|
connectionMode: settings.get().connectionMode ?? "sse",
|
|
6406
7974
|
reviewModel: settings.get().reviewModel ?? "session",
|
|
7975
|
+
remoteControl: settings.get().remoteControl ?? "off",
|
|
6407
7976
|
subagentBackend: settings.get().subagentBackend ?? "dsh",
|
|
6408
7977
|
subagentBackendAvailable: subagentBackend !== void 0,
|
|
6409
7978
|
subagentRuntimeInstalled: inspectSubagentRuntime().installed,
|
|
@@ -6597,6 +8166,65 @@ function apply(ctx, config = {}) {
|
|
|
6597
8166
|
}
|
|
6598
8167
|
});
|
|
6599
8168
|
const coordinator = new CodexLoginCoordinator(auth);
|
|
8169
|
+
const remoteBridge = createDshRemoteControl({
|
|
8170
|
+
controller: () => ctx.get?.("sessionController"),
|
|
8171
|
+
agents: () => ctx.get?.("agents"),
|
|
8172
|
+
permissions: () => ctx.get?.("permissionPresets"),
|
|
8173
|
+
workspaces: () => ctx.get?.("workspaceRegistry"),
|
|
8174
|
+
trace: (entry) => {
|
|
8175
|
+
try {
|
|
8176
|
+
remoteControl.host.note(entry);
|
|
8177
|
+
} catch {}
|
|
8178
|
+
},
|
|
8179
|
+
userAgent: USER_AGENT
|
|
8180
|
+
});
|
|
8181
|
+
const remoteControl = createRemoteControl({
|
|
8182
|
+
credentials: (signal) => readSubscriptionCredentials(resolveAuth, (options) => store.read(PROVIDER, {
|
|
8183
|
+
...options,
|
|
8184
|
+
signal
|
|
8185
|
+
}), signal),
|
|
8186
|
+
WebSocket,
|
|
8187
|
+
fetch: (input, init) => network.fetch("remote", input, init),
|
|
8188
|
+
stateFile: dshHomePath("state", "codex-subscription", "remote-control.json"),
|
|
8189
|
+
hostName: `DSH (${hostname()})`,
|
|
8190
|
+
userAgent: USER_AGENT,
|
|
8191
|
+
methods: remoteBridge.methods,
|
|
8192
|
+
onClose: (_client, notify) => remoteBridge.forget(notify)
|
|
8193
|
+
});
|
|
8194
|
+
ctx.on("session/event", (session, event) => {
|
|
8195
|
+
try {
|
|
8196
|
+
remoteBridge.onSessionEvent(session, event);
|
|
8197
|
+
} catch {}
|
|
8198
|
+
});
|
|
8199
|
+
ctx.on("approval/request", (request, next) => {
|
|
8200
|
+
try {
|
|
8201
|
+
return remoteBridge.onApproval(request, next);
|
|
8202
|
+
} catch {
|
|
8203
|
+
return next();
|
|
8204
|
+
}
|
|
8205
|
+
}, true);
|
|
8206
|
+
ctx.on("user-questions/request", (request, next) => {
|
|
8207
|
+
try {
|
|
8208
|
+
return remoteBridge.onQuestion(request, next);
|
|
8209
|
+
} catch {
|
|
8210
|
+
return next();
|
|
8211
|
+
}
|
|
8212
|
+
}, true);
|
|
8213
|
+
ctx.effect(() => {
|
|
8214
|
+
let wanted = false;
|
|
8215
|
+
const follow = (value) => {
|
|
8216
|
+
const next = value.remoteControl === "on";
|
|
8217
|
+
if (next === wanted) return;
|
|
8218
|
+
wanted = next;
|
|
8219
|
+
(next ? remoteControl.enable() : remoteControl.disable()).catch((error) => ctx.logger?.debug?.("Remote Control: %s", error?.code ?? error?.message));
|
|
8220
|
+
};
|
|
8221
|
+
follow(settings.get());
|
|
8222
|
+
const unwatch = settings.watch(follow);
|
|
8223
|
+
return () => {
|
|
8224
|
+
unwatch();
|
|
8225
|
+
remoteControl.disable().catch(() => {});
|
|
8226
|
+
};
|
|
8227
|
+
}, "codex-subscription: Remote Control host");
|
|
6600
8228
|
const usageReader = createQuotaForecastReader({
|
|
6601
8229
|
reader: createCodexUsageReader({
|
|
6602
8230
|
getAuth: resolveAuth,
|
|
@@ -6682,6 +8310,7 @@ function apply(ctx, config = {}) {
|
|
|
6682
8310
|
preferences,
|
|
6683
8311
|
defaultModel,
|
|
6684
8312
|
runtimeManagement,
|
|
8313
|
+
remoteControl,
|
|
6685
8314
|
onAccountChanged: quotaRetryHandler.notifyAccountChanged,
|
|
6686
8315
|
diagnosticsReader: () => createSubscriptionDiagnostics({
|
|
6687
8316
|
auth,
|