@goodandready/dsh-lanmode 0.1.0
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/LICENSE +21 -0
- package/README.md +78 -0
- package/cordis.patch.yml +11 -0
- package/lib/client.js +253 -0
- package/lib/index.js +20 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-voice contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# dsh-lanmode
|
|
2
|
+
|
|
3
|
+
**Settings over the LAN** for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh).
|
|
4
|
+
|
|
5
|
+
Open the Web UI at `http://192.168.1.50:3080` instead of `http://localhost:3080` and every settings card in the deployment goes blank — your plugins' cards, and **Settings → Plugins → Plugin configuration** with them. No error appears, the plugins load fine, and reloading does not help. Saving silently does nothing.
|
|
6
|
+
|
|
7
|
+
This plugin fixes that. Install it, reload the page, and the settings work from any address.
|
|
8
|
+
|
|
9
|
+
## Why it happens
|
|
10
|
+
|
|
11
|
+
The decision is made in the browser, from the page's hostname:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
isLoopback: pageLocation === undefined || isLoopbackHostname(pageLocation.hostname)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`isLoopbackHostname` accepts `localhost`, `[::1]` and `127.0.0.0/8` — nothing else. A page served at a LAN address is therefore "remote", and the settings service switches to a process-local mode where the shared mirror of the settings document is never read at all:
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
ensure() { if (this.persistence === "memory") return Promise.resolve() }
|
|
21
|
+
status: persistence === "host" ? "loading" : "unavailable"
|
|
22
|
+
enqueue() { if (this.persistence === "memory") return Promise.resolve() }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Every bound namespace reports `status: "unavailable"` for the life of the page, and writes are dropped before they reach the wire.
|
|
26
|
+
|
|
27
|
+
**The server does not share this restriction.** Both `settings.describe` and `settings.mutate` answer normally over the network as long as the request's `Origin` matches the page it came from — which is exactly the case for requests the UI itself makes. Verified against a live harness behind a reverse proxy.
|
|
28
|
+
|
|
29
|
+
## What the plugin does
|
|
30
|
+
|
|
31
|
+
On a loopback page: nothing at all — the core works there, and a second implementation would only be a second source of truth.
|
|
32
|
+
|
|
33
|
+
On any other page it stands up its own copy of the same machinery over the same two calls, and publishes it:
|
|
34
|
+
|
|
35
|
+
- as a service named `lanSettings`, for plugins that want to ask for it explicitly;
|
|
36
|
+
- and, if the runtime allows a plugin to claim the name, in place of `settingsScope` — which repairs every settings surface at once, including the core's plugin configuration tab and plugins that know nothing about this one.
|
|
37
|
+
|
|
38
|
+
The snapshot it hands out has the same shape the core's has — `status`, `value`, `base`, `user`, `revision`, `writable` — so cards cannot tell the difference.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
dsh plugin --profile web add @goodandready/dsh-lanmode
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Restart the Web UI afterwards, then reload the browser.
|
|
47
|
+
|
|
48
|
+
## Checking it on a machine where it is not needed
|
|
49
|
+
|
|
50
|
+
The plugin stands aside on a loopback page, which makes it awkward to try out
|
|
51
|
+
on the machine that runs the harness. A debug switch turns it on there anyway:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
localStorage.setItem('dsh-lanmode:force', '1'); location.reload()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Remove the key to go back to normal. This is only for looking at the plugin
|
|
58
|
+
itself; nothing in day-to-day use needs it.
|
|
59
|
+
|
|
60
|
+
## What it is not
|
|
61
|
+
|
|
62
|
+
Not authentication. The plugin does not add a password and does not widen what the server accepts — the harness answers those same calls with or without it. If your harness is reachable by other people, put a real gate in front of it (HTTP auth in your reverse proxy, or a VPN); a plugin cannot do that job, because the web server service hands plugins their own routes and no way to intercept anyone else's.
|
|
63
|
+
|
|
64
|
+
## Structure
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
dsh-lanmode/
|
|
68
|
+
├── package.json # dsh bundle/plugin metadata
|
|
69
|
+
├── cordis.patch.yml # bundle layer: inserts the plugin row
|
|
70
|
+
├── lib/index.js # host: nothing but a log line — the work is in the browser
|
|
71
|
+
├── lib/client.js # browser: the settings mirror and namespace scopes
|
|
72
|
+
├── README.md
|
|
73
|
+
└── LICENSE # MIT
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# dsh-lanmode bundle layer: applied automatically when the package is installed
|
|
2
|
+
# as a profile bundle (package.json declares dsh.bundle.patch).
|
|
3
|
+
#
|
|
4
|
+
# `name` must stay the full npm package name: the client-modules registry
|
|
5
|
+
# resolves the browser bundle by the loader entry name, so a shortened name
|
|
6
|
+
# leaves the UI half silently out of window.__DSH_BOOT__ — and the UI half is
|
|
7
|
+
# the whole plugin here.
|
|
8
|
+
- insert:
|
|
9
|
+
- id: dsh-lanmode
|
|
10
|
+
name: '@goodandready/dsh-lanmode'
|
|
11
|
+
config: {}
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// dsh-lanmode — клиентская половина.
|
|
2
|
+
//
|
|
3
|
+
// Зачем это существует.
|
|
4
|
+
//
|
|
5
|
+
// Веб-интерфейс харнесса отключает настройки на любой странице, открытой не с
|
|
6
|
+
// localhost. Решение принимается в браузере по имени хоста:
|
|
7
|
+
//
|
|
8
|
+
// isLoopback: pageLocation === undefined || isLoopbackHostname(pageLocation.hostname)
|
|
9
|
+
//
|
|
10
|
+
// и дальше сервис настроек уходит в режим «памяти»: общее зеркало документа
|
|
11
|
+
// никогда не читается, каждый связанный раздел сразу получает статус
|
|
12
|
+
// "unavailable", а запись молча выбрасывается. Внешне это выглядит как пустые
|
|
13
|
+
// карточки настроек у всех плагинов сразу и пустая вкладка «Настройки
|
|
14
|
+
// плагинов» — при полностью исправном сервере.
|
|
15
|
+
//
|
|
16
|
+
// Сервер этого ограничения не разделяет: и чтение (settings.describe), и
|
|
17
|
+
// запись (settings.mutate) по сети работают штатно, если Origin совпадает с
|
|
18
|
+
// адресом страницы — то есть ровно в том случае, когда запрос шлёт сам
|
|
19
|
+
// интерфейс. Проверено на живом харнессе через обратный прокси.
|
|
20
|
+
//
|
|
21
|
+
// Поэтому плагин делает одно: на не-loopback странице поднимает собственный
|
|
22
|
+
// экземпляр той же механики поверх тех же вызовов и отдаёт его как службу.
|
|
23
|
+
// На loopback-странице он не делает ничего — там ядро справляется само.
|
|
24
|
+
|
|
25
|
+
window.__ModuleLoader__.load({
|
|
26
|
+
id: '@goodandready/dsh-lanmode',
|
|
27
|
+
factory: (require) => {
|
|
28
|
+
var module = { exports: {} }
|
|
29
|
+
var exports = module.exports
|
|
30
|
+
|
|
31
|
+
// ------------------------------------------------- зеркало документа
|
|
32
|
+
//
|
|
33
|
+
// Один читатель settings.describe на всю страницу: разделы выводятся из
|
|
34
|
+
// него, поэтому они не могут разойтись во мнении о документе.
|
|
35
|
+
function createMirror(api) {
|
|
36
|
+
let snapshot = { status: 'idle', view: undefined, error: null }
|
|
37
|
+
const listeners = new Set()
|
|
38
|
+
let inFlight
|
|
39
|
+
let rerun = false
|
|
40
|
+
|
|
41
|
+
const notify = () => {
|
|
42
|
+
for (const listener of [...listeners]) {
|
|
43
|
+
try { listener() } catch (listenerFailure) { /* чужой слушатель нам не судья */ }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const put = (next) => { snapshot = next; notify() }
|
|
47
|
+
|
|
48
|
+
async function run() {
|
|
49
|
+
do {
|
|
50
|
+
rerun = false
|
|
51
|
+
let outcome
|
|
52
|
+
try {
|
|
53
|
+
const response = await api.settings.describe({})
|
|
54
|
+
outcome = response.result.ok
|
|
55
|
+
? { view: response.result.value }
|
|
56
|
+
: { failure: response.result.error.message }
|
|
57
|
+
} catch (wireFailure) {
|
|
58
|
+
outcome = { failure: String(wireFailure && wireFailure.message || wireFailure) }
|
|
59
|
+
}
|
|
60
|
+
if (outcome.view !== undefined) {
|
|
61
|
+
put({ status: 'ready', view: outcome.view, error: null })
|
|
62
|
+
} else {
|
|
63
|
+
// Держим то, что уже прочитали: неудачное обновление не должно
|
|
64
|
+
// опустошать готовые разделы.
|
|
65
|
+
put({ status: snapshot.view === undefined ? 'idle' : 'ready', view: snapshot.view, error: outcome.failure })
|
|
66
|
+
}
|
|
67
|
+
} while (rerun)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
getSnapshot: () => snapshot,
|
|
72
|
+
subscribe: (listener) => {
|
|
73
|
+
listeners.add(listener)
|
|
74
|
+
return () => { listeners.delete(listener) }
|
|
75
|
+
},
|
|
76
|
+
load() {
|
|
77
|
+
if (inFlight !== undefined) { rerun = true; return inFlight }
|
|
78
|
+
inFlight = run().finally(() => { inFlight = undefined })
|
|
79
|
+
return inFlight
|
|
80
|
+
},
|
|
81
|
+
ensure() {
|
|
82
|
+
if (inFlight !== undefined) return inFlight
|
|
83
|
+
if (snapshot.status === 'idle') return this.load()
|
|
84
|
+
return Promise.resolve()
|
|
85
|
+
},
|
|
86
|
+
// Ответ на запись возвращает свежий вид одного раздела — вкладываем его
|
|
87
|
+
// на место, чтобы не перечитывать весь документ ради одного поля.
|
|
88
|
+
acceptView(view) {
|
|
89
|
+
const before = snapshot
|
|
90
|
+
if (before.view === undefined) { this.load(); return }
|
|
91
|
+
const known = before.view.namespaces.some((row) => row.ns === view.ns)
|
|
92
|
+
const namespaces = known
|
|
93
|
+
? before.view.namespaces.map((row) => (row.ns === view.ns ? view : row))
|
|
94
|
+
: before.view.namespaces.concat([view])
|
|
95
|
+
put({ ...before, view: { ...before.view, namespaces } })
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ------------------------------------------------------- раздел настроек
|
|
101
|
+
//
|
|
102
|
+
// Снимок повторяет форму ядрового: status, value, base, user, revision,
|
|
103
|
+
// writable. Карточки читают именно эти поля, поэтому подмена для них
|
|
104
|
+
// незаметна.
|
|
105
|
+
function createScope(api, mirror, namespace) {
|
|
106
|
+
let snapshot = {
|
|
107
|
+
status: 'loading',
|
|
108
|
+
value: undefined,
|
|
109
|
+
base: undefined,
|
|
110
|
+
user: undefined,
|
|
111
|
+
revision: undefined,
|
|
112
|
+
writable: false,
|
|
113
|
+
mode: 'host',
|
|
114
|
+
}
|
|
115
|
+
const listeners = new Set()
|
|
116
|
+
const notify = () => {
|
|
117
|
+
for (const listener of [...listeners]) {
|
|
118
|
+
try { listener() } catch (listenerFailure) { /* см. выше */ }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function derive() {
|
|
123
|
+
const held = mirror.getSnapshot()
|
|
124
|
+
if (held.view === undefined) return
|
|
125
|
+
const row = held.view.namespaces.find((candidate) => candidate.ns === namespace)
|
|
126
|
+
if (row === undefined) {
|
|
127
|
+
// Хост про такой раздел не знает. Это законное состояние: плагин
|
|
128
|
+
// может быть ещё не применён.
|
|
129
|
+
snapshot = { ...snapshot, status: 'unavailable', writable: held.view.writable }
|
|
130
|
+
notify()
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
snapshot = {
|
|
134
|
+
status: 'ready',
|
|
135
|
+
value: row.value,
|
|
136
|
+
base: row.base,
|
|
137
|
+
user: row.user,
|
|
138
|
+
revision: row.revision,
|
|
139
|
+
writable: held.view.writable,
|
|
140
|
+
mode: 'host',
|
|
141
|
+
}
|
|
142
|
+
notify()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const off = mirror.subscribe(derive)
|
|
146
|
+
derive()
|
|
147
|
+
|
|
148
|
+
// Записи выстраиваем в очередь: правка ревизии, пришедшая из ответа,
|
|
149
|
+
// должна попасть в следующий запрос, иначе хост отвергнет его как
|
|
150
|
+
// устаревший.
|
|
151
|
+
let tail = Promise.resolve()
|
|
152
|
+
function write(op) {
|
|
153
|
+
const task = tail.then(async () => {
|
|
154
|
+
const revision = snapshot.revision
|
|
155
|
+
let response
|
|
156
|
+
try {
|
|
157
|
+
response = await api.settings.mutate({
|
|
158
|
+
ns: namespace,
|
|
159
|
+
ops: [op],
|
|
160
|
+
...(revision === undefined ? {} : { expectedRevision: revision }),
|
|
161
|
+
})
|
|
162
|
+
} catch (wireFailure) {
|
|
163
|
+
await mirror.load()
|
|
164
|
+
throw wireFailure
|
|
165
|
+
}
|
|
166
|
+
if (!response.result.ok) {
|
|
167
|
+
// Чаще всего это разошедшаяся ревизия: перечитываем и отдаём
|
|
168
|
+
// ошибку наверх, чтобы карточка показала неудачу сохранения.
|
|
169
|
+
await mirror.load()
|
|
170
|
+
throw new Error(response.result.error.message)
|
|
171
|
+
}
|
|
172
|
+
mirror.acceptView(response.result.value)
|
|
173
|
+
})
|
|
174
|
+
tail = task.catch(() => {})
|
|
175
|
+
return task
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
getSnapshot: () => snapshot,
|
|
180
|
+
subscribe: (listener) => {
|
|
181
|
+
listeners.add(listener)
|
|
182
|
+
return () => { listeners.delete(listener) }
|
|
183
|
+
},
|
|
184
|
+
set: (field, value) => write({ op: 'set', path: [field], value }),
|
|
185
|
+
unset: (field) => write({ op: 'unset', path: [field] }),
|
|
186
|
+
dispose: () => { off() },
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
exports.inject = ['connection']
|
|
191
|
+
|
|
192
|
+
// Отладочный переключатель: включает режим и на loopback-странице, где
|
|
193
|
+
// ядро и без нас справляется. Нужен, чтобы проверить сам плагин там, где
|
|
194
|
+
// это удобно сделать:
|
|
195
|
+
//
|
|
196
|
+
// localStorage.setItem('dsh-lanmode:force', '1'); location.reload()
|
|
197
|
+
//
|
|
198
|
+
// В обычной работе не нужен и по умолчанию выключен.
|
|
199
|
+
function forcedOn() {
|
|
200
|
+
try { return window.localStorage.getItem('dsh-lanmode:force') === '1' }
|
|
201
|
+
catch (noStorage) { return false }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
exports.apply = function apply(ctx) {
|
|
205
|
+
const connection = ctx.get('connection')
|
|
206
|
+
|
|
207
|
+
// На loopback-странице ядро работает штатно, и вмешиваться незачем.
|
|
208
|
+
if (connection.isLoopback && !forcedOn()) return
|
|
209
|
+
|
|
210
|
+
const mirror = createMirror(connection.api)
|
|
211
|
+
const binder = {
|
|
212
|
+
bind: (spec) => createScope(connection.api, mirror, spec && spec.namespace),
|
|
213
|
+
describe: () => mirror,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Документ меняют не только из этой вкладки; ядро слушает тот же сигнал.
|
|
217
|
+
try {
|
|
218
|
+
const remote = ctx.get('remote')
|
|
219
|
+
if (remote && typeof remote.$on === 'function') {
|
|
220
|
+
ctx.effect(() => remote.$on('settings/document-updated', () => { mirror.load() }),
|
|
221
|
+
'dsh-lanmode: перечитывать документ настроек по сигналу хоста')
|
|
222
|
+
}
|
|
223
|
+
} catch (noRemoteService) { /* без сигнала обойдёмся, останется ручное перечитывание */ }
|
|
224
|
+
|
|
225
|
+
mirror.ensure()
|
|
226
|
+
|
|
227
|
+
// Своя служба: ею может пользоваться любой плагин, которому нужны
|
|
228
|
+
// настройки по сети.
|
|
229
|
+
ctx.provide('lanSettings', binder)
|
|
230
|
+
|
|
231
|
+
// Попытка встать на место ядровой службы. Если разрешено — оживает весь
|
|
232
|
+
// интерфейс настроек разом, включая чужие плагины и вкладку ядра.
|
|
233
|
+
// Если ядро не отдаёт имя, остаёмся со своей службой: наши плагины
|
|
234
|
+
// умеют её спрашивать.
|
|
235
|
+
let tookOver = false
|
|
236
|
+
try {
|
|
237
|
+
ctx.provide('settingsScope', binder)
|
|
238
|
+
tookOver = true
|
|
239
|
+
} catch (nameTaken) {
|
|
240
|
+
tookOver = false
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Одна строка в консоли: без неё непонятно, работает режим или нет.
|
|
244
|
+
try {
|
|
245
|
+
console.info('[dsh-lanmode] режим включён (' + location.hostname + (connection.isLoopback ? ', принудительно' : '') + '); '
|
|
246
|
+
+ 'настройки подняты через сеть, ядровая служба '
|
|
247
|
+
+ (tookOver ? 'подменена' : 'оставлена как есть'))
|
|
248
|
+
} catch (noConsole) { /* незачем */ }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return module.exports
|
|
252
|
+
},
|
|
253
|
+
})
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// dsh-lanmode — хост-половина.
|
|
2
|
+
//
|
|
3
|
+
// Работа плагина целиком в браузере: ограничение, которое он снимает, живёт
|
|
4
|
+
// в клиентском коде харнесса, а сервер и без него отдаёт настройки по сети.
|
|
5
|
+
// Хост-половина существует потому, что запись в дереве плагинов — это модуль
|
|
6
|
+
// на хосте; она ничего не регистрирует и ничего не слушает.
|
|
7
|
+
//
|
|
8
|
+
// Единственное, что здесь есть, — строчка в журнале при запуске, чтобы по
|
|
9
|
+
// логу было видно, что плагин установлен.
|
|
10
|
+
|
|
11
|
+
export const name = 'dsh-lanmode'
|
|
12
|
+
export const inject = []
|
|
13
|
+
|
|
14
|
+
export function apply(ctx) {
|
|
15
|
+
ctx.effect(() => {
|
|
16
|
+
// eslint-disable-next-line no-console
|
|
17
|
+
console.info('[dsh-lanmode] установлен: настройки будут работать и на страницах, открытых не с localhost')
|
|
18
|
+
return () => {}
|
|
19
|
+
}, 'dsh-lanmode: отметка о запуске')
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goodandready/dsh-lanmode",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Settings over the LAN for DeepSeek Harness: the Web UI turns the settings service off on any page that is not localhost, even though the server serves settings over the network just fine. This plugin brings them back.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./client": "./lib/client.js",
|
|
11
|
+
"./package.json": "./package.json",
|
|
12
|
+
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib/",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"dsh",
|
|
22
|
+
"dsh-plugin",
|
|
23
|
+
"deepseek-harness",
|
|
24
|
+
"settings",
|
|
25
|
+
"lan",
|
|
26
|
+
"remote"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/GooDAnDReaDY/dsh-lanmode.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/GooDAnDReaDY/dsh-lanmode",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/GooDAnDReaDY/dsh-lanmode/issues"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"test": "node --test test/*.test.mjs"
|
|
38
|
+
},
|
|
39
|
+
"dsh": {
|
|
40
|
+
"bundle": {
|
|
41
|
+
"patch": "./cordis.patch.yml"
|
|
42
|
+
},
|
|
43
|
+
"client": {
|
|
44
|
+
"platform": "web",
|
|
45
|
+
"inject": [
|
|
46
|
+
"@deepseek-ai/dsh-client-runtime"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
52
|
+
}
|
|
53
|
+
}
|