@lvy-jsd/dsh-tool-manager 0.0.0-stage → 0.4.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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +302 -2
  3. package/cordis.patch.yml +29 -0
  4. package/lib/catalog.d.ts +35 -0
  5. package/lib/catalog.d.ts.map +1 -0
  6. package/lib/catalog.js +311 -0
  7. package/lib/catalog.js.map +1 -0
  8. package/lib/exempt.d.ts +45 -0
  9. package/lib/exempt.d.ts.map +1 -0
  10. package/lib/exempt.js +122 -0
  11. package/lib/exempt.js.map +1 -0
  12. package/lib/index.d.ts +24 -0
  13. package/lib/index.d.ts.map +1 -0
  14. package/lib/index.js +797 -0
  15. package/lib/index.js.map +1 -0
  16. package/lib/manifest.d.ts +21 -0
  17. package/lib/manifest.d.ts.map +1 -0
  18. package/lib/manifest.js +112 -0
  19. package/lib/manifest.js.map +1 -0
  20. package/lib/metadata.d.ts +38 -0
  21. package/lib/metadata.d.ts.map +1 -0
  22. package/lib/metadata.js +102 -0
  23. package/lib/metadata.js.map +1 -0
  24. package/lib/router/bm25.d.ts +21 -0
  25. package/lib/router/bm25.d.ts.map +1 -0
  26. package/lib/router/bm25.js +63 -0
  27. package/lib/router/bm25.js.map +1 -0
  28. package/lib/router/index.d.ts +9 -0
  29. package/lib/router/index.d.ts.map +1 -0
  30. package/lib/router/index.js +422 -0
  31. package/lib/router/index.js.map +1 -0
  32. package/lib/router/sidecar.d.ts +34 -0
  33. package/lib/router/sidecar.d.ts.map +1 -0
  34. package/lib/router/sidecar.js +156 -0
  35. package/lib/router/sidecar.js.map +1 -0
  36. package/lib/router/types.d.ts +164 -0
  37. package/lib/router/types.d.ts.map +1 -0
  38. package/lib/router/types.js +7 -0
  39. package/lib/router/types.js.map +1 -0
  40. package/lib/service.d.ts +51 -0
  41. package/lib/service.d.ts.map +1 -0
  42. package/lib/service.js +199 -0
  43. package/lib/service.js.map +1 -0
  44. package/lib/settings.d.ts +32 -0
  45. package/lib/settings.d.ts.map +1 -0
  46. package/lib/settings.js +93 -0
  47. package/lib/settings.js.map +1 -0
  48. package/lib/types.d.ts +453 -0
  49. package/lib/types.d.ts.map +1 -0
  50. package/lib/types.js +7 -0
  51. package/lib/types.js.map +1 -0
  52. package/lib/visibility.d.ts +33 -0
  53. package/lib/visibility.d.ts.map +1 -0
  54. package/lib/visibility.js +305 -0
  55. package/lib/visibility.js.map +1 -0
  56. package/package.json +54 -4
  57. package/sidecar/bin/tool-router-sidecar +0 -0
  58. package/src/catalog.ts +305 -0
  59. package/src/exempt.ts +148 -0
  60. package/src/index.ts +805 -0
  61. package/src/manifest.ts +116 -0
  62. package/src/metadata.ts +123 -0
  63. package/src/router/bm25.ts +68 -0
  64. package/src/router/index.ts +440 -0
  65. package/src/router/sidecar.ts +159 -0
  66. package/src/router/types.ts +149 -0
  67. package/src/service.ts +207 -0
  68. package/src/settings.ts +108 -0
  69. package/src/types.ts +450 -0
  70. package/src/visibility.ts +317 -0
package/src/types.ts ADDED
@@ -0,0 +1,450 @@
1
+ // Types shared by the core modules and exported for extension plugins.
2
+ //
3
+ // The host surface is described structurally (only what this plugin touches)
4
+ // instead of importing the host's Cordis types, so the package type-checks
5
+ // without the host packages installed next to it.
6
+
7
+ // --- catalog -------------------------------------------------------------------
8
+
9
+ /** A tool schema as the host lists it. */
10
+ export interface ToolSchema {
11
+ name: string
12
+ description?: string
13
+ parameters?: unknown
14
+ [key: string]: unknown
15
+ }
16
+
17
+ /** Metadata a plugin can attach to tools (package.json `dsh.toolCatalog` or annotate()). */
18
+ export interface ToolMeta {
19
+ group?: string
20
+ category?: string
21
+ risk?: string
22
+ summary?: string
23
+ }
24
+
25
+ export interface Annotation {
26
+ source?: string
27
+ match: string | RegExp
28
+ meta: ToolMeta
29
+ }
30
+
31
+ export interface ToolEntry {
32
+ name: string
33
+ /** Name inside its group (MCP routing prefix dropped). */
34
+ short: string
35
+ group: string
36
+ /** Where the structural fallback alone would have put the tool. */
37
+ fallbackGroup: string
38
+ category: string
39
+ summary: string
40
+ description: string
41
+ tokens: number
42
+ highRisk: boolean
43
+ annotation: ToolMeta
44
+ }
45
+
46
+ export interface Group {
47
+ name: string
48
+ category: string
49
+ tools: ToolEntry[]
50
+ }
51
+
52
+ export interface Category {
53
+ name: string
54
+ description: string
55
+ groups: Group[]
56
+ /** Set on a synthetic alias node (several targets behind one name). */
57
+ alias?: string[]
58
+ /** The exact tools an alias stands for; load these, never their whole groups. */
59
+ aliasTools?: ToolEntry[]
60
+ }
61
+
62
+ export interface Catalog {
63
+ tools: Map<string, ToolEntry>
64
+ groups: Map<string, Group>
65
+ categories: Map<string, Category>
66
+ }
67
+
68
+ export type CatalogNode =
69
+ | { kind: 'root' }
70
+ | { kind: 'category', category: Category }
71
+ | { kind: 'group', group: Group }
72
+ | { kind: 'tool', tool: ToolEntry }
73
+ | { kind: 'missing', target: string }
74
+
75
+ /** What a classifier sees of one tool. */
76
+ export interface ToolProbe {
77
+ name: string
78
+ description: string
79
+ annotation: ToolMeta
80
+ }
81
+
82
+ /** What a classifier sees of one group. */
83
+ export interface GroupProbe {
84
+ name: string
85
+ tools: ToolProbe[]
86
+ }
87
+
88
+ export interface Classifier {
89
+ name?: string
90
+ priority?: number
91
+ /** Group name for a tool, or undefined to pass. */
92
+ group?(tool: ToolProbe): string | undefined
93
+ /** Category for a group, or undefined to pass. */
94
+ categorize?(group: GroupProbe): string | undefined
95
+ }
96
+
97
+ export interface VisibilityFilter {
98
+ name?: string
99
+ /** Names to keep out of `names`; may only narrow. */
100
+ filter(agent: Agent, names: string[]): Iterable<string> | undefined | null
101
+ }
102
+
103
+ export interface TargetResolver {
104
+ name?: string
105
+ /** Targets starting with this prefix are offered to `resolve` first. */
106
+ prefix?: string
107
+ /** One or more ordinary targets to resolve instead, or undefined to pass. */
108
+ resolve(target: string): string | string[] | undefined | null
109
+ }
110
+
111
+ /** A registry entry after registration (defaults filled in). */
112
+ export interface RegisteredResolver {
113
+ name: string
114
+ prefix: string
115
+ resolve: TargetResolver['resolve']
116
+ }
117
+
118
+ export type Disposer = () => void
119
+
120
+ // --- per-agent state ---------------------------------------------------------
121
+
122
+ export interface LoadState {
123
+ /** Individual tool names. */
124
+ loaded: Set<string>
125
+ /** Subscribed group names (tools the group gains later included). */
126
+ groups: Set<string>
127
+ /** Tools unloaded from inside a subscribed group. */
128
+ excluded: Set<string>
129
+ /** Groups handed to subagents started later. */
130
+ handoffGroups: Set<string>
131
+ /** Tools handed to subagents started later. */
132
+ handoffTools: Set<string>
133
+ }
134
+
135
+ /** Plain JSON form of a LoadState (sorted arrays). */
136
+ export type SerializedState = { [K in keyof LoadState]: string[] }
137
+
138
+ export interface StateProvider {
139
+ name?: string
140
+ /** A serialized state for the agent, or null when there is none. */
141
+ load?(agent: Agent): Partial<SerializedState> | null | undefined
142
+ save?(agent: Agent, state: SerializedState): void
143
+ }
144
+
145
+ export interface RiskGate {
146
+ name?: string
147
+ }
148
+
149
+ // --- host surface --------------------------------------------------------------
150
+
151
+ export interface Logger {
152
+ debug?(message: string): void
153
+ info?(message: string): void
154
+ warn?(message: string): void
155
+ }
156
+
157
+ export interface SessionEvent {
158
+ type?: string
159
+ data?: { provider?: string, [key: string]: unknown }
160
+ }
161
+
162
+ export interface Session {
163
+ id?: unknown
164
+ seq?: number
165
+ eventAt?(seq: number): SessionEvent | undefined
166
+ requestHeader?(): { provider?: string } | undefined
167
+ }
168
+
169
+ /** An agent as this plugin uses it. */
170
+ export interface Agent {
171
+ id: unknown
172
+ options?: { provider?: string }
173
+ session?: Session
174
+ ctx?: {
175
+ tools?: {
176
+ restrict?(spec: { allow?: string[], deny?: string[] }): Disposer
177
+ }
178
+ }
179
+ }
180
+
181
+ export interface AgentsApi {
182
+ list(): Agent[]
183
+ get?(id: unknown): Agent | undefined
184
+ isOwnedBy?(childId: unknown, candidate: Agent): boolean
185
+ }
186
+
187
+ /** Context a plugin receives once `agents` is injected. */
188
+ export interface AgentContext {
189
+ agents: AgentsApi
190
+ effect(fn: () => Disposer | void, label?: string): void
191
+ // any: each event passes its own argument types, and a handler typed
192
+ // (...args: unknown[]) would reject every concretely typed listener.
193
+ on(event: string, handler: (...args: any[]) => unknown): Disposer
194
+ }
195
+
196
+ export interface ToolLayer {
197
+ tools: Map<string, { description?: string, parameters?: unknown }>
198
+ }
199
+
200
+ export interface HostTools {
201
+ schemas(agent?: Agent): ToolSchema[]
202
+ register(definition: unknown): Disposer | void
203
+ /** `visible` is what the agent ends up with after every layer's restrictions. */
204
+ view?(agent: Agent): { restrictableNames: Iterable<string>, visible?: { has(name: string): boolean } }
205
+ schemaOf?(definition: unknown, deferred: boolean): ToolSchema
206
+ layers?: {
207
+ chainLayers?(agent: Agent): Iterable<ToolLayer>
208
+ peek?(agent: Agent): ToolLayer | undefined
209
+ }
210
+ }
211
+
212
+ export interface LoaderEntry {
213
+ options?: { name?: unknown, disabled?: boolean }
214
+ }
215
+
216
+ export interface Loader {
217
+ entries?(): Iterable<LoaderEntry>
218
+ baseUrl?: string
219
+ ctx?: { baseUrl?: string }
220
+ }
221
+
222
+ /** The host context the core plugin is applied to. */
223
+ export interface HostContext {
224
+ tools: HostTools
225
+ logger?: ((name: string) => Logger) | Logger
226
+ baseUrl?: string
227
+ get?(name: string): unknown
228
+ emit(event: string, ...args: unknown[]): void
229
+ serial(event: string, ...args: unknown[]): Promise<unknown>
230
+ // any: each event passes its own argument types, and a handler typed
231
+ // (...args: unknown[]) would reject every concretely typed listener.
232
+ on(event: string, handler: (...args: any[]) => unknown): Disposer
233
+ effect(fn: () => Disposer | void, label?: string): void
234
+ inject?(deps: string[], fn: (ctx: AgentContext) => void): void
235
+ provide(name: string, service: unknown): void
236
+ }
237
+
238
+ /**
239
+ * Settings a UI may edit at runtime. Each key is an independent list, so two
240
+ * writers editing different keys cannot clobber each other.
241
+ */
242
+ export interface ToolManagerSettings {
243
+ /** Extra always-on tools. */
244
+ pinned?: string[]
245
+ /** Tools removed from the default always-on list. */
246
+ unpin?: string[]
247
+ /** Tool names treated as high risk. */
248
+ highRisk?: string[]
249
+ }
250
+
251
+ /** Configuration of the core plugin. */
252
+ export interface CoreConfig {
253
+ /** Extra always-on tools. */
254
+ pinned?: string[]
255
+ /** Tools removed from the default always-on list. */
256
+ unpin?: string[]
257
+ /** Tool names treated as high risk. */
258
+ highRisk?: string[]
259
+ /** Model routes never narrowed (default ["claude-code"]). */
260
+ exemptProviders?: string[]
261
+ /**
262
+ * Where UI-edited settings are persisted (default
263
+ * <DSH_HOME>/tool-manager/config.json). Those values win over the fields
264
+ * above, per key.
265
+ */
266
+ settingsFile?: string
267
+ /**
268
+ * Where UI-edited tool metadata is persisted (default
269
+ * <DSH_HOME>/tool-manager/metadata.json).
270
+ */
271
+ metadataFile?: string
272
+ /**
273
+ * Where the plain-text tool manifest is written (default
274
+ * ~/.claude/dsh-tool-catalog.md). Set to '' to turn it off.
275
+ */
276
+ manifestFile?: string
277
+ /**
278
+ * Where the exemption list is read from (default
279
+ * <DSH_HOME>/tool-manager/exempt.json). When the file exists its `providers`
280
+ * and `sessions` win over `exemptProviders` above; when it does not, the
281
+ * config list is used.
282
+ */
283
+ exemptFile?: string
284
+ // --- retrieval + routing (tools_find / tools_route / skills_find / skills_route) ---
285
+ /** Model used to re-rank tools_route / skills_route candidates. */
286
+ model?: { provider?: string; model?: string }
287
+ /** Max tokens for the re-rank call. */
288
+ maxTokens?: number
289
+ /** Default number of results for the find/route tools. */
290
+ topK?: number
291
+ /** Token budget for loaded tool schemas, reported by tools_route. */
292
+ maxBudget?: number
293
+ /** BM25 parameters (JS fallback index). */
294
+ k1?: number
295
+ b?: number
296
+ /** Rust sidecar overrides. */
297
+ sidecar?: { bin?: string; socket?: string; timeoutMs?: number }
298
+ }
299
+
300
+ /** A plugin package the host loader knows. */
301
+ export interface PluginPackage {
302
+ name: string
303
+ dir: string
304
+ manifest: string
305
+ }
306
+
307
+ // --- the toolCatalog service -----------------------------------------------------
308
+
309
+ /** A search hit from toolCatalog.find(). */
310
+ export interface FoundTool {
311
+ name: string
312
+ short: string
313
+ path: string
314
+ category: string
315
+ group: string
316
+ summary: string
317
+ tokens: number
318
+ highRisk: boolean
319
+ }
320
+
321
+ /** The service the core provides as `toolCatalog` (inject it to extend the catalog). */
322
+ export interface ToolCatalogService {
323
+ registerClassifier(classifier: Classifier): Disposer
324
+ annotate(match: string | RegExp, meta: ToolMeta): Disposer
325
+ defineCategory(name: string, description?: string): Disposer
326
+ registerVisibilityFilter(filter: VisibilityFilter): Disposer
327
+ /** Own a target prefix (`"cap:"`) in tools_load / tools_ls / tools_unload. */
328
+ registerTargetResolver(resolver: TargetResolver): Disposer
329
+ /** Let loaded state outlive the process (see dsh-tool-manager-persist). */
330
+ registerStateProvider(provider?: StateProvider): Disposer
331
+ /** Declare that a plugin confirms high-risk loads; without one they are refused. */
332
+ registerRiskGate(gate?: RiskGate): Disposer
333
+ riskGates(): string[]
334
+ /** Re-read package declarations and rebuild. */
335
+ refresh(): void
336
+ /** Catalog as one agent sees it, or the union over live agents. */
337
+ get(agent?: Agent): Catalog
338
+ resolve(target: string, agent?: Agent): CatalogNode
339
+ stateOf(agent: Agent): LoadState
340
+ exportState(agent: Agent): SerializedState
341
+ importState(agent: Agent, state: Partial<SerializedState>): void
342
+ /** Names visible to the agent, or null when this plugin does not restrict it. */
343
+ allowFor(agent: Agent): string[] | null
344
+ /**
345
+ * Names this plugin lets the agent see (pinned or loaded) that another layer
346
+ * still hides: an MCP connector governance `deny` rule, the connector's
347
+ * per-session injection, or any other plugin's `restrict()`. Empty when the
348
+ * host cannot report the agent's final view.
349
+ */
350
+ deniedFor(agent: Agent): string[]
351
+ /** True when the agent is never narrowed, by route or by its session id. */
352
+ isExempt(agent: Agent): boolean
353
+ /**
354
+ * Why the agent is exempt, so the UI can tell a route-wide exemption from one
355
+ * session: { exempt, source } with source 'file:provider' | 'file:session' |
356
+ * 'config:provider' | ''.
357
+ */
358
+ exemptionOf(agent: Agent): { exempt: boolean, source: string }
359
+ /** Load through the same path as tools_load; returns the model-facing text. */
360
+ load(agent: Agent, targets: readonly string[], options?: { forSubagents?: boolean }): Promise<string>
361
+ unload(agent: Agent, targets: readonly string[]): Promise<string>
362
+ /** Live agents, each with its parent when it is a subagent. */
363
+ agents(): { agent: Agent, id: string, parentId: string | null }[]
364
+ /** Effective configuration, for UI display. */
365
+ config(): {
366
+ pinned: string[]
367
+ entryTools: string[]
368
+ extraPinned: string[]
369
+ unpinned: string[]
370
+ highRisk: string[]
371
+ exemptProviders: string[]
372
+ /** Session ids exempted for their own lifetime by the exemption file. */
373
+ exemptSessions: string[]
374
+ defaults: string[]
375
+ /** Where UI edits are persisted. */
376
+ settingsFile: string
377
+ /** True when a settings file exists and overrides the plugin config. */
378
+ hasSettings: boolean
379
+ /** Where the exemption list is read from. */
380
+ exemptFile: string
381
+ /** True when the exemption file exists and overrides `exemptProviders`. */
382
+ hasExemptFile: boolean
383
+ }
384
+ /**
385
+ * Add or remove one session id in the exemption file and apply it at once.
386
+ * The first call creates the file from the lists already in force, so entries
387
+ * visible in the UI are never dropped by a write.
388
+ */
389
+ setExemptSession(sessionId: string, exempt: boolean): ReturnType<ToolCatalogService['config']>
390
+ /** Re-read the exemption file, for after an external edit. */
391
+ reloadExempt(): ReturnType<ToolCatalogService['config']>
392
+ /**
393
+ * Persist UI-edited settings (merged per key) and apply them at once. The
394
+ * values win over the plugin config from the next call on. Returns the new
395
+ * effective configuration, in the same shape as config().
396
+ */
397
+ updateConfig(patch: ToolManagerSettings): ReturnType<ToolCatalogService['config']>
398
+ /** Re-read the settings file, for after an external edit. */
399
+ reloadConfig(): ReturnType<ToolCatalogService['config']>
400
+ /**
401
+ * Metadata of one tool as the catalog currently shows it, including which
402
+ * layer decided each field (`manifest`, `runtime`, `metadata` or `classifier`).
403
+ */
404
+ toolMeta(name: string): {
405
+ name: string
406
+ found: boolean
407
+ /** Effective values after every layer. */
408
+ effective: ToolMeta
409
+ /** Values edited from the panel, which win over the layers above. */
410
+ edited: ToolMeta
411
+ /** Values from package.json and runtime annotate() calls. */
412
+ declared: ToolMeta
413
+ group: string
414
+ category: string
415
+ summary: string
416
+ highRisk: boolean
417
+ /** The tool's directory path, for linking back to the Directory tab. */
418
+ path: string
419
+ /** True when the tool is not in the catalog (unregistered or pinned away). */
420
+ missing: boolean
421
+ }
422
+ /**
423
+ * Replace the editable metadata of one tool. Fields left out are dropped, so
424
+ * an emptied field falls back to the package declaration; an empty object
425
+ * removes the override entirely.
426
+ */
427
+ updateToolMeta(name: string, meta: ToolMeta): ReturnType<ToolCatalogService['toolMeta']>
428
+ /** Drop every edited field of one tool, falling back to its declaration. */
429
+ clearToolMeta(name: string): ReturnType<ToolCatalogService['toolMeta']>
430
+ /** Where edited metadata is persisted. */
431
+ metadataFile(): string
432
+ packages(): PluginPackage[]
433
+ extensions(): {
434
+ classifiers: { name: string, priority: number }[]
435
+ filters: string[]
436
+ annotations: { source: string | undefined, match: string }[]
437
+ categories: string[]
438
+ riskGates: string[]
439
+ stateProviders: string[]
440
+ }
441
+ /** Structured cross-directory search (same matching as tools_ls with a query). */
442
+ find(query: string, path?: string): FoundTool[]
443
+ /** Schema tokens actually sent for an agent: pinned tools plus its loaded set. */
444
+ wireTokens(agent: Agent): number
445
+ }
446
+
447
+ /** What an extension plugin uses of its context after `inject: ['toolCatalog']`. */
448
+ export interface ExtensionContext extends Pick<HostContext, 'logger' | 'effect' | 'on'> {
449
+ toolCatalog: ToolCatalogService
450
+ }