prompt-skill-armory 0.4.1

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,106 @@
1
+ /**
2
+ * Switchblade management page dictionaries.
3
+ * @module @deepseek-ai/dsh-client-ui-switchblade
4
+ */
5
+
6
+ /** Locale namespace owned by this plugin (settings.* prefix per the slot contract). */
7
+ export const NS = 'settings.switchblade'
8
+
9
+ /** Keys translated by this plugin. */
10
+ export type SwitchbladeKey =
11
+ | 'nav' | 'skillsTitle' | 'presetsTitle' | 'promptsTitle' | 'commandsTitle'
12
+ | 'empty' | 'enabled' | 'disabled' | 'installed'
13
+ | 'refresh' | 'setDefault' | 'loadFailed'
14
+ | 'globalHint' | 'promptNamePlaceholder' | 'promptDescPlaceholder' | 'promptContentPlaceholder'
15
+ | 'addPrompt' | 'enable' | 'disable' | 'delete'
16
+ | 'installSkill' | 'skillNamePlaceholder' | 'skillDescPlaceholder' | 'skillContentPlaceholder'
17
+ | 'install' | 'uninstall' | 'pickSkillFile' | 'searchPlaceholder'
18
+ | 'installedSkills' | 'installedSkillsGoRight' | 'manage'
19
+ | 'localSkills' | 'agentPresetsTitle' | 'edit' | 'save' | 'cancel' | 'addSkill'
20
+ | 'pickZipFile' | 'cliHint'
21
+
22
+ /** zh-CN copy. */
23
+ export const zh: Record<SwitchbladeKey, string> = {
24
+ nav: 'Prompt-SkillArmory',
25
+ skillsTitle: '技能',
26
+ presetsTitle: '提示词预设',
27
+ promptsTitle: '提示词',
28
+ commandsTitle: '命令',
29
+ empty: '(空)',
30
+ enabled: '启用',
31
+ disabled: '停用',
32
+ installed: '已装',
33
+ refresh: '刷新',
34
+ setDefault: '设为默认',
35
+ loadFailed: '加载失败',
36
+ globalHint: '· 全局生效',
37
+ promptNamePlaceholder: '提示词名称',
38
+ promptDescPlaceholder: '描述(可选)',
39
+ promptContentPlaceholder: '提示词内容…',
40
+ addPrompt: '添加提示词',
41
+ enable: '启用',
42
+ disable: '停用',
43
+ delete: '删除',
44
+ installSkill: '技能',
45
+ skillNamePlaceholder: '技能名称(kebab-case)',
46
+ skillDescPlaceholder: '描述(可选)',
47
+ skillContentPlaceholder: '技能指令内容…',
48
+ install: '安装',
49
+ uninstall: '卸载',
50
+ pickSkillFile: '选择本地 .md 技能文件导入',
51
+ searchPlaceholder: '搜索…',
52
+ installedSkills: '本地化技能',
53
+ installedSkillsGoRight: '已安装的技能在右侧第四列管理',
54
+ manage: '托管',
55
+ localSkills: '本地扫描技能',
56
+ agentPresetsTitle: 'Agent预设',
57
+ edit: '编辑',
58
+ save: '保存',
59
+ cancel: '取消',
60
+ addSkill: '添加技能',
61
+ pickZipFile: '导入 .zip 技能包',
62
+ cliHint: 'CLI 直接安装(在会话里输入):',
63
+ }
64
+
65
+ /** en-US copy. */
66
+ export const en: Record<SwitchbladeKey, string> = {
67
+ nav: 'Prompt-SkillArmory',
68
+ skillsTitle: 'Skills',
69
+ presetsTitle: 'Prompt Presets',
70
+ promptsTitle: 'Prompts',
71
+ commandsTitle: 'Commands',
72
+ empty: '(empty)',
73
+ enabled: 'enabled',
74
+ disabled: 'disabled',
75
+ installed: 'installed',
76
+ refresh: 'Refresh',
77
+ setDefault: 'Set default',
78
+ loadFailed: 'Failed to load',
79
+ globalHint: '· global',
80
+ promptNamePlaceholder: 'Prompt name',
81
+ promptDescPlaceholder: 'Description (optional)',
82
+ promptContentPlaceholder: 'Prompt content…',
83
+ addPrompt: 'Add prompt',
84
+ enable: 'Enable',
85
+ disable: 'Disable',
86
+ delete: 'Delete',
87
+ installSkill: 'Skills',
88
+ skillNamePlaceholder: 'Skill name (kebab-case)',
89
+ skillDescPlaceholder: 'Description (optional)',
90
+ skillContentPlaceholder: 'Skill instructions…',
91
+ install: 'Install',
92
+ uninstall: 'Uninstall',
93
+ pickSkillFile: 'Pick a local .md skill file',
94
+ searchPlaceholder: 'Search…',
95
+ installedSkills: 'Local skills',
96
+ installedSkillsGoRight: 'Installed skills are managed in the right column',
97
+ manage: 'Manage',
98
+ localSkills: 'Scanned skills',
99
+ agentPresetsTitle: 'Agent Presets',
100
+ edit: 'Edit',
101
+ save: 'Save',
102
+ cancel: 'Cancel',
103
+ addSkill: 'Add skill',
104
+ pickZipFile: 'Import .zip skill bundle',
105
+ cliHint: 'Install via CLI (type in a session):',
106
+ }
@@ -0,0 +1,350 @@
1
+ /**
2
+ * Switchblade section data controller: reads the skill catalog and the prompt
3
+ * preset roster through the existing connection RPC surface (no Typert
4
+ * generation needed — these methods are already wired).
5
+ * @module @deepseek-ai/dsh-client-ui-switchblade
6
+ */
7
+
8
+ import type { ConnectionHandle, SessionId } from '@deepseek-ai/dsh-api-remotes/client'
9
+ import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
10
+
11
+ /** A skill row as reported by skill.list. */
12
+ export interface SkillRow {
13
+ readonly name: string
14
+ readonly description: string
15
+ readonly modelInvocable: boolean
16
+ }
17
+
18
+ /** A prompt-preset row as reported by agentPreset.list. */
19
+ export interface PresetRow {
20
+ readonly id: string
21
+ readonly name?: string
22
+ readonly description?: string
23
+ readonly isDefault: boolean
24
+ readonly trust: string
25
+ readonly broken?: string
26
+ }
27
+
28
+ /** A command row (not yet wired to an RPC; kept for the section's third group). */
29
+ export interface CommandRow {
30
+ readonly name: string
31
+ readonly description: string
32
+ }
33
+
34
+ /** One user-authored prompt (mirrors the Host ManagedPrompt). */
35
+ export interface PromptRow {
36
+ readonly id: string
37
+ readonly name: string
38
+ readonly description: string
39
+ readonly content: string
40
+ readonly order: number
41
+ readonly enabled: boolean
42
+ readonly isDefault: boolean
43
+ }
44
+
45
+ /** An installed skill row. */
46
+ export interface InstalledSkillRow {
47
+ readonly name: string
48
+ readonly description: string
49
+ readonly content: string
50
+ /** Whether this skill is currently registered (enabled) as a runtime skill. */
51
+ readonly enabled: boolean
52
+ }
53
+
54
+ /** The section's loaded view state. */
55
+ export interface SwitchbladeSectionState {
56
+ readonly status: 'idle' | 'loading' | 'ready' | 'error'
57
+ readonly message?: string
58
+ readonly skills: readonly SkillRow[]
59
+ readonly presets: readonly PresetRow[]
60
+ readonly commands: readonly CommandRow[]
61
+ readonly prompts: readonly PromptRow[]
62
+ readonly installedSkills: readonly InstalledSkillRow[]
63
+ }
64
+
65
+ /** Initial (idle) state. */
66
+ const IDLE: SwitchbladeSectionState = {
67
+ status: 'idle',
68
+ skills: [],
69
+ presets: [],
70
+ commands: [],
71
+ prompts: [],
72
+ installedSkills: [],
73
+ }
74
+
75
+ /** Normalize a thrown wire error to a message. */
76
+ function messageOf(error: unknown): string {
77
+ return error instanceof Error ? error.message : String(error)
78
+ }
79
+
80
+ /**
81
+ * Data controller bound to one session's connection.
82
+ * @param api - the connection's API client.
83
+ * @param sessionId - session the skill catalog resolves against.
84
+ */
85
+ export class SwitchbladeSectionController {
86
+ /** Snapshot store backing the section's view state. */
87
+ readonly store: SnapshotStore<SwitchbladeSectionState> = createSnapshotStore(IDLE)
88
+
89
+ constructor(
90
+ private readonly api: ConnectionHandle['api'],
91
+ private readonly sessionId?: () => SessionId | undefined,
92
+ ) {}
93
+
94
+ /**
95
+ * Load skills, presets, prompts, and installed skills. Prompts and installed
96
+ * skills come from the `switchblade` settings namespace (the Host watches
97
+ * it and re-injects on change).
98
+ */
99
+ async load(): Promise<void> {
100
+ this.store.set({ ...IDLE, status: 'loading' })
101
+ try {
102
+ // skill.list requires a live session; without one we skip it (never
103
+ // hang). Presets + settings always resolve, so the panel opens reliably.
104
+ const sessionId = this.sessionId?.()
105
+ const calls: Promise<unknown>[] = [
106
+ this.api.agentPresets.list({}),
107
+ this.api.settings.describe({}),
108
+ ]
109
+ if (sessionId !== undefined) calls.push(this.api.skills.list({ sessionId }))
110
+ const [presetRes, settingsRes, skillRes] = await Promise.all(calls) as [
111
+ Awaited<ReturnType<ConnectionHandle['api']['agentPresets']['list']>>,
112
+ Awaited<ReturnType<ConnectionHandle['api']['settings']['describe']>>,
113
+ Awaited<ReturnType<ConnectionHandle['api']['skills']['list']>> | undefined,
114
+ ]
115
+ if (!presetRes.result.ok) throw new Error(`agentPreset.list: ${presetRes.result.error.message}`)
116
+ if (!settingsRes.result.ok) throw new Error(`settings.describe: ${settingsRes.result.error.message}`)
117
+
118
+ const skills: SkillRow[] = skillRes !== undefined && skillRes.result.ok
119
+ ? skillRes.result.value.skills.map((skill) => ({
120
+ name: skill.name,
121
+ description: skill.description,
122
+ modelInvocable: skill.modelInvocable,
123
+ }))
124
+ : []
125
+
126
+ const presets: PresetRow[] = presetRes.result.value.presets.map((preset) => ({
127
+ id: preset.id,
128
+ isDefault: preset.isDefault,
129
+ trust: preset.trust,
130
+ ...preset.name === undefined ? {} : { name: preset.name },
131
+ ...preset.description === undefined ? {} : { description: preset.description },
132
+ ...preset.broken === undefined ? {} : { broken: preset.broken },
133
+ }))
134
+
135
+ const switchbladeSection = this.sectionFromSettings(settingsRes.result.value, 'switchblade')
136
+ const prompts: PromptRow[] = Array.isArray(switchbladeSection?.prompts) ? switchbladeSection.prompts : []
137
+ const installedSkills: InstalledSkillRow[] = Array.isArray(switchbladeSection?.installedSkills)
138
+ ? switchbladeSection.installedSkills.map((s: { name?: string; description?: string; content?: string; enabled?: boolean }) => ({
139
+ name: s.name ?? '',
140
+ description: s.description ?? '',
141
+ content: s.content ?? '',
142
+ enabled: s.enabled ?? true,
143
+ }))
144
+ : []
145
+
146
+ this.store.set({
147
+ status: 'ready',
148
+ skills,
149
+ presets,
150
+ commands: [],
151
+ prompts,
152
+ installedSkills,
153
+ })
154
+ } catch (error) {
155
+ this.store.set({ ...IDLE, status: 'error', message: messageOf(error) })
156
+ }
157
+ }
158
+
159
+ /** Read one namespace's user section from a settings.describe value. */
160
+ private sectionFromSettings(value: unknown, ns: string): Record<string, unknown> | undefined {
161
+ if (typeof value !== 'object' || value === null) return undefined
162
+ const entries = (value as { namespaces?: unknown }).namespaces
163
+ if (!Array.isArray(entries)) return undefined
164
+ for (const entry of entries) {
165
+ const row = entry as { ns?: unknown; value?: unknown }
166
+ if (row.ns === ns) {
167
+ const section = row.value
168
+ return typeof section === 'object' && section !== null ? section as Record<string, unknown> : undefined
169
+ }
170
+ }
171
+ return undefined
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Prompt CRUD (writes to the switchblade settings namespace; Host re-injects)
176
+ // ---------------------------------------------------------------------------
177
+
178
+ /** Add a prompt. */
179
+ async addPrompt(input: { name: string; description: string; content: string }): Promise<void> {
180
+ const res = await this.api.settings.mutate({
181
+ ns: 'switchblade',
182
+ ops: [{ op: 'set', path: ['prompts'], value: [...this.currentPrompts(), {
183
+ id: this.slugify(input.name),
184
+ name: input.name,
185
+ description: input.description,
186
+ content: input.content,
187
+ order: this.currentPrompts().length,
188
+ enabled: true,
189
+ isDefault: this.currentPrompts().length === 0,
190
+ }] }],
191
+ })
192
+ if (!res.result.ok) throw new Error(res.result.error.message)
193
+ await this.load()
194
+ }
195
+
196
+ /** Toggle one prompt's enabled state. */
197
+ async setPromptEnabled(id: string, enabled: boolean): Promise<void> {
198
+ const next = this.currentPrompts().map((p) => p.id === id ? { ...p, enabled } : p)
199
+ await this.writePrompts(next)
200
+ }
201
+
202
+ /** Mark one prompt default; clears others. */
203
+ async setDefaultPrompt(id: string): Promise<void> {
204
+ const next = this.currentPrompts().map((p) => ({ ...p, isDefault: p.id === id }))
205
+ await this.writePrompts(next)
206
+ }
207
+
208
+ /** Delete one prompt. */
209
+ async deletePrompt(id: string): Promise<void> {
210
+ const next = this.currentPrompts().filter((p) => p.id !== id)
211
+ await this.writePrompts(next)
212
+ }
213
+
214
+ /** Update a prompt's name/description/content. */
215
+ async updatePrompt(id: string, patch: { name?: string; description?: string; content?: string }): Promise<void> {
216
+ const next = this.currentPrompts().map((p) => p.id === id ? {
217
+ ...p,
218
+ name: patch.name?.trim() || p.name,
219
+ description: patch.description ?? p.description,
220
+ content: patch.content ?? p.content,
221
+ } : p)
222
+ await this.writePrompts(next)
223
+ }
224
+
225
+ /** Persist the prompt list through the settings RPC. */
226
+ private async writePrompts(prompts: readonly PromptRow[]): Promise<void> {
227
+ const res = await this.api.settings.mutate({
228
+ ns: 'switchblade',
229
+ ops: [{ op: 'set', path: ['prompts'], value: prompts }],
230
+ })
231
+ if (!res.result.ok) throw new Error(res.result.error.message)
232
+ await this.load()
233
+ }
234
+
235
+ /** Current prompt list from the loaded snapshot. */
236
+ private currentPrompts(): readonly PromptRow[] {
237
+ return this.store.getSnapshot().prompts
238
+ }
239
+
240
+ /** Sluggify a name into an id. */
241
+ private slugify(value: string): string {
242
+ const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
243
+ return slug.length > 0 ? slug : `prompt-${Date.now()}`
244
+ }
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // Skill install/uninstall (writes to the switchblade settings namespace)
248
+ // ---------------------------------------------------------------------------
249
+
250
+ /** Install a skill from a name + content (enabled by default). */
251
+ async installSkill(input: { name: string; description: string; content: string }): Promise<void> {
252
+ const next = [...this.currentInstalledSkills(), {
253
+ name: input.name,
254
+ description: input.description,
255
+ content: input.content,
256
+ enabled: true,
257
+ }]
258
+ const res = await this.api.settings.mutate({
259
+ ns: 'switchblade',
260
+ ops: [{ op: 'set', path: ['installedSkills'], value: next }],
261
+ })
262
+ if (!res.result.ok) throw new Error(res.result.error.message)
263
+ await this.load()
264
+ }
265
+
266
+ /** Toggle one installed skill's enabled state. */
267
+ async setSkillEnabled(name: string, enabled: boolean): Promise<void> {
268
+ const next = this.currentInstalledSkills().map((s) => s.name === name ? { ...s, enabled } : s)
269
+ const res = await this.api.settings.mutate({
270
+ ns: 'switchblade',
271
+ ops: [{ op: 'set', path: ['installedSkills'], value: next }],
272
+ })
273
+ if (!res.result.ok) throw new Error(res.result.error.message)
274
+ await this.load()
275
+ }
276
+
277
+ /** Uninstall one installed skill. */
278
+ async uninstallSkill(name: string): Promise<void> {
279
+ const next = this.currentInstalledSkills().filter((s) => s.name !== name)
280
+ const res = await this.api.settings.mutate({
281
+ ns: 'switchblade',
282
+ ops: [{ op: 'set', path: ['installedSkills'], value: next }],
283
+ })
284
+ if (!res.result.ok) throw new Error(res.result.error.message)
285
+ await this.load()
286
+ }
287
+
288
+ /** Update an installed skill's name/description/content. */
289
+ async updateSkill(name: string, patch: { name?: string; description?: string; content?: string }): Promise<void> {
290
+ const next = this.currentInstalledSkills().map((s) => s.name === name ? {
291
+ ...s,
292
+ name: patch.name?.trim() || s.name,
293
+ description: patch.description ?? s.description,
294
+ content: patch.content ?? s.content,
295
+ } : s)
296
+ const res = await this.api.settings.mutate({
297
+ ns: 'switchblade',
298
+ ops: [{ op: 'set', path: ['installedSkills'], value: next }],
299
+ })
300
+ if (!res.result.ok) throw new Error(res.result.error.message)
301
+ await this.load()
302
+ }
303
+
304
+ /** Current installed skills from the loaded snapshot. */
305
+ private currentInstalledSkills(): readonly InstalledSkillRow[] {
306
+ return this.store.getSnapshot().installedSkills
307
+ }
308
+
309
+ /**
310
+ * Queue a zip archive (base64) for extraction into ~/.dsh/skills. The Host
311
+ * watch sees pendingZip and installs it (skil-filesystem then discovers it).
312
+ */
313
+ async installSkillFromZip(name: string, dataBase64: string): Promise<void> {
314
+ const res = await this.api.settings.mutate({
315
+ ns: 'switchblade',
316
+ ops: [{ op: 'set', path: ['pendingZip'], value: { name, dataBase64 } }],
317
+ })
318
+ if (!res.result.ok) throw new Error(res.result.error.message)
319
+ // Wait a tick for the Host watch to extract, then refresh.
320
+ await new Promise((r) => setTimeout(r, 500))
321
+ await this.load()
322
+ }
323
+
324
+ /** Set the default prompt preset. */
325
+ async setDefaultPreset(id: string): Promise<void> {
326
+ const res = await this.api.settings.update({ ns: 'agent-presets', patch: { default: id } })
327
+ if (!res.result.ok) throw new Error(res.result.error.message)
328
+ await this.load()
329
+ }
330
+ }
331
+
332
+ /** The section's injected face: hooks (snapshot store) + actions. */
333
+ export interface SwitchbladeSectionInjected {
334
+ hooks: {
335
+ /** Page snapshot bound by the renderer as useSwitchblade. */
336
+ switchblade: SnapshotStore<SwitchbladeSectionState>
337
+ }
338
+ load: () => Promise<void>
339
+ setDefaultPreset: (id: string) => Promise<void>
340
+ addPrompt: (input: { name: string; description: string; content: string }) => Promise<void>
341
+ updatePrompt: (id: string, patch: { name?: string; description?: string; content?: string }) => Promise<void>
342
+ setPromptEnabled: (id: string, enabled: boolean) => Promise<void>
343
+ setDefaultPrompt: (id: string) => Promise<void>
344
+ deletePrompt: (id: string) => Promise<void>
345
+ installSkill: (input: { name: string; description: string; content: string }) => Promise<void>
346
+ updateSkill: (name: string, patch: { name?: string; description?: string; content?: string }) => Promise<void>
347
+ setSkillEnabled: (name: string, enabled: boolean) => Promise<void>
348
+ uninstallSkill: (name: string) => Promise<void>
349
+ installSkillFromZip: (name: string, dataBase64: string) => Promise<void>
350
+ }
@@ -0,0 +1,5 @@
1
+ /** CSS module type shim for .module.css imports (unused; kept for parity). */
2
+ declare module '*.module.css' {
3
+ const classes: Record<string, string>
4
+ export default classes
5
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Switchblade management page, node half. Pure UI plugin: the empty apply
3
+ * exists so the plugin appears in the host cordis.yml / Loader; the browser
4
+ * half ships via exports["./client"], discovered through the package.json
5
+ * dsh.client declaration.
6
+ */
7
+
8
+ /** Host plugin body — no host-side behavior for this UI plugin. */
9
+ export function apply(): void {}
10
+
11
+ /** Cordis plugin identity (host face). */
12
+ export const name = 'ui-switchblade'
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Validation invariants for `@deepseek-ai/dsh-client-ui-switchblade`.
3
+ * @module @deepseek-ai/dsh-client-ui-switchblade
4
+ */
5
+
6
+ /** The settings.section id this package registers. */
7
+ export const SECTION_ID = 'switchblade'
8
+
9
+ /** Display order in the settings nav (after Models/Agent Presets). */
10
+ export const SECTION_ORDER = 30