dsh-codex-oauth 0.2.0 → 0.2.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.md CHANGED
@@ -26,6 +26,10 @@ npx @deepseek-ai/dsh web
26
26
 
27
27
  The plugin owns its own credential file and login flow. No Pi installation or token copying is needed.
28
28
 
29
+ ### Web UI
30
+
31
+ Open **Settings → OpenAI Codex** and click **Sign in with ChatGPT**. A browser window opens for the OAuth approval; after it completes, return to DSH and select `openai-codex` in the model picker.
32
+
29
33
  ### CLI
30
34
 
31
35
  ```bash
package/README.zh.md CHANGED
@@ -26,6 +26,10 @@ npx @deepseek-ai/dsh web
26
26
 
27
27
  插件拥有独立的 OAuth 登录流程和凭据文件。新用户不需要安装 Pi,也不需要复制任何 token。
28
28
 
29
+ ### Web UI
30
+
31
+ 打开 **设置 → OpenAI Codex**,点击 **使用 ChatGPT 登录**。插件会打开浏览器完成 OAuth 授权;返回 DSH 后,在模型选择器中选择 `openai-codex`。
32
+
29
33
  ### 命令行登录
30
34
 
31
35
  ```bash
package/SPEC.md CHANGED
@@ -17,9 +17,9 @@
17
17
 
18
18
  无图形浏览器时使用 `login --device-code`。
19
19
 
20
- ### Web(后续)
20
+ ### Web
21
21
 
22
- 后续可在 DSH 设置页增加登录按钮;第一版不承诺 Web 登录入口。
22
+ 在 DSH 设置页提供登录状态、浏览器 OAuth 登录和退出登录按钮。
23
23
 
24
24
  ### 命令
25
25
 
@@ -0,0 +1,49 @@
1
+ import React, { useCallback, useEffect, useState } from "react";
2
+
3
+ const STATUS = "/plugins/dsh-codex-oauth/auth/status";
4
+ const LOGIN = "/plugins/dsh-codex-oauth/auth/login";
5
+ const LOGOUT = "/plugins/dsh-codex-oauth/auth/logout";
6
+ const h = React.createElement;
7
+
8
+ async function request(path, method = "GET") {
9
+ const response = await fetch(path, { method, credentials: "same-origin", headers: { accept: "application/json" } });
10
+ const value = await response.json().catch(() => ({}));
11
+ if (!response.ok) throw new Error(value.error || `HTTP ${response.status}`);
12
+ return value;
13
+ }
14
+
15
+ function CodexSettings() {
16
+ const [state, setState] = useState({ status: "loading" });
17
+ const [busy, setBusy] = useState(false);
18
+ const refresh = useCallback(() => request(STATUS).then(setState).catch((error) => setState({ status: "error", message: error.message })), []);
19
+ useEffect(() => { refresh(); }, [refresh]);
20
+ useEffect(() => {
21
+ if (state.status !== "signing-in") return undefined;
22
+ const timer = setInterval(refresh, 1000);
23
+ return () => clearInterval(timer);
24
+ }, [refresh, state.status]);
25
+ const signIn = async () => {
26
+ setBusy(true); setState({ status: "signing-in" });
27
+ const popup = window.open("about:blank", "_blank", "noopener,noreferrer");
28
+ try { const challenge = await request(LOGIN, "POST"); if (!popup) throw new Error("Please allow popups to sign in"); popup.location.replace(challenge.url); }
29
+ catch (error) { popup?.close(); setState({ status: "error", message: error.message }); }
30
+ finally { setBusy(false); }
31
+ };
32
+ const signOut = async () => { setBusy(true); try { await request(LOGOUT, "POST"); setState({ status: "signed-out" }); } catch (error) { setState({ status: "error", message: error.message }); } finally { setBusy(false); } };
33
+ const signedIn = state.status === "signed-in";
34
+ const label = state.status === "signed-in" ? "已登录" : state.status === "signing-in" ? "等待浏览器完成登录…" : state.status === "loading" ? "检查登录状态…" : state.status === "error" ? "登录失败" : "未登录";
35
+ return h("section", { style: { display: "flex", flexDirection: "column", gap: 12, maxWidth: 640 } },
36
+ h("h2", { style: { margin: 0 } }, "OpenAI Codex"),
37
+ h("p", { style: { margin: 0, color: "var(--dsw-alias-label-secondary)" } }, "使用 ChatGPT Plus/Pro 的 Codex 订阅,无需 OpenAI API Key。"),
38
+ h("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", padding: 16, border: "1px solid var(--dsw-alias-border-l2)", borderRadius: 10 } },
39
+ h("span", null, label),
40
+ state.status === "loading" ? null : h("button", { type: "button", disabled: busy || state.status === "signing-in", onClick: signedIn ? signOut : signIn }, signedIn ? "退出登录" : "使用 ChatGPT 登录")),
41
+ state.status === "error" ? h("p", { style: { color: "var(--dsw-alias-state-error-primary)" } }, state.message) : null,
42
+ h("p", { style: { margin: 0, color: "var(--dsw-alias-label-secondary)", fontSize: 13 } }, "凭据保存在 DSH_HOME/.openai-codex-auth.json,不会读取 Pi 或 Codex CLI 的登录文件。"));
43
+ }
44
+
45
+ export const name = "dsh-codex-oauth-client";
46
+ export const inject = ["slots"];
47
+ export function apply(ctx) {
48
+ ctx.slots.inject("settings.section", () => ctx.slots.register({ name: "settings.section", id: "dsh-codex-oauth", order: 15, label: () => "OpenAI Codex" }, CodexSettings));
49
+ }
package/lib/client.js ADDED
@@ -0,0 +1,14 @@
1
+ var G=Object.create;var k=Object.defineProperty;var H=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var z=Object.getPrototypeOf,B=Object.prototype.hasOwnProperty;var O=(e,t)=>()=>{try{return t||e((t={exports:{}}).exports,t),t.exports}catch(r){throw t=0,r}};var W=(e,t,r,u)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of M(t))!B.call(e,n)&&n!==r&&k(e,n,{get:()=>t[n],enumerable:!(u=H(t,n))||u.enumerable});return e};var K=(e,t,r)=>(r=e!=null?G(z(e)):{},W(t||!e||!e.__esModule?k(r,"default",{value:e,enumerable:!0}):r,e));var V=O(o=>{"use strict";var h=Symbol.for("react.element"),J=Symbol.for("react.portal"),Q=Symbol.for("react.fragment"),X=Symbol.for("react.strict_mode"),Y=Symbol.for("react.profiler"),Z=Symbol.for("react.provider"),ee=Symbol.for("react.context"),te=Symbol.for("react.forward_ref"),re=Symbol.for("react.suspense"),ne=Symbol.for("react.memo"),oe=Symbol.for("react.lazy"),R=Symbol.iterator;function ue(e){return e===null||typeof e!="object"?null:(e=R&&e[R]||e["@@iterator"],typeof e=="function"?e:null)}var P={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},I=Object.assign,T={};function y(e,t,r){this.props=e,this.context=t,this.refs=T,this.updater=r||P}y.prototype.isReactComponent={};y.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};y.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function A(){}A.prototype=y.prototype;function w(e,t,r){this.props=e,this.context=t,this.refs=T,this.updater=r||P}var b=w.prototype=new A;b.constructor=w;I(b,y.prototype);b.isPureReactComponent=!0;var j=Array.isArray,D=Object.prototype.hasOwnProperty,E={current:null},q={key:!0,ref:!0,__self:!0,__source:!0};function L(e,t,r){var u,n={},c=null,a=null;if(t!=null)for(u in t.ref!==void 0&&(a=t.ref),t.key!==void 0&&(c=""+t.key),t)D.call(t,u)&&!q.hasOwnProperty(u)&&(n[u]=t[u]);var s=arguments.length-2;if(s===1)n.children=r;else if(1<s){for(var i=Array(s),l=0;l<s;l++)i[l]=arguments[l+2];n.children=i}if(e&&e.defaultProps)for(u in s=e.defaultProps,s)n[u]===void 0&&(n[u]=s[u]);return{$$typeof:h,type:e,key:c,ref:a,props:n,_owner:E.current}}function se(e,t){return{$$typeof:h,type:e.type,key:t,ref:e.ref,props:e.props,_owner:e._owner}}function x(e){return typeof e=="object"&&e!==null&&e.$$typeof===h}function ie(e){var t={"=":"=0",":":"=2"};return"$"+e.replace(/[=:]/g,function(r){return t[r]})}var $=/\/+/g;function S(e,t){return typeof e=="object"&&e!==null&&e.key!=null?ie(""+e.key):t.toString(36)}function g(e,t,r,u,n){var c=typeof e;(c==="undefined"||c==="boolean")&&(e=null);var a=!1;if(e===null)a=!0;else switch(c){case"string":case"number":a=!0;break;case"object":switch(e.$$typeof){case h:case J:a=!0}}if(a)return a=e,n=n(a),e=u===""?"."+S(a,0):u,j(n)?(r="",e!=null&&(r=e.replace($,"$&/")+"/"),g(n,t,r,"",function(l){return l})):n!=null&&(x(n)&&(n=se(n,r+(!n.key||a&&a.key===n.key?"":(""+n.key).replace($,"$&/")+"/")+e)),t.push(n)),1;if(a=0,u=u===""?".":u+":",j(e))for(var s=0;s<e.length;s++){c=e[s];var i=u+S(c,s);a+=g(c,t,r,i,n)}else if(i=ue(e),typeof i=="function")for(e=i.call(e),s=0;!(c=e.next()).done;)c=c.value,i=u+S(c,s++),a+=g(c,t,r,i,n);else if(c==="object")throw t=String(e),Error("Objects are not valid as a React child (found: "+(t==="[object Object]"?"object with keys {"+Object.keys(e).join(", ")+"}":t)+"). If you meant to render a collection of children, use an array instead.");return a}function m(e,t,r){if(e==null)return e;var u=[],n=0;return g(e,u,"","",function(c){return t.call(r,c,n++)}),u}function ce(e){if(e._status===-1){var t=e._result;t=t(),t.then(function(r){(e._status===0||e._status===-1)&&(e._status=1,e._result=r)},function(r){(e._status===0||e._status===-1)&&(e._status=2,e._result=r)}),e._status===-1&&(e._status=0,e._result=t)}if(e._status===1)return e._result.default;throw e._result}var f={current:null},v={transition:null},ae={ReactCurrentDispatcher:f,ReactCurrentBatchConfig:v,ReactCurrentOwner:E};function U(){throw Error("act(...) is not supported in production builds of React.")}o.Children={map:m,forEach:function(e,t,r){m(e,function(){t.apply(this,arguments)},r)},count:function(e){var t=0;return m(e,function(){t++}),t},toArray:function(e){return m(e,function(t){return t})||[]},only:function(e){if(!x(e))throw Error("React.Children.only expected to receive a single React element child.");return e}};o.Component=y;o.Fragment=Q;o.Profiler=Y;o.PureComponent=w;o.StrictMode=X;o.Suspense=re;o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=ae;o.act=U;o.cloneElement=function(e,t,r){if(e==null)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+e+".");var u=I({},e.props),n=e.key,c=e.ref,a=e._owner;if(t!=null){if(t.ref!==void 0&&(c=t.ref,a=E.current),t.key!==void 0&&(n=""+t.key),e.type&&e.type.defaultProps)var s=e.type.defaultProps;for(i in t)D.call(t,i)&&!q.hasOwnProperty(i)&&(u[i]=t[i]===void 0&&s!==void 0?s[i]:t[i])}var i=arguments.length-2;if(i===1)u.children=r;else if(1<i){s=Array(i);for(var l=0;l<i;l++)s[l]=arguments[l+2];u.children=s}return{$$typeof:h,type:e.type,key:n,ref:c,props:u,_owner:a}};o.createContext=function(e){return e={$$typeof:ee,_currentValue:e,_currentValue2:e,_threadCount:0,Provider:null,Consumer:null,_defaultValue:null,_globalName:null},e.Provider={$$typeof:Z,_context:e},e.Consumer=e};o.createElement=L;o.createFactory=function(e){var t=L.bind(null,e);return t.type=e,t};o.createRef=function(){return{current:null}};o.forwardRef=function(e){return{$$typeof:te,render:e}};o.isValidElement=x;o.lazy=function(e){return{$$typeof:oe,_payload:{_status:-1,_result:e},_init:ce}};o.memo=function(e,t){return{$$typeof:ne,type:e,compare:t===void 0?null:t}};o.startTransition=function(e){var t=v.transition;v.transition={};try{e()}finally{v.transition=t}};o.unstable_act=U;o.useCallback=function(e,t){return f.current.useCallback(e,t)};o.useContext=function(e){return f.current.useContext(e)};o.useDebugValue=function(){};o.useDeferredValue=function(e){return f.current.useDeferredValue(e)};o.useEffect=function(e,t){return f.current.useEffect(e,t)};o.useId=function(){return f.current.useId()};o.useImperativeHandle=function(e,t,r){return f.current.useImperativeHandle(e,t,r)};o.useInsertionEffect=function(e,t){return f.current.useInsertionEffect(e,t)};o.useLayoutEffect=function(e,t){return f.current.useLayoutEffect(e,t)};o.useMemo=function(e,t){return f.current.useMemo(e,t)};o.useReducer=function(e,t,r){return f.current.useReducer(e,t,r)};o.useRef=function(e){return f.current.useRef(e)};o.useState=function(e){return f.current.useState(e)};o.useSyncExternalStore=function(e,t,r){return f.current.useSyncExternalStore(e,t,r)};o.useTransition=function(){return f.current.useTransition()};o.version="18.3.1"});var F=O((me,N)=>{"use strict";N.exports=V()});var p=K(F(),1),le="/plugins/dsh-codex-oauth/auth/status",fe="/plugins/dsh-codex-oauth/auth/login",pe="/plugins/dsh-codex-oauth/auth/logout",d=p.default.createElement;async function C(e,t="GET"){let r=await fetch(e,{method:t,credentials:"same-origin",headers:{accept:"application/json"}}),u=await r.json().catch(()=>({}));if(!r.ok)throw new Error(u.error||`HTTP ${r.status}`);return u}function de(){let[e,t]=(0,p.useState)({status:"loading"}),[r,u]=(0,p.useState)(!1),n=(0,p.useCallback)(()=>C(le).then(t).catch(l=>t({status:"error",message:l.message})),[]);(0,p.useEffect)(()=>{n()},[n]),(0,p.useEffect)(()=>{if(e.status!=="signing-in")return;let l=setInterval(n,1e3);return()=>clearInterval(l)},[n,e.status]);let c=async()=>{u(!0),t({status:"signing-in"});let l=window.open("about:blank","_blank","noopener,noreferrer");try{let _=await C(fe,"POST");if(!l)throw new Error("Please allow popups to sign in");l.location.replace(_.url)}catch(_){l?.close(),t({status:"error",message:_.message})}finally{u(!1)}},a=async()=>{u(!0);try{await C(pe,"POST"),t({status:"signed-out"})}catch(l){t({status:"error",message:l.message})}finally{u(!1)}},s=e.status==="signed-in",i=e.status==="signed-in"?"\u5DF2\u767B\u5F55":e.status==="signing-in"?"\u7B49\u5F85\u6D4F\u89C8\u5668\u5B8C\u6210\u767B\u5F55\u2026":e.status==="loading"?"\u68C0\u67E5\u767B\u5F55\u72B6\u6001\u2026":e.status==="error"?"\u767B\u5F55\u5931\u8D25":"\u672A\u767B\u5F55";return d("section",{style:{display:"flex",flexDirection:"column",gap:12,maxWidth:640}},d("h2",{style:{margin:0}},"OpenAI Codex"),d("p",{style:{margin:0,color:"var(--dsw-alias-label-secondary)"}},"\u4F7F\u7528 ChatGPT Plus/Pro \u7684 Codex \u8BA2\u9605\uFF0C\u65E0\u9700 OpenAI API Key\u3002"),d("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",padding:16,border:"1px solid var(--dsw-alias-border-l2)",borderRadius:10}},d("span",null,i),e.status==="loading"?null:d("button",{type:"button",disabled:r||e.status==="signing-in",onClick:s?a:c},s?"\u9000\u51FA\u767B\u5F55":"\u4F7F\u7528 ChatGPT \u767B\u5F55")),e.status==="error"?d("p",{style:{color:"var(--dsw-alias-state-error-primary)"}},e.message):null,d("p",{style:{margin:0,color:"var(--dsw-alias-label-secondary)",fontSize:13}},"\u51ED\u636E\u4FDD\u5B58\u5728 DSH_HOME/.openai-codex-auth.json\uFF0C\u4E0D\u4F1A\u8BFB\u53D6 Pi \u6216 Codex CLI \u7684\u767B\u5F55\u6587\u4EF6\u3002"))}var ge="dsh-codex-oauth-client",ve=["slots"];function _e(e){e.slots.inject("settings.section",()=>e.slots.register({name:"settings.section",id:"dsh-codex-oauth",order:15,label:()=>"OpenAI Codex"},de))}export{_e as apply,ve as inject,ge as name};
2
+ /*! Bundled license information:
3
+
4
+ react/cjs/react.production.min.js:
5
+ (**
6
+ * @license React
7
+ * react.production.min.js
8
+ *
9
+ * Copyright (c) Facebook, Inc. and its affiliates.
10
+ *
11
+ * This source code is licensed under the MIT license found in the
12
+ * LICENSE file in the root directory of this source tree.
13
+ *)
14
+ */
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createCodexAdapter } from "./adapter.js";
2
2
  import { CodexCredentialStore, OPENAI_CODEX_PROVIDER, authPath } from "./store.js";
3
+ import { registerWebAuth } from "./web-auth.js";
3
4
  export { login, logout, status } from "./auth.js";
4
5
 
5
6
  export const name = "llm-codex-oauth";
@@ -13,4 +14,5 @@ export function apply(ctx) {
13
14
  [OPENAI_CODEX_PROVIDER],
14
15
  createCodexAdapter(credentials, () => ctx.get?.("attachments")),
15
16
  );
17
+ registerWebAuth(ctx, credentials);
16
18
  }
@@ -0,0 +1,62 @@
1
+ import { login, logout, status } from "./auth.js";
2
+
3
+ export const AUTH_STATUS_PATH = "/plugins/dsh-codex-oauth/auth/status";
4
+ export const AUTH_LOGIN_PATH = "/plugins/dsh-codex-oauth/auth/login";
5
+ export const AUTH_LOGOUT_PATH = "/plugins/dsh-codex-oauth/auth/logout";
6
+
7
+ function safeMessage(error) {
8
+ return String(error?.message ?? error)
9
+ .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]")
10
+ .replace(/(\b(?:code|token|refresh_token|access_token)=)[^&\s]+/giu, "$1[redacted]")
11
+ .slice(0, 500);
12
+ }
13
+ function trusted(req) {
14
+ const remote = req.socket.remoteAddress;
15
+ if (!["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(remote)) return false;
16
+ if (req.headers["sec-fetch-site"] === "cross-site") return false;
17
+ if (!req.headers.host) return false;
18
+ if (!req.headers.origin) return true;
19
+ try { return new URL(req.headers.origin).host === new URL(`http://${req.headers.host}`).host; }
20
+ catch { return false; }
21
+ }
22
+ function json(res, statusCode, value) {
23
+ res.writeHead(statusCode, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "x-content-type-options": "nosniff" });
24
+ res.end(JSON.stringify(value));
25
+ }
26
+
27
+ export function registerWebAuth(ctx, store) {
28
+ let operation;
29
+ let challenge;
30
+ let state = { status: "signed-out" };
31
+ const cancellation = new AbortController();
32
+ const update = () => status(store).then((value) => { state = value.authenticated ? { status: "signed-in" } : { status: "signed-out" }; return state; });
33
+ const start = () => {
34
+ if (operation) return;
35
+ state = { status: "signing-in" };
36
+ challenge = undefined;
37
+ operation = login(store, {
38
+ signal: cancellation.signal,
39
+ prompt: async (prompt) => prompt.type === "select" ? "browser" : new Promise((_resolve, reject) => cancellation.signal.addEventListener("abort", () => reject(cancellation.signal.reason), { once: true })),
40
+ notify: (event) => { if (event.type === "auth_url") challenge = { url: event.url }; },
41
+ }).then(update, (error) => { state = { status: "error", message: safeMessage(error) }; }).finally(() => { operation = undefined; });
42
+ };
43
+ ctx.inject(["webServer"], (web) => ctx.effect(() => {
44
+ const routes = [
45
+ web.webServer.register({ kind: "exact", path: AUTH_STATUS_PATH, handler: async (req, res) => {
46
+ if (req.method !== "GET" || !trusted(req)) return json(res, req.method === "GET" ? 403 : 405, { error: "forbidden" });
47
+ if (!operation) await update();
48
+ json(res, 200, state);
49
+ }}),
50
+ web.webServer.register({ kind: "exact", path: AUTH_LOGIN_PATH, handler: async (req, res) => {
51
+ if (req.method !== "POST" || !trusted(req)) return json(res, req.method === "POST" ? 403 : 405, { error: "forbidden" });
52
+ try { start(); for (let i = 0; i < 50 && !challenge && operation; i++) await new Promise((resolve) => setTimeout(resolve, 100)); if (!challenge) throw new Error("OAuth URL was not created"); json(res, 200, challenge); }
53
+ catch (error) { json(res, 500, { error: safeMessage(error) }); }
54
+ }}),
55
+ web.webServer.register({ kind: "exact", path: AUTH_LOGOUT_PATH, handler: async (req, res) => {
56
+ if (req.method !== "POST" || !trusted(req)) return json(res, req.method === "POST" ? 403 : 405, { error: "forbidden" });
57
+ cancellation.abort(new Error("sign-in cancelled")); await operation?.catch(() => {}); await logout(store); state = { status: "signed-out" }; json(res, 200, { ok: true });
58
+ }}),
59
+ ];
60
+ return async () => { cancellation.abort(new Error("plugin disposed")); await operation?.catch(() => {}); for (const dispose of routes) dispose(); };
61
+ }, "dsh-codex-oauth: Web OAuth routes"));
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-oauth",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Use a ChatGPT Codex subscription in DeepSeek Harness through standalone OAuth",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,9 +16,15 @@
16
16
  "type": "module",
17
17
  "main": "lib/index.js",
18
18
  "bin": { "dsh-openai-codex": "bin/dsh-openai-codex.js" },
19
- "exports": { ".": "./lib/index.js" },
20
- "files": ["lib", "bin", "cordis.patch.yml", "README.md", "README.zh.md", "SPEC.md", "LICENSE"],
21
- "dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
19
+ "exports": { ".": "./lib/index.js", "./client": "./lib/client.js", "./cordis.patch.yml": "./cordis.patch.yml", "./package.json": "./package.json" },
20
+ "files": ["lib", "bin", "client", "cordis.patch.yml", "README.md", "README.zh.md", "SPEC.md", "LICENSE"],
21
+ "dsh": {
22
+ "bundle": { "patch": "./cordis.patch.yml" },
23
+ "client": {
24
+ "inject": ["@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-ui-settings", "@deepseek-ai/dsh-client-locale"],
25
+ "platform": "web"
26
+ }
27
+ },
22
28
  "peerDependencies": {
23
29
  "@earendil-works/pi-ai": "^0.84.1",
24
30
  "@deepseek-ai/cordis": "^4.0.1",