@lvy-jsd/dsh-tool-manager 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +205 -2
- package/cordis.patch.yml +16 -0
- package/lib/catalog.d.ts +35 -0
- package/lib/catalog.d.ts.map +1 -0
- package/lib/catalog.js +311 -0
- package/lib/catalog.js.map +1 -0
- package/lib/index.d.ts +23 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +588 -0
- package/lib/index.js.map +1 -0
- package/lib/service.d.ts +51 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +199 -0
- package/lib/service.js.map +1 -0
- package/lib/types.d.ts +317 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +7 -0
- package/lib/types.js.map +1 -0
- package/lib/visibility.d.ts +33 -0
- package/lib/visibility.d.ts.map +1 -0
- package/lib/visibility.js +305 -0
- package/lib/visibility.js.map +1 -0
- package/package.json +50 -4
- package/src/catalog.ts +305 -0
- package/src/index.ts +595 -0
- package/src/service.ts +207 -0
- package/src/types.ts +319 -0
- package/src/visibility.ts +317 -0
package/src/service.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// The `toolCatalog` service: the extension surface other plugins build on.
|
|
2
|
+
//
|
|
3
|
+
// registerClassifier({ name, priority, group?(tool), categorize?(group) })
|
|
4
|
+
// annotate(match, { group?, category?, risk?, summary? })
|
|
5
|
+
// defineCategory(name, description)
|
|
6
|
+
// registerVisibilityFilter({ name, filter(agent, names) })
|
|
7
|
+
// refresh(), get(), resolve(target), stateOf(agent)
|
|
8
|
+
//
|
|
9
|
+
// Every register/annotate/define call returns a disposer. Extension plugins
|
|
10
|
+
// should register inside an effect so unloading them undoes the registration:
|
|
11
|
+
// ctx.effect(() => ctx.toolCatalog.registerClassifier({...}))
|
|
12
|
+
//
|
|
13
|
+
// Hooks are plain Cordis events on the host context:
|
|
14
|
+
// tool-catalog/before-load (serial; throw to veto) { agent, targets, names }
|
|
15
|
+
// also fired by the auto-load of a directly called
|
|
16
|
+
// hidden tool, with `rescue: true` in the payload
|
|
17
|
+
// tool-catalog/after-load (emit) { agent, targets, added }
|
|
18
|
+
// tool-catalog/before-unload (serial; throw to veto) { agent, targets }
|
|
19
|
+
// tool-catalog/after-unload (emit) { agent, targets, removed }
|
|
20
|
+
// tool-catalog/before-rebuild (emit) { }
|
|
21
|
+
// tool-catalog/after-rebuild (emit) { catalog }
|
|
22
|
+
|
|
23
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
24
|
+
import { createRequire } from 'node:module'
|
|
25
|
+
import { dirname, join } from 'node:path'
|
|
26
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
27
|
+
import { DEFAULT_CATEGORIES } from './catalog.js'
|
|
28
|
+
import type { Annotation, Classifier, Disposer, HostContext, Loader, Logger, PluginPackage, RegisteredResolver, TargetResolver, ToolMeta, VisibilityFilter } from './types.js'
|
|
29
|
+
|
|
30
|
+
/** A classifier after registration (defaults filled in). */
|
|
31
|
+
export interface RegisteredClassifier extends Classifier {
|
|
32
|
+
name: string
|
|
33
|
+
priority: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A visibility filter after registration (defaults filled in). */
|
|
37
|
+
export interface RegisteredFilter {
|
|
38
|
+
name: string
|
|
39
|
+
filter: VisibilityFilter['filter']
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Package name of a loader specifier: "@scope/pkg/sub" -> "@scope/pkg", "pkg/sub" -> "pkg". */
|
|
43
|
+
export function packageNameOf(specifier: string): string {
|
|
44
|
+
const parts = String(specifier).split('/')
|
|
45
|
+
return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0] ?? specifier
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Find a package's package.json starting from a resolvable entry file. */
|
|
49
|
+
function findManifest(requireFrom: NodeJS.Require, pkg: string): string | undefined {
|
|
50
|
+
try {
|
|
51
|
+
return requireFrom.resolve(`${pkg}/package.json`)
|
|
52
|
+
} catch {
|
|
53
|
+
// exports may hide package.json; walk up from the main entry instead.
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
let dir = dirname(requireFrom.resolve(pkg))
|
|
57
|
+
while (dir !== dirname(dir)) {
|
|
58
|
+
const candidate = join(dir, 'package.json')
|
|
59
|
+
if (existsSync(candidate)) {
|
|
60
|
+
const json = JSON.parse(readFileSync(candidate, 'utf8'))
|
|
61
|
+
if (json.name === pkg) return candidate
|
|
62
|
+
}
|
|
63
|
+
dir = dirname(dir)
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// unresolvable from here
|
|
67
|
+
}
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Read `dsh.toolCatalog` declarations from every plugin package the host loader
|
|
73
|
+
* knows. Declaration format (package.json):
|
|
74
|
+
*
|
|
75
|
+
* "dsh": { "toolCatalog": {
|
|
76
|
+
* "match": "^adb_", // regex over tool names (optional)
|
|
77
|
+
* "group": "adb", "category": "device",
|
|
78
|
+
* "tools": { "adb_install": { "risk": "high", "summary": "..." } }
|
|
79
|
+
* } }
|
|
80
|
+
*
|
|
81
|
+
* `group`/`category`/`risk`/`summary` at the top level apply to `match`;
|
|
82
|
+
* entries under `tools` apply to exact names and win over the top level.
|
|
83
|
+
*/
|
|
84
|
+
export function readManifestAnnotations(ctx: Pick<HostContext, 'get' | 'baseUrl'>, logger?: Logger): Annotation[] {
|
|
85
|
+
const annotations: Annotation[] = []
|
|
86
|
+
for (const { name: pkg, manifest: manifestPath } of listPluginPackages(ctx)) {
|
|
87
|
+
try {
|
|
88
|
+
const declared = JSON.parse(readFileSync(manifestPath, 'utf8'))?.dsh?.toolCatalog
|
|
89
|
+
if (!declared) continue
|
|
90
|
+
const top = pick(declared)
|
|
91
|
+
if (declared.match && Object.keys(top).length) annotations.push({ source: pkg, match: new RegExp(declared.match), meta: top })
|
|
92
|
+
for (const [name, meta] of Object.entries((declared.tools ?? {}) as Record<string, unknown>)) {
|
|
93
|
+
annotations.push({ source: pkg, match: name, meta: { ...top, ...pick(meta) } })
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
logger?.warn?.(`tool-manager: bad dsh.toolCatalog in ${pkg}: ${(error as Error).message}`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return annotations
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Every enabled plugin package the host loader knows, as
|
|
104
|
+
* `{ name, dir, manifest }`. Relative (local) entries are skipped.
|
|
105
|
+
*/
|
|
106
|
+
export function listPluginPackages(ctx: Pick<HostContext, 'get' | 'baseUrl'>): PluginPackage[] {
|
|
107
|
+
const loader = ctx.get?.('loader') as Loader | undefined
|
|
108
|
+
if (!loader?.entries) return []
|
|
109
|
+
const bases = [loader.baseUrl, loader.ctx?.baseUrl, ctx.baseUrl, pathToFileURL(join(process.cwd(), 'noop.js')).href].filter((b): b is string => Boolean(b))
|
|
110
|
+
const seen = new Set<string>()
|
|
111
|
+
const out: PluginPackage[] = []
|
|
112
|
+
for (const entry of loader.entries()) {
|
|
113
|
+
const spec = entry?.options?.name
|
|
114
|
+
if (typeof spec !== 'string' || spec.startsWith('.') || entry.options?.disabled === true) continue
|
|
115
|
+
const pkg = packageNameOf(spec)
|
|
116
|
+
if (seen.has(pkg)) continue
|
|
117
|
+
seen.add(pkg)
|
|
118
|
+
for (const base of bases) {
|
|
119
|
+
const from = createRequire(base.startsWith('file:') ? fileURLToPath(base) : join(base, 'noop.js'))
|
|
120
|
+
const manifest = findManifest(from, pkg)
|
|
121
|
+
if (manifest) {
|
|
122
|
+
out.push({ name: pkg, dir: dirname(manifest), manifest })
|
|
123
|
+
break
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const META_KEYS = ['group', 'category', 'risk', 'summary'] as const
|
|
131
|
+
|
|
132
|
+
function pick(meta: unknown = {}): ToolMeta {
|
|
133
|
+
const out: ToolMeta = {}
|
|
134
|
+
if (typeof meta !== 'object' || meta === null) return out
|
|
135
|
+
for (const key of META_KEYS) {
|
|
136
|
+
const value = (meta as Record<string, unknown>)[key]
|
|
137
|
+
if (typeof value === 'string' && value) out[key] = value
|
|
138
|
+
}
|
|
139
|
+
return out
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Registries behind the service. `onChange` rebuilds the catalog. */
|
|
143
|
+
export function createRegistries(onChange: () => void) {
|
|
144
|
+
const classifiers = new Set<RegisteredClassifier>()
|
|
145
|
+
const annotations: Annotation[] = []
|
|
146
|
+
const categories = new Map(Object.entries(DEFAULT_CATEGORIES))
|
|
147
|
+
const filters = new Set<RegisteredFilter>()
|
|
148
|
+
const resolvers = new Set<RegisteredResolver>()
|
|
149
|
+
|
|
150
|
+
const remover = (fn: () => unknown): Disposer => {
|
|
151
|
+
let done = false
|
|
152
|
+
return () => {
|
|
153
|
+
if (done) return
|
|
154
|
+
done = true
|
|
155
|
+
fn()
|
|
156
|
+
onChange()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
classifiers,
|
|
162
|
+
annotations,
|
|
163
|
+
categories,
|
|
164
|
+
filters,
|
|
165
|
+
resolvers,
|
|
166
|
+
registerClassifier(classifier: Classifier): Disposer {
|
|
167
|
+
if (!classifier || (typeof classifier.group !== 'function' && typeof classifier.categorize !== 'function')) {
|
|
168
|
+
throw new TypeError('registerClassifier: provide group(tool) and/or categorize(group)')
|
|
169
|
+
}
|
|
170
|
+
const entry: RegisteredClassifier = { name: classifier.name ?? 'anonymous', priority: Number(classifier.priority ?? 0), group: classifier.group, categorize: classifier.categorize }
|
|
171
|
+
classifiers.add(entry)
|
|
172
|
+
onChange()
|
|
173
|
+
return remover(() => classifiers.delete(entry))
|
|
174
|
+
},
|
|
175
|
+
annotate(match: string | RegExp, meta: ToolMeta): Disposer {
|
|
176
|
+
const entry: Annotation = { source: 'runtime', match: typeof match === 'string' ? match : new RegExp(match), meta: pick(meta) }
|
|
177
|
+
annotations.push(entry)
|
|
178
|
+
onChange()
|
|
179
|
+
return remover(() => annotations.splice(annotations.indexOf(entry), 1))
|
|
180
|
+
},
|
|
181
|
+
defineCategory(name: string, description = ''): Disposer {
|
|
182
|
+
const previous = categories.get(name)
|
|
183
|
+
categories.set(name, description)
|
|
184
|
+
onChange()
|
|
185
|
+
return remover(() => (previous === undefined ? categories.delete(name) : categories.set(name, previous)))
|
|
186
|
+
},
|
|
187
|
+
registerVisibilityFilter(filter: VisibilityFilter): Disposer {
|
|
188
|
+
if (typeof filter?.filter !== 'function') throw new TypeError('registerVisibilityFilter: provide filter(agent, names)')
|
|
189
|
+
const entry = { name: filter.name ?? 'anonymous', filter: filter.filter }
|
|
190
|
+
filters.add(entry)
|
|
191
|
+
onChange()
|
|
192
|
+
return remover(() => filters.delete(entry))
|
|
193
|
+
},
|
|
194
|
+
/**
|
|
195
|
+
* Own a target prefix in tools_load / tools_ls / tools_unload. `resolve` gets
|
|
196
|
+
* the raw target and returns one or more ordinary targets (paths, group or
|
|
197
|
+
* tool names), or undefined to pass.
|
|
198
|
+
*/
|
|
199
|
+
registerTargetResolver(resolver: TargetResolver): Disposer {
|
|
200
|
+
if (typeof resolver?.resolve !== 'function') throw new TypeError('registerTargetResolver: provide resolve(target)')
|
|
201
|
+
const entry = { name: resolver.name ?? 'anonymous', prefix: String(resolver.prefix ?? ''), resolve: resolver.resolve }
|
|
202
|
+
resolvers.add(entry)
|
|
203
|
+
onChange()
|
|
204
|
+
return remover(() => resolvers.delete(entry))
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
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
|
+
view?(agent: Agent): { restrictableNames: Iterable<string> }
|
|
204
|
+
schemaOf?(definition: unknown, deferred: boolean): ToolSchema
|
|
205
|
+
layers?: {
|
|
206
|
+
chainLayers?(agent: Agent): Iterable<ToolLayer>
|
|
207
|
+
peek?(agent: Agent): ToolLayer | undefined
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface LoaderEntry {
|
|
212
|
+
options?: { name?: unknown, disabled?: boolean }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface Loader {
|
|
216
|
+
entries?(): Iterable<LoaderEntry>
|
|
217
|
+
baseUrl?: string
|
|
218
|
+
ctx?: { baseUrl?: string }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The host context the core plugin is applied to. */
|
|
222
|
+
export interface HostContext {
|
|
223
|
+
tools: HostTools
|
|
224
|
+
logger?: ((name: string) => Logger) | Logger
|
|
225
|
+
baseUrl?: string
|
|
226
|
+
get?(name: string): unknown
|
|
227
|
+
emit(event: string, ...args: unknown[]): void
|
|
228
|
+
serial(event: string, ...args: unknown[]): Promise<unknown>
|
|
229
|
+
// any: each event passes its own argument types, and a handler typed
|
|
230
|
+
// (...args: unknown[]) would reject every concretely typed listener.
|
|
231
|
+
on(event: string, handler: (...args: any[]) => unknown): Disposer
|
|
232
|
+
effect(fn: () => Disposer | void, label?: string): void
|
|
233
|
+
inject?(deps: string[], fn: (ctx: AgentContext) => void): void
|
|
234
|
+
provide(name: string, service: unknown): void
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Configuration of the core plugin. */
|
|
238
|
+
export interface CoreConfig {
|
|
239
|
+
/** Extra always-on tools. */
|
|
240
|
+
pinned?: string[]
|
|
241
|
+
/** Tools removed from the default always-on list. */
|
|
242
|
+
unpin?: string[]
|
|
243
|
+
/** Tool names treated as high risk. */
|
|
244
|
+
highRisk?: string[]
|
|
245
|
+
/** Model routes never narrowed (default ["claude-code"]). */
|
|
246
|
+
exemptProviders?: string[]
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** A plugin package the host loader knows. */
|
|
250
|
+
export interface PluginPackage {
|
|
251
|
+
name: string
|
|
252
|
+
dir: string
|
|
253
|
+
manifest: string
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// --- the toolCatalog service -----------------------------------------------------
|
|
257
|
+
|
|
258
|
+
/** A search hit from toolCatalog.find(). */
|
|
259
|
+
export interface FoundTool {
|
|
260
|
+
name: string
|
|
261
|
+
short: string
|
|
262
|
+
path: string
|
|
263
|
+
category: string
|
|
264
|
+
group: string
|
|
265
|
+
summary: string
|
|
266
|
+
tokens: number
|
|
267
|
+
highRisk: boolean
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The service the core provides as `toolCatalog` (inject it to extend the catalog). */
|
|
271
|
+
export interface ToolCatalogService {
|
|
272
|
+
registerClassifier(classifier: Classifier): Disposer
|
|
273
|
+
annotate(match: string | RegExp, meta: ToolMeta): Disposer
|
|
274
|
+
defineCategory(name: string, description?: string): Disposer
|
|
275
|
+
registerVisibilityFilter(filter: VisibilityFilter): Disposer
|
|
276
|
+
/** Own a target prefix (`"cap:"`) in tools_load / tools_ls / tools_unload. */
|
|
277
|
+
registerTargetResolver(resolver: TargetResolver): Disposer
|
|
278
|
+
/** Let loaded state outlive the process (see dsh-tool-manager-persist). */
|
|
279
|
+
registerStateProvider(provider?: StateProvider): Disposer
|
|
280
|
+
/** Declare that a plugin confirms high-risk loads; without one they are refused. */
|
|
281
|
+
registerRiskGate(gate?: RiskGate): Disposer
|
|
282
|
+
riskGates(): string[]
|
|
283
|
+
/** Re-read package declarations and rebuild. */
|
|
284
|
+
refresh(): void
|
|
285
|
+
/** Catalog as one agent sees it, or the union over live agents. */
|
|
286
|
+
get(agent?: Agent): Catalog
|
|
287
|
+
resolve(target: string, agent?: Agent): CatalogNode
|
|
288
|
+
stateOf(agent: Agent): LoadState
|
|
289
|
+
exportState(agent: Agent): SerializedState
|
|
290
|
+
importState(agent: Agent, state: Partial<SerializedState>): void
|
|
291
|
+
/** Names visible to the agent, or null when this plugin does not restrict it. */
|
|
292
|
+
allowFor(agent: Agent): string[] | null
|
|
293
|
+
/** True when the agent's model route is in `exemptProviders` (never narrowed). */
|
|
294
|
+
isExempt(agent: Agent): boolean
|
|
295
|
+
/** Load through the same path as tools_load; returns the model-facing text. */
|
|
296
|
+
load(agent: Agent, targets: readonly string[], options?: { forSubagents?: boolean }): Promise<string>
|
|
297
|
+
unload(agent: Agent, targets: readonly string[]): Promise<string>
|
|
298
|
+
/** Live agents, each with its parent when it is a subagent. */
|
|
299
|
+
agents(): { agent: Agent, id: string, parentId: string | null }[]
|
|
300
|
+
config(): { pinned: string[], entryTools: string[], extraPinned: string[], unpinned: string[], highRisk: string[] }
|
|
301
|
+
packages(): PluginPackage[]
|
|
302
|
+
extensions(): {
|
|
303
|
+
classifiers: { name: string, priority: number }[]
|
|
304
|
+
filters: string[]
|
|
305
|
+
annotations: { source: string | undefined, match: string }[]
|
|
306
|
+
categories: string[]
|
|
307
|
+
riskGates: string[]
|
|
308
|
+
stateProviders: string[]
|
|
309
|
+
}
|
|
310
|
+
/** Structured cross-directory search (same matching as tools_ls with a query). */
|
|
311
|
+
find(query: string, path?: string): FoundTool[]
|
|
312
|
+
/** Schema tokens actually sent for an agent: pinned tools plus its loaded set. */
|
|
313
|
+
wireTokens(agent: Agent): number
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** What an extension plugin uses of its context after `inject: ['toolCatalog']`. */
|
|
317
|
+
export interface ExtensionContext extends Pick<HostContext, 'logger' | 'effect' | 'on'> {
|
|
318
|
+
toolCatalog: ToolCatalogService
|
|
319
|
+
}
|