@eddyskywalker/dsh-chatgpt-subscription 0.1.5 → 0.1.7

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +32 -15
  3. package/lib/client.js +34 -249
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +392 -16
  6. package/lib/types/client/CodexSubscriptionSection.d.ts +3 -1
  7. package/lib/types/client/CodexSubscriptionSection.d.ts.map +1 -1
  8. package/lib/types/client/index.d.ts.map +1 -1
  9. package/lib/types/client/locales.d.ts +17 -5
  10. package/lib/types/client/locales.d.ts.map +1 -1
  11. package/lib/types/client/styles.d.ts.map +1 -1
  12. package/lib/types/host/bash-prompt-scheduling-compat.d.ts +51 -0
  13. package/lib/types/host/bash-prompt-scheduling-compat.d.ts.map +1 -0
  14. package/lib/types/host/oauth-service.d.ts.map +1 -1
  15. package/lib/types/host/platform-token-store.d.ts +3 -0
  16. package/lib/types/host/platform-token-store.d.ts.map +1 -0
  17. package/lib/types/host/subagent-report-scheduling-compat.d.ts +21 -0
  18. package/lib/types/host/subagent-report-scheduling-compat.d.ts.map +1 -0
  19. package/lib/types/host/token-store-linux.d.ts +20 -0
  20. package/lib/types/host/token-store-linux.d.ts.map +1 -0
  21. package/lib/types/host/token-store-windows.d.ts +4 -0
  22. package/lib/types/host/token-store-windows.d.ts.map +1 -1
  23. package/lib/types/host/token-store.d.ts +6 -0
  24. package/lib/types/host/token-store.d.ts.map +1 -1
  25. package/lib/types/index.d.ts +3 -0
  26. package/lib/types/index.d.ts.map +1 -1
  27. package/lib/types/shared/contracts.d.ts +7 -4
  28. package/lib/types/shared/contracts.d.ts.map +1 -1
  29. package/package.json +3 -1
  30. package/lib/types/client/process-folding.d.ts +0 -5
  31. package/lib/types/client/process-folding.d.ts.map +0 -1
package/lib/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { CallId, LlmAdapter, LlmError, ProviderRequestId, ReasoningEffortId, attributionHeaders, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
2
2
  import { createHash, randomBytes, randomUUID } from "node:crypto";
3
3
  import http from "node:http";
4
+ import { constants } from "node:fs";
5
+ import { chmod, lstat, mkdir, open, rename, stat, unlink } from "node:fs/promises";
4
6
  import { homedir } from "node:os";
5
7
  import { dirname, join } from "node:path";
6
8
  import { spawn } from "node:child_process";
@@ -122,6 +124,260 @@ var CodexChatGptAdapter = class extends LlmAdapter {
122
124
  }
123
125
  };
124
126
  //#endregion
127
+ //#region src/host/subagent-report-scheduling-compat.ts
128
+ /**
129
+ * DSH_COMPAT_REMOVE(subagent-report-settlement-dedup)
130
+ *
131
+ * Temporary compatibility shim for DSH 0.1.0-rc.6. A continuable child is told
132
+ * to report its result before finishing, while DSH also unconditionally sends
133
+ * the same closing output in a `subagent-settled` notice. The report is often
134
+ * still queued when the settlement reaches the parent, so the parent sees the
135
+ * result once and the equivalent report remains as duplicate next-turn work.
136
+ *
137
+ * Remove this module, its installation in `src/index.ts`, and its focused test
138
+ * once upstream coalesces an equivalent final report with settlement delivery.
139
+ */
140
+ const DSH_SUBAGENT_REPORT_DEDUP_COMPAT_MARKER = "__dshChatgptSubscriptionSubagentReportDedupCompatV1";
141
+ function sourceOf(message) {
142
+ return message.source;
143
+ }
144
+ function isTextBlock(value, text) {
145
+ return typeof value === "object" && value !== null && value.type === "text" && value.text === text;
146
+ }
147
+ function sameValue(left, right) {
148
+ if (left === right) return true;
149
+ if (Array.isArray(left) || Array.isArray(right)) return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => sameValue(value, right[index]));
150
+ if (typeof left !== "object" || left === null || typeof right !== "object" || right === null) return false;
151
+ const leftRecord = left;
152
+ const rightRecord = right;
153
+ const leftKeys = Object.keys(leftRecord).sort();
154
+ const rightKeys = Object.keys(rightRecord).sort();
155
+ return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => key === rightKeys[index] && sameValue(leftRecord[key], rightRecord[key]));
156
+ }
157
+ function duplicatePendingReports(agent, settlement) {
158
+ const settlementSource = sourceOf(settlement);
159
+ if (settlementSource.kind !== "subagent-settled" || settlementSource.senderSessionId === void 0) return [];
160
+ if (settlement.content.length < 2 || !isTextBlock(settlement.content[1], "Its closing message:")) return [];
161
+ const closingContent = settlement.content.slice(2);
162
+ return [...agent.inbox.nextStep, ...agent.inbox.nextTurn].filter((pending) => {
163
+ const pendingSource = sourceOf(pending);
164
+ return pendingSource.kind === "subagent-report" && pendingSource.senderSessionId === settlementSource.senderSessionId && sameValue(pending.content.slice(1), closingContent);
165
+ });
166
+ }
167
+ function errorMessage(error) {
168
+ return error instanceof Error ? error.message : String(error);
169
+ }
170
+ /**
171
+ * Discard only an exact, same-child report duplicate immediately before DSH
172
+ * delivers the corresponding settlement notice. Partial reports, reports with
173
+ * different content, and all unrelated inbox work remain untouched.
174
+ */
175
+ function installSubagentReportDedupCompat(ctx) {
176
+ const patches = /* @__PURE__ */ new Map();
177
+ const patch = (agent) => {
178
+ if (patches.has(agent)) return;
179
+ const shared = agent.followup[DSH_SUBAGENT_REPORT_DEDUP_COMPAT_MARKER];
180
+ if (shared?.wrappers.followup === agent.followup && shared.wrappers.steer === agent.steer && shared.wrappers.inject === agent.inject) {
181
+ shared.owners += 1;
182
+ patches.set(agent, shared);
183
+ return;
184
+ }
185
+ const originals = {
186
+ followup: agent.followup,
187
+ steer: agent.steer,
188
+ inject: agent.inject
189
+ };
190
+ let record;
191
+ const deliver = (name, message) => {
192
+ for (const duplicate of duplicatePendingReports(agent, message)) try {
193
+ agent.inbox.remove(duplicate.id);
194
+ } catch (error) {
195
+ ctx.logger.warn("[dsh-chatgpt-subscription] Could not discard a duplicate DSH subagent report: " + errorMessage(error));
196
+ }
197
+ originals[name].call(agent, message);
198
+ };
199
+ const wrappers = {
200
+ followup(message) {
201
+ deliver("followup", message);
202
+ },
203
+ steer(message) {
204
+ deliver("steer", message);
205
+ },
206
+ inject(message) {
207
+ deliver("inject", message);
208
+ }
209
+ };
210
+ record = {
211
+ originals,
212
+ wrappers,
213
+ owners: 1
214
+ };
215
+ for (const wrapper of Object.values(wrappers)) Object.defineProperty(wrapper, DSH_SUBAGENT_REPORT_DEDUP_COMPAT_MARKER, { value: record });
216
+ try {
217
+ agent.followup = wrappers.followup;
218
+ agent.steer = wrappers.steer;
219
+ agent.inject = wrappers.inject;
220
+ patches.set(agent, record);
221
+ } catch (error) {
222
+ record.owners = 0;
223
+ for (const name of [
224
+ "followup",
225
+ "steer",
226
+ "inject"
227
+ ]) if (agent[name] === wrappers[name]) try {
228
+ agent[name] = originals[name];
229
+ } catch {}
230
+ ctx.logger.warn("[dsh-chatgpt-subscription] Could not install temporary DSH subagent dedup compatibility: " + errorMessage(error));
231
+ }
232
+ };
233
+ const unpatch = (agent) => {
234
+ const record = patches.get(agent);
235
+ if (!record) return;
236
+ patches.delete(agent);
237
+ record.owners -= 1;
238
+ if (record.owners > 0) return;
239
+ for (const name of [
240
+ "followup",
241
+ "steer",
242
+ "inject"
243
+ ]) {
244
+ if (agent[name] !== record.wrappers[name]) continue;
245
+ try {
246
+ agent[name] = record.originals[name];
247
+ } catch (error) {
248
+ ctx.logger.warn("[dsh-chatgpt-subscription] Could not remove temporary DSH subagent dedup compatibility: " + errorMessage(error));
249
+ }
250
+ }
251
+ };
252
+ for (const agent of ctx.agents.list()) patch(agent);
253
+ const disposeCreated = ctx.on("agent/created", ({ agent }) => patch(agent));
254
+ const disposeDisposed = ctx.on("agent/disposed", ({ agent }) => unpatch(agent));
255
+ return () => {
256
+ disposeDisposed();
257
+ disposeCreated();
258
+ for (const agent of [...patches.keys()]) unpatch(agent);
259
+ };
260
+ }
261
+ //#endregion
262
+ //#region src/host/bash-prompt-scheduling-compat.ts
263
+ /**
264
+ * DSH_COMPAT_REMOVE(persistent-bash-prompt-mismatch)
265
+ *
266
+ * Temporary compatibility shim for DSH 0.1.0-rc.6. The persistent bash tool
267
+ * (`@deepseek-ai/dsh-tool-bash-persistent`) configures the bash shell PS1 to
268
+ * `__DSH_PERSISTENT_BASH_PROMPT__ `, while the underlying PTY backend
269
+ * (`@deepseek-ai/dsh-terminal-bash`) only checks for a hardcoded `dsh> `
270
+ * prompt with a 6-character truncation limit.
271
+ *
272
+ * When the prompt does not match, `promptTextSeen` remains false and the PTY
273
+ * session falls back to the 3.5s idle silence timeout (`idleSilenceMs: 3000ms`
274
+ * + `handoffGraceMs: 500ms`) on every command execution.
275
+ *
276
+ * This compatibility module patches the PTY session `onData` handler so that
277
+ * both `dsh> ` and `__DSH_PERSISTENT_BASH_PROMPT__ ` (and its trimmed variants)
278
+ * satisfy `promptTextSeen`, restoring instant 50ms readiness settling.
279
+ *
280
+ * Remove this module, its installation in `src/index.ts`, and its focused test
281
+ * once upstream aligns `CONTROLLED_PROMPT` with persistent bash tools.
282
+ */
283
+ const DSH_BASH_PROMPT_COMPAT_MARKER = "__dshChatgptSubscriptionBashPromptCompatV1";
284
+ const KNOWN_CONTROLLED_PROMPTS = [
285
+ "dsh> ",
286
+ "dsh>",
287
+ "__DSH_PERSISTENT_BASH_PROMPT__ ",
288
+ "__DSH_PERSISTENT_BASH_PROMPT__"
289
+ ];
290
+ const MAX_PROMPT_BUFFER_LENGTH = 64;
291
+ function isKnownControlledPrompt(tail) {
292
+ if (!tail) return false;
293
+ const cleaned = tail.replaceAll(/[\x00-\x1f\x7f]/g, "").trim();
294
+ for (const known of KNOWN_CONTROLLED_PROMPTS) if (cleaned === known.replaceAll(/[\x00-\x1f\x7f]/g, "").trim()) return true;
295
+ return false;
296
+ }
297
+ function patchSessionOnData(target) {
298
+ const obj = target.prototype ?? target;
299
+ if (!obj || typeof obj.onData !== "function") return () => {};
300
+ const existing = obj.onData[DSH_BASH_PROMPT_COMPAT_MARKER];
301
+ if (existing) {
302
+ existing.owners += 1;
303
+ return () => {
304
+ existing.owners -= 1;
305
+ if (existing.owners <= 0 && obj.onData["__dshChatgptSubscriptionBashPromptCompatV1"] === existing) obj.onData = existing.originalOnData;
306
+ };
307
+ }
308
+ const originalOnData = obj.onData;
309
+ const record = {
310
+ originalOnData,
311
+ owners: 1
312
+ };
313
+ function wrapper(data) {
314
+ const sanitized = this.sanitizer.push(data);
315
+ this.appendOutput(sanitized.text);
316
+ if (sanitized.prompt) {
317
+ this.promptSeen = true;
318
+ this.promptTail = "";
319
+ this.lastOutputAt = Date.now();
320
+ }
321
+ if (this.promptSeen && sanitized.promptTail !== void 0) {
322
+ const remaining = Math.max(0, MAX_PROMPT_BUFFER_LENGTH - this.promptTail.length);
323
+ this.promptTail += sanitized.promptTail.slice(0, remaining);
324
+ if (sanitized.promptTail.length > remaining) this.promptTail = `${this.promptTail}\0`;
325
+ this.promptTextSeen = isKnownControlledPrompt(this.promptTail);
326
+ }
327
+ }
328
+ Object.defineProperty(wrapper, DSH_BASH_PROMPT_COMPAT_MARKER, { value: record });
329
+ obj.onData = wrapper;
330
+ return () => {
331
+ record.owners -= 1;
332
+ if (record.owners <= 0 && obj.onData === wrapper) obj.onData = originalOnData;
333
+ };
334
+ }
335
+ /**
336
+ * Installs the persistent bash prompt compatibility patch onto registered and
337
+ * future PTY backends.
338
+ */
339
+ function installBashPromptCompat(ctx) {
340
+ const disposers = [];
341
+ const patchBackend = (backend) => {
342
+ if (!backend) return;
343
+ if (typeof backend.createSession === "function") {
344
+ const originalCreate = backend.createSession;
345
+ backend.createSession = function(terminal, config) {
346
+ const session = originalCreate.call(this, terminal, config);
347
+ if (session) patchSessionOnData(session);
348
+ return session;
349
+ };
350
+ disposers.push(() => {
351
+ if (backend.createSession) backend.createSession = originalCreate;
352
+ });
353
+ }
354
+ };
355
+ const terminals = ctx.terminals;
356
+ if (terminals?.backends) {
357
+ for (const backend of terminals.backends.values()) patchBackend(backend);
358
+ if (typeof terminals.registerBackend === "function") {
359
+ const originalRegister = terminals.registerBackend;
360
+ terminals.registerBackend = function(backend) {
361
+ patchBackend(backend);
362
+ return originalRegister.call(this, backend);
363
+ };
364
+ disposers.push(() => {
365
+ if (terminals.registerBackend) terminals.registerBackend = originalRegister;
366
+ });
367
+ }
368
+ }
369
+ return () => {
370
+ while (disposers.length > 0) {
371
+ const dispose = disposers.pop();
372
+ try {
373
+ dispose?.();
374
+ } catch (error) {
375
+ ctx.logger.warn("[dsh-chatgpt-subscription] Could not remove bash prompt compatibility: " + (error instanceof Error ? error.message : String(error)));
376
+ }
377
+ }
378
+ };
379
+ }
380
+ //#endregion
125
381
  //#region src/compat.ts
126
382
  /**
127
383
  * Compatibility constants for the ChatGPT-backed Codex flow. The backend and
@@ -293,13 +549,16 @@ var OAuthService = class {
293
549
  return this.statusFromCredentials(credentials);
294
550
  } catch {
295
551
  return {
296
- ...this.statusFromCredentials(null),
552
+ ...this.statusFromCredentials(null, false),
297
553
  error: publicError(new OAuthServiceError("storage-failed", "Secure credential storage could not be read."))
298
554
  };
299
555
  }
300
556
  }
301
557
  async startLogin() {
302
558
  this.assertAvailable();
559
+ await this.store.load().catch(() => {
560
+ throw new OAuthServiceError("storage-failed", "Secure credential storage is unavailable. Fix its ownership or permissions before signing in.");
561
+ });
303
562
  if (this.activeLogin !== null) throw new OAuthServiceError("login-active", "A ChatGPT sign-in is already in progress.");
304
563
  this.lastLoginError = void 0;
305
564
  const loginId = this.random(24).toString("base64url");
@@ -452,14 +711,14 @@ var OAuthService = class {
452
711
  if (stored === null) throw new OAuthServiceError("not-authenticated", "Sign in with ChatGPT first.");
453
712
  return stored;
454
713
  }
455
- statusFromCredentials(credentials) {
714
+ statusFromCredentials(credentials, storageAvailable = true) {
456
715
  const active = this.activeLogin;
457
716
  if (credentials === null) return {
458
717
  authenticated: false,
459
718
  account: null,
460
719
  storage: {
461
- kind: "windows-dpapi",
462
- encrypted: true
720
+ ...this.store.storage,
721
+ available: storageAvailable
463
722
  },
464
723
  login: {
465
724
  active: active !== null,
@@ -478,8 +737,8 @@ var OAuthService = class {
478
737
  tokenExpiresAt: Math.floor(credentials.expiresAt / 1e3)
479
738
  },
480
739
  storage: {
481
- kind: "windows-dpapi",
482
- encrypted: true
740
+ ...this.store.storage,
741
+ available: storageAvailable
483
742
  },
484
743
  login: {
485
744
  active: active !== null,
@@ -718,7 +977,7 @@ async function buildResponsesPayload(options, attachments, localRawImages = {})
718
977
  }
719
978
  function runCodeInstruction(tools) {
720
979
  if (!tools?.some((tool) => tool.name === "run_code")) return void 0;
721
- return "run_code compatibility rule: its code is parsed as strict JavaScript/TypeScript before execution. On Windows, do not embed PowerShell containing $, ${...}, backslashes, or here-strings in JavaScript template literals; String.raw does not disable ${...} interpolation. Prefer arrays of ordinary quoted strings joined with \"\\n\", escaping backslashes, or use a file-write tool for large scripts and then invoke pwsh.";
980
+ return "run_code compatibility rule: its code is parsed as strict JavaScript/TypeScript before execution. Shell commands are nested string data: JavaScript template literals may consume ${...}, backticks, backslashes, and escape sequences before PowerShell, Bash, or POSIX sh sees them. On Windows, avoid embedding PowerShell containing $, ${...}, backslashes, or here-strings in template literals; String.raw does not disable ${...} interpolation. On Linux, prefer ordinary quoted strings or write a script file before invoking bash/sh, especially for commands containing backticks or ${...}. Prefer arrays of ordinary quoted strings joined with \"\\n\", escaping backslashes, or use a file-write tool for large scripts.";
722
981
  }
723
982
  function localRawImageInstruction(stats) {
724
983
  if (stats.failed === 0) return void 0;
@@ -728,18 +987,25 @@ function supportsImageInput(options) {
728
987
  return options.provider === "codex-chatgpt" && options.model.toLowerCase().startsWith("gpt-");
729
988
  }
730
989
  function toolDescriptionForCodex(name, description) {
731
- if (name === "run_code") return `${description}\n\nCompatibility: code is strict JavaScript/TypeScript. When composing PowerShell, avoid JavaScript template literals containing $, \${...}, Windows backslashes, or PowerShell here-strings. Prefer ordinary quoted string arrays joined with "\\n", or write a script file with a dedicated file tool before invoking pwsh.`;
990
+ if (name === "run_code") return `${description}\n\nCompatibility: code is strict JavaScript/TypeScript and nested shell commands are string data. Template literals may consume \${...}, backticks, backslashes, and escape sequences before PowerShell, Bash, or POSIX sh sees them. Prefer ordinary quoted string arrays joined with "\\n", or write a script file with a dedicated file tool before invoking the shell.`;
732
991
  if (isCommandTool(name)) return `${description}\n\n${commandToolCompatibilityText(name)}`;
733
992
  return description;
734
993
  }
735
994
  function commandToolInstruction(tools) {
736
995
  const names = tools?.filter((tool) => isCommandTool(tool.name)).map((tool) => tool.name);
737
996
  if (!names?.length) return void 0;
738
- return `Command tool compatibility rule (${[...new Set(names)].join(", ")}): each command call runs in a fresh process, so do not rely on cd, aliases, functions, or variables from previous calls; set workdir when the tool supports it. On Windows/pwsh, keep commands in native PowerShell syntax and native Windows paths. For deletion or move operations, first resolve and verify exact absolute target paths, then operate on those literal paths only; avoid dynamically deleting paths built from $HOME, wildcards, command substitution, or another shell's output. Treat [auto-mode hard deny] and similar policy denials as non-retriable; choose a safer non-destructive inspection or report the limitation instead of repeating the same command or adding sandbox escalation. If downloads fail with TLS credential or connection-closed errors, treat that as an environment/network failure and use local sources or report the limitation instead of cycling through equivalent download commands.`;
997
+ const uniqueNames = [...new Set(names)];
998
+ const normalized = uniqueNames.map((name) => name.toLowerCase());
999
+ const shellGuidance = [
1000
+ normalized.some((name) => name === "pwsh" || name.includes("powershell")) ? "For pwsh/PowerShell, use native PowerShell syntax and native Windows paths." : void 0,
1001
+ normalized.includes("bash") ? "For bash, use native POSIX paths and Bash syntax in a fresh non-interactive process." : void 0,
1002
+ normalized.some((name) => name === "sh" || name === "shell") ? "For sh/generic shell, prefer portable POSIX syntax and avoid Bash-only arrays, [[ ... ]], process substitution, and source." : void 0
1003
+ ].filter((value) => value !== void 0).join(" ");
1004
+ return `Command tool compatibility rule (${uniqueNames.join(", ")}): each command call runs in a fresh process, so do not rely on cd, aliases, functions, or variables from previous calls; set workdir when the tool supports it. ${shellGuidance} For deletion or move operations, first resolve and verify exact absolute target paths, then operate on those literal paths only; avoid dynamically deleting paths built from home-directory expansion, wildcards, command substitution, or another shell's output. Treat [auto-mode hard deny] and similar policy denials as non-retriable; choose a safer non-destructive inspection or report the limitation instead of repeating the same command or adding sandbox escalation. If downloads fail with TLS credential or connection-closed errors, treat that as an environment/network failure and use local sources or report the limitation instead of cycling through equivalent download commands.`;
739
1005
  }
740
1006
  function commandToolCompatibilityText(name) {
741
1007
  const shell = name.toLowerCase();
742
- return `Compatibility: command execution is stateless between calls.${shell === "pwsh" || shell.includes("powershell") ? " Use native PowerShell syntax and native Windows paths; prefer workdir over cd because every call starts a fresh process." : " Prefer workdir over cd because every call starts a fresh process."} For destructive operations, verify exact absolute targets first and use literal paths; policy hard-deny results require a safer command shape, not sandbox escalation.`;
1008
+ return `Compatibility: command execution is stateless between calls.${shell === "pwsh" || shell.includes("powershell") ? " Use native PowerShell syntax and native Windows paths." : shell === "bash" ? " Use Bash syntax and native POSIX paths." : " Use portable POSIX syntax and native POSIX paths; avoid Bash-only arrays, [[ ... ]], process substitution, and source."} Prefer workdir over cd because every call starts a fresh process. For destructive operations, verify exact absolute targets first and use literal paths; policy hard-deny results require a safer command shape, not sandbox escalation.`;
743
1009
  }
744
1010
  function sandboxToolInstruction(tools, sandboxRetryTools) {
745
1011
  if (!tools?.some((tool) => hasSandboxControls(tool.parameters))) return void 0;
@@ -761,7 +1027,7 @@ function toolParametersForCodex(toolName, parameters, allowSandboxRetry) {
761
1027
  const code = record$2(properties.code);
762
1028
  if (code !== null) {
763
1029
  const current = typeof code.description === "string" ? code.description.trim() : "";
764
- const compatibility = "Strict JavaScript/TypeScript source. For PowerShell on Windows, avoid JavaScript template literals containing $, ${...}, backslashes, or here-strings; String.raw still performs ${...} interpolation. Prefer ordinary quoted string arrays joined with \"\\n\" and escape backslashes, or write a script file with a dedicated file tool.";
1030
+ const compatibility = "Strict JavaScript/TypeScript source. Nested shell commands are string data: template literals may consume ${...}, backticks, backslashes, and escape sequences before PowerShell, Bash, or POSIX sh sees them; String.raw still performs ${...} interpolation. Prefer ordinary quoted string arrays joined with \"\\n\", or write a script file with a dedicated file tool.";
765
1031
  code.description = current ? `${current}\n\n${compatibility}` : compatibility;
766
1032
  }
767
1033
  }
@@ -784,7 +1050,7 @@ function appendPropertyDescription(value, addition) {
784
1050
  }
785
1051
  function isCommandTool(name) {
786
1052
  const normalized = name.toLowerCase();
787
- return normalized === "pwsh" || normalized === "powershell" || normalized === "bash" || normalized === "shell";
1053
+ return normalized === "pwsh" || normalized === "powershell" || normalized === "bash" || normalized === "sh" || normalized === "shell";
788
1054
  }
789
1055
  function hasSandboxControls(parameters) {
790
1056
  const properties = record$2(parameters.properties);
@@ -828,7 +1094,7 @@ function appendMissingToolCalls(input, knownToolCalls, message) {
828
1094
  }
829
1095
  function runCodeErrorOutput(output) {
830
1096
  if (!isRunCodeParserError(output)) return output;
831
- return `${output}\n\nCompatibility hint: run_code failed while parsing strict JavaScript/TypeScript, before the nested tool ran. Avoid JavaScript template literals for PowerShell containing $, \${...}, Windows backslashes, or here-strings; String.raw does not prevent \${...} interpolation. Build the script from ordinary quoted strings joined with "\\n" (escaping backslashes), or write a script file with a dedicated file tool and then invoke pwsh.`;
1097
+ return `${output}\n\nCompatibility hint: run_code failed while parsing strict JavaScript/TypeScript, before the nested tool ran. Shell commands are nested string data; template literals can consume \${...}, backticks, backslashes, and escape sequences before PowerShell, Bash, or POSIX sh sees them, and String.raw does not prevent \${...} interpolation. Build the script from ordinary quoted strings joined with "\\n", or write a script file with a dedicated file tool and then invoke the shell.`;
832
1098
  }
833
1099
  function isRunCodeParserError(output) {
834
1100
  return /(?:Legacy octal escape is not permitted in strict mode|Unexpected token|Invalid or unexpected token|Unterminated template|Expected ['"]?\}['"]?)/i.test(output);
@@ -1833,6 +2099,100 @@ function parseStoredCredentials(value) {
1833
2099
  };
1834
2100
  }
1835
2101
  //#endregion
2102
+ //#region src/host/token-store-linux.ts
2103
+ const DIRECTORY_MODE = 448;
2104
+ const FILE_MODE = 384;
2105
+ function defaultLinuxCredentialPath() {
2106
+ return join(process.env.DSH_HOME?.trim() || join(homedir(), ".dsh"), "storages", "dsh-chatgpt-subscription", "oauth.json");
2107
+ }
2108
+ /**
2109
+ * Linux credential storage protected by owner-only filesystem permissions.
2110
+ * The payload is not encrypted at rest, so callers must report that distinction
2111
+ * instead of presenting this store as equivalent to Windows DPAPI.
2112
+ */
2113
+ var LinuxFileTokenStore = class {
2114
+ path;
2115
+ storage = {
2116
+ kind: "linux-file",
2117
+ encrypted: false
2118
+ };
2119
+ noFollow = constants.O_NOFOLLOW;
2120
+ constructor(path = defaultLinuxCredentialPath()) {
2121
+ this.path = path;
2122
+ if (process.platform !== "linux") throw new Error("Linux credential storage requires Linux");
2123
+ if (this.noFollow === void 0) throw new Error("Linux credential storage requires O_NOFOLLOW support");
2124
+ if (dirname(path) === path) throw new Error("invalid Linux credential path");
2125
+ }
2126
+ async load() {
2127
+ let handle;
2128
+ try {
2129
+ handle = await open(this.path, constants.O_RDONLY | this.noFollow);
2130
+ } catch (error) {
2131
+ if (isMissing(error)) return null;
2132
+ throw new Error("Linux credential read failed", { cause: error });
2133
+ }
2134
+ try {
2135
+ const stats = await handle.stat();
2136
+ if (!stats.isFile()) throw new Error("credential path is not a regular file");
2137
+ if (typeof process.getuid === "function" && stats.uid !== process.getuid()) throw new Error("credential file is owned by another user");
2138
+ if ((stats.mode & 511) !== FILE_MODE) throw new Error("credential file permissions must be 0600");
2139
+ const payload = await handle.readFile({ encoding: "utf8" });
2140
+ return parseStoredCredentials(JSON.parse(payload));
2141
+ } catch (error) {
2142
+ throw new Error("Linux credential payload is invalid or insecure", { cause: error });
2143
+ } finally {
2144
+ await handle.close();
2145
+ }
2146
+ }
2147
+ async save(value) {
2148
+ const directory = dirname(this.path);
2149
+ const temporary = `${this.path}.tmp-${randomUUID()}`;
2150
+ try {
2151
+ const existing = await lstat(this.path);
2152
+ if (existing.isSymbolicLink() || !existing.isFile()) throw new Error("credential path is not a regular file");
2153
+ assertOwnedByCurrentUser(existing.uid, "credential file");
2154
+ } catch (error) {
2155
+ if (!isMissing(error)) throw new Error("Linux credential write failed", { cause: error });
2156
+ }
2157
+ await mkdir(directory, {
2158
+ recursive: true,
2159
+ mode: DIRECTORY_MODE
2160
+ });
2161
+ const directoryStats = await stat(directory);
2162
+ if (!directoryStats.isDirectory()) throw new Error("Linux credential directory is invalid");
2163
+ assertOwnedByCurrentUser(directoryStats.uid, "credential directory");
2164
+ await chmod(directory, DIRECTORY_MODE);
2165
+ let handle;
2166
+ try {
2167
+ handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, FILE_MODE);
2168
+ await handle.writeFile(JSON.stringify(value), { encoding: "utf8" });
2169
+ await handle.sync();
2170
+ await handle.close();
2171
+ handle = void 0;
2172
+ await rename(temporary, this.path);
2173
+ await chmod(this.path, FILE_MODE);
2174
+ } catch (error) {
2175
+ await handle?.close().catch(() => void 0);
2176
+ await unlink(temporary).catch(() => void 0);
2177
+ throw new Error("Linux credential write failed", { cause: error });
2178
+ }
2179
+ }
2180
+ async clear() {
2181
+ try {
2182
+ await unlink(this.path);
2183
+ } catch (error) {
2184
+ if (isMissing(error)) return;
2185
+ throw new Error("Linux credential deletion failed", { cause: error });
2186
+ }
2187
+ }
2188
+ };
2189
+ function assertOwnedByCurrentUser(owner, label) {
2190
+ if (typeof process.getuid === "function" && owner !== process.getuid()) throw new Error(`${label} is owned by another user`);
2191
+ }
2192
+ function isMissing(error) {
2193
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2194
+ }
2195
+ //#endregion
1836
2196
  //#region src/host/token-store-windows.ts
1837
2197
  const PROTECT_SCRIPT = String.raw`
1838
2198
  $ErrorActionPreference = 'Stop'
@@ -1866,6 +2226,10 @@ function defaultDpapiCredentialPath() {
1866
2226
  }
1867
2227
  var WindowsDpapiTokenStore = class {
1868
2228
  path;
2229
+ storage = {
2230
+ kind: "windows-dpapi",
2231
+ encrypted: true
2232
+ };
1869
2233
  constructor(path = defaultDpapiCredentialPath()) {
1870
2234
  this.path = path;
1871
2235
  if (process.platform !== "win32") throw new Error("Windows DPAPI storage requires Windows");
@@ -1938,14 +2302,22 @@ function runPowerShell(script, path, stdin) {
1938
2302
  });
1939
2303
  }
1940
2304
  //#endregion
2305
+ //#region src/host/platform-token-store.ts
2306
+ function createPlatformTokenStore(platform = process.platform) {
2307
+ if (platform === "win32") return new WindowsDpapiTokenStore();
2308
+ if (platform === "linux") return new LinuxFileTokenStore();
2309
+ throw new Error(`Unsupported platform ${platform}; dsh-chatgpt-subscription supports Windows and Linux.`);
2310
+ }
2311
+ //#endregion
1941
2312
  //#region src/index.ts
1942
2313
  const inject = [
1943
2314
  "webServer",
1944
2315
  "llm",
1945
- "attachments"
2316
+ "attachments",
2317
+ "agents"
1946
2318
  ];
1947
2319
  function apply(ctx) {
1948
- const oauth = new OAuthService(new WindowsDpapiTokenStore(), { logger: ctx.logger });
2320
+ const oauth = new OAuthService(createPlatformTokenStore(), { logger: ctx.logger });
1949
2321
  const usage = new UsageService(oauth);
1950
2322
  const adapter = new CodexChatGptAdapter(new ResponsesClient(oauth, ctx.attachments, {
1951
2323
  localRawImages: { baseUrl: localWebServerBaseUrl(ctx.webServer.host, ctx.webServer.port) },
@@ -1954,7 +2326,11 @@ function apply(ctx) {
1954
2326
  ctx.effect(() => {
1955
2327
  const disposeRoutes = registerRoutes(ctx, oauth, usage);
1956
2328
  const disposeAdapter = ctx.llm.registerAdapter([PROVIDER_ID], adapter);
2329
+ const disposeSubagentReportCompat = installSubagentReportDedupCompat(ctx);
2330
+ const disposeBashPromptCompat = installBashPromptCompat(ctx);
1957
2331
  return () => {
2332
+ disposeBashPromptCompat();
2333
+ disposeSubagentReportCompat();
1958
2334
  disposeAdapter();
1959
2335
  disposeRoutes();
1960
2336
  oauth.dispose();
@@ -1965,4 +2341,4 @@ function localWebServerBaseUrl(host, port) {
1965
2341
  return `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}`;
1966
2342
  }
1967
2343
  //#endregion
1968
- export { CodexChatGptAdapter, OAuthService, ResponsesClient, UsageService, apply, inject, mapCodexUsage, parseResponsesStream };
2344
+ export { CodexChatGptAdapter, LinuxFileTokenStore, OAuthService, ResponsesClient, UsageService, WindowsDpapiTokenStore, apply, createPlatformTokenStore, inject, mapCodexUsage, parseResponsesStream };
@@ -1,9 +1,11 @@
1
1
  import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
2
- import type { QuotaWindowDto } from '../shared/contracts.ts';
2
+ import type { CredentialStorageDto, QuotaWindowDto } from '../shared/contracts.ts';
3
3
  import { NS } from './locales.ts';
4
4
  type Props = PropsRuntime<'settings.section'> & PropsLocale<typeof NS>;
5
5
  type Translate = Props['t'];
6
6
  export declare function CodexSubscriptionSection({ t }: Props): React.JSX.Element;
7
+ export declare function storageLabel(storage: CredentialStorageDto | undefined, t: Translate): string;
8
+ export declare function storageNotice(storage: CredentialStorageDto | undefined, t: Translate): string;
7
9
  export declare function QuotaBar({ label, window, t }: {
8
10
  label: string;
9
11
  window: QuotaWindowDto;
@@ -1 +1 @@
1
- {"version":3,"file":"CodexSubscriptionSection.d.ts","sourceRoot":"","sources":["../../../src/client/CodexSubscriptionSection.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AACjF,OAAO,KAAK,EAAmC,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAE7F,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAEjC,KAAK,KAAK,GAAG,YAAY,CAAC,kBAAkB,CAAC,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;AAEtE,KAAK,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA;AAI3B,wBAAgB,wBAAwB,CAAC,EAAE,CAAC,EAAE,EAAE,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAoMxE;AAyBD,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,cAAc,CAAC;IAAC,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAczH;AAMD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,CAAC,EAAE,SAAS,GAAG,MAAM,CAQxE;AAWD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAUnD"}
1
+ {"version":3,"file":"CodexSubscriptionSection.d.ts","sourceRoot":"","sources":["../../../src/client/CodexSubscriptionSection.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AACjF,OAAO,KAAK,EAAE,oBAAoB,EAAmC,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEnH,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAEjC,KAAK,KAAK,GAAG,YAAY,CAAC,kBAAkB,CAAC,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;AAEtE,KAAK,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA;AAI3B,wBAAgB,wBAAwB,CAAC,EAAE,CAAC,EAAE,EAAE,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAoMxE;AAiBD,wBAAgB,YAAY,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,EAAE,CAAC,EAAE,SAAS,GAAG,MAAM,CAM5F;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,EAAE,CAAC,EAAE,SAAS,GAAG,MAAM,CAM7F;AAUD,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,cAAc,CAAC;IAAC,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAczH;AAMD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,CAAC,EAAE,SAAS,GAAG,MAAM,CAQxE;AAWD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAUnD"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAK3E,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAI/D,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,0BAA0B,EAAE,SAAS,CAAA;KACtC;CACF;AAED,eAAO,MAAM,MAAM,UAAsB,CAAA;AAEzC,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAW9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAK3E,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAG/D,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,0BAA0B,EAAE,SAAS,CAAA;KACtC;CACF;AAED,eAAO,MAAM,MAAM,UAAsB,CAAA;AAEzC,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAU9C"}
@@ -9,8 +9,14 @@ export declare const zh: {
9
9
  readonly accountId: "账号 ID";
10
10
  readonly expires: "令牌到期";
11
11
  readonly storage: "凭据存储";
12
- readonly storageValue: "Windows DPAPI(当前用户加密)";
13
- readonly securityNotice: "令牌仅保存在 Host 端的 DPAPI 加密文件中,不会进入浏览器、settings.yaml 或日志。";
12
+ readonly storageWindows: "Windows DPAPI(当前用户加密)";
13
+ readonly storageLinuxFile: "Linux 用户私有文件(权限 0600)";
14
+ readonly storageMemory: "仅 Host 内存(不持久化)";
15
+ readonly storageUnavailable: "凭据存储不可用";
16
+ readonly securityWindows: "令牌 Windows CurrentUser DPAPI 加密。";
17
+ readonly securityLinuxFile: "令牌仅写入 Host 上当前 Linux 用户可读的 0600 文件。";
18
+ readonly securityMemory: "令牌仅保留在 Host 内存中,Host 退出后丢失。";
19
+ readonly securityUnavailable: "Host 无法安全访问凭据存储;请修复文件所有者或权限后重试。";
14
20
  readonly signIn: "使用 ChatGPT 登录";
15
21
  readonly signInAgain: "重新登录";
16
22
  readonly cancel: "取消登录";
@@ -60,8 +66,14 @@ export declare const dictionaries: {
60
66
  readonly accountId: "账号 ID";
61
67
  readonly expires: "令牌到期";
62
68
  readonly storage: "凭据存储";
63
- readonly storageValue: "Windows DPAPI(当前用户加密)";
64
- readonly securityNotice: "令牌仅保存在 Host 端的 DPAPI 加密文件中,不会进入浏览器、settings.yaml 或日志。";
69
+ readonly storageWindows: "Windows DPAPI(当前用户加密)";
70
+ readonly storageLinuxFile: "Linux 用户私有文件(权限 0600)";
71
+ readonly storageMemory: "仅 Host 内存(不持久化)";
72
+ readonly storageUnavailable: "凭据存储不可用";
73
+ readonly securityWindows: "令牌由 Host 使用 Windows CurrentUser DPAPI 加密,不会进入浏览器、settings.yaml 或日志。";
74
+ readonly securityLinuxFile: "令牌仅写入 Host 上当前 Linux 用户可读的 0600 文件,但不会额外加密;同 UID 进程、root、备份和磁盘快照仍可能读取。";
75
+ readonly securityMemory: "令牌仅保留在 Host 内存中,Host 退出后丢失。";
76
+ readonly securityUnavailable: "Host 无法安全访问凭据存储;请修复文件所有者或权限后重试。";
65
77
  readonly signIn: "使用 ChatGPT 登录";
66
78
  readonly signInAgain: "重新登录";
67
79
  readonly cancel: "取消登录";
@@ -98,6 +110,6 @@ export declare const dictionaries: {
98
110
  readonly retry: "重试";
99
111
  readonly unknown: "未知";
100
112
  };
101
- en: Record<"title" | "intro" | "account" | "signedOut" | "signedIn" | "plan" | "accountId" | "expires" | "storage" | "storageValue" | "securityNotice" | "signIn" | "signInAgain" | "cancel" | "signOut" | "refreshToken" | "pending" | "popupBlocked" | "continueLogin" | "loading" | "connection" | "provider" | "connectionState" | "connected" | "untested" | "testConnection" | "testing" | "latency" | "models" | "quota" | "quotaIntro" | "refreshQuota" | "refreshing" | "noQuota" | "quotaSignedOut" | "stale" | "updated" | "primary" | "secondary" | "limitWindow" | "used" | "remaining" | "exhausted" | "resets" | "retry" | "unknown", string>;
113
+ en: Record<"quota" | "account" | "storage" | "stale" | "pending" | "title" | "intro" | "signedOut" | "signedIn" | "plan" | "accountId" | "expires" | "storageWindows" | "storageLinuxFile" | "storageMemory" | "storageUnavailable" | "securityWindows" | "securityLinuxFile" | "securityMemory" | "securityUnavailable" | "signIn" | "signInAgain" | "cancel" | "signOut" | "refreshToken" | "popupBlocked" | "continueLogin" | "loading" | "connection" | "provider" | "connectionState" | "connected" | "untested" | "testConnection" | "testing" | "latency" | "models" | "quotaIntro" | "refreshQuota" | "refreshing" | "noQuota" | "quotaSignedOut" | "updated" | "primary" | "secondary" | "limitWindow" | "used" | "remaining" | "exhausted" | "resets" | "retry" | "unknown", string>;
102
114
  };
103
115
  //# sourceMappingURL=locales.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE,EAAG,0BAAmC,CAAA;AAErD,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+CL,CAAA;AAEV,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,OAAO,EAAE,EAAE,MAAM,CA+C9C,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,CAAA;AACvC,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAa,CAAA"}
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE,EAAG,0BAAmC,CAAA;AAErD,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqDL,CAAA;AAEV,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,OAAO,EAAE,EAAE,MAAM,CAqD9C,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,CAAA;AACvC,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAa,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../src/client/styles.ts"],"names":[],"mappings":"AAwDA,wBAAgB,aAAa,IAAI,MAAM,IAAI,CAQ1C"}
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../src/client/styles.ts"],"names":[],"mappings":"AAiDA,wBAAgB,aAAa,IAAI,MAAM,IAAI,CAQ1C"}