dsh-cline-pass 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.en.md +20 -2
- package/README.md +20 -2
- package/lib/adapter.js +98 -1
- package/lib/client.js +151 -17
- package/lib/index.js +44 -2
- package/lib/panel.js +28 -2
- package/package.json +9 -9
package/README.en.md
CHANGED
|
@@ -13,7 +13,7 @@ Connect [Cline Pass](https://cline.bot/cline-pass) subscription models to the [D
|
|
|
13
13
|
|
|
14
14
|
## Requirements
|
|
15
15
|
|
|
16
|
-
- dsh `>= 0.1.2-alpha.3 < 0.
|
|
16
|
+
- dsh `>= 0.1.2-alpha.3 < 0.3.0`
|
|
17
17
|
- Node.js `>= 20.3`
|
|
18
18
|
- A Cline Pass API key
|
|
19
19
|
|
|
@@ -68,7 +68,25 @@ To override defaults, add a `cline-pass` section to your profile's `$DSH_HOME/pr
|
|
|
68
68
|
apiKeyEnv: CLINE_PASS_BACKUP_KEY
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
Common settings include `baseURL`, `knownModels`, `models`, `perModel`, `exposeCatalog`, and `historyLimit`. The default gateway is `https://api.cline.bot/api/v1`.
|
|
71
|
+
Common settings include `baseURL`, `knownModels`, `models`, `perModel`, `exposeTools`, `exposeCatalog`, and `historyLimit`. The default gateway is `https://api.cline.bot/api/v1`. `maxConcurrentRequests` caps how many streams each account keeps open to the gateway at once (default 16); further requests wait for a free slot, and `0` lifts the cap.
|
|
72
|
+
|
|
73
|
+
### Management tools
|
|
74
|
+
|
|
75
|
+
The `cline_pass_*` tools are **not registered by default**. Their eight definitions total roughly 5.5k characters of schema and description that ride in every request's tool list, and managing the route is an occasional need, so the default keeps that cost out of every conversation.
|
|
76
|
+
|
|
77
|
+
Turn them on with **Expose the tools (takes effect after a restart)** in **Settings → Cline Pass**, or in the profile configuration:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
- id: cline-pass
|
|
81
|
+
config:
|
|
82
|
+
exposeTools: true
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**The value is read when the plugin activates, so a change needs a dsh restart** — a tool already registered cannot be pulled back out of a conversation in progress.
|
|
86
|
+
|
|
87
|
+
The provider route and the setup panel are unaffected — the panel is a browser page rather than a tool, and it stays available with the tools off.
|
|
88
|
+
|
|
89
|
+
> A profile upgraded from 0.2.1 or earlier has no `exposeTools` key, so it adopts the new default of `false` and its `cline_pass_*` tools stop being registered. The panel reports this; tick the checkbox and restart to bring them back.
|
|
72
90
|
|
|
73
91
|
## Upstream channels
|
|
74
92
|
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
## 要求
|
|
15
15
|
|
|
16
|
-
- dsh `>= 0.1.2-alpha.3`
|
|
16
|
+
- dsh `>= 0.1.2-alpha.3 < 0.3.0`
|
|
17
17
|
- Node.js `>= 20.3`
|
|
18
18
|
- Cline Pass API Key
|
|
19
19
|
|
|
@@ -68,7 +68,25 @@ cline_pass_accounts action=mode mode=roundrobin
|
|
|
68
68
|
apiKeyEnv: CLINE_PASS_BACKUP_KEY
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
### 管理工具
|
|
72
|
+
|
|
73
|
+
`cline_pass_*` 工具**默认不注册**。8 个工具定义合计约 5.5k 字符,会出现在每次请求的工具列表里,而管理路由只是偶尔需要,所以默认让所有对话都不必付这份开销。
|
|
74
|
+
|
|
75
|
+
需要时在**设置 → Cline Pass** 中勾选「开启工具注入(重启后生效)」,或在 profile 配置里打开:
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
- id: cline-pass
|
|
79
|
+
config:
|
|
80
|
+
exposeTools: true
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**该值在插件激活时读取,改动后需重启 dsh 才生效** —— 已经注册的工具无法从进行中的对话里撤回。
|
|
84
|
+
|
|
85
|
+
提供商路由和设置面板不受影响 —— 面板是浏览器页面而非工具,工具关闭时仍然可用。
|
|
86
|
+
|
|
87
|
+
> 从 0.2.1 及更早版本升级的 profile 里没有 `exposeTools` 键,因此会采用新的默认值 `false`:升级后 `cline_pass_*` 工具将不再注册。面板会提示,勾选该复选框并在重启后恢复。
|
|
88
|
+
|
|
89
|
+
常用设置包括 `baseURL`、`knownModels`、`models`、`perModel`、`exposeCatalog` 和 `historyLimit`。默认网关地址为 `https://api.cline.bot/api/v1`。`maxConcurrentRequests` 限制每个账号同时发往网关的流数,默认 16,超出的请求排队等待空位,设为 `0` 表示不限。
|
|
72
90
|
|
|
73
91
|
## 上游渠道
|
|
74
92
|
|
package/lib/adapter.js
CHANGED
|
@@ -38,6 +38,15 @@ import { parseServerSentEvents } from './engine.js'
|
|
|
38
38
|
|
|
39
39
|
/** Default maximum idle interval while a stream read is outstanding. */
|
|
40
40
|
export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300000
|
|
41
|
+
/**
|
|
42
|
+
* Default number of streams one account keeps open to the gateway at once.
|
|
43
|
+
*
|
|
44
|
+
* Twice dsh 0.1.7's default `maxActiveSubagents` (8): a main agent with a full
|
|
45
|
+
* set of subagents, plus a title or compaction call, never queues, while a
|
|
46
|
+
* runaway fan-out still cannot open an unbounded number of upstream streams
|
|
47
|
+
* (CWE-770). `0` lifts the cap.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_MAX_CONCURRENT_REQUESTS = 16
|
|
41
50
|
/**
|
|
42
51
|
* Conservative context capacity for a model the catalog does not describe.
|
|
43
52
|
* Shipped models carry their published window, so this only bounds the
|
|
@@ -558,6 +567,81 @@ async function* tapRouting(payloads, observed) {
|
|
|
558
567
|
}
|
|
559
568
|
}
|
|
560
569
|
|
|
570
|
+
//#region upstream concurrency
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Per-account slots for streams open to the gateway.
|
|
574
|
+
*
|
|
575
|
+
* A slot is held for a whole attempt — from before `fetch` until the response
|
|
576
|
+
* body is closed — because the stream, not the wait for headers, is what keeps
|
|
577
|
+
* a socket and the account's concurrency busy. Accounts are counted apart, so a
|
|
578
|
+
* stalled account cannot hold up requests routed to a healthy one; absorbing
|
|
579
|
+
* exactly that is what the pool is for.
|
|
580
|
+
*
|
|
581
|
+
* Waiters are served first in, first out, and a freed slot is handed to the
|
|
582
|
+
* next waiter and counted in the same step, so a request arriving in between
|
|
583
|
+
* cannot take it as well. A waiter whose signal aborts leaves the queue at
|
|
584
|
+
* once; a held slot is also freed when its signal aborts, so a consumer that
|
|
585
|
+
* stops reading without closing the stream does not keep it.
|
|
586
|
+
*
|
|
587
|
+
* @returns `{ acquire(key, limit, signal) }`, resolving to a release function
|
|
588
|
+
* that is safe to call more than once. A `limit` below 1 means no limit.
|
|
589
|
+
*/
|
|
590
|
+
export function createUpstreamSlots() {
|
|
591
|
+
const pools = new Map()
|
|
592
|
+
const admit = (pool) => {
|
|
593
|
+
while (pool.waiting.length > 0 && pool.active < pool.limit) {
|
|
594
|
+
pool.active += 1
|
|
595
|
+
pool.waiting.shift()()
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const hold = (pool, signal) => {
|
|
599
|
+
let held = true
|
|
600
|
+
const release = () => {
|
|
601
|
+
if (!held) return
|
|
602
|
+
held = false
|
|
603
|
+
signal?.removeEventListener('abort', release)
|
|
604
|
+
pool.active -= 1
|
|
605
|
+
admit(pool)
|
|
606
|
+
}
|
|
607
|
+
signal?.addEventListener('abort', release, { once: true })
|
|
608
|
+
return release
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
async acquire(key, limit, signal) {
|
|
612
|
+
signal?.throwIfAborted()
|
|
613
|
+
let pool = pools.get(key)
|
|
614
|
+
if (pool === undefined) {
|
|
615
|
+
pool = { active: 0, limit: Infinity, waiting: [] }
|
|
616
|
+
pools.set(key, pool)
|
|
617
|
+
}
|
|
618
|
+
// Read on every call, so a changed setting reaches the next request; a
|
|
619
|
+
// raised limit admits the queue before this caller joins it.
|
|
620
|
+
pool.limit = Number.isFinite(limit) && limit >= 1 ? Math.floor(limit) : Infinity
|
|
621
|
+
admit(pool)
|
|
622
|
+
if (pool.waiting.length === 0 && pool.active < pool.limit) {
|
|
623
|
+
pool.active += 1
|
|
624
|
+
return hold(pool, signal)
|
|
625
|
+
}
|
|
626
|
+
return await new Promise((resolve, reject) => {
|
|
627
|
+
const grant = () => {
|
|
628
|
+
signal?.removeEventListener('abort', abandon)
|
|
629
|
+
resolve(hold(pool, signal))
|
|
630
|
+
}
|
|
631
|
+
const abandon = () => {
|
|
632
|
+
const at = pool.waiting.indexOf(grant)
|
|
633
|
+
if (at !== -1) pool.waiting.splice(at, 1)
|
|
634
|
+
reject(signal.reason)
|
|
635
|
+
}
|
|
636
|
+
pool.waiting.push(grant)
|
|
637
|
+
signal?.addEventListener('abort', abandon, { once: true })
|
|
638
|
+
})
|
|
639
|
+
},
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
//#endregion
|
|
644
|
+
|
|
561
645
|
/**
|
|
562
646
|
* The Cline Pass adapter. One instance serves the plugin's provider route(s);
|
|
563
647
|
* the harness model id IS the gateway's wire model id.
|
|
@@ -566,6 +650,7 @@ export class ClinePassAdapter extends LlmAdapter {
|
|
|
566
650
|
constructor(config) {
|
|
567
651
|
super()
|
|
568
652
|
this.config = config
|
|
653
|
+
this.upstreamSlots = createUpstreamSlots()
|
|
569
654
|
}
|
|
570
655
|
|
|
571
656
|
providerInfo(provider) {
|
|
@@ -668,9 +753,18 @@ export class ClinePassAdapter extends LlmAdapter {
|
|
|
668
753
|
const account = await this.config.resolveAccount({ exclude: [...refused] })
|
|
669
754
|
if (account === undefined) break
|
|
670
755
|
const body = injectPrefs(buildRequestBody(options, connection, images), meta, attempt)
|
|
671
|
-
const attemptStarted = Date.now()
|
|
672
756
|
const consumer = new AbortController()
|
|
673
757
|
const upstream = options.signal === undefined ? consumer.signal : AbortSignal.any([options.signal, consumer.signal])
|
|
758
|
+
// The account's slot is taken before the attempt's clock starts: the wait
|
|
759
|
+
// is local, and counting it would teach the channel a latency it never had.
|
|
760
|
+
let release
|
|
761
|
+
try {
|
|
762
|
+
release = await this.upstreamSlots.acquire(account.name, connection.maxConcurrentRequests ?? DEFAULT_MAX_CONCURRENT_REQUESTS, upstream)
|
|
763
|
+
} catch (error) {
|
|
764
|
+
if (options.signal?.aborted) throw new LlmError('Cline Pass request aborted by caller', 'ABORTED', { cause: error })
|
|
765
|
+
throw error
|
|
766
|
+
}
|
|
767
|
+
const attemptStarted = Date.now()
|
|
674
768
|
const watchdog = idleWatchdog(upstream, connection.streamIdleTimeoutMs, STREAM_IDLE_TIMEOUT_CODE)
|
|
675
769
|
try {
|
|
676
770
|
let response
|
|
@@ -847,6 +941,9 @@ export class ClinePassAdapter extends LlmAdapter {
|
|
|
847
941
|
if (attempt.upstream !== null && adherence === 'adopted') this.config.learnUpstream(options.model, attempt.upstream, 'ok', '', Date.now() - attemptStarted)
|
|
848
942
|
return
|
|
849
943
|
} finally {
|
|
944
|
+
// Held until the attempt ends, response body included, so the slot
|
|
945
|
+
// bounds open streams rather than requests waiting for headers.
|
|
946
|
+
release()
|
|
850
947
|
watchdog[Symbol.dispose]()
|
|
851
948
|
}
|
|
852
949
|
}
|
package/lib/client.js
CHANGED
|
@@ -119,16 +119,43 @@ window.__ModuleLoader__.load({
|
|
|
119
119
|
// track is resolved from that row's own content, so a row whose upstream
|
|
120
120
|
// is a long pill and a row whose upstream is "(auto)" end up with
|
|
121
121
|
// different column widths — and nothing lines up with the header.
|
|
122
|
-
|
|
122
|
+
//
|
|
123
|
+
// `table-layout:auto`, i.e. the browser's own algorithm, and no `min-width`:
|
|
124
|
+
// the table always fits its container, so the log never needs sideways
|
|
125
|
+
// scrolling, and the sizing decision is left to the engine that knows each
|
|
126
|
+
// cell's real content width.
|
|
127
|
+
//
|
|
128
|
+
// That decision matters because the content genuinely does not fit. Measured
|
|
129
|
+
// at this card's real width (about 490px of table inside the settings card,
|
|
130
|
+
// which caps at 760px): the columns need roughly 85 + 208 + 234 + 111 =
|
|
131
|
+
// 638px, so something has to give. Under `table-layout:fixed` plus shares,
|
|
132
|
+
// what gave was the right-hand figures — they were ellipsised to "13…" and
|
|
133
|
+
// "4…", losing the number the reader came for. Left to itself the engine
|
|
134
|
+
// gives way on the token column instead, which simply wraps to a second
|
|
135
|
+
// line: the figures stay whole and nothing is lost.
|
|
136
|
+
'.cp-history-table{width:100%;border-collapse:collapse;table-layout:auto}',
|
|
123
137
|
// Header and data share one alignment: a table whose labels sit left of
|
|
124
138
|
// their values is the thing this panel keeps getting asked to fix.
|
|
125
139
|
'.cp-history-table th{position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-layer-1);text-align:center;font-weight:400;font-size:11px;color:var(--dsw-alias-label-tertiary);padding:2px 0 6px;border-bottom:0.5px solid var(--dsw-alias-border-l3)}',
|
|
126
140
|
'.cp-history-table td{padding:6px 0;border-bottom:0.5px solid var(--dsw-alias-border-l2);font-size:12px;vertical-align:middle;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
127
141
|
'.cp-history-table tbody tr:hover td{background:var(--dsw-alias-bg-layer-2)}',
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
142
|
+
// Only two columns are pinned, and each to its measured need:
|
|
143
|
+
// 点 16px — the status dot
|
|
144
|
+
// 时间 82px — a clock, or a dated stamp like "09-24 04:48" (needs 85px,
|
|
145
|
+
// but the engine measures a little differently than the
|
|
146
|
+
// intrinsic probe; 82px is where the stamp stops clipping)
|
|
147
|
+
// TOKEN and 延迟 are left to the engine. 延迟 holds single-line figures, so
|
|
148
|
+
// it always gets what it needs — which is the point: previously the model
|
|
149
|
+
// column took every spare pixel while the figures on the right were clipped,
|
|
150
|
+
// and that is the "left too loose, right too tight" shape being fixed.
|
|
151
|
+
'.cp-history-col-dot{width:16px}',
|
|
152
|
+
'.cp-history-col-when{width:82px}',
|
|
153
|
+
// The columns used to sit flush against one another at `padding:0`, which is
|
|
154
|
+
// what made the right-hand figures read as cramped. A little padding on each
|
|
155
|
+
// side of the three content columns separates label from neighbour.
|
|
156
|
+
'.cp-history-table th:nth-child(2),.cp-history-table td:nth-child(2){padding-left:10px;padding-right:10px}',
|
|
157
|
+
'.cp-history-table th:nth-child(4),.cp-history-table td:nth-child(4){padding-left:10px;padding-right:10px}',
|
|
158
|
+
'.cp-history-table th:nth-child(5),.cp-history-table td:nth-child(5){padding-left:10px;padding-right:10px}',
|
|
132
159
|
'.cp-history-when{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}',
|
|
133
160
|
'.cp-history-model-cell{min-width:0}',
|
|
134
161
|
// The flex stack lives INSIDE the cell, never on the `<td>` itself: a td
|
|
@@ -142,7 +169,20 @@ window.__ModuleLoader__.load({
|
|
|
142
169
|
'.cp-history-table td.cp-history-tokens{color:var(--dsw-alias-label-secondary);font-size:11.5px;font-variant-numeric:tabular-nums;white-space:normal;overflow:visible;text-overflow:clip;line-height:1.5}',
|
|
143
170
|
// Three stacked figures read like a request log: label left, value right.
|
|
144
171
|
'.cp-history-table td.cp-history-load{font-variant-numeric:tabular-nums;white-space:normal;overflow:visible;text-overflow:clip}',
|
|
145
|
-
|
|
172
|
+
// `space-between` is what keeps the three figures right-aligned with each
|
|
173
|
+
// other across rows, but it also turns every spare pixel of the column into
|
|
174
|
+
// a gap between label and value. Capping the row and centring it makes that
|
|
175
|
+
// surplus invisible instead, so the column can be given generous width at a
|
|
176
|
+
// wide panel without the figures drifting apart.
|
|
177
|
+
'.cp-history-load-row{display:flex;justify-content:space-between;gap:5px;max-width:112px;margin:0 auto;font-size:11.5px;line-height:1.5}',
|
|
178
|
+
// With the table no longer allowed to scroll sideways, a narrow pane can
|
|
179
|
+
// leave this column short of its need. The label must not wrap then — a
|
|
180
|
+
// taller cell would change the row height — so the label stays whole and
|
|
181
|
+
// the figure is the part that gives way, ellipsised with its exact value
|
|
182
|
+
// still available in the row tooltip.
|
|
183
|
+
'.cp-history-load-row>span{white-space:nowrap}',
|
|
184
|
+
'.cp-history-load-row>span:first-child{flex:none}',
|
|
185
|
+
'.cp-history-load-row>span:last-child{min-width:0;overflow:hidden;text-overflow:ellipsis;text-align:right}',
|
|
146
186
|
'.cp-history-meta{display:flex;align-items:center;justify-content:center;gap:6px;min-width:0;overflow:hidden}',
|
|
147
187
|
// A failure gets its own row under the entry rather than widening it: the
|
|
148
188
|
// gateway's messages are long enough to wreck a one-line layout.
|
|
@@ -173,8 +213,15 @@ window.__ModuleLoader__.load({
|
|
|
173
213
|
'.cp-fold:hover .cp-chevron{color:var(--dsw-alias-label-primary)}',
|
|
174
214
|
'.cp-set-body{border-top:0.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding:12px 0 8px}',
|
|
175
215
|
'.cp-usage-groups{display:flex;flex-direction:column;gap:12px}',
|
|
176
|
-
|
|
177
|
-
|
|
216
|
+
// A pool shows one account at a time: the arrows are the only way to reach
|
|
217
|
+
// the others, so they sit on the same row as the name they act on.
|
|
218
|
+
'.cp-usage-pager{display:flex;align-items:center;gap:8px}',
|
|
219
|
+
'.cp-usage-nav{box-sizing:border-box;width:24px;height:24px;flex:none;padding:0;border:0.5px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:14px;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}',
|
|
220
|
+
'.cp-usage-nav:hover{border-color:var(--dsw-alias-brand-primary)}',
|
|
221
|
+
'.cp-usage-name{display:flex;align-items:center;gap:8px;min-width:0}',
|
|
222
|
+
'.cp-usage-acct{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
223
|
+
'.cp-usage-active{border:0.5px solid var(--dsw-alias-brand-primary);border-radius:999px;padding:1px 7px;font-size:11px;font-weight:600;flex:none}',
|
|
224
|
+
'.cp-usage-pos{font-size:12px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary)}',
|
|
178
225
|
'.cp-usage{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px}',
|
|
179
226
|
'.cp-usage-item{border:0.5px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:8px}',
|
|
180
227
|
'.cp-usage-head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}',
|
|
@@ -193,6 +240,7 @@ window.__ModuleLoader__.load({
|
|
|
193
240
|
'.cp-usage-model-cost{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary)}',
|
|
194
241
|
'.cp-usage-model-tokens{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary);min-width:52px;text-align:right}',
|
|
195
242
|
'.cp-check{width:14px;height:14px;flex:none;cursor:pointer;accent-color:var(--dsw-alias-brand-primary)}',
|
|
243
|
+
'.cp-tools-label{cursor:pointer;gap:8px}',
|
|
196
244
|
'.cp-model-hidden .cp-mono,.cp-model-hidden .cp-check{opacity:.55}',
|
|
197
245
|
].join('')
|
|
198
246
|
|
|
@@ -262,6 +310,9 @@ window.__ModuleLoader__.load({
|
|
|
262
310
|
usageUnavailable: '暂时读不到官方额度。',
|
|
263
311
|
usageEmpty: '官方未返回额度窗口。',
|
|
264
312
|
usageAccount: '账号 {name}',
|
|
313
|
+
usagePrev: '上一个账号',
|
|
314
|
+
usageNext: '下一个账号',
|
|
315
|
+
usagePosition: '第 {i} / {n} 个',
|
|
265
316
|
historyWhen: '时间',
|
|
266
317
|
historyModel: '模型',
|
|
267
318
|
historyUpstream: '上游',
|
|
@@ -286,6 +337,7 @@ window.__ModuleLoader__.load({
|
|
|
286
337
|
noHistory: '暂无记录。',
|
|
287
338
|
busy: '处理中…',
|
|
288
339
|
settingsUnavailable: '设置服务不可用,改动无法保存。',
|
|
340
|
+
toolsExpose: '开启工具注入(重启后生效)',
|
|
289
341
|
retry: '重试',
|
|
290
342
|
}
|
|
291
343
|
const EN = {
|
|
@@ -341,6 +393,9 @@ window.__ModuleLoader__.load({
|
|
|
341
393
|
usageUnavailable: 'The official quota is unavailable right now.',
|
|
342
394
|
usageEmpty: 'The gateway reported no quota windows.',
|
|
343
395
|
usageAccount: 'account {name}',
|
|
396
|
+
usagePrev: 'Previous account',
|
|
397
|
+
usageNext: 'Next account',
|
|
398
|
+
usagePosition: '{i} of {n}',
|
|
344
399
|
historyWhen: 'Time',
|
|
345
400
|
historyModel: 'Model',
|
|
346
401
|
historyUpstream: 'Upstream',
|
|
@@ -365,6 +420,7 @@ window.__ModuleLoader__.load({
|
|
|
365
420
|
noHistory: 'Nothing recorded.',
|
|
366
421
|
busy: 'Working…',
|
|
367
422
|
settingsUnavailable: 'Settings are read-only; changes will not persist.',
|
|
423
|
+
toolsExpose: 'Expose the tools (takes effect after a restart)',
|
|
368
424
|
retry: 'Retry',
|
|
369
425
|
}
|
|
370
426
|
|
|
@@ -644,6 +700,7 @@ window.__ModuleLoader__.load({
|
|
|
644
700
|
setAccountEnabled: (name, enabled) => controller.setAccountEnabled(name, enabled),
|
|
645
701
|
pinModel: (payload) => controller.pinModel(payload),
|
|
646
702
|
setModelVisible: (model, visible) => controller.setModelVisible(model, visible),
|
|
703
|
+
setToolsExposed: (value) => controller.setToolsExposed(value),
|
|
647
704
|
setModelsVisibility: (mode) => controller.setModelsVisibility(mode),
|
|
648
705
|
probeModel: (model) => controller.probeModel(model),
|
|
649
706
|
validateModel: (model) => controller.validateModel(model),
|
|
@@ -819,6 +876,21 @@ window.__ModuleLoader__.load({
|
|
|
819
876
|
return result
|
|
820
877
|
}
|
|
821
878
|
|
|
879
|
+
/**
|
|
880
|
+
* Turn the `cline_pass_*` tools on or off.
|
|
881
|
+
*
|
|
882
|
+
* The host reads the setting when the plugin activates, so this cannot
|
|
883
|
+
* change the running plugin: a tool already registered cannot be pulled
|
|
884
|
+
* back out of a conversation's tool list. The host says as much in its
|
|
885
|
+
* reply, and the panel relays it rather than implying the switch already
|
|
886
|
+
* took effect.
|
|
887
|
+
*/
|
|
888
|
+
async setToolsExposed(value) {
|
|
889
|
+
const result = await this.run('tools', async () => await this.call('tools.expose', { value }))
|
|
890
|
+
if (result !== undefined) this.applyState(result)
|
|
891
|
+
return result
|
|
892
|
+
}
|
|
893
|
+
|
|
822
894
|
/**
|
|
823
895
|
* Show, hide or swap every subscription model at once.
|
|
824
896
|
*
|
|
@@ -1443,10 +1515,17 @@ window.__ModuleLoader__.load({
|
|
|
1443
1515
|
* One reading per window: the official percentage, plus when it resets.
|
|
1444
1516
|
*/
|
|
1445
1517
|
function UsageCard(props) {
|
|
1446
|
-
const { t, usage, disabled, actions, unavailable } = props
|
|
1518
|
+
const { t, usage, disabled, actions, unavailable, activeAccount, accountMode } = props
|
|
1447
1519
|
const fetched = Number(usage?.fetchedAt ?? 0) > 0
|
|
1448
1520
|
const accounts = usage?.accounts ?? []
|
|
1449
|
-
|
|
1521
|
+
// A pool is shown one account at a time. Stacking every account turned the
|
|
1522
|
+
// card into a wall of bars and pushed the one reading a user came for below
|
|
1523
|
+
// the fold, so the arrows — not a scroll — are what moves between accounts.
|
|
1524
|
+
const [picked, setPicked] = React.useState(0)
|
|
1525
|
+
// The selection is an index, so a shorter list — an account removed, or its
|
|
1526
|
+
// key deleted — can leave it past the end. Clamp instead of rendering nothing.
|
|
1527
|
+
const index = accounts.length === 0 ? 0 : Math.min(Math.max(picked, 0), accounts.length - 1)
|
|
1528
|
+
const step = (delta) => setPicked((accounts.length + index + delta) % accounts.length)
|
|
1450
1529
|
const windowRow = (type, key, limit) => {
|
|
1451
1530
|
const percent = Math.max(0, Math.min(100, Number(limit.percentUsed ?? 0)))
|
|
1452
1531
|
const left = countdown(t, limit.resetsAt)
|
|
@@ -1464,8 +1543,14 @@ window.__ModuleLoader__.load({
|
|
|
1464
1543
|
left === '' ? null : h('div', { key: 'note', className: 'cp-usage-note' }, fill(t('usageResetsIn'), { t: left })),
|
|
1465
1544
|
])
|
|
1466
1545
|
}
|
|
1467
|
-
/**
|
|
1468
|
-
|
|
1546
|
+
/**
|
|
1547
|
+
* One account's block: a name row that doubles as the account switcher,
|
|
1548
|
+
* then its windows.
|
|
1549
|
+
*
|
|
1550
|
+
* The arrows only exist in a pool. With a single account there is nowhere
|
|
1551
|
+
* to go, so they would be two controls that cannot do anything.
|
|
1552
|
+
*/
|
|
1553
|
+
const accountGroup = (account, at) => {
|
|
1469
1554
|
const limits = account.limits ?? []
|
|
1470
1555
|
const known = new Map(limits.map((limit) => [String(limit.type), limit]))
|
|
1471
1556
|
const windows = [
|
|
@@ -1479,15 +1564,41 @@ window.__ModuleLoader__.load({
|
|
|
1479
1564
|
: windows.length === 0
|
|
1480
1565
|
? h('div', { key: 'none', className: 'cp-muted' }, t('usageEmpty'))
|
|
1481
1566
|
: h('div', { key: 'bars', className: 'cp-usage' }, windows.map(([type, key, limit]) => windowRow(type, key, limit)))
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1567
|
+
const pooled = accounts.length > 1
|
|
1568
|
+
const label = account.displayName || account.account
|
|
1569
|
+
// Only a single-account pool has a "current" account: in round-robin every
|
|
1570
|
+
// request rotates, so a badge here would name a fact that keeps changing.
|
|
1571
|
+
const isActive = accountMode !== 'roundrobin' && activeAccount !== '' && account.account === activeAccount
|
|
1572
|
+
const head = pooled
|
|
1573
|
+
? h('div', { key: 'name', className: 'cp-usage-pager' }, [
|
|
1574
|
+
// Switching accounts re-reads nothing: the whole pool was read in one
|
|
1575
|
+
// request, so paging is local and stays available while the panel is
|
|
1576
|
+
// busy or the settings service is read-only.
|
|
1577
|
+
h('button', {
|
|
1578
|
+
key: 'prev', type: 'button', className: 'cp-usage-nav',
|
|
1579
|
+
title: t('usagePrev'), 'aria-label': t('usagePrev'),
|
|
1580
|
+
onClick: () => step(-1),
|
|
1581
|
+
}, '‹'),
|
|
1582
|
+
h('div', { key: 'who', className: 'cp-usage-name' }, [
|
|
1583
|
+
h('span', { key: 'n', className: 'cp-usage-acct', title: label }, label),
|
|
1584
|
+
isActive ? h('span', { key: 'a', className: 'cp-usage-active' }, t('accountsActive')) : null,
|
|
1585
|
+
h('span', { key: 'p', className: 'cp-usage-pos' }, fill(t('usagePosition'), { i: at + 1, n: accounts.length })),
|
|
1586
|
+
]),
|
|
1587
|
+
h('button', {
|
|
1588
|
+
key: 'next', type: 'button', className: 'cp-usage-nav',
|
|
1589
|
+
title: t('usageNext'), 'aria-label': t('usageNext'),
|
|
1590
|
+
onClick: () => step(1),
|
|
1591
|
+
}, '›'),
|
|
1592
|
+
])
|
|
1593
|
+
: null
|
|
1594
|
+
return h('div', { key: String(account.account), className: 'cp-usage-group' }, [head, inner])
|
|
1486
1595
|
}
|
|
1487
1596
|
let body
|
|
1488
1597
|
if (!fetched) body = h('div', { key: 'wait', className: 'cp-muted' }, unavailable === true ? t('usageUnavailable') : t('usageLoading'))
|
|
1489
1598
|
else if (accounts.length === 0) body = h('div', { key: 'none', className: 'cp-muted' }, t('usageEmpty'))
|
|
1490
|
-
|
|
1599
|
+
// `index` is the account on screen; its own key remounts the block so the
|
|
1600
|
+
// windows never animate between two accounts' readings.
|
|
1601
|
+
else body = h('div', { key: 'groups', className: 'cp-usage-groups' }, accountGroup(accounts[index], index))
|
|
1491
1602
|
const title = accounts.length > 1
|
|
1492
1603
|
? `${t('usageTitle')} · ${fill(t('accountsCount'), { n: accounts.length })}`
|
|
1493
1604
|
: `${t('usageTitle')}${accounts[0] ? ` · ${fill(t('usageAccount'), { name: accounts[0].account })}` : ''}`
|
|
@@ -1568,6 +1679,7 @@ window.__ModuleLoader__.load({
|
|
|
1568
1679
|
setAccountEnabled: props.setAccountEnabled,
|
|
1569
1680
|
pinModel: props.pinModel,
|
|
1570
1681
|
setModelVisible: props.setModelVisible,
|
|
1682
|
+
setToolsExposed: props.setToolsExposed,
|
|
1571
1683
|
setModelsVisibility: props.setModelsVisibility,
|
|
1572
1684
|
probeModel: props.probeModel,
|
|
1573
1685
|
validateModel: props.validateModel,
|
|
@@ -1607,6 +1719,13 @@ window.__ModuleLoader__.load({
|
|
|
1607
1719
|
key: 'usage',
|
|
1608
1720
|
t,
|
|
1609
1721
|
usage: data.usage,
|
|
1722
|
+
// Which account the pool would use, resolved the same way the accounts
|
|
1723
|
+
// card resolves it: an empty `activeAccount` means "first enabled", not
|
|
1724
|
+
// "none", so the badge has to name that account rather than nobody.
|
|
1725
|
+
activeAccount: data.activeAccount !== ''
|
|
1726
|
+
? data.activeAccount
|
|
1727
|
+
: (data.accounts.find((account) => account.enabled)?.key ?? ''),
|
|
1728
|
+
accountMode: data.accountMode,
|
|
1610
1729
|
unavailable: readFailed,
|
|
1611
1730
|
disabled,
|
|
1612
1731
|
actions,
|
|
@@ -1614,6 +1733,21 @@ window.__ModuleLoader__.load({
|
|
|
1614
1733
|
// Everything below the accounts card reads through the key that card
|
|
1615
1734
|
// stores, so none of it exists until there is one.
|
|
1616
1735
|
data === null || !ready ? null : h(ModelsCard, { key: 'models', t, data, disabled, busy: state.busy, actions }),
|
|
1736
|
+
// Route-wide switches last: they are not about one model or account, and
|
|
1737
|
+
// nothing below them depends on their value.
|
|
1738
|
+
data === null ? null : h('div', { key: 'tools', className: 'cp-card' }, [
|
|
1739
|
+
h('label', { key: 'head', className: 'cp-row cp-tools-label' }, [
|
|
1740
|
+
h('input', {
|
|
1741
|
+
key: 'on',
|
|
1742
|
+
type: 'checkbox',
|
|
1743
|
+
className: 'cp-check',
|
|
1744
|
+
checked: data.exposeTools !== false,
|
|
1745
|
+
disabled,
|
|
1746
|
+
onChange: (event) => actions.setToolsExposed(event.target.checked),
|
|
1747
|
+
}),
|
|
1748
|
+
h('span', { key: 'l' }, t('toolsExpose')),
|
|
1749
|
+
]),
|
|
1750
|
+
]),
|
|
1617
1751
|
// Diagnostic detail, so it stays folded away until asked for.
|
|
1618
1752
|
data === null || !ready ? null : h('details', { key: 'history', className: 'cp-card' }, [
|
|
1619
1753
|
foldHeader(`${t('history')} · ${total}`),
|
package/lib/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
|
|
|
22
22
|
import {
|
|
23
23
|
ClinePassAdapter,
|
|
24
24
|
DEFAULT_CONTEXT_WINDOW,
|
|
25
|
+
DEFAULT_MAX_CONCURRENT_REQUESTS,
|
|
25
26
|
DEFAULT_MAX_TOKENS,
|
|
26
27
|
DEFAULT_REQUEST_IMAGE_POLICY,
|
|
27
28
|
DEFAULT_STREAM_IDLE_TIMEOUT_MS,
|
|
@@ -34,7 +35,7 @@ import { BUILD_TAG } from './protocol.js'
|
|
|
34
35
|
import { createStore } from './store.js'
|
|
35
36
|
import { registerTools } from './tools.js'
|
|
36
37
|
|
|
37
|
-
export { ClinePassAdapter, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_TOKENS, DEFAULT_REQUEST_IMAGE_POLICY, DEFAULT_STREAM_IDLE_TIMEOUT_MS }
|
|
38
|
+
export { ClinePassAdapter, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_CONCURRENT_REQUESTS, DEFAULT_MAX_TOKENS, DEFAULT_REQUEST_IMAGE_POLICY, DEFAULT_STREAM_IDLE_TIMEOUT_MS }
|
|
38
39
|
export { collectImageRefs, prepareRequestImages, projectImageDimensions, requestImageTarget } from './adapter.js'
|
|
39
40
|
export { MODEL_CATALOG, REASONING_EFFORTS } from './catalog.js'
|
|
40
41
|
export { createEngine } from './engine.js'
|
|
@@ -136,6 +137,31 @@ export const Config = z.object({
|
|
|
136
137
|
defaultContextWindow: z.number().default(DEFAULT_CONTEXT_WINDOW),
|
|
137
138
|
defaultMaxTokens: z.number().default(DEFAULT_MAX_TOKENS),
|
|
138
139
|
streamIdleTimeoutMs: z.number().default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
140
|
+
/**
|
|
141
|
+
* Streams one account may keep open to the gateway at once.
|
|
142
|
+
*
|
|
143
|
+
* A request past the cap waits for a slot (and can still be cancelled while it
|
|
144
|
+
* waits) instead of opening another stream. `0` lifts the cap.
|
|
145
|
+
*/
|
|
146
|
+
maxConcurrentRequests: z.number().default(DEFAULT_MAX_CONCURRENT_REQUESTS),
|
|
147
|
+
/**
|
|
148
|
+
* Whether the `cline_pass_*` management tools are registered at all.
|
|
149
|
+
*
|
|
150
|
+
* The eight definitions are ~5.5k characters of schema and description that
|
|
151
|
+
* ride in every request's tool list, and the route itself does not need them:
|
|
152
|
+
* a conversation that never probes, pins or edits the pool pays for them on
|
|
153
|
+
* every turn. Off leaves the provider and the setup panel exactly as they are
|
|
154
|
+
* — the panel is a browser page, not a tool, so it keeps working.
|
|
155
|
+
*
|
|
156
|
+
* Off by default: the cost is paid by every conversation, while the tools are
|
|
157
|
+
* wanted by the few that manage the route. Turning them on is one checkbox.
|
|
158
|
+
* Existing profiles have no `exposeTools` key, so they adopt this default on
|
|
159
|
+
* upgrade and their tools stop being registered — the panel says so, and the
|
|
160
|
+
* checkbox restores them.
|
|
161
|
+
*
|
|
162
|
+
* Read when the plugin activates, so a change needs a restart.
|
|
163
|
+
*/
|
|
164
|
+
exposeTools: z.boolean().default(false),
|
|
139
165
|
exposeCatalog: z.boolean().default(false),
|
|
140
166
|
historyLimit: z.number().default(100),
|
|
141
167
|
/**
|
|
@@ -303,6 +329,8 @@ export function apply(ctx, config) {
|
|
|
303
329
|
const key = await resolveKey(profile.apiKeyEnv).catch(() => undefined)
|
|
304
330
|
return {
|
|
305
331
|
name,
|
|
332
|
+
// The name a person recognizes, for the panel's account switcher.
|
|
333
|
+
displayName: profile.displayName,
|
|
306
334
|
key: key ?? '',
|
|
307
335
|
keyConfigured: key !== undefined,
|
|
308
336
|
baseURL: profile.baseURL || cfg.baseURL,
|
|
@@ -342,6 +370,7 @@ export function apply(ctx, config) {
|
|
|
342
370
|
maxTokens: cfg.defaultMaxTokens,
|
|
343
371
|
reasoningModels: cfg.reasoningModels,
|
|
344
372
|
streamIdleTimeoutMs: cfg.streamIdleTimeoutMs,
|
|
373
|
+
maxConcurrentRequests: cfg.maxConcurrentRequests,
|
|
345
374
|
}
|
|
346
375
|
}
|
|
347
376
|
|
|
@@ -532,9 +561,22 @@ export function apply(ctx, config) {
|
|
|
532
561
|
await credentials.set(credentialRef(ref), String(value))
|
|
533
562
|
},
|
|
534
563
|
refreshCatalog,
|
|
564
|
+
// Read at activation, so this reports the value the running plugin was
|
|
565
|
+
// built with rather than the current config field.
|
|
566
|
+
exposeTools: () => toolsExposed,
|
|
535
567
|
}
|
|
536
568
|
|
|
537
|
-
|
|
569
|
+
// The agent-facing management surface. Optional: the eight definitions are
|
|
570
|
+
// ~5.5k characters of tool list on every request, and a conversation that
|
|
571
|
+
// never probes, pins or edits the pool does not need them. `exposeTools:
|
|
572
|
+
// false` drops them while leaving the provider and the setup panel — a
|
|
573
|
+
// browser page rather than a tool — untouched.
|
|
574
|
+
const toolsExposed = config.exposeTools !== false
|
|
575
|
+
if (toolsExposed) {
|
|
576
|
+
registerTools(ctx, { store, engine, control })
|
|
577
|
+
} else {
|
|
578
|
+
ctx.logger?.info('[cline-pass] cline_pass_* tools are not exposed (exposeTools: false); the provider route and the setup panel are unaffected')
|
|
579
|
+
}
|
|
538
580
|
|
|
539
581
|
// The same surface as an ordinary browser page, for users who would rather
|
|
540
582
|
// click than compose a tool call. Absent in a headless composition.
|
package/lib/panel.js
CHANGED
|
@@ -139,6 +139,10 @@ export function createPanel({ control, engine, store }) {
|
|
|
139
139
|
usage: store.usage(),
|
|
140
140
|
catalogCount: Number(store.catalog()?.ids?.length ?? 0),
|
|
141
141
|
historySize: store.historySize(),
|
|
142
|
+
// Whether the `cline_pass_*` tools are registered. Reported, not switched:
|
|
143
|
+
// the value is read when the plugin activates, so a control here could not
|
|
144
|
+
// take effect without a reload.
|
|
145
|
+
exposeTools: control.exposeTools(),
|
|
142
146
|
}
|
|
143
147
|
}
|
|
144
148
|
|
|
@@ -155,12 +159,18 @@ export function createPanel({ control, engine, store }) {
|
|
|
155
159
|
const accounts = await control.usageAccounts()
|
|
156
160
|
if (accounts.length === 0) throw new Error('every configured Cline Pass account is disabled; enable one first')
|
|
157
161
|
const readings = await Promise.all(accounts.map(async (account) => {
|
|
158
|
-
|
|
162
|
+
// The reading is keyed by the configuration name, but the panel's switcher
|
|
163
|
+
// shows the name the accounts card shows, so both travel together.
|
|
164
|
+
const label = {
|
|
165
|
+
account: account.name,
|
|
166
|
+
displayName: String(account.displayName ?? '') || String(account.name),
|
|
167
|
+
}
|
|
168
|
+
if (account.keyConfigured !== true) return { ...label, ok: false, limits: [], error: `no API key stored for ${account.name}` }
|
|
159
169
|
const result = await fetchUsageLimits({
|
|
160
170
|
baseURL: account.baseURL || String(config.baseURL ?? ''),
|
|
161
171
|
apiKey: account.key,
|
|
162
172
|
})
|
|
163
|
-
return {
|
|
173
|
+
return { ...label, ...result }
|
|
164
174
|
}))
|
|
165
175
|
const reading = store.setUsage({ accounts: readings })
|
|
166
176
|
return { ...(await state()), usage: reading }
|
|
@@ -464,6 +474,21 @@ export function createPanel({ control, engine, store }) {
|
|
|
464
474
|
}
|
|
465
475
|
}
|
|
466
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Whether the `cline_pass_*` tools are registered.
|
|
479
|
+
*
|
|
480
|
+
* The value is read when the plugin activates, so this writes the setting for
|
|
481
|
+
* the next start rather than changing the running plugin: a tool that is
|
|
482
|
+
* already registered cannot be pulled back out of a conversation's tool list.
|
|
483
|
+
* The response carries `restartRequired` so the panel can say so instead of
|
|
484
|
+
* implying the switch already took effect.
|
|
485
|
+
*/
|
|
486
|
+
async function toolsExpose(payload) {
|
|
487
|
+
if (typeof payload?.value !== 'boolean') throw new Error('this action needs a boolean `value`')
|
|
488
|
+
await control.updateConfig({ exposeTools: payload.value })
|
|
489
|
+
return { restartRequired: true, ...(await state()) }
|
|
490
|
+
}
|
|
491
|
+
|
|
467
492
|
return {
|
|
468
493
|
state,
|
|
469
494
|
usage,
|
|
@@ -481,6 +506,7 @@ export function createPanel({ control, engine, store }) {
|
|
|
481
506
|
'model.test': modelTest,
|
|
482
507
|
'model.reset': modelReset,
|
|
483
508
|
'models.refresh': modelsRefresh,
|
|
509
|
+
'tools.expose': toolsExpose,
|
|
484
510
|
history,
|
|
485
511
|
}
|
|
486
512
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cline-pass",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Self-contained Cline Pass provider for dsh: register Cline Pass subscription models on the harness LLM seam, enumerate and validate the upstream channels behind each model, pin or exclude them with failover, and drive an account pool. No proxy process and no external cline-pass-switcher install required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -56,18 +56,18 @@
|
|
|
56
56
|
]
|
|
57
57
|
},
|
|
58
58
|
"compatibility": {
|
|
59
|
-
"dsh": ">=0.1.2-alpha.3 <0.
|
|
59
|
+
"dsh": ">=0.1.2-alpha.3 <0.3.0"
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
64
|
-
"@deepseek-ai/dsh-credentials": ">=0.1.2-alpha.3 <0.
|
|
65
|
-
"@deepseek-ai/dsh-launch-environment": ">=0.1.2-alpha.3 <0.
|
|
66
|
-
"@deepseek-ai/dsh-llm": ">=0.1.2-alpha.3 <0.
|
|
67
|
-
"@deepseek-ai/dsh-settings": ">=0.1.2-alpha.3 <0.
|
|
68
|
-
"@deepseek-ai/dsh-timeout": ">=0.1.2-alpha.3 <0.
|
|
69
|
-
"@deepseek-ai/dsh-tools": ">=0.1.2-alpha.3 <0.
|
|
70
|
-
"@deepseek-ai/dsh-util-values": ">=0.1.2-alpha.3 <0.
|
|
64
|
+
"@deepseek-ai/dsh-credentials": ">=0.1.2-alpha.3 <0.3.0",
|
|
65
|
+
"@deepseek-ai/dsh-launch-environment": ">=0.1.2-alpha.3 <0.3.0",
|
|
66
|
+
"@deepseek-ai/dsh-llm": ">=0.1.2-alpha.3 <0.3.0",
|
|
67
|
+
"@deepseek-ai/dsh-settings": ">=0.1.2-alpha.3 <0.3.0",
|
|
68
|
+
"@deepseek-ai/dsh-timeout": ">=0.1.2-alpha.3 <0.3.0",
|
|
69
|
+
"@deepseek-ai/dsh-tools": ">=0.1.2-alpha.3 <0.3.0",
|
|
70
|
+
"@deepseek-ai/dsh-util-values": ">=0.1.2-alpha.3 <0.3.0",
|
|
71
71
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
72
72
|
}
|
|
73
73
|
}
|