@try-works/dsh-righthand 0.1.18 → 0.1.20
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/lib/cards.d.ts +16 -0
- package/lib/cards.d.ts.map +1 -0
- package/lib/cards.js +18 -0
- package/lib/cards.js.map +1 -0
- package/lib/client/index.d.ts +14 -0
- package/lib/client/index.d.ts.map +1 -0
- package/lib/client/index.js +118 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client.js +373 -0
- package/lib/client.js.map +1 -0
- package/lib/events-tools.d.ts.map +1 -1
- package/lib/events-tools.js +11 -0
- package/lib/events-tools.js.map +1 -1
- package/lib/exec-tools.d.ts.map +1 -1
- package/lib/exec-tools.js +9 -0
- package/lib/exec-tools.js.map +1 -1
- package/lib/files-tools.d.ts.map +1 -1
- package/lib/files-tools.js +11 -0
- package/lib/files-tools.js.map +1 -1
- package/lib/gui.d.ts +12 -0
- package/lib/gui.d.ts.map +1 -0
- package/lib/gui.js +160 -0
- package/lib/gui.js.map +1 -0
- package/lib/index.d.ts +3 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -1
- package/lib/index.js.map +1 -1
- package/lib/notify-tools.d.ts.map +1 -1
- package/lib/notify-tools.js +3 -0
- package/lib/notify-tools.js.map +1 -1
- package/lib/places-tools.d.ts.map +1 -1
- package/lib/places-tools.js +9 -0
- package/lib/places-tools.js.map +1 -1
- package/lib/secrets-tools.d.ts.map +1 -1
- package/lib/secrets-tools.js +11 -0
- package/lib/secrets-tools.js.map +1 -1
- package/lib/store-tools.d.ts.map +1 -1
- package/lib/store-tools.js +8 -0
- package/lib/store-tools.js.map +1 -1
- package/lib/task-tools.d.ts.map +1 -1
- package/lib/task-tools.js +11 -0
- package/lib/task-tools.js.map +1 -1
- package/lib/text-tools.d.ts.map +1 -1
- package/lib/text-tools.js +9 -0
- package/lib/text-tools.js.map +1 -1
- package/lib/weather-tools.d.ts.map +1 -1
- package/lib/weather-tools.js +5 -0
- package/lib/weather-tools.js.map +1 -1
- package/package.json +24 -3
- package/src/cards.ts +23 -0
- package/src/client/index.ts +184 -0
- package/src/events-tools.ts +11 -0
- package/src/exec-tools.ts +10 -0
- package/src/files-tools.ts +11 -0
- package/src/gui.ts +157 -0
- package/src/index.ts +4 -1
- package/src/notify-tools.ts +3 -0
- package/src/places-tools.ts +9 -0
- package/src/secrets-tools.ts +11 -0
- package/src/store-tools.ts +9 -0
- package/src/task-tools.ts +11 -0
- package/src/text-tools.ts +9 -0
- package/src/weather-tools.ts +5 -0
|
@@ -0,0 +1,184 @@
|
|
|
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 { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
16
|
+
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
17
|
+
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
|
18
|
+
|
|
19
|
+
/** Required services: the slot registry (client runtime). */
|
|
20
|
+
export const inject = ['slots']
|
|
21
|
+
|
|
22
|
+
const C = {
|
|
23
|
+
bg: '#15171a',
|
|
24
|
+
panel: '#1b1e23',
|
|
25
|
+
row: '#23272e',
|
|
26
|
+
border: '#2d323a',
|
|
27
|
+
text: '#e6e8ec',
|
|
28
|
+
dim: '#9aa3ad',
|
|
29
|
+
accent: '#3b82f6',
|
|
30
|
+
green: '#3fbf6e',
|
|
31
|
+
red: '#e5484d',
|
|
32
|
+
amber: '#d9a13b',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface State {
|
|
36
|
+
settings: Record<string, unknown>
|
|
37
|
+
storeKeys: string[]
|
|
38
|
+
tasks: any[]
|
|
39
|
+
events: any[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const EMPTY: State = { settings: {}, storeKeys: [], tasks: [], events: [] }
|
|
43
|
+
|
|
44
|
+
function fmt(s: unknown, n: number): string {
|
|
45
|
+
try { const t = JSON.stringify(s); return t === undefined ? '' : (t.length > n ? t.slice(0, n) + '...' : t) }
|
|
46
|
+
catch { return String(s) }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function Row(props: { label: string; value: string; tone?: string }): ReturnType<typeof h> {
|
|
50
|
+
return h('div', { style: { display: 'flex', gap: 8, justifyContent: 'space-between', padding: '4px 0', borderBottom: '1px solid ' + C.border, fontSize: 12 } },
|
|
51
|
+
h('span', { style: { color: C.dim, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, props.label),
|
|
52
|
+
h('span', { style: { color: props.tone ?? C.text, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '55%' } }, props.value),
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Btn(props: { label: string; onClick: () => void; tone?: string; danger?: boolean }): ReturnType<typeof h> {
|
|
57
|
+
const base: CSSProperties = {
|
|
58
|
+
background: C.row, color: props.danger ? C.red : (props.tone ?? C.text),
|
|
59
|
+
border: '1px solid ' + C.border, borderRadius: 6, padding: '3px 10px',
|
|
60
|
+
fontSize: 11, cursor: 'pointer', whiteSpace: 'nowrap'
|
|
61
|
+
}
|
|
62
|
+
return h('button', { style: base, onClick: props.onClick }, props.label)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function Section(props: { title: string; children?: any }): ReturnType<typeof h> {
|
|
66
|
+
return h('div', { style: { marginTop: 14 } },
|
|
67
|
+
h('div', { style: { fontSize: 11, letterSpacing: '0.12em', textTransform: 'uppercase', color: C.dim, marginBottom: 6 } }, props.title),
|
|
68
|
+
props.children,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function RighthandPanel(): ReturnType<typeof h> {
|
|
73
|
+
const [collapsed, setCollapsed] = useState(false)
|
|
74
|
+
const [state, setState] = useState<State>(EMPTY)
|
|
75
|
+
const [error, setError] = useState('')
|
|
76
|
+
const [patch, setPatch] = useState('{}')
|
|
77
|
+
const [newKey, setNewKey] = useState('')
|
|
78
|
+
const [newValue, setNewValue] = useState('')
|
|
79
|
+
|
|
80
|
+
const load = (): void => {
|
|
81
|
+
fetch('/righthand/state').then(r => r.json()).then((d: State) => { setState(d); setError('') })
|
|
82
|
+
.catch((e: Error) => setError(String(e)))
|
|
83
|
+
}
|
|
84
|
+
useEffect(() => { load() }, [])
|
|
85
|
+
|
|
86
|
+
const post = (path: string, body: unknown, then: () => void): void => {
|
|
87
|
+
fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
|
|
88
|
+
.then(r => r.json()).then((d: any) => { if (!d.ok && d.error) setError(String(d.error)); then() })
|
|
89
|
+
.catch((e: Error) => setError(String(e)))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const railStyle: CSSProperties = {
|
|
93
|
+
position: 'fixed', right: 0, top: 0, bottom: 0, width: 44, background: C.panel,
|
|
94
|
+
borderLeft: '1px solid ' + C.border, display: 'flex', flexDirection: 'column',
|
|
95
|
+
alignItems: 'center', paddingTop: 12, gap: 10, zIndex: 9998
|
|
96
|
+
}
|
|
97
|
+
const panelStyle: CSSProperties = {
|
|
98
|
+
position: 'fixed', right: 0, top: 0, bottom: 0, width: 340, background: C.bg,
|
|
99
|
+
borderLeft: '1px solid ' + C.border, padding: 12, overflowY: 'auto', zIndex: 9998,
|
|
100
|
+
fontFamily: 'ui-sans-serif, system-ui, sans-serif', color: C.text, fontSize: 13
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const toggle = h('button', {
|
|
104
|
+
style: { background: 'none', border: 'none', color: C.dim, fontSize: 18, cursor: 'pointer' },
|
|
105
|
+
onClick: () => setCollapsed(!collapsed),
|
|
106
|
+
}, collapsed ? '<' : '>')
|
|
107
|
+
|
|
108
|
+
const refreshBtn = h(Btn, { label: 'Refresh', onClick: load })
|
|
109
|
+
|
|
110
|
+
if (collapsed) {
|
|
111
|
+
return createPortal(h('div', { style: railStyle, 'data-dsh-righthand-panel': 'collapsed' },
|
|
112
|
+
toggle,
|
|
113
|
+
h('span', { style: { color: C.green, fontSize: 10, width: 8, height: 8, borderRadius: '50%', background: C.green } }),
|
|
114
|
+
h('span', { style: { color: C.dim, fontSize: 11, writingMode: 'vertical-rl', letterSpacing: '0.12em', userSelect: 'none' } }, 'Righthand'),
|
|
115
|
+
), document.body)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return createPortal(h('div', { style: panelStyle, 'data-dsh-righthand-panel': 'expanded' },
|
|
119
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 } },
|
|
120
|
+
h('span', { style: { fontWeight: 600, letterSpacing: '0.05em' } }, 'Righthand'),
|
|
121
|
+
h('span', { style: { display: 'flex', gap: 6 } }, refreshBtn, toggle),
|
|
122
|
+
),
|
|
123
|
+
error !== '' ? h('div', { style: { color: C.red, fontSize: 11, marginBottom: 8 } }, error) : null,
|
|
124
|
+
|
|
125
|
+
h(Section, { title: 'Settings' },
|
|
126
|
+
Object.entries(state.settings).map(([k, v]) => h(Row, { key: k, label: k, value: fmt(v, 90) })),
|
|
127
|
+
h('div', { style: { display: 'flex', gap: 6, marginTop: 8 } },
|
|
128
|
+
h('input', {
|
|
129
|
+
value: patch, onChange: (e: any) => setPatch(e.target.value),
|
|
130
|
+
style: { flex: 1, background: C.row, color: C.text, border: '1px solid ' + C.border, borderRadius: 6, padding: '3px 8px', fontSize: 11, fontFamily: 'monospace' },
|
|
131
|
+
}),
|
|
132
|
+
h(Btn, { label: 'Patch', onClick: () => {
|
|
133
|
+
try { const p = JSON.parse(patch); post('/righthand/settings/set', { patch: p }, load) }
|
|
134
|
+
catch { setError('patch must be JSON') }
|
|
135
|
+
} }),
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
|
|
139
|
+
h(Section, { title: 'Store (' + state.storeKeys.length + ')' },
|
|
140
|
+
state.storeKeys.length === 0 ? h('div', { style: { color: C.dim, fontSize: 12 } }, '(empty)') : null,
|
|
141
|
+
state.storeKeys.map(k => h('div', { key: k, style: { display: 'flex', gap: 6, alignItems: 'center', padding: '2px 0' } },
|
|
142
|
+
h('span', { style: { flex: 1, fontFamily: 'monospace', fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, k),
|
|
143
|
+
h(Btn, { label: 'Delete', danger: true, onClick: () => post('/righthand/store/delete', { key: k }, load) }),
|
|
144
|
+
)),
|
|
145
|
+
h('div', { style: { display: 'flex', gap: 6, marginTop: 8 } },
|
|
146
|
+
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' } }),
|
|
147
|
+
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' } }),
|
|
148
|
+
h(Btn, { label: 'Put', onClick: () => {
|
|
149
|
+
if (newKey === '') { setError('key required'); return }
|
|
150
|
+
try { const v = JSON.parse(newValue === '' ? 'null' : newValue); post('/righthand/store/put', { key: newKey, value: v }, () => { load(); setNewKey(''); setNewValue('') }) }
|
|
151
|
+
catch { setError('value must be JSON') }
|
|
152
|
+
} }),
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
|
|
156
|
+
h(Section, { title: 'Tasks (' + state.tasks.length + ')' },
|
|
157
|
+
state.tasks.length === 0 ? h('div', { style: { color: C.dim, fontSize: 12 } }, '(none)') : null,
|
|
158
|
+
state.tasks.map((t: any) => h('div', { key: t.id, style: { display: 'flex', gap: 6, alignItems: 'center', padding: '2px 0' } },
|
|
159
|
+
h('span', { style: { flex: 1, fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, (t.state === 'done' ? '[x] ' : t.state === 'failed' ? '[!] ' : '[ ] ') + t.title),
|
|
160
|
+
h(Btn, { label: 'Done', tone: C.green, onClick: () => post('/righthand/task/update', { id: t.id, state: 'done' }, load) }),
|
|
161
|
+
h(Btn, { label: 'Fail', danger: true, onClick: () => post('/righthand/task/update', { id: t.id, state: 'failed' }, load) }),
|
|
162
|
+
)),
|
|
163
|
+
),
|
|
164
|
+
|
|
165
|
+
h(Section, { title: 'Events (' + state.events.length + ')' },
|
|
166
|
+
state.events.length === 0 ? h('div', { style: { color: C.dim, fontSize: 12 } }, '(none)') : null,
|
|
167
|
+
state.events.map((e: any) => h('div', { key: e.id, style: { display: 'flex', gap: 6, alignItems: 'center', padding: '2px 0' } },
|
|
168
|
+
h('span', { style: { flex: 1, fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, e.title + ' @ ' + e.at + ' [' + e.state + ']'),
|
|
169
|
+
e.state === 'pending' ? h(Btn, { label: 'Cancel', danger: true, onClick: () => post('/righthand/event/cancel', { id: e.id }, load) }) : null,
|
|
170
|
+
)),
|
|
171
|
+
),
|
|
172
|
+
), document.body)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Client plugin body: register the panel into the frame-wide overlay slot.
|
|
177
|
+
*/
|
|
178
|
+
export function apply(ctx: ClientContext): void {
|
|
179
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
180
|
+
name: 'shell.overlay',
|
|
181
|
+
id: 'righthand-panel',
|
|
182
|
+
order: 60,
|
|
183
|
+
}, RighthandPanel))
|
|
184
|
+
}
|
package/src/events-tools.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
13
13
|
import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain'
|
|
14
14
|
import type { Domain } from '@deepseek-ai/dsh-storage-domain'
|
|
15
15
|
import { z } from 'zod'
|
|
16
|
+
import { genCall, genResult } from './cards.ts'
|
|
16
17
|
|
|
17
18
|
export const name = 'righthand-events'
|
|
18
19
|
export const inject = ['tools', 'storageDomain']
|
|
@@ -134,6 +135,8 @@ export function apply(ctx: Context): void {
|
|
|
134
135
|
},
|
|
135
136
|
render: (_args, value) => [{ type: 'text', text: 'event ' + value.id + ': ' + value.title + ' at ' + value.at }],
|
|
136
137
|
},
|
|
138
|
+
presentCall: (args: any) => genCall('Reminder: ' + args.title + ' at ' + args.at, 'edit'),
|
|
139
|
+
presentResult: (_args, result) => genResult(result),
|
|
137
140
|
async execute(args) {
|
|
138
141
|
const d = await ensure()
|
|
139
142
|
const at = new Date(args.at)
|
|
@@ -166,6 +169,8 @@ export function apply(ctx: Context): void {
|
|
|
166
169
|
},
|
|
167
170
|
render: (_args, value) => [{ type: 'text', text: value.due.length === 0 ? '(nothing due)' : value.due.map((e: any) => e.title + ' at ' + e.at).join(' / ') }],
|
|
168
171
|
},
|
|
172
|
+
presentCall: () => genCall('Due events', 'read'),
|
|
173
|
+
presentResult: (_args, result) => genResult(result),
|
|
169
174
|
async execute() {
|
|
170
175
|
const d = await ensure()
|
|
171
176
|
const now = Date.now()
|
|
@@ -192,6 +197,8 @@ export function apply(ctx: Context): void {
|
|
|
192
197
|
schema: { type: 'array', items: { type: 'json' } },
|
|
193
198
|
render: (_args, value) => [{ type: 'text', text: value.length === 0 ? '(no events)' : (value as any[]).map((e: any) => e.at + ' ' + e.title + ' [' + e.state + ']').join(' / ') }],
|
|
194
199
|
},
|
|
200
|
+
presentCall: () => genCall('Events', 'read'),
|
|
201
|
+
presentResult: (_args, result) => genResult(result),
|
|
195
202
|
async execute(args) {
|
|
196
203
|
const events = await all()
|
|
197
204
|
const wanted = args.state ?? null
|
|
@@ -213,6 +220,8 @@ export function apply(ctx: Context): void {
|
|
|
213
220
|
schema: { type: 'array', items: { type: 'string' } },
|
|
214
221
|
render: (_args, value) => [{ type: 'text', text: value.length === 0 ? '(no free slot)' : (value as string[]).join(' / ') }],
|
|
215
222
|
},
|
|
223
|
+
presentCall: (args: any) => genCall('Free slots (' + args.durationMinutes + 'm)', 'read'),
|
|
224
|
+
presentResult: (_args, result) => genResult(result),
|
|
216
225
|
async execute(args) {
|
|
217
226
|
const events = await all()
|
|
218
227
|
return freeSlots(events, new Date(), args.durationMinutes, args.horizonHours ?? 24)
|
|
@@ -236,6 +245,8 @@ export function apply(ctx: Context): void {
|
|
|
236
245
|
},
|
|
237
246
|
render: (_args, value) => [{ type: 'text', text: 'cancelled ' + value.id + ': ' + value.existed }],
|
|
238
247
|
},
|
|
248
|
+
presentCall: (args: any) => genCall('Cancel event ' + args.id, 'edit'),
|
|
249
|
+
presentResult: (_args, result) => genResult(result),
|
|
239
250
|
async execute(args) {
|
|
240
251
|
const d = await ensure()
|
|
241
252
|
const e = d.table('events').get(args.id)
|
package/src/exec-tools.ts
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis'
|
|
11
11
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
12
|
+
import type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools'
|
|
12
13
|
import type { SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
|
|
14
|
+
import { genCall, genResult, textOf } from './cards.ts'
|
|
13
15
|
import type {} from '@deepseek-ai/dsh-jobs'
|
|
14
16
|
|
|
15
17
|
export const name = 'righthand-exec'
|
|
@@ -41,6 +43,12 @@ export function apply(ctx: Context): void {
|
|
|
41
43
|
},
|
|
42
44
|
},
|
|
43
45
|
render: (_args, value) => [{ type: 'text', text: `exit ${value.exitCode ?? ('signal ' + value.signal)}; stdout=${value.stdout.length}b stderr=${value.stderr.length}b` }],
|
|
46
|
+
presentationMeta: (_args, value) => ({ exitCode: value.exitCode ?? null, signal: value.signal ?? null }),
|
|
47
|
+
},
|
|
48
|
+
presentCall: (args: any): ToolCallView => ({ card: 'terminal', title: args.argv.join(' '), description: 'run one command (bounded collect)' }),
|
|
49
|
+
presentResult: (_args, result): ToolResultView => {
|
|
50
|
+
const meta = result.meta as any
|
|
51
|
+
return { card: 'terminal', output: textOf(result.content) + '\n' + (meta?.exitCode ?? ''), exitCode: meta?.exitCode ?? undefined, signal: meta?.signal ?? undefined }
|
|
44
52
|
},
|
|
45
53
|
async execute(args, exec) {
|
|
46
54
|
const maxBytes = args.maxOutputBytes ?? 4096
|
|
@@ -89,6 +97,8 @@ export function apply(ctx: Context): void {
|
|
|
89
97
|
},
|
|
90
98
|
render: (_args, value) => [{ type: 'text', text: `started background job ${value.jobId} (${value.label})` }],
|
|
91
99
|
},
|
|
100
|
+
presentCall: (args: any): ToolCallView => genCall('Background job: ' + (args.label ?? args.argv[0]), 'execute'),
|
|
101
|
+
presentResult: (_args, result): ToolResultView => genResult(result),
|
|
92
102
|
async execute(args, exec) {
|
|
93
103
|
const label = args.label ?? args.argv.join(' ')
|
|
94
104
|
const jobId = ctx.jobs.start({
|
package/src/files-tools.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
15
15
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
16
16
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
17
17
|
import { signRequest, presignGet } from './sigv4.ts'
|
|
18
|
+
import { genCall, genResult } from './cards.ts'
|
|
18
19
|
|
|
19
20
|
export const name = 'righthand-files'
|
|
20
21
|
export const inject = ['tools', 'credentials', 'settings']
|
|
@@ -178,6 +179,8 @@ export function apply(ctx: Context, config: FilesConfig = {}): void {
|
|
|
178
179
|
},
|
|
179
180
|
render: (_args, value) => [{ type: 'text', text: 'stored ' + value.key + ' (' + value.size + ' bytes)' }],
|
|
180
181
|
},
|
|
182
|
+
presentCall: (args: any) => genCall('Upload ' + args.key, 'edit'),
|
|
183
|
+
presentResult: (_args, result) => genResult(result),
|
|
181
184
|
async execute(args) {
|
|
182
185
|
const c = await client()
|
|
183
186
|
return c.put(args.key, args.content, args.contentType)
|
|
@@ -203,6 +206,8 @@ export function apply(ctx: Context, config: FilesConfig = {}): void {
|
|
|
203
206
|
},
|
|
204
207
|
render: (_args, value) => [{ type: 'text', text: value.found && typeof value.content === 'string' ? value.content : '(absent)' }],
|
|
205
208
|
},
|
|
209
|
+
presentCall: (args: any) => genCall('Read object ' + args.key, 'read'),
|
|
210
|
+
presentResult: (_args, result) => genResult(result),
|
|
206
211
|
async execute(args) {
|
|
207
212
|
const c = await client()
|
|
208
213
|
const out = await c.get(args.key)
|
|
@@ -231,6 +236,8 @@ export function apply(ctx: Context, config: FilesConfig = {}): void {
|
|
|
231
236
|
},
|
|
232
237
|
render: (_args, value) => [{ type: 'text', text: value.entries.length + ' objects' }],
|
|
233
238
|
},
|
|
239
|
+
presentCall: () => genCall('List bucket', 'read'),
|
|
240
|
+
presentResult: (_args, result) => genResult(result),
|
|
234
241
|
async execute(args) {
|
|
235
242
|
const c = await client()
|
|
236
243
|
const entries = await c.list(args.prefix, args.maxKeys)
|
|
@@ -257,6 +264,8 @@ export function apply(ctx: Context, config: FilesConfig = {}): void {
|
|
|
257
264
|
},
|
|
258
265
|
render: (_args, value) => [{ type: 'text', text: value.url }],
|
|
259
266
|
},
|
|
267
|
+
presentCall: (args: any) => genCall('Share ' + args.key + ' for ' + (args.minutes ?? 60) + ' min', 'execute'),
|
|
268
|
+
presentResult: (_args, result) => genResult(result),
|
|
260
269
|
async execute(args) {
|
|
261
270
|
const minutes = Math.min(Math.max(args.minutes ?? 60, 1), 10080)
|
|
262
271
|
const c = await client()
|
|
@@ -282,6 +291,8 @@ export function apply(ctx: Context, config: FilesConfig = {}): void {
|
|
|
282
291
|
},
|
|
283
292
|
render: (_args, value) => [{ type: 'text', text: 'deleted ' + value.key + ': ' + value.existed }],
|
|
284
293
|
},
|
|
294
|
+
presentCall: (args: any) => genCall('Delete object ' + args.key, 'delete'),
|
|
295
|
+
presentResult: (_args, result) => genResult(result),
|
|
285
296
|
async execute(args) {
|
|
286
297
|
const c = await client()
|
|
287
298
|
return c.delete(args.key)
|
package/src/gui.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
12
|
+
import { storeDomain } from './store-tools.ts'
|
|
13
|
+
import { taskDomain } from './task-tools.ts'
|
|
14
|
+
import { eventsDomain } from './events-tools.ts'
|
|
15
|
+
// Type-only: activates the cordis Context merge for ctx.webServer.
|
|
16
|
+
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
17
|
+
|
|
18
|
+
export const name = 'righthand-gui'
|
|
19
|
+
export const inject = ['webServer', 'storageDomain', 'settings']
|
|
20
|
+
|
|
21
|
+
const ns = settingsNamespace('righthand')
|
|
22
|
+
|
|
23
|
+
function json(res: ServerResponse, code: number, body: unknown): void {
|
|
24
|
+
const text = JSON.stringify(body)
|
|
25
|
+
res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
26
|
+
res.end(text)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readBody(req: IncomingMessage): Promise<string> {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const chunks: Buffer[] = []
|
|
32
|
+
let bytes = 0
|
|
33
|
+
req.on('data', (c: Buffer) => {
|
|
34
|
+
bytes += c.length
|
|
35
|
+
if (bytes > 65536) { reject(new Error('body too large')); req.destroy() }
|
|
36
|
+
else chunks.push(c)
|
|
37
|
+
})
|
|
38
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
39
|
+
req.on('error', reject)
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function apply(ctx: Context): void {
|
|
44
|
+
let domains: { store: any; tasks: any; events: any } | undefined
|
|
45
|
+
let ready: Promise<void> | undefined
|
|
46
|
+
|
|
47
|
+
ctx.effect(() => {
|
|
48
|
+
ready = (async () => {
|
|
49
|
+
const [store, tasks, events] = await Promise.all([
|
|
50
|
+
ctx.storageDomain.open(storeDomain),
|
|
51
|
+
ctx.storageDomain.open(taskDomain),
|
|
52
|
+
ctx.storageDomain.open(eventsDomain),
|
|
53
|
+
])
|
|
54
|
+
domains = { store, tasks, events }
|
|
55
|
+
})()
|
|
56
|
+
return () => { }
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const ensure = async (): Promise<NonNullable<typeof domains>> => {
|
|
60
|
+
if (domains === undefined) { await ready }
|
|
61
|
+
if (domains === undefined) throw new Error('righthand domains not open')
|
|
62
|
+
return domains
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const getSettings = () => {
|
|
66
|
+
const v = ctx.settings.get(ns) as Record<string, unknown> | undefined
|
|
67
|
+
return v ?? {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const disposers: Array<() => void> = []
|
|
71
|
+
ctx.effect(() => () => { for (const d of disposers.splice(0)) d() })
|
|
72
|
+
|
|
73
|
+
const route = (path: string, handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>): void => {
|
|
74
|
+
disposers.push(ctx.webServer.register({ kind: 'exact', path, handler }))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
route('/righthand/state', async (_req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
const d = await ensure()
|
|
80
|
+
const storeKeys = [...d.store.table('rows').keys()]
|
|
81
|
+
const tasks = [...d.tasks.table('tasks').entries()]
|
|
82
|
+
const events = [...d.events.table('events').entries()]
|
|
83
|
+
json(res, 200, {
|
|
84
|
+
settings: getSettings(),
|
|
85
|
+
storeKeys,
|
|
86
|
+
tasks: tasks.map(([id, row]) => ({ id, ...row })),
|
|
87
|
+
events: events.map(([id, row]) => ({ id, ...row })),
|
|
88
|
+
})
|
|
89
|
+
} catch (e) {
|
|
90
|
+
json(res, 500, { error: String(e) })
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
route('/righthand/settings/set', async (req, res) => {
|
|
95
|
+
try {
|
|
96
|
+
const body = JSON.parse(await readBody(req))
|
|
97
|
+
await ctx.settings.update(ns, body.patch)
|
|
98
|
+
json(res, 200, { ok: true })
|
|
99
|
+
} catch (e) {
|
|
100
|
+
json(res, 400, { error: String(e) })
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
route('/righthand/store/put', async (req, res) => {
|
|
105
|
+
try {
|
|
106
|
+
const d = await ensure()
|
|
107
|
+
const body = JSON.parse(await readBody(req))
|
|
108
|
+
if (typeof body.key !== 'string' || body.key === '') throw new Error('key required')
|
|
109
|
+
await d.store.table('rows').put(body.key, { value: body.value ?? null, updatedAt: new Date().toISOString() })
|
|
110
|
+
const writes = d.store.global.get()
|
|
111
|
+
await d.store.global.set({ writes: writes.writes + 1 })
|
|
112
|
+
json(res, 200, { ok: true })
|
|
113
|
+
} catch (e) {
|
|
114
|
+
json(res, 400, { error: String(e) })
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
route('/righthand/store/delete', async (req, res) => {
|
|
119
|
+
try {
|
|
120
|
+
const d = await ensure()
|
|
121
|
+
const body = JSON.parse(await readBody(req))
|
|
122
|
+
const existed = await d.store.table('rows').delete(body.key)
|
|
123
|
+
json(res, 200, { ok: true, existed })
|
|
124
|
+
} catch (e) {
|
|
125
|
+
json(res, 400, { error: String(e) })
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
route('/righthand/task/update', async (req, res) => {
|
|
130
|
+
try {
|
|
131
|
+
const d = await ensure()
|
|
132
|
+
const body = JSON.parse(await readBody(req))
|
|
133
|
+
const row = await d.tasks.table('tasks').get(body.id)
|
|
134
|
+
if (row === undefined) { json(res, 404, { error: 'task not found' }); return }
|
|
135
|
+
const next: any = { ...row }
|
|
136
|
+
if (typeof body.state === 'string') next.state = body.state
|
|
137
|
+
if (body.result !== undefined) next.result = body.result
|
|
138
|
+
await d.tasks.table('tasks').put(body.id, next)
|
|
139
|
+
json(res, 200, { ok: true })
|
|
140
|
+
} catch (e) {
|
|
141
|
+
json(res, 400, { error: String(e) })
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
route('/righthand/event/cancel', async (req, res) => {
|
|
146
|
+
try {
|
|
147
|
+
const d = await ensure()
|
|
148
|
+
const body = JSON.parse(await readBody(req))
|
|
149
|
+
const row = await d.events.table('events').get(body.id)
|
|
150
|
+
if (row === undefined) { json(res, 404, { error: 'event not found' }); return }
|
|
151
|
+
await d.events.table('events').put(body.id, { ...row, state: 'cancelled' })
|
|
152
|
+
json(res, 200, { ok: true })
|
|
153
|
+
} catch (e) {
|
|
154
|
+
json(res, 400, { error: String(e) })
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
}
|
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'
|
package/src/notify-tools.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { Context } from '@deepseek-ai/cordis'
|
|
|
10
10
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
11
11
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
12
12
|
import { guardedFetch } from './weather-tools.ts'
|
|
13
|
+
import { genCall, genResult } from './cards.ts'
|
|
13
14
|
|
|
14
15
|
export const name = 'righthand-notify'
|
|
15
16
|
export const inject = ['tools', 'settings']
|
|
@@ -45,6 +46,8 @@ export function apply(ctx: Context, config: NotifyConfig = {}): void {
|
|
|
45
46
|
},
|
|
46
47
|
render: (_args, value) => [{ type: 'text', text: 'notified ' + value.topic }],
|
|
47
48
|
},
|
|
49
|
+
presentCall: (args: any) => genCall('Notify: ' + args.message.slice(0, 60), 'execute'),
|
|
50
|
+
presentResult: (_args, result) => genResult(result),
|
|
48
51
|
async execute(args) {
|
|
49
52
|
const settings = ctx.settings.get(ns) as { defaultNotifyTopic?: string } | undefined
|
|
50
53
|
const topic = args.topic ?? config.topic ?? settings?.defaultNotifyTopic ?? ''
|
package/src/places-tools.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type { Context } from '@deepseek-ai/cordis'
|
|
13
13
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
14
14
|
import { guardedFetch } from './weather-tools.ts'
|
|
15
|
+
import { genCall, genResult } from './cards.ts'
|
|
15
16
|
|
|
16
17
|
export const name = 'righthand-places'
|
|
17
18
|
export const inject = ['tools']
|
|
@@ -122,6 +123,8 @@ export function apply(ctx: Context, config: PlacesConfig = {}): void {
|
|
|
122
123
|
return [{ type: 'text', text }]
|
|
123
124
|
},
|
|
124
125
|
},
|
|
126
|
+
presentCall: (args: any) => genCall('Places: ' + args.query, 'search'),
|
|
127
|
+
presentResult: (_args, result) => genResult(result),
|
|
125
128
|
async execute(args) {
|
|
126
129
|
const limit = Math.min(Math.max(1, Math.round(args.limit ?? 5)), 20)
|
|
127
130
|
const qs = new URLSearchParams({ q: args.query, format: 'jsonv2', addressdetails: '1', limit: String(limit) })
|
|
@@ -153,6 +156,8 @@ export function apply(ctx: Context, config: PlacesConfig = {}): void {
|
|
|
153
156
|
},
|
|
154
157
|
render: (_args, value) => [{ type: 'text', text: value.displayName }],
|
|
155
158
|
},
|
|
159
|
+
presentCall: (args: any) => genCall('Reverse geocode ' + args.latitude + ', ' + args.longitude, 'fetch'),
|
|
160
|
+
presentResult: (_args, result) => genResult(result),
|
|
156
161
|
async execute(args) {
|
|
157
162
|
const qs = new URLSearchParams({ lat: String(args.latitude), lon: String(args.longitude), format: 'jsonv2', addressdetails: '1' })
|
|
158
163
|
const body = await guardedFetch(cfg.nominatimUrl + '/reverse?' + qs.toString(), { init: { headers: nomHeaders } })
|
|
@@ -181,6 +186,8 @@ export function apply(ctx: Context, config: PlacesConfig = {}): void {
|
|
|
181
186
|
},
|
|
182
187
|
render: (_args, value) => [{ type: 'text', text: value.elevation + ' m' }],
|
|
183
188
|
},
|
|
189
|
+
presentCall: (args: any) => genCall('Elevation at ' + args.latitude + ', ' + args.longitude, 'fetch'),
|
|
190
|
+
presentResult: (_args, result) => genResult(result),
|
|
184
191
|
async execute(args) {
|
|
185
192
|
const qs = new URLSearchParams({ latitude: String(args.latitude), longitude: String(args.longitude) })
|
|
186
193
|
const body = await guardedFetch(cfg.elevationUrl + '?' + qs.toString())
|
|
@@ -207,6 +214,8 @@ export function apply(ctx: Context, config: PlacesConfig = {}): void {
|
|
|
207
214
|
return [{ type: 'text', text }]
|
|
208
215
|
},
|
|
209
216
|
},
|
|
217
|
+
presentCall: (args: any) => genCall('Nearby: ' + args.query + ' within ' + (args.radiusKm ?? 1) + ' km', 'search'),
|
|
218
|
+
presentResult: (_args, result) => genResult(result),
|
|
210
219
|
async execute(args) {
|
|
211
220
|
const radius = Math.min(Math.max(0.1, Number(args.radiusKm ?? 1)), 20)
|
|
212
221
|
const limit = Math.min(Math.max(1, Math.round(args.limit ?? 5)), 20)
|
package/src/secrets-tools.ts
CHANGED
|
@@ -11,6 +11,7 @@ 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
13
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
14
|
+
import { genCall, genResult } from './cards.ts'
|
|
14
15
|
import z from '@deepseek-ai/schemastery'
|
|
15
16
|
|
|
16
17
|
export const name = 'righthand-secrets'
|
|
@@ -56,6 +57,8 @@ export function apply(ctx: Context): void {
|
|
|
56
57
|
},
|
|
57
58
|
render: (_args, value) => [{ type: 'text', text: `${value.ref}: configured=${value.configured}${value.source ? ' (source=' + value.source + ')' : ''}, writable=${value.writable}` }],
|
|
58
59
|
},
|
|
60
|
+
presentCall: (args: any) => genCall('Check credential ' + args.ref, 'read'),
|
|
61
|
+
presentResult: (_args, result) => genResult(result),
|
|
59
62
|
async execute(args) {
|
|
60
63
|
const ref = credentialRef(args.ref)
|
|
61
64
|
const info = await ctx.credentials.describe(ref)
|
|
@@ -81,6 +84,8 @@ export function apply(ctx: Context): void {
|
|
|
81
84
|
},
|
|
82
85
|
render: (_args, value) => [{ type: 'text', text: `stored credential ${value.ref} (value not echoed)` }],
|
|
83
86
|
},
|
|
87
|
+
presentCall: (args: any) => genCall('Store credential ' + args.ref, 'edit'),
|
|
88
|
+
presentResult: (_args, result) => genResult(result),
|
|
84
89
|
async execute(args) {
|
|
85
90
|
if (args.value.length === 0) throw new Error('credential value must be non-empty')
|
|
86
91
|
await ctx.credentials.set(credentialRef(args.ref), args.value)
|
|
@@ -105,6 +110,8 @@ export function apply(ctx: Context): void {
|
|
|
105
110
|
},
|
|
106
111
|
render: (_args, value) => [{ type: 'text', text: `removed credential ${value.ref}` }],
|
|
107
112
|
},
|
|
113
|
+
presentCall: (args: any) => genCall('Remove credential ' + args.ref, 'delete'),
|
|
114
|
+
presentResult: (_args, result) => genResult(result),
|
|
108
115
|
async execute(args) {
|
|
109
116
|
await ctx.credentials.unset(credentialRef(args.ref))
|
|
110
117
|
return { ref: args.ref, removed: true }
|
|
@@ -119,6 +126,8 @@ export function apply(ctx: Context): void {
|
|
|
119
126
|
schema: { type: 'object', additionalProperties: true },
|
|
120
127
|
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
121
128
|
},
|
|
129
|
+
presentCall: () => genCall('righthand settings', 'read'),
|
|
130
|
+
presentResult: (_args, result) => genResult(result),
|
|
122
131
|
async execute() {
|
|
123
132
|
const v = scope.get()
|
|
124
133
|
return { accountId: v.accountId, defaultScriptPrefix: v.defaultScriptPrefix, defaultZone: v.defaultZone, defaultR2Bucket: v.defaultR2Bucket, defaultNotifyTopic: v.defaultNotifyTopic }
|
|
@@ -141,6 +150,8 @@ export function apply(ctx: Context): void {
|
|
|
141
150
|
},
|
|
142
151
|
render: () => [{ type: 'text', text: 'settings updated' }],
|
|
143
152
|
},
|
|
153
|
+
presentCall: (args: any) => genCall('Patch righthand settings (' + Object.keys(args.patch).join(', ') + ')', 'edit'),
|
|
154
|
+
presentResult: (_args, result) => genResult(result),
|
|
144
155
|
async execute(args) {
|
|
145
156
|
await scope.update(args.patch as Partial<RighthandSettings>)
|
|
146
157
|
return { applied: true }
|
package/src/store-tools.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { Context } from '@deepseek-ai/cordis'
|
|
10
10
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
11
|
+
import type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools'
|
|
11
12
|
import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain'
|
|
12
13
|
import type { Domain } from '@deepseek-ai/dsh-storage-domain'
|
|
13
14
|
import { z } from 'zod'
|
|
@@ -110,6 +111,8 @@ export function apply(ctx: Context): void {
|
|
|
110
111
|
},
|
|
111
112
|
render: (_args, value) => [{ type: 'text', text: `stored ${value.key} (writes=${value.writes})` }],
|
|
112
113
|
},
|
|
114
|
+
presentCall: (args: any): ToolCallView => ({ card: 'generic', title: 'Store ' + args.key, kind: 'edit' }),
|
|
115
|
+
presentResult: (_args, result): ToolResultView => ({ card: 'generic', content: result.content }),
|
|
113
116
|
async execute(args) {
|
|
114
117
|
const s = await ensure()
|
|
115
118
|
const row = await s.put(args.key, args.value)
|
|
@@ -136,6 +139,8 @@ export function apply(ctx: Context): void {
|
|
|
136
139
|
},
|
|
137
140
|
render: (_args, value) => [{ type: 'text', text: value.found ? `${value.key} = ${JSON.stringify(value.value)}` : `${value.key} (absent)` }],
|
|
138
141
|
},
|
|
142
|
+
presentCall: (args: any): ToolCallView => ({ card: 'generic', title: 'Read ' + args.key, kind: 'read' }),
|
|
143
|
+
presentResult: (_args, result): ToolResultView => ({ card: 'generic', content: result.content }),
|
|
139
144
|
async execute(args) {
|
|
140
145
|
const s = await ensure()
|
|
141
146
|
const row = await s.get(args.key)
|
|
@@ -161,6 +166,8 @@ export function apply(ctx: Context): void {
|
|
|
161
166
|
},
|
|
162
167
|
render: (_args, value) => [{ type: 'text', text: `deleted ${value.key}: ${value.existed}` }],
|
|
163
168
|
},
|
|
169
|
+
presentCall: (args: any): ToolCallView => ({ card: 'generic', title: 'Delete ' + args.key, kind: 'delete' }),
|
|
170
|
+
presentResult: (_args, result): ToolResultView => ({ card: 'generic', content: result.content }),
|
|
164
171
|
async execute(args) {
|
|
165
172
|
const s = await ensure()
|
|
166
173
|
const existed = await s.delete(args.key)
|
|
@@ -176,6 +183,8 @@ export function apply(ctx: Context): void {
|
|
|
176
183
|
schema: { type: 'array', items: { type: 'string' } },
|
|
177
184
|
render: (_args, keys) => [{ type: 'text', text: keys.length === 0 ? '(empty)' : keys.join('\n') }],
|
|
178
185
|
},
|
|
186
|
+
presentCall: (): ToolCallView => ({ card: 'generic', title: 'List store keys', kind: 'read' }),
|
|
187
|
+
presentResult: (_args, result): ToolResultView => ({ card: 'generic', content: result.content }),
|
|
179
188
|
async execute() {
|
|
180
189
|
const s = await ensure()
|
|
181
190
|
return s.list()
|