tools.ts 4.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. /** Agent-facing current-profile management using the same service as Web controls. */
  2. import { assertNever } from '@deepseek-ai/dsh-util-values'
  3. import type { Context } from '@deepseek-ai/cordis'
  4. import type {} from './index.ts'
  5. import type { PluginEntryId } from './types.ts'
  6. import { defineTool } from '@deepseek-ai/dsh-tools'
  7. /** Required services for the management tool. */
  8. export const inject = ['tools', 'pluginManager']
  9. /** Register one management tool for discovery and the four persistent actions.
  10. * @param ctx Agent-scoped tool registration context.
  11. */
  12. export function apply(ctx: Context): void {
  13. ctx.tools.register(defineTool({
  14. name: 'plugin_manager',
  15. description: 'List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart.',
  16. parameters: {
  17. action: { type: 'string', required: true, enum: ['list_plugins', 'list_bundles', 'set_plugin', 'set_bundle', 'install_bundle', 'remove_bundle'], description: 'Management operation.' },
  18. target: { type: 'string', description: 'Plugin entry id, bundle package name, or installation spec, according to action.' },
  19. enabled: { type: 'boolean', description: 'Required for set operations; defaults to true for installation.' },
  20. approvedBuilds: { type: 'array', items: { type: 'string' }, description: 'For install_bundle: pass names from pendingBuilds only after the user explicitly approves running their install scripts in the conversation. This grants persistent permission for this profile.' },
  21. offset: { type: 'number', description: 'Zero-based list offset; defaults to 0.' },
  22. limit: { type: 'number', description: 'List page size, from 1 to 100; defaults to 25.' },
  23. },
  24. output: {
  25. schema: { type: 'string' },
  26. render: (_args, value) => [{ type: 'text', text: value }],
  27. },
  28. async execute(args) {
  29. const manager = ctx.pluginManager
  30. switch (args.action) {
  31. case 'list_plugins':
  32. case 'list_bundles': {
  33. const offset = args.offset ?? 0
  34. const limit = args.limit ?? 25
  35. if (!Number.isInteger(offset) || offset < 0 || !Number.isInteger(limit) || limit < 1 || limit > 100) {
  36. throw new Error('offset must be a non-negative integer and limit must be an integer from 1 to 100')
  37. }
  38. const rows = args.action === 'list_plugins' ? await manager.listPlugins() : await manager.listBundles()
  39. const entries = rows.slice(offset, offset + limit)
  40. return JSON.stringify({ entries, total: rows.length,
  41. nextOffset: offset + entries.length < rows.length ? offset + entries.length : null })
  42. }
  43. case 'set_plugin':
  44. case 'set_bundle': {
  45. if (args.target === undefined || args.enabled === undefined) throw new Error('target and enabled are required')
  46. return JSON.stringify(await (args.action === 'set_plugin'
  47. ? manager.setPluginEnabled(args.target as PluginEntryId, args.enabled)
  48. : manager.setBundleEnabled(args.target, args.enabled)))
  49. }
  50. case 'install_bundle':
  51. if (args.target === undefined) throw new Error('target package spec is required')
  52. return JSON.stringify(await manager.installBundle(args.target, {
  53. ...args.enabled === undefined ? {} : { enabled: args.enabled },
  54. ...args.approvedBuilds === undefined ? {} : { approvedBuilds: args.approvedBuilds },
  55. }))
  56. case 'remove_bundle':
  57. if (args.target === undefined) throw new Error('target bundle name is required')
  58. return JSON.stringify(await manager.removeBundle(args.target))
  59. /* v8 ignore next -- tool JSON validation rejects actions outside the declared enum */
  60. default: return assertNever(args.action)
  61. }
  62. },
  63. presentCall: args => ({ card: 'generic', title: 'Manage profile plugins', kind: args.action.startsWith('list_') ? 'read' : 'other', rawInput: args }),
  64. }))
  65. }