@try-works/dsh-righthand 0.1.19 → 0.1.21

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.
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Righthand panel client half: a collapsible right-side panel inside the
3
+ * DSH Web GUI showing the plugin's live state - settings, store keys,
4
+ * tasks and events - and mutating it through the /righthand/* routes the
5
+ * host half (src/gui.ts) registers. Same-plugin GUI: no separate package.
6
+ */
7
+
8
+ import {
9
+ createElement as h,
10
+ useEffect,
11
+ useState,
12
+ type CSSProperties,
13
+ } from 'react'
14
+ import { createPortal } from 'react-dom'
15
+ import type { Context as ClientContext } from '@deepseek-ai/cordis'
16
+ // Type-only: declares `ctx.slots` (the client kernel's slot registry).
17
+ import type {} from '@deepseek-ai/dsh-cordis-client-runner/client'
18
+ import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
19
+
20
+ /** Required services: the slot registry (client runtime). */
21
+ export const inject = ['slots']
22
+
23
+ const C = {
24
+ bg: '#15171a',
25
+ panel: '#1b1e23',
26
+ row: '#23272e',
27
+ border: '#2d323a',
28
+ text: '#e6e8ec',
29
+ dim: '#9aa3ad',
30
+ accent: '#3b82f6',
31
+ green: '#3fbf6e',
32
+ red: '#e5484d',
33
+ amber: '#d9a13b',
34
+ }
35
+
36
+ interface State {
37
+ settings: Record<string, unknown>
38
+ storeKeys: string[]
39
+ tasks: any[]
40
+ events: any[]
41
+ }
42
+
43
+ const EMPTY: State = { settings: {}, storeKeys: [], tasks: [], events: [] }
44
+
45
+ function fmt(s: unknown, n: number): string {
46
+ try { const t = JSON.stringify(s); return t === undefined ? '' : (t.length > n ? t.slice(0, n) + '...' : t) }
47
+ catch { return String(s) }
48
+ }
49
+
50
+ function Row(props: { label: string; value: string; tone?: string }): ReturnType<typeof h> {
51
+ return h('div', { style: { display: 'flex', gap: 8, justifyContent: 'space-between', padding: '4px 0', borderBottom: '1px solid ' + C.border, fontSize: 12 } },
52
+ h('span', { style: { color: C.dim, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, props.label),
53
+ h('span', { style: { color: props.tone ?? C.text, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '55%' } }, props.value),
54
+ )
55
+ }
56
+
57
+ function Btn(props: { label: string; onClick: () => void; tone?: string; danger?: boolean }): ReturnType<typeof h> {
58
+ const base: CSSProperties = {
59
+ background: C.row, color: props.danger ? C.red : (props.tone ?? C.text),
60
+ border: '1px solid ' + C.border, borderRadius: 6, padding: '3px 10px',
61
+ fontSize: 11, cursor: 'pointer', whiteSpace: 'nowrap'
62
+ }
63
+ return h('button', { style: base, onClick: props.onClick }, props.label)
64
+ }
65
+
66
+ function Section(props: { title: string; children?: any }): ReturnType<typeof h> {
67
+ return h('div', { style: { marginTop: 14 } },
68
+ h('div', { style: { fontSize: 11, letterSpacing: '0.12em', textTransform: 'uppercase', color: C.dim, marginBottom: 6 } }, props.title),
69
+ props.children,
70
+ )
71
+ }
72
+
73
+ function RighthandPanel(): ReturnType<typeof h> {
74
+ const [collapsed, setCollapsed] = useState(false)
75
+ const [state, setState] = useState<State>(EMPTY)
76
+ const [error, setError] = useState('')
77
+ const [patch, setPatch] = useState('{}')
78
+ const [newKey, setNewKey] = useState('')
79
+ const [newValue, setNewValue] = useState('')
80
+
81
+ const load = (): void => {
82
+ fetch('/righthand/state').then(r => r.json()).then((d: State) => { setState(d); setError('') })
83
+ .catch((e: Error) => setError(String(e)))
84
+ }
85
+ useEffect(() => { load() }, [])
86
+
87
+ const post = (path: string, body: unknown, then: () => void): void => {
88
+ fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
89
+ .then(r => r.json()).then((d: any) => { if (!d.ok && d.error) setError(String(d.error)); then() })
90
+ .catch((e: Error) => setError(String(e)))
91
+ }
92
+
93
+ const railStyle: CSSProperties = {
94
+ position: 'fixed', right: 0, top: 0, bottom: 0, width: 44, background: C.panel,
95
+ borderLeft: '1px solid ' + C.border, display: 'flex', flexDirection: 'column',
96
+ alignItems: 'center', paddingTop: 12, gap: 10, zIndex: 9998
97
+ }
98
+ const panelStyle: CSSProperties = {
99
+ position: 'fixed', right: 0, top: 0, bottom: 0, width: 340, background: C.bg,
100
+ borderLeft: '1px solid ' + C.border, padding: 12, overflowY: 'auto', zIndex: 9998,
101
+ fontFamily: 'ui-sans-serif, system-ui, sans-serif', color: C.text, fontSize: 13
102
+ }
103
+
104
+ const toggle = h('button', {
105
+ style: { background: 'none', border: 'none', color: C.dim, fontSize: 18, cursor: 'pointer' },
106
+ onClick: () => setCollapsed(!collapsed),
107
+ }, collapsed ? '<' : '>')
108
+
109
+ const refreshBtn = h(Btn, { label: 'Refresh', onClick: load })
110
+
111
+ if (collapsed) {
112
+ return createPortal(h('div', { style: railStyle, 'data-dsh-righthand-panel': 'collapsed' },
113
+ toggle,
114
+ h('span', { style: { color: C.green, fontSize: 10, width: 8, height: 8, borderRadius: '50%', background: C.green } }),
115
+ h('span', { style: { color: C.dim, fontSize: 11, writingMode: 'vertical-rl', letterSpacing: '0.12em', userSelect: 'none' } }, 'Righthand'),
116
+ ), document.body)
117
+ }
118
+
119
+ return createPortal(h('div', { style: panelStyle, 'data-dsh-righthand-panel': 'expanded' },
120
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 } },
121
+ h('span', { style: { fontWeight: 600, letterSpacing: '0.05em' } }, 'Righthand'),
122
+ h('span', { style: { display: 'flex', gap: 6 } }, refreshBtn, toggle),
123
+ ),
124
+ error !== '' ? h('div', { style: { color: C.red, fontSize: 11, marginBottom: 8 } }, error) : null,
125
+
126
+ h(Section, { title: 'Settings' },
127
+ Object.entries(state.settings).map(([k, v]) => h(Row, { key: k, label: k, value: fmt(v, 90) })),
128
+ h('div', { style: { display: 'flex', gap: 6, marginTop: 8 } },
129
+ h('input', {
130
+ value: patch, onChange: (e: any) => setPatch(e.target.value),
131
+ style: { flex: 1, background: C.row, color: C.text, border: '1px solid ' + C.border, borderRadius: 6, padding: '3px 8px', fontSize: 11, fontFamily: 'monospace' },
132
+ }),
133
+ h(Btn, { label: 'Patch', onClick: () => {
134
+ try { const p = JSON.parse(patch); post('/righthand/settings/set', { patch: p }, load) }
135
+ catch { setError('patch must be JSON') }
136
+ } }),
137
+ ),
138
+ ),
139
+
140
+ h(Section, { title: 'Store (' + state.storeKeys.length + ')' },
141
+ state.storeKeys.length === 0 ? h('div', { style: { color: C.dim, fontSize: 12 } }, '(empty)') : null,
142
+ state.storeKeys.map(k => h('div', { key: k, style: { display: 'flex', gap: 6, alignItems: 'center', padding: '2px 0' } },
143
+ h('span', { style: { flex: 1, fontFamily: 'monospace', fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, k),
144
+ h(Btn, { label: 'Delete', danger: true, onClick: () => post('/righthand/store/delete', { key: k }, load) }),
145
+ )),
146
+ h('div', { style: { display: 'flex', gap: 6, marginTop: 8 } },
147
+ h('input', { placeholder: 'key', value: newKey, onChange: (e: any) => setNewKey(e.target.value), style: { flex: 1, background: C.row, color: C.text, border: '1px solid ' + C.border, borderRadius: 6, padding: '3px 8px', fontSize: 11, fontFamily: 'monospace' } }),
148
+ h('input', { placeholder: 'json value', value: newValue, onChange: (e: any) => setNewValue(e.target.value), style: { flex: 1, background: C.row, color: C.text, border: '1px solid ' + C.border, borderRadius: 6, padding: '3px 8px', fontSize: 11, fontFamily: 'monospace' } }),
149
+ h(Btn, { label: 'Put', onClick: () => {
150
+ if (newKey === '') { setError('key required'); return }
151
+ try { const v = JSON.parse(newValue === '' ? 'null' : newValue); post('/righthand/store/put', { key: newKey, value: v }, () => { load(); setNewKey(''); setNewValue('') }) }
152
+ catch { setError('value must be JSON') }
153
+ } }),
154
+ ),
155
+ ),
156
+
157
+ h(Section, { title: 'Tasks (' + state.tasks.length + ')' },
158
+ state.tasks.length === 0 ? h('div', { style: { color: C.dim, fontSize: 12 } }, '(none)') : null,
159
+ state.tasks.map((t: any) => h('div', { key: t.id, style: { display: 'flex', gap: 6, alignItems: 'center', padding: '2px 0' } },
160
+ h('span', { style: { flex: 1, fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, (t.state === 'done' ? '[x] ' : t.state === 'failed' ? '[!] ' : '[ ] ') + t.title),
161
+ h(Btn, { label: 'Done', tone: C.green, onClick: () => post('/righthand/task/update', { id: t.id, state: 'done' }, load) }),
162
+ h(Btn, { label: 'Fail', danger: true, onClick: () => post('/righthand/task/update', { id: t.id, state: 'failed' }, load) }),
163
+ )),
164
+ ),
165
+
166
+ h(Section, { title: 'Events (' + state.events.length + ')' },
167
+ state.events.length === 0 ? h('div', { style: { color: C.dim, fontSize: 12 } }, '(none)') : null,
168
+ state.events.map((e: any) => h('div', { key: e.id, style: { display: 'flex', gap: 6, alignItems: 'center', padding: '2px 0' } },
169
+ h('span', { style: { flex: 1, fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, e.title + ' @ ' + e.at + ' [' + e.state + ']'),
170
+ e.state === 'pending' ? h(Btn, { label: 'Cancel', danger: true, onClick: () => post('/righthand/event/cancel', { id: e.id }, load) }) : null,
171
+ )),
172
+ ),
173
+ ), document.body)
174
+ }
175
+
176
+ /**
177
+ * Client plugin body: register the panel into the frame-wide overlay slot.
178
+ */
179
+ export function apply(ctx: ClientContext): void {
180
+ ctx.slots.inject('shell.overlay', () => ctx.slots.register({
181
+ name: 'shell.overlay',
182
+ id: 'righthand-panel',
183
+ order: 60,
184
+ }, RighthandPanel))
185
+ }
@@ -13,7 +13,8 @@
13
13
  import type { Context } from '@deepseek-ai/cordis'
14
14
  import { defineTool } from '@deepseek-ai/dsh-tools'
15
15
  import { credentialRef } from '@deepseek-ai/dsh-credentials'
16
- import { settingsNamespace } from '@deepseek-ai/dsh-settings'
16
+ // Type-only: declares `ctx.settings`.
17
+ import type {} from '@deepseek-ai/dsh-settings'
17
18
  import { signRequest, presignGet } from './sigv4.ts'
18
19
  import { genCall, genResult } from './cards.ts'
19
20
 
@@ -143,7 +144,7 @@ export function createR2Client(opts: R2Options) {
143
144
 
144
145
 
145
146
  export function apply(ctx: Context, config: FilesConfig = {}): void {
146
- const ns = settingsNamespace('righthand')
147
+ const ns = 'righthand'
147
148
 
148
149
  /** Resolve accountId + credentials + bucket at call time. */
149
150
  async function client(): Promise<ReturnType<typeof createR2Client>> {
@@ -301,5 +302,3 @@ export function apply(ctx: Context, config: FilesConfig = {}): void {
301
302
  }
302
303
 
303
304
 
304
-
305
-
package/src/gui.ts ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * gui.ts - the righthand GUI host half: HTTP routes under /righthand/* on
3
+ * the DSH web server that read and mutate the same domains the tools use.
4
+ * The client half (src/client/index.ts) renders these as a panel inside the
5
+ * DSH Web GUI. Mounts only when a webServer exists (the Web surface);
6
+ * elsewhere the child fiber simply does not start.
7
+ */
8
+
9
+ import type { Context } from '@deepseek-ai/cordis'
10
+ import type { IncomingMessage, ServerResponse } from 'node:http'
11
+ // Type-only: declares `ctx.settings`.
12
+ import type {} from '@deepseek-ai/dsh-settings'
13
+ import { storeDomain } from './store-tools.ts'
14
+ import { taskDomain } from './task-tools.ts'
15
+ import { eventsDomain } from './events-tools.ts'
16
+ // Type-only: activates the cordis Context merge for ctx.webServer.
17
+ import type {} from '@deepseek-ai/dsh-host-webserver'
18
+
19
+ export const name = 'righthand-gui'
20
+ export const inject = ['webServer', 'storageDomain', 'settings']
21
+
22
+ const ns = 'righthand'
23
+
24
+ function json(res: ServerResponse, code: number, body: unknown): void {
25
+ const text = JSON.stringify(body)
26
+ res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
27
+ res.end(text)
28
+ }
29
+
30
+ function readBody(req: IncomingMessage): Promise<string> {
31
+ return new Promise((resolve, reject) => {
32
+ const chunks: Buffer[] = []
33
+ let bytes = 0
34
+ req.on('data', (c: Buffer) => {
35
+ bytes += c.length
36
+ if (bytes > 65536) { reject(new Error('body too large')); req.destroy() }
37
+ else chunks.push(c)
38
+ })
39
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
40
+ req.on('error', reject)
41
+ })
42
+ }
43
+
44
+ export function apply(ctx: Context): void {
45
+ let domains: { store: any; tasks: any; events: any } | undefined
46
+ let ready: Promise<void> | undefined
47
+
48
+ ctx.effect(() => {
49
+ ready = (async () => {
50
+ const [store, tasks, events] = await Promise.all([
51
+ ctx.storageDomain.open(storeDomain),
52
+ ctx.storageDomain.open(taskDomain),
53
+ ctx.storageDomain.open(eventsDomain),
54
+ ])
55
+ domains = { store, tasks, events }
56
+ })()
57
+ return () => { }
58
+ })
59
+
60
+ const ensure = async (): Promise<NonNullable<typeof domains>> => {
61
+ if (domains === undefined) { await ready }
62
+ if (domains === undefined) throw new Error('righthand domains not open')
63
+ return domains
64
+ }
65
+
66
+ const getSettings = () => {
67
+ const v = ctx.settings.get(ns) as Record<string, unknown> | undefined
68
+ return v ?? {}
69
+ }
70
+
71
+ const disposers: Array<() => void> = []
72
+ ctx.effect(() => () => { for (const d of disposers.splice(0)) d() })
73
+
74
+ const route = (path: string, handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>): void => {
75
+ disposers.push(ctx.webServer.register({ kind: 'exact', path, handler }))
76
+ }
77
+
78
+ route('/righthand/state', async (_req, res) => {
79
+ try {
80
+ const d = await ensure()
81
+ const storeKeys = [...d.store.table('rows').keys()]
82
+ const tasks = [...d.tasks.table('tasks').entries()]
83
+ const events = [...d.events.table('events').entries()]
84
+ json(res, 200, {
85
+ settings: getSettings(),
86
+ storeKeys,
87
+ tasks: tasks.map(([id, row]) => ({ id, ...row })),
88
+ events: events.map(([id, row]) => ({ id, ...row })),
89
+ })
90
+ } catch (e) {
91
+ json(res, 500, { error: String(e) })
92
+ }
93
+ })
94
+
95
+ route('/righthand/settings/set', async (req, res) => {
96
+ try {
97
+ const body = JSON.parse(await readBody(req))
98
+ await ctx.settings.update(ns, body.patch)
99
+ json(res, 200, { ok: true })
100
+ } catch (e) {
101
+ json(res, 400, { error: String(e) })
102
+ }
103
+ })
104
+
105
+ route('/righthand/store/put', async (req, res) => {
106
+ try {
107
+ const d = await ensure()
108
+ const body = JSON.parse(await readBody(req))
109
+ if (typeof body.key !== 'string' || body.key === '') throw new Error('key required')
110
+ await d.store.table('rows').put(body.key, { value: body.value ?? null, updatedAt: new Date().toISOString() })
111
+ const writes = d.store.global.get()
112
+ await d.store.global.set({ writes: writes.writes + 1 })
113
+ json(res, 200, { ok: true })
114
+ } catch (e) {
115
+ json(res, 400, { error: String(e) })
116
+ }
117
+ })
118
+
119
+ route('/righthand/store/delete', async (req, res) => {
120
+ try {
121
+ const d = await ensure()
122
+ const body = JSON.parse(await readBody(req))
123
+ const existed = await d.store.table('rows').delete(body.key)
124
+ json(res, 200, { ok: true, existed })
125
+ } catch (e) {
126
+ json(res, 400, { error: String(e) })
127
+ }
128
+ })
129
+
130
+ route('/righthand/task/update', async (req, res) => {
131
+ try {
132
+ const d = await ensure()
133
+ const body = JSON.parse(await readBody(req))
134
+ const row = await d.tasks.table('tasks').get(body.id)
135
+ if (row === undefined) { json(res, 404, { error: 'task not found' }); return }
136
+ const next: any = { ...row }
137
+ if (typeof body.state === 'string') next.state = body.state
138
+ if (body.result !== undefined) next.result = body.result
139
+ await d.tasks.table('tasks').put(body.id, next)
140
+ json(res, 200, { ok: true })
141
+ } catch (e) {
142
+ json(res, 400, { error: String(e) })
143
+ }
144
+ })
145
+
146
+ route('/righthand/event/cancel', async (req, res) => {
147
+ try {
148
+ const d = await ensure()
149
+ const body = JSON.parse(await readBody(req))
150
+ const row = await d.events.table('events').get(body.id)
151
+ if (row === undefined) { json(res, 404, { error: 'event not found' }); return }
152
+ await d.events.table('events').put(body.id, { ...row, state: 'cancelled' })
153
+ json(res, 200, { ok: true })
154
+ } catch (e) {
155
+ json(res, 400, { error: String(e) })
156
+ }
157
+ })
158
+ }
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@
25
25
  * - notify-tools: rh_notify_send over ntfy.sh (keyless)
26
26
  * - exec-tools: rh_run / rh_run_bg over ctx.subprocess + ctx.jobs
27
27
  * - guard-tools: tools/pre-execute policy (config.rules)
28
+ * - gui: /righthand/* web routes + the client panel (src/client)
28
29
  * - skills: the packaged dsh-righthand skill (ctx.skills)
29
30
  *
30
31
  * @module @try-works/dsh-righthand
@@ -42,6 +43,7 @@ import * as placesTools from './places-tools.ts'
42
43
  import * as filesTools from './files-tools.ts'
43
44
  import * as eventsTools from './events-tools.ts'
44
45
  import * as notifyTools from './notify-tools.ts'
46
+ import * as guiTools from './gui.ts'
45
47
  import * as righthandSkills from './skills.ts'
46
48
  import type { GuardRule } from './guard-tools.ts'
47
49
 
@@ -73,12 +75,13 @@ export function apply(ctx: Context, config: RighthandConfig = {}): void {
73
75
  ctx.plugin(filesTools)
74
76
  ctx.plugin(eventsTools)
75
77
  ctx.plugin(notifyTools)
78
+ ctx.plugin(guiTools)
76
79
  ctx.plugin(guardTools, { rules: config.rules ?? [] })
77
80
  ctx.plugin(righthandSkills)
78
81
  }
79
82
 
80
83
  // Individual modules stay importable for selective mounting.
81
- export { storeTools, secretsTools, execTools, taskTools, textTools, weatherTools, placesTools, filesTools, eventsTools, notifyTools, guardTools, righthandSkills }
84
+ export { storeTools, secretsTools, execTools, taskTools, textTools, weatherTools, placesTools, filesTools, eventsTools, notifyTools, guiTools, guardTools, righthandSkills }
82
85
  export { SKILL_NAME, SKILL_DESCRIPTION, skillDirectory, skillBody } from './skills.ts'
83
86
  export type { GuardRule } from './guard-tools.ts'
84
87
  export { guardFactsFor } from './guard-tools.ts'
@@ -8,7 +8,8 @@
8
8
 
9
9
  import type { Context } from '@deepseek-ai/cordis'
10
10
  import { defineTool } from '@deepseek-ai/dsh-tools'
11
- import { settingsNamespace } from '@deepseek-ai/dsh-settings'
11
+ // Type-only: declares `ctx.settings`.
12
+ import type {} from '@deepseek-ai/dsh-settings'
12
13
  import { guardedFetch } from './weather-tools.ts'
13
14
  import { genCall, genResult } from './cards.ts'
14
15
 
@@ -23,7 +24,7 @@ export interface NotifyConfig {
23
24
  }
24
25
 
25
26
  export function apply(ctx: Context, config: NotifyConfig = {}): void {
26
- const ns = settingsNamespace('righthand')
27
+ const ns = 'righthand'
27
28
  const baseUrl = config.baseUrl ?? 'https://ntfy.sh'
28
29
 
29
30
  ctx.tools.register(defineTool({
@@ -61,5 +62,4 @@ export function apply(ctx: Context, config: NotifyConfig = {}): void {
61
62
  return { topic, sent: true }
62
63
  },
63
64
  }))
64
- }
65
-
65
+ }
@@ -10,7 +10,8 @@
10
10
  import type { Context } from '@deepseek-ai/cordis'
11
11
  import { defineTool } from '@deepseek-ai/dsh-tools'
12
12
  import { credentialRef } from '@deepseek-ai/dsh-credentials'
13
- import { settingsNamespace } from '@deepseek-ai/dsh-settings'
13
+ // Type-only: declares `ctx.settings` (the settings provider seam).
14
+ import type {} from '@deepseek-ai/dsh-settings'
14
15
  import { genCall, genResult } from './cards.ts'
15
16
  import z from '@deepseek-ai/schemastery'
16
17
 
@@ -34,7 +35,7 @@ export const righthandSettingsSchema = z.object({
34
35
 
35
36
  /** Register the settings namespace + the secret/credential tools. */
36
37
  export function apply(ctx: Context): void {
37
- const ns = settingsNamespace('righthand')
38
+ const ns = 'righthand'
38
39
  // Register once; the settings provider merges schema defaults + user doc.
39
40
  const scope = ctx.settings.register(ns, righthandSettingsSchema)
40
41
 
@@ -157,4 +158,4 @@ export function apply(ctx: Context): void {
157
158
  return { applied: true }
158
159
  },
159
160
  }))
160
- }
161
+ }
package/src/text-tools.ts CHANGED
@@ -9,8 +9,9 @@
9
9
 
10
10
  import type { Context } from '@deepseek-ai/cordis'
11
11
  import { defineTool } from '@deepseek-ai/dsh-tools'
12
- import { BlockAssembler, createUserMessage, deepFreeze } from '@deepseek-ai/dsh-llm'
12
+ import { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm'
13
13
  import type { GenerateOptions } from '@deepseek-ai/dsh-llm'
14
+ import { deepFreeze } from '@deepseek-ai/dsh-util-values'
14
15
  import { genCall, genResult } from './cards.ts'
15
16
 
16
17
  export const name = 'righthand-text'