@tokennotincluded/dsh-lmm-provider 0.1.0-alpha.3

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 ADDED
@@ -0,0 +1,64 @@
1
+ # LMM provider for DSH
2
+
3
+ Use the models available to your [LMM](https://api.lmm.best) account directly in DeepSeek Harness. Authentication uses browser OAuth with PKCE and a loopback callback. No API key is pasted into DSH.
4
+
5
+ This package is an alpha tested with DSH `0.1.5-rc.2`. It requires Node.js `22.19.0` or newer in the supported Node 22 / Node 24+ lines.
6
+
7
+ ## Official DSH Desktop
8
+
9
+ Open **Plugins** in the Desktop application and add `@tokennotincluded/dsh-lmm-provider@alpha`. The official Desktop application owns an isolated `desktop` profile; installing into a CLI `web` profile does not install the plugin in Desktop. If an older local, Git or release archive copy of this package is already installed, remove that package in Desktop Plugins first, then add the npm package. The Desktop manager permits one dependency with this package name per profile, so two sources cannot run together there. Restart Desktop if the manager asks for it.
10
+
11
+ In **Settings → Models**, choose **Sign in with LMM**. Use **Open LMM authorization** if it opens your normal browser. If the link does nothing or opens a browser without your LMM login, choose **Copy link** and paste it into the browser where you are already signed in. On the LMM authorization page, choose **Continue** when already signed in. The **Sign in** link is for a browser without an active LMM session. Review the account and permissions, choose **Allow access**, and wait for DSH to show **Sign-in complete**. If the browser does not return to DSH automatically, use **Return to DSH** on the completion page.
12
+
13
+ Keep Desktop open throughout the attempt. The authorization link is short lived and tied to the local callback listener, so start a fresh attempt if it expires. A failed attempt now shows a credential-free reason in the card.
14
+
15
+ ## DSH CLI / Web profile
16
+
17
+ Install the npm package in the profile you run:
18
+
19
+ ```sh
20
+ dsh plugin --profile web add @tokennotincluded/dsh-lmm-provider@alpha
21
+ ```
22
+
23
+ The DSH package manager also keys this dependency by package name. To switch from a local, Git or archive installation, remove `@tokennotincluded/dsh-lmm-provider` from that profile, then add the npm package. Credential records are stored separately from the package dependency.
24
+
25
+ ### Install from source
26
+
27
+ ```sh
28
+ npm install -g @deepseek-ai/dsh@0.1.5-rc.2
29
+ git clone --recurse-submodules https://github.com/TokenNotIncluded/dsh-lmm-provider.git
30
+ cd dsh-lmm-provider
31
+ npm ci
32
+ npm run build
33
+ dsh plugin --profile web add .
34
+ ```
35
+
36
+ The submodule is required by the build. If the repository was cloned without it, run `git submodule update --init --recursive` before building. Keep the checkout after installation: DSH links local plugin directories.
37
+
38
+ The bundle includes the authorization service and the Web Models-page login card. Install it separately in each profile that needs LMM. A headless profile can use the credential after a Web login when both profiles share the same `DSH_HOME`:
39
+
40
+ ```sh
41
+ dsh plugin --profile headless add /absolute/path/to/dsh-lmm-provider
42
+ ```
43
+
44
+ ## Sign in from the Web profile
45
+
46
+ 1. Switch to your project directory, start `dsh web`, and open Settings → Models.
47
+ 2. Find the **LMM** card and select **Sign in with LMM**.
48
+ 3. Open the authorization link in the browser where you are signed in to LMM. Click **Continue** on the LMM page, review and approve access, then return to DSH.
49
+ 4. The card reports **Sign-in complete** only after DSH observes the saved credential. Select an LMM model in your conversation.
50
+
51
+ Cancel stops the pending login; closing Settings also cancels that page's active attempt. A sign-in attempt expires after five minutes. Prompt answers and OAuth credentials are never returned in status responses. The Web controls use DSH's existing authenticated API transport and its Host/Origin checks.
52
+
53
+ **Sign out** removes the local credential. It does not revoke the server grant; revoke the authorization in your LMM account when needed. Model access, groups, capabilities, and prices come from the account-scoped LMM catalog. Token refresh is serialized by DSH and additionally fenced by a credential-free refresh journal under `DSH_HOME`.
54
+
55
+ ## Development
56
+
57
+ ```sh
58
+ npm ci
59
+ npm test
60
+ npm run typecheck
61
+ npm run pack:check
62
+ ```
63
+
64
+ The package ships both the Host adapter and the browser bundle. Tests cover credential isolation, OAuth client identity, loader dependency metadata, authorization prompts, cancellation, and secret-free responses. A successful build or installation alone is not proof of live OAuth and model invocation; verify those against an authorized test account before relying on a new release.
@@ -0,0 +1,5 @@
1
+ - insert:
2
+ - id: authorization
3
+ name: '@deepseek-ai/dsh-authorization'
4
+ - id: lmm-model-provider
5
+ name: '@tokennotincluded/dsh-lmm-provider'
package/lib/client.js ADDED
@@ -0,0 +1,211 @@
1
+ window.__ModuleLoader__.load({id:"@tokennotincluded/dsh-lmm-provider",factory:(require)=>{var module={exports:{}};var exports=module.exports;
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/client.tsx
22
+ var client_exports = {};
23
+ __export(client_exports, {
24
+ apply: () => apply,
25
+ inject: () => inject,
26
+ safeLoginUrl: () => safeLoginUrl
27
+ });
28
+ module.exports = __toCommonJS(client_exports);
29
+ var import_react = require("react");
30
+ var import_jsx_runtime = require("react/jsx-runtime");
31
+ var buttonStyle = { padding: "8px 14px", border: "1px solid currentColor", borderRadius: 6, background: "transparent", color: "inherit", cursor: "pointer" };
32
+ function LoginCard({ call }) {
33
+ const [signedIn, setSignedIn] = (0, import_react.useState)(false);
34
+ const [busy, setBusy] = (0, import_react.useState)(false);
35
+ const [view, setView] = (0, import_react.useState)();
36
+ const [answer, setAnswer] = (0, import_react.useState)("");
37
+ const [error, setError] = (0, import_react.useState)("");
38
+ const [copyState, setCopyState] = (0, import_react.useState)("idle");
39
+ const active = (0, import_react.useRef)();
40
+ const refresh = (0, import_react.useCallback)(async () => {
41
+ const status = await call("status");
42
+ setSignedIn(status.signedIn);
43
+ setBusy(status.busy);
44
+ }, [call]);
45
+ (0, import_react.useEffect)(() => {
46
+ let disposed = false;
47
+ void refresh().catch(() => {
48
+ if (!disposed) setError("Cannot read LMM sign-in status. Reopen Settings to retry.");
49
+ });
50
+ return () => {
51
+ disposed = true;
52
+ };
53
+ }, [refresh]);
54
+ (0, import_react.useEffect)(() => () => {
55
+ if (active.current) void call("cancel", { attempt: active.current }).catch(() => {
56
+ });
57
+ }, [call]);
58
+ (0, import_react.useEffect)(() => {
59
+ if (view?.state !== "pending") return;
60
+ let disposed = false;
61
+ let timer;
62
+ const poll = async () => {
63
+ try {
64
+ const next = await call("poll", { attempt: view.attempt });
65
+ if (disposed) return;
66
+ setView(next);
67
+ if (next.state === "pending") timer = setTimeout(() => void poll(), 750);
68
+ else {
69
+ active.current = void 0;
70
+ setBusy(false);
71
+ await refresh();
72
+ }
73
+ } catch {
74
+ if (!disposed) {
75
+ setError("Sign-in connection was interrupted. Cancel and try again.");
76
+ setBusy(false);
77
+ }
78
+ }
79
+ };
80
+ timer = setTimeout(() => void poll(), 300);
81
+ return () => {
82
+ disposed = true;
83
+ clearTimeout(timer);
84
+ };
85
+ }, [view?.attempt, view?.state, call, refresh]);
86
+ (0, import_react.useEffect)(() => {
87
+ setAnswer("");
88
+ }, [view?.prompt?.id]);
89
+ const loginUrl = view?.state === "pending" ? [...view.notices].reverse().find((notice) => notice.url && safeLoginUrl(notice.url))?.url : void 0;
90
+ const start = async () => {
91
+ setError("");
92
+ setCopyState("idle");
93
+ setBusy(true);
94
+ try {
95
+ const next = await call("begin");
96
+ active.current = next.attempt;
97
+ setView(next);
98
+ } catch (cause) {
99
+ setBusy(false);
100
+ setError(cause instanceof Error ? cause.message : "Sign-in could not start.");
101
+ }
102
+ };
103
+ const cancel = async () => {
104
+ if (view) {
105
+ await call("cancel", { attempt: view.attempt });
106
+ active.current = void 0;
107
+ setView(void 0);
108
+ await refresh();
109
+ }
110
+ };
111
+ const submit = async () => {
112
+ if (!view?.prompt) return;
113
+ try {
114
+ setView(await call("answer", { attempt: view.attempt, prompt: view.prompt.id, value: answer }));
115
+ setAnswer("");
116
+ } catch {
117
+ setError("This question expired. Wait for the current sign-in step.");
118
+ }
119
+ };
120
+ const signOut = async () => {
121
+ try {
122
+ await call("logout");
123
+ setView(void 0);
124
+ await refresh();
125
+ } catch {
126
+ setError("Sign-out failed. Try again.");
127
+ }
128
+ };
129
+ const copyLoginUrl = async () => {
130
+ if (!loginUrl) return;
131
+ try {
132
+ await navigator.clipboard.writeText(loginUrl);
133
+ setCopyState("copied");
134
+ } catch {
135
+ setCopyState("manual");
136
+ }
137
+ };
138
+ const prompt = view?.prompt;
139
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { "aria-label": "LMM", style: { marginTop: 24, padding: 20, border: "1px solid #8886", borderRadius: 8 }, children: [
140
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { style: { margin: "0 0 8px" }, children: "LMM" }),
141
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: signedIn ? "Signed in with LMM. Choose an LMM model in your conversation." : "Connect your LMM account in the browser. No API key is needed." }),
142
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 10 }, children: [
143
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: buttonStyle, disabled: busy, onClick: () => void start(), children: "Sign in with LMM" }),
144
+ signedIn && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: buttonStyle, disabled: busy, onClick: () => void signOut(), children: "Sign out" }),
145
+ view?.state === "pending" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: buttonStyle, onClick: () => void cancel().catch(() => setError("Cancellation failed. Try again.")), children: "Cancel" })
146
+ ] }),
147
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "aria-live": "polite", children: [
148
+ view?.notices.map((notice, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [
149
+ notice.message,
150
+ notice.code && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("code", { children: [
151
+ " ",
152
+ notice.code
153
+ ] })
154
+ ] }, i)),
155
+ loginUrl && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
156
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [
157
+ "Open the authorization link in the browser where you are already signed in to LMM. If the Desktop window cannot open it, copy the link and paste it into that browser. On the LMM page, choose ",
158
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "Continue" }),
159
+ " if you are already signed in; use ",
160
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "Sign in" }),
161
+ " only if that browser is signed out."
162
+ ] }),
163
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: loginUrl, target: "_blank", rel: "noopener noreferrer", children: "Open LMM authorization" }),
164
+ " ",
165
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: buttonStyle, onClick: () => void copyLoginUrl(), children: "Copy link" }),
166
+ copyState === "copied" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { role: "status", children: " Link copied." }),
167
+ copyState === "manual" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
168
+ " Select and copy this link: ",
169
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { value: loginUrl, readOnly: true, onFocus: (event) => event.currentTarget.select(), style: { width: "100%" } })
170
+ ] })
171
+ ] }),
172
+ view?.state === "authorized" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Sign-in complete." }),
173
+ view?.state === "cancelled" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Sign-in cancelled." }),
174
+ view?.state === "failed" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", children: view.error ?? "Sign-in failed. Try again." }),
175
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", children: error })
176
+ ] }),
177
+ prompt && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: (event) => {
178
+ event.preventDefault();
179
+ void submit();
180
+ }, children: [
181
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
182
+ prompt.message,
183
+ prompt.kind === "select" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { value: answer, onChange: (e) => setAnswer(e.target.value), children: [
184
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "", children: "Select\u2026" }),
185
+ prompt.options?.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: option.id, children: option.label }, option.id))
186
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: prompt.kind === "secret" ? "password" : "text", value: answer, placeholder: prompt.placeholder, autoComplete: "off", onChange: (e) => setAnswer(e.target.value) })
187
+ ] }),
188
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: buttonStyle, disabled: !answer, type: "submit", children: "Continue" })
189
+ ] }),
190
+ signedIn && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: "Sign out removes this local login. Revoke account access on LMM when needed." })
191
+ ] });
192
+ }
193
+ function safeLoginUrl(value) {
194
+ try {
195
+ const url = new URL(value);
196
+ return url.protocol === "https:" && url.hostname === "api.lmm.best" && url.port === "" && url.pathname === "/api/oauth2/authorize" && url.username === "" && url.password === "";
197
+ } catch {
198
+ return false;
199
+ }
200
+ }
201
+ var inject = ["slots", "connection"];
202
+ function apply(ctx) {
203
+ const call = async (action, payload = {}) => {
204
+ const result = await ctx.connection.rpc.call("/api", "lmm-auth/" + action, payload);
205
+ if (!result.ok) throw new Error(result.error.message);
206
+ return result.value;
207
+ };
208
+ ctx.slots.inject("settings.models.footer", () => ctx.slots.register({ name: "settings.models.footer", id: "lmm-sign-in", order: 20, inject: () => ({ call }) }, LoginCard));
209
+ }
210
+
211
+ return module.exports;}});