dsh-blackjack 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/dist/router.js ADDED
@@ -0,0 +1,179 @@
1
+ import { QUOTA_EXCEEDED_CODE, isQuotaExceededError } from '@deepseek-ai/dsh-llm';
2
+ import { createPoolAdapter } from './adapter.js';
3
+ /** Provider route the pool adapter is registered under; also the loop-guard for `shouldFallback`. */
4
+ export const POOL_PROVIDER = 'blackjack-pool';
5
+ /** Chunk types that count as "content already reached the consumer" for the no-duplicate-output rule. */
6
+ const CONTENT_TYPES = new Set(['block-start', 'text-delta', 'reasoning-delta', 'tool-call-delta', 'block-end']);
7
+ /** Narrow a finish chunk's `error` failure to whether it reads as quota exhaustion. */
8
+ function isQuotaFailure(failure) {
9
+ // Our own pool adapter (DeepSeekAdapter) already normalizes provider wire
10
+ // errors to the stable QUOTA_EXCEEDED_CODE (see dsh-llm-deepseek's
11
+ // httpErrorCode), so the direct code check is the common case. The
12
+ // isQuotaExceededError(...) fallback — over the same
13
+ // `[code, message].filter(Boolean).join(' ')` shape dsh-llm-deepseek's own
14
+ // httpErrorCode uses — catches a third-party adapter that never normalized
15
+ // its wording into that code at all (e.g. it fell through to `UNKNOWN`
16
+ // carrying a plainly quota-worded message).
17
+ return failure.code === QUOTA_EXCEEDED_CODE
18
+ || isQuotaExceededError([failure.code, failure.message].filter(Boolean).join(' '));
19
+ }
20
+ /**
21
+ * Decide whether one finished call should be retried on the pool route.
22
+ *
23
+ * NOTE on `QUOTA_EXCEEDED_CODE` vs `isQuotaExceededError`: the brief's
24
+ * original sketch called `isQuotaExceededError(reason.failure)` directly on
25
+ * the failure object. Checking the installed `@deepseek-ai/dsh-llm@0.1.0-rc.6`
26
+ * package directly shows `isQuotaExceededError(detail: string): boolean` is a
27
+ * *raw provider wording* classifier (regexes over free text such as
28
+ * "insufficient quota") — it is what an adapter uses internally while
29
+ * normalizing a thrown provider error into a `LlmFailure`, not something a
30
+ * downstream consumer applies to an already-normalized `finish` chunk as-is.
31
+ * The normalized signal to compare first is `LlmFailure.code` against
32
+ * `QUOTA_EXCEEDED_CODE`; `isQuotaExceededError` is kept only as a secondary,
33
+ * string-joined fallback for adapters that never normalized at all (see
34
+ * `isQuotaFailure` above).
35
+ */
36
+ export function shouldFallback(input) {
37
+ if (input.provider === POOL_PROVIDER)
38
+ return false;
39
+ if (!input.hasToken || input.emittedContent)
40
+ return false;
41
+ const reason = input.finish.reason;
42
+ if (reason.kind !== 'error')
43
+ return false;
44
+ return isQuotaFailure(reason.failure);
45
+ }
46
+ /**
47
+ * Wrap one model call: pass everything through, and only when the call ends
48
+ * with a content-free quota failure retry it against the pool route. Any
49
+ * failure of that retry surfaces the ORIGINAL failure, so installing this
50
+ * plugin can never make a call worse than not having it.
51
+ *
52
+ * Two invariants the pool-retry branch upholds that are easy to get wrong:
53
+ * - The stream ALWAYS ends with exactly one terminal `finish` chunk. A pool
54
+ * retry that throws mid-stream (after already forwarding content), or one
55
+ * that simply ends without ever sending its own finish, would otherwise
56
+ * leave the consumer's `BlockAssembler` defaulting an absent finish to
57
+ * `{kind: 'stop'}` — silently presenting a FAILED fallback as a successful,
58
+ * truncated turn. `poolFinished` tracks whether the pool ever produced its
59
+ * own terminal finish; whenever it did not, the ORIGINAL failure is yielded
60
+ * as the terminal chunk instead (legal even after open content blocks,
61
+ * since its `reason.kind` is `'error'`).
62
+ * - `usage` never gets forwarded twice. Adapters emit at most one `usage`
63
+ * chunk, just before their finish. The primary attempt's `usage` (if any)
64
+ * is buffered rather than forwarded immediately, so it can be dropped
65
+ * entirely once a fallback is underway — the pool call reports its own —
66
+ * instead of both being forwarded and double-counting tokens.
67
+ */
68
+ export async function* runWithFallback(options, next, deps) {
69
+ let emittedContent = false;
70
+ let finish;
71
+ const pendingUsage = [];
72
+ for await (const c of next()) {
73
+ if (c.type === 'finish') {
74
+ finish = c;
75
+ break;
76
+ }
77
+ if (c.type === 'usage') {
78
+ pendingUsage.push(c);
79
+ continue;
80
+ }
81
+ if (CONTENT_TYPES.has(c.type))
82
+ emittedContent = true;
83
+ yield c;
84
+ }
85
+ if (!finish)
86
+ return;
87
+ // Cheap, synchronous eligibility (provider / content / finish code) comes
88
+ // first. `deps.hasToken()` is awaited only once everything else already
89
+ // says yes, so a normal call — success or a non-quota failure — never pays
90
+ // a credential resolve, and a credentials-provider rejection can only ever
91
+ // affect a call this plugin was already about to intervene on.
92
+ if (!shouldFallback({ finish, emittedContent, hasToken: true, provider: options.provider })) {
93
+ yield* pendingUsage;
94
+ yield finish;
95
+ return;
96
+ }
97
+ let hasToken;
98
+ try {
99
+ hasToken = await deps.hasToken();
100
+ }
101
+ catch {
102
+ hasToken = false;
103
+ }
104
+ if (!hasToken) {
105
+ yield* pendingUsage;
106
+ yield finish;
107
+ return;
108
+ }
109
+ // Falling back: the primary attempt's pre-retry usage (if any) is dropped
110
+ // on purpose — see the usage invariant note above.
111
+ let announced = false;
112
+ let poolFinished = false;
113
+ try {
114
+ for await (const c of deps.poolStream({ ...options, provider: POOL_PROVIDER })) {
115
+ if (c.type === 'finish' && c.reason.kind === 'error') {
116
+ yield finish;
117
+ return;
118
+ }
119
+ if (!announced) {
120
+ announced = true;
121
+ deps.notify('已切换至奖池余额继续本次请求。');
122
+ }
123
+ if (c.type === 'finish')
124
+ poolFinished = true;
125
+ yield c;
126
+ }
127
+ }
128
+ catch (error) {
129
+ deps.logError?.(error);
130
+ }
131
+ if (!poolFinished)
132
+ yield finish;
133
+ }
134
+ /**
135
+ * Register the pool-route adapter and hang the `llm/stream` fallback
136
+ * waterfall off it.
137
+ *
138
+ * KNOWN LIMITATION — where the switch notice actually goes. `notify` defaults
139
+ * to `ctx.logger.info`, which means the player normally never SEES it: in dsh
140
+ * 0.1.0-rc.6 a waterfall listener has no FIRE-AND-FORGET notice channel it may
141
+ * use. Everything reachable from here was checked (see the plugin spec's §7
142
+ * appendix for the full record): no host service in this plugin's dependency
143
+ * closure is a notification sink (`agents`, `sessions`, `credentials`,
144
+ * `commands`, `llm`, `webServer`, `conversation`, `chatFileMentions`). The one
145
+ * human-visible in-conversation seam that does exist elsewhere in the host,
146
+ * `ctx.userQuestions.ask()`, BLOCKS on a human answer and needs a GUI
147
+ * provider — wrong semantics for a passive notice, and it would stall the very
148
+ * stream this fallback is rescuing. `commands` only runs when the player types
149
+ * a slash command;
150
+ * `agent.inject()` writes a model-visible `user/message`, which is exactly
151
+ * what this project forbids; and `SurfaceEventType` is a CLOSED three-member
152
+ * set, so the UI's generic unclaimed-event fallback covers only
153
+ * model-visible events — a log-only custom event has no renderer at all.
154
+ * Surfacing it would take a plugin-owned `ConversationNodeDefinition` in the
155
+ * client bundle, i.e. a new feature that still would not reach TUI/headless
156
+ * players. The README says plainly that the notice lands in the log, rather
157
+ * than promising an on-screen one. Revisit when dsh grows a plugin-usable
158
+ * in-session notice slot.
159
+ *
160
+ * What is NOT negotiable either way: the notice must never be injected into
161
+ * model messages, so a fallback can never pollute conversation history.
162
+ *
163
+ * `ctx.llm.registerAdapter()` already ties its disposal to the calling
164
+ * fiber (cordis re-binds a service's `this.ctx` to whichever context reads
165
+ * it, so the registration's own internal `ctx.effect` lands on OUR plugin's
166
+ * fiber, not `llm`'s construction context — confirmed by disposing a probe
167
+ * fiber and observing the route drop from `listProviders()` with no extra
168
+ * wrapping). No separate `ctx.effect` around `registration` is needed.
169
+ */
170
+ export function registerRouter(ctx, deps) {
171
+ const notify = deps.notify ?? ((line) => ctx.logger.info(line));
172
+ ctx.llm.registerAdapter([POOL_PROVIDER], createPoolAdapter(ctx, deps.serverUrl));
173
+ ctx.on('llm/stream', (options, next) => runWithFallback(options, next, {
174
+ hasToken: async () => (await deps.identity.current()) !== undefined,
175
+ poolStream: (o) => ctx.llm.stream(o),
176
+ notify,
177
+ logError: (error) => ctx.logger.warn('dsh-blackjack: pool fallback failed after a quota exhaustion; the original failure was surfaced instead', error),
178
+ }));
179
+ }
@@ -0,0 +1,54 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type Api } from './api.js';
3
+ import type { Identity } from './identity.js';
4
+ /**
5
+ * Host half of the graphical table: a JSON read/act surface under
6
+ * `/blackjack/api/*` on the web GUI's own `ctx.webServer`, consumed by the
7
+ * browser table (a separate module, task 8). Registered as a NESTED plugin
8
+ * from `src/index.ts` with its own `inject: ['webServer']`, so a headless
9
+ * host without a web server leaves this fiber pending while the text
10
+ * commands keep working — the table is an optional surface, not a
11
+ * requirement.
12
+ *
13
+ * Security: the player's bearer token lives only in the dsh process
14
+ * (`Identity`) — it MUST NEVER appear in a response body. Before consent (no
15
+ * token yet) `GET .../state` reports `{ consented: false }` and every write
16
+ * route refuses with 403; consent can only be granted from the command plane
17
+ * (`/blackjack agree`), never from the browser.
18
+ *
19
+ * The bind address is NOT the whole trust boundary. A dsh host may bind
20
+ * `0.0.0.0`, and even on loopback any page the player has open can reach
21
+ * `http://localhost:<port>` — a cross-site `<form enctype="text/plain">` needs
22
+ * no CORS preflight, so without a check a hostile page could burn free hands
23
+ * through `/api/deal` or force actions through `/api/action`. Two same-origin
24
+ * gates close that (see `isSameOrigin` / `hasJsonBody`):
25
+ * - a request carrying an `Origin` whose host differs from this request's
26
+ * `Host`, or a `Sec-Fetch-Site` that is anything other than `same-origin`,
27
+ * is refused 403 outright (the browser sets both and a page cannot forge
28
+ * either);
29
+ * - the write routes require `content-type: application/json`, which a
30
+ * simple (preflight-free) cross-site form can never send.
31
+ * The browser table (`src/client/index.tsx`) calls these with same-origin
32
+ * relative URLs and an explicit JSON content type, so it satisfies both.
33
+ *
34
+ * Exchange is deliberately NOT here: it is a command-plane concern
35
+ * (`/blackjack exchange`), because the pairing `code` is itself a bearer
36
+ * capability — whoever holds it can claim the exchange — and must never reach
37
+ * the browser through a route this file owns.
38
+ *
39
+ * @module dsh-blackjack/routes
40
+ */
41
+ /** Route prefix this plugin owns on the host's web server. */
42
+ export declare const PREFIX = "/blackjack";
43
+ /**
44
+ * Register the table's API routes into `ctx`'s effect scope (the route
45
+ * disposer rides the fiber, so plugin dispose unregisters the prefix).
46
+ * @param ctx - a context whose `webServer` service is live (the nested
47
+ * plugin's `inject` guarantees it).
48
+ * @param deps - the same `Api`/`Identity` the command plane uses, so the
49
+ * browser table and the zero-token commands stay consistent.
50
+ */
51
+ export declare function registerRoutes(ctx: Context, deps: {
52
+ api: Api;
53
+ identity: Identity;
54
+ }): void;
package/dist/routes.js ADDED
@@ -0,0 +1,195 @@
1
+ import { ApiError } from './api.js';
2
+ /**
3
+ * Host half of the graphical table: a JSON read/act surface under
4
+ * `/blackjack/api/*` on the web GUI's own `ctx.webServer`, consumed by the
5
+ * browser table (a separate module, task 8). Registered as a NESTED plugin
6
+ * from `src/index.ts` with its own `inject: ['webServer']`, so a headless
7
+ * host without a web server leaves this fiber pending while the text
8
+ * commands keep working — the table is an optional surface, not a
9
+ * requirement.
10
+ *
11
+ * Security: the player's bearer token lives only in the dsh process
12
+ * (`Identity`) — it MUST NEVER appear in a response body. Before consent (no
13
+ * token yet) `GET .../state` reports `{ consented: false }` and every write
14
+ * route refuses with 403; consent can only be granted from the command plane
15
+ * (`/blackjack agree`), never from the browser.
16
+ *
17
+ * The bind address is NOT the whole trust boundary. A dsh host may bind
18
+ * `0.0.0.0`, and even on loopback any page the player has open can reach
19
+ * `http://localhost:<port>` — a cross-site `<form enctype="text/plain">` needs
20
+ * no CORS preflight, so without a check a hostile page could burn free hands
21
+ * through `/api/deal` or force actions through `/api/action`. Two same-origin
22
+ * gates close that (see `isSameOrigin` / `hasJsonBody`):
23
+ * - a request carrying an `Origin` whose host differs from this request's
24
+ * `Host`, or a `Sec-Fetch-Site` that is anything other than `same-origin`,
25
+ * is refused 403 outright (the browser sets both and a page cannot forge
26
+ * either);
27
+ * - the write routes require `content-type: application/json`, which a
28
+ * simple (preflight-free) cross-site form can never send.
29
+ * The browser table (`src/client/index.tsx`) calls these with same-origin
30
+ * relative URLs and an explicit JSON content type, so it satisfies both.
31
+ *
32
+ * Exchange is deliberately NOT here: it is a command-plane concern
33
+ * (`/blackjack exchange`), because the pairing `code` is itself a bearer
34
+ * capability — whoever holds it can claim the exchange — and must never reach
35
+ * the browser through a route this file owns.
36
+ *
37
+ * @module dsh-blackjack/routes
38
+ */
39
+ /** Route prefix this plugin owns on the host's web server. */
40
+ export const PREFIX = '/blackjack';
41
+ function sendJson(res, status, body) {
42
+ const text = JSON.stringify(body);
43
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
44
+ res.end(text);
45
+ }
46
+ /**
47
+ * Whether this request provably did not come from another origin.
48
+ *
49
+ * Both headers are browser-controlled and unforgeable from page script, so a
50
+ * present-and-wrong value is a positive signal of a cross-site attempt:
51
+ * - `Sec-Fetch-Site` must be exactly `same-origin` when present. `cross-site`
52
+ * and `same-site` are refused for the obvious reason; `none` (a
53
+ * user-typed URL or bookmark) is refused too, because nothing legitimate
54
+ * navigates directly to a JSON endpoint here and refusing costs nothing.
55
+ * - `Origin`, when present, must name the same host this request was
56
+ * addressed to (`Host`). A non-browser client (curl, a test harness) sends
57
+ * neither header and is unaffected — the goal is stopping a hostile PAGE,
58
+ * which cannot suppress them.
59
+ */
60
+ function isSameOrigin(req) {
61
+ const site = req.headers['sec-fetch-site'];
62
+ if (typeof site === 'string' && site !== 'same-origin')
63
+ return false;
64
+ const origin = req.headers.origin;
65
+ if (typeof origin === 'string' && origin.length > 0) {
66
+ const host = req.headers.host;
67
+ if (!host)
68
+ return false;
69
+ try {
70
+ if (new URL(origin).host !== host)
71
+ return false;
72
+ }
73
+ catch {
74
+ return false; // an unparseable Origin (including the opaque `null`) is not ours
75
+ }
76
+ }
77
+ return true;
78
+ }
79
+ /**
80
+ * Whether a write request declared a JSON body. Required on every write route:
81
+ * the cross-site form POST this file defends against can only send
82
+ * `text/plain`, `application/x-www-form-urlencoded`, or `multipart/form-data`
83
+ * without triggering a CORS preflight, so demanding JSON is a second,
84
+ * independent gate that does not depend on any `Sec-Fetch-*` support.
85
+ */
86
+ function hasJsonBody(req) {
87
+ const type = req.headers['content-type'];
88
+ return typeof type === 'string' && type.split(';')[0].trim().toLowerCase() === 'application/json';
89
+ }
90
+ /** Read the request body as JSON. Any non-object result — empty body, parse failure, array, null — becomes `{}`. */
91
+ async function readJsonBody(req) {
92
+ const chunks = [];
93
+ for await (const chunk of req) {
94
+ chunks.push(chunk);
95
+ }
96
+ if (chunks.length === 0)
97
+ return {};
98
+ try {
99
+ const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
100
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
101
+ ? parsed
102
+ : {};
103
+ }
104
+ catch {
105
+ return {};
106
+ }
107
+ }
108
+ /** Map a caught failure to a player-safe status/body pair. Never leaks stack traces, URLs, or raw messages. */
109
+ function mapError(e) {
110
+ if (e instanceof ApiError) {
111
+ const status = e.status === 0 ? 502 : e.status;
112
+ return { status, body: { error: e.code ?? 'upstream error' } };
113
+ }
114
+ return { status: 500, body: { error: 'internal error' } };
115
+ }
116
+ /**
117
+ * Register the table's API routes into `ctx`'s effect scope (the route
118
+ * disposer rides the fiber, so plugin dispose unregisters the prefix).
119
+ * @param ctx - a context whose `webServer` service is live (the nested
120
+ * plugin's `inject` guarantees it).
121
+ * @param deps - the same `Api`/`Identity` the command plane uses, so the
122
+ * browser table and the zero-token commands stay consistent.
123
+ */
124
+ export function registerRoutes(ctx, deps) {
125
+ const { api, identity } = deps;
126
+ async function handle(req, res) {
127
+ const url = new URL(req.url ?? '/', 'http://blackjack.local');
128
+ const route = url.pathname;
129
+ const method = req.method ?? 'GET';
130
+ if (!isSameOrigin(req))
131
+ return sendJson(res, 403, { error: 'cross-origin request refused' });
132
+ try {
133
+ if (route === `${PREFIX}/api/state` && method === 'GET') {
134
+ const token = await identity.current();
135
+ if (!token)
136
+ return sendJson(res, 200, { consented: false });
137
+ const [me, round] = await Promise.all([api.me(token), api.activeRound(token)]);
138
+ return sendJson(res, 200, { consented: true, me, round });
139
+ }
140
+ if (route === `${PREFIX}/api/deal` && method === 'POST') {
141
+ if (!hasJsonBody(req))
142
+ return sendJson(res, 415, { error: 'expected content-type: application/json' });
143
+ const token = await identity.current();
144
+ if (!token)
145
+ return sendJson(res, 403, { error: 'not consented' });
146
+ const body = await readJsonBody(req);
147
+ const betChips = body.betChips;
148
+ let startBody;
149
+ if (betChips === undefined) {
150
+ startBody = { mode: 'free' };
151
+ }
152
+ else if (typeof betChips === 'number' && Number.isInteger(betChips) && betChips > 0) {
153
+ startBody = { mode: 'raise', betChips };
154
+ }
155
+ else {
156
+ return sendJson(res, 400, { error: 'invalid betChips' });
157
+ }
158
+ const round = await api.startRound(token, startBody);
159
+ return sendJson(res, 200, round);
160
+ }
161
+ if (route === `${PREFIX}/api/action` && method === 'POST') {
162
+ if (!hasJsonBody(req))
163
+ return sendJson(res, 415, { error: 'expected content-type: application/json' });
164
+ const token = await identity.current();
165
+ if (!token)
166
+ return sendJson(res, 403, { error: 'not consented' });
167
+ const body = await readJsonBody(req);
168
+ if (typeof body.action !== 'string' || body.action.length === 0) {
169
+ return sendJson(res, 400, { error: 'invalid action' });
170
+ }
171
+ const round = await api.activeRound(token);
172
+ if (!round)
173
+ return sendJson(res, 404, { error: 'no active round' });
174
+ const result = await api.act(token, round.id, body.action);
175
+ return sendJson(res, 200, result);
176
+ }
177
+ return sendJson(res, 404, { error: 'unknown route' });
178
+ }
179
+ catch (e) {
180
+ const { status, body } = mapError(e);
181
+ return sendJson(res, status, body);
182
+ }
183
+ }
184
+ ctx.effect(() => ctx.webServer.register({
185
+ kind: 'prefix',
186
+ path: PREFIX,
187
+ handler: (req, res) => handle(req, res).catch((err) => {
188
+ ctx.logger.warn('dsh-blackjack: web route failed: %o', err);
189
+ if (!res.headersSent)
190
+ sendJson(res, 500, { error: 'internal error' });
191
+ else
192
+ res.end();
193
+ }),
194
+ }), 'dsh-blackjack: /blackjack web routes');
195
+ }
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "dsh-blackjack",
3
+ "version": "0.1.0",
4
+ "description": "社区福利小游戏插件:在 dsh 里玩 21 点赢取可消费的模型额度",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "dsh-plugin",
8
+ "blackjack",
9
+ "community"
10
+ ],
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ },
19
+ "./client": "./dist/client.js",
20
+ "./package.json": "./package.json"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "blackjack.cordis.yml",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "engines": {
29
+ "node": "^22.19.0 || >=24.0.0"
30
+ },
31
+ "dsh": {
32
+ "bundle": {
33
+ "patch": "./blackjack.cordis.yml"
34
+ },
35
+ "client": {
36
+ "platform": "web",
37
+ "inject": [
38
+ "@deepseek-ai/dsh-client-runtime",
39
+ "@deepseek-ai/dsh-client-ui-conversation"
40
+ ]
41
+ }
42
+ },
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.json && node build.client.mjs",
45
+ "prepack": "pnpm run build",
46
+ "test": "vitest run",
47
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
48
+ "prepare": "pnpm run build"
49
+ },
50
+ "peerDependencies": {
51
+ "@deepseek-ai/cordis": "^4.0.1",
52
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
53
+ "@deepseek-ai/dsh-anonymous-user-id": "^0.1.0-rc.6",
54
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
55
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
56
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
57
+ "@deepseek-ai/dsh-llm-deepseek": "^0.1.0-rc.6",
58
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6"
59
+ },
60
+ "dependencies": {
61
+ "@deepseek-ai/schemastery": "^3.18.1"
62
+ },
63
+ "devDependencies": {
64
+ "@dsh-blackjack/server": "workspace:*",
65
+ "@deepseek-ai/cordis": "4.0.1",
66
+ "@deepseek-ai/dsh-agent": "0.1.0-rc.6",
67
+ "@deepseek-ai/dsh-anonymous-user-id": "0.1.0-rc.6",
68
+ "@deepseek-ai/dsh-client-runtime": "0.1.0-rc.6",
69
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.0-rc.6",
70
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.6",
71
+ "@deepseek-ai/dsh-commands": "0.1.0-rc.6",
72
+ "@deepseek-ai/dsh-credentials": "0.1.0-rc.6",
73
+ "@deepseek-ai/dsh-host-webserver": "0.1.0-rc.8",
74
+ "@deepseek-ai/dsh-llm": "0.1.0-rc.6",
75
+ "@deepseek-ai/dsh-llm-deepseek": "0.1.0-rc.6",
76
+ "@deepseek-ai/dsh-session": "0.1.0-rc.6",
77
+ "@types/node": "^22.0.0",
78
+ "@types/react": "~18.3.1",
79
+ "@types/react-dom": "~18.3.0",
80
+ "esbuild": "^0.24.0",
81
+ "react": "^18.2.0",
82
+ "react-dom": "^18.2.0",
83
+ "typescript": "^5.6.0",
84
+ "vitest": "^2.1.0"
85
+ },
86
+ "repository": {
87
+ "type": "git",
88
+ "url": "git+https://github.com/yul761/dsh-blackjack.git",
89
+ "directory": "packages/plugin"
90
+ },
91
+ "homepage": "https://github.com/yul761/dsh-blackjack/tree/master/packages/plugin#readme",
92
+ "bugs": {
93
+ "url": "https://github.com/yul761/dsh-blackjack/issues"
94
+ }
95
+ }