@miphamai/cli 0.10.0 → 0.12.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.
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Lightweight Prometheus-compatible metrics registry for mipham-code.
3
+ *
4
+ * Provides Counter, Gauge, and Histogram metric types compatible with
5
+ * the Python shared/metrics.py module in MegaSystem. Metrics are
6
+ * collected in-memory and can be exported in Prometheus text format
7
+ * or JSON.
8
+ *
9
+ * Usage:
10
+ * import { getMetrics } from '../core/metrics.js'
11
+ *
12
+ * const m = getMetrics()
13
+ * m.cliInvocations.inc()
14
+ * m.toolCalls.inc({ tool_name: 'bash' })
15
+ * m.modelRequestDuration.observe(150.0, { provider: 'anthropic' })
16
+ *
17
+ * Endpoints (via ArtifactServer):
18
+ * GET /metrics — Prometheus text format
19
+ * GET /metrics/json — JSON format
20
+ */
21
+
22
+ // ── Metric types ──────────────────────────────────────────────────────────
23
+
24
+ export interface MetricLabels {
25
+ [key: string]: string
26
+ }
27
+
28
+ /** Format labels into Prometheus {...} string. */
29
+ function formatLabels(labels?: MetricLabels): string {
30
+ if (!labels || Object.keys(labels).length === 0) return ''
31
+ const parts = Object.entries(labels)
32
+ .sort(([a], [b]) => a.localeCompare(b))
33
+ .map(([k, v]) => `${k}="${v.replace(/"/g, '\\"')}"`)
34
+ return `{${parts.join(',')}}`
35
+ }
36
+
37
+ // ── Counter ──────────────────────────────────────────────────────────────
38
+
39
+ export class Counter {
40
+ readonly name: string
41
+ readonly help: string
42
+ readonly labels: MetricLabels
43
+ private _value = 0
44
+
45
+ constructor(name: string, help: string, labels?: MetricLabels) {
46
+ this.name = name
47
+ this.help = help
48
+ this.labels = labels ?? {}
49
+ }
50
+
51
+ inc(amount = 1): void {
52
+ this._value += amount
53
+ }
54
+
55
+ get value(): number {
56
+ return this._value
57
+ }
58
+
59
+ /** Full metric name with labels for dedup key. */
60
+ get key(): string {
61
+ return this.name + formatLabels(this.labels)
62
+ }
63
+
64
+ toPrometheus(): string {
65
+ const labelStr = formatLabels(this.labels)
66
+ return `${this.name}${labelStr} ${this._value}`
67
+ }
68
+
69
+ toJSON(): object {
70
+ return {
71
+ name: this.name,
72
+ type: 'counter',
73
+ help: this.help,
74
+ labels: this.labels,
75
+ value: this._value,
76
+ }
77
+ }
78
+ }
79
+
80
+ // ── Gauge ────────────────────────────────────────────────────────────────
81
+
82
+ export class Gauge {
83
+ readonly name: string
84
+ readonly help: string
85
+ readonly labels: MetricLabels
86
+ private _value = 0
87
+
88
+ constructor(name: string, help: string, labels?: MetricLabels) {
89
+ this.name = name
90
+ this.help = help
91
+ this.labels = labels ?? {}
92
+ }
93
+
94
+ inc(amount = 1): void {
95
+ this._value += amount
96
+ }
97
+
98
+ dec(amount = 1): void {
99
+ this._value -= amount
100
+ }
101
+
102
+ set(value: number): void {
103
+ this._value = value
104
+ }
105
+
106
+ get value(): number {
107
+ return this._value
108
+ }
109
+
110
+ get key(): string {
111
+ return this.name + formatLabels(this.labels)
112
+ }
113
+
114
+ toPrometheus(): string {
115
+ const labelStr = formatLabels(this.labels)
116
+ return `${this.name}${labelStr} ${this._value}`
117
+ }
118
+
119
+ toJSON(): object {
120
+ return {
121
+ name: this.name,
122
+ type: 'gauge',
123
+ help: this.help,
124
+ labels: this.labels,
125
+ value: this._value,
126
+ }
127
+ }
128
+ }
129
+
130
+ // ── Histogram ────────────────────────────────────────────────────────────
131
+
132
+ const DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
133
+
134
+ export class Histogram {
135
+ readonly name: string
136
+ readonly help: string
137
+ readonly labels: MetricLabels
138
+ readonly buckets: number[]
139
+ private _count = 0
140
+ private _sum = 0
141
+ private _bucketCounts: number[]
142
+
143
+ constructor(name: string, help: string, buckets?: number[], labels?: MetricLabels) {
144
+ this.name = name
145
+ this.help = help
146
+ this.labels = labels ?? {}
147
+ this.buckets = buckets ?? DEFAULT_BUCKETS
148
+ this._bucketCounts = new Array(this.buckets.length).fill(0)
149
+ }
150
+
151
+ observe(value: number): void {
152
+ this._count++
153
+ this._sum += value
154
+ for (let i = 0; i < this.buckets.length; i++) {
155
+ if (value <= (this.buckets[i] ?? Infinity)) {
156
+ this._bucketCounts[i] = (this._bucketCounts[i] ?? 0) + 1
157
+ }
158
+ }
159
+ }
160
+
161
+ get count(): number {
162
+ return this._count
163
+ }
164
+
165
+ get sum(): number {
166
+ return this._sum
167
+ }
168
+
169
+ get key(): string {
170
+ return this.name + formatLabels(this.labels)
171
+ }
172
+
173
+ toPrometheus(): string {
174
+ const labelStr = formatLabels(this.labels)
175
+ const lines: string[] = []
176
+
177
+ // _bucket values
178
+ for (let i = 0; i < this.buckets.length; i++) {
179
+ const bucketLabel = formatLabels({
180
+ ...this.labels,
181
+ le: String(this.buckets[i]),
182
+ })
183
+ lines.push(`${this.name}_bucket${bucketLabel} ${this._bucketCounts[i]}`)
184
+ }
185
+ // +Inf bucket
186
+ const infLabel = formatLabels({ ...this.labels, le: '+Inf' })
187
+ lines.push(`${this.name}_bucket${infLabel} ${this._count}`)
188
+
189
+ // _sum and _count
190
+ lines.push(`${this.name}_sum${labelStr} ${this._sum}`)
191
+ lines.push(`${this.name}_count${labelStr} ${this._count}`)
192
+
193
+ return lines.join('\n')
194
+ }
195
+
196
+ toJSON(): object {
197
+ const bucketResults: { le: string; count: number }[] = []
198
+ for (let i = 0; i < this.buckets.length; i++) {
199
+ bucketResults.push({ le: String(this.buckets[i]), count: this._bucketCounts[i] ?? 0 })
200
+ }
201
+ bucketResults.push({ le: '+Inf', count: this._count })
202
+ return {
203
+ name: this.name,
204
+ type: 'histogram',
205
+ help: this.help,
206
+ labels: this.labels,
207
+ count: this._count,
208
+ sum: this._sum,
209
+ buckets: bucketResults,
210
+ }
211
+ }
212
+ }
213
+
214
+ // ── MetricsRegistry ──────────────────────────────────────────────────────
215
+
216
+ export class MetricsRegistry {
217
+ private _counters = new Map<string, Counter>()
218
+ private _gauges = new Map<string, Gauge>()
219
+ private _histograms = new Map<string, Histogram>()
220
+
221
+ // ── Predefined metrics ─────────────────────────────────────────
222
+
223
+ /** CLI invocation counter. Incremented on each CLI startup. */
224
+ readonly cliInvocations: Counter
225
+
226
+ /** Tool call counter, labelled by tool_name. Callers use .inc({tool_name}). */
227
+ readonly toolCalls: Counter
228
+
229
+ /** Model API request counter, labelled by provider and model. */
230
+ readonly modelRequests: Counter
231
+
232
+ /** Model API request error counter, labelled by provider and error type. */
233
+ readonly modelRequestErrors: Counter
234
+
235
+ /** Model API request latency histogram (milliseconds). */
236
+ readonly modelRequestDurationMs: Histogram
237
+
238
+ /** Active CLI sessions gauge. */
239
+ readonly activeSessions: Gauge
240
+
241
+ constructor() {
242
+ // Pre-register standard metrics
243
+ this.cliInvocations = this.counter(
244
+ 'mipham_code_cli_invocations_total',
245
+ 'Number of CLI invocations',
246
+ )
247
+
248
+ this.toolCalls = this.counter('mipham_code_tool_calls_total', 'Number of tool invocations')
249
+
250
+ this.modelRequests = this.counter(
251
+ 'mipham_code_model_requests_total',
252
+ 'Number of model API requests',
253
+ )
254
+
255
+ this.modelRequestErrors = this.counter(
256
+ 'mipham_code_model_request_errors_total',
257
+ 'Number of model API request errors',
258
+ )
259
+
260
+ this.modelRequestDurationMs = this.histogram(
261
+ 'mipham_code_model_request_duration_ms',
262
+ 'Model API request duration in milliseconds',
263
+ [50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000],
264
+ )
265
+
266
+ this.activeSessions = this.gauge('mipham_code_active_sessions', 'Number of active CLI sessions')
267
+ }
268
+
269
+ // ── Factory methods ─────────────────────────────────────────────
270
+
271
+ counter(name: string, help: string, labels?: MetricLabels): Counter {
272
+ const c = new Counter(name, help, labels)
273
+ if (this._counters.has(c.key)) return this._counters.get(c.key)!
274
+ this._counters.set(c.key, c)
275
+ return c
276
+ }
277
+
278
+ gauge(name: string, help: string, labels?: MetricLabels): Gauge {
279
+ const g = new Gauge(name, help, labels)
280
+ if (this._gauges.has(g.key)) return this._gauges.get(g.key)!
281
+ this._gauges.set(g.key, g)
282
+ return g
283
+ }
284
+
285
+ histogram(name: string, help: string, buckets?: number[], labels?: MetricLabels): Histogram {
286
+ const h = new Histogram(name, help, buckets, labels)
287
+ if (this._histograms.has(h.key)) return this._histograms.get(h.key)!
288
+ this._histograms.set(h.key, h)
289
+ return h
290
+ }
291
+
292
+ // ── Export ──────────────────────────────────────────────────────
293
+
294
+ /** Export all metrics in Prometheus text format. */
295
+ toPrometheusText(): string {
296
+ const lines: string[] = []
297
+ const seen = new Set<string>()
298
+
299
+ for (const c of this._counters.values()) {
300
+ if (!seen.has(c.name)) {
301
+ lines.push(`# HELP ${c.name} ${c.help}`)
302
+ lines.push(`# TYPE ${c.name} counter`)
303
+ seen.add(c.name)
304
+ }
305
+ lines.push(c.toPrometheus())
306
+ }
307
+
308
+ for (const g of this._gauges.values()) {
309
+ if (!seen.has(g.name)) {
310
+ lines.push(`# HELP ${g.name} ${g.help}`)
311
+ lines.push(`# TYPE ${g.name} gauge`)
312
+ seen.add(g.name)
313
+ }
314
+ lines.push(g.toPrometheus())
315
+ }
316
+
317
+ for (const h of this._histograms.values()) {
318
+ if (!seen.has(h.name)) {
319
+ lines.push(`# HELP ${h.name} ${h.help}`)
320
+ lines.push(`# TYPE ${h.name} histogram`)
321
+ seen.add(h.name)
322
+ }
323
+ lines.push(h.toPrometheus())
324
+ }
325
+
326
+ return lines.join('\n') + '\n'
327
+ }
328
+
329
+ /** Export all metrics as JSON. */
330
+ toJSON(): object {
331
+ return {
332
+ counters: Array.from(this._counters.values()).map((c) => c.toJSON()),
333
+ gauges: Array.from(this._gauges.values()).map((g) => g.toJSON()),
334
+ histograms: Array.from(this._histograms.values()).map((h) => h.toJSON()),
335
+ }
336
+ }
337
+
338
+ /** Reset all metrics (useful for testing). */
339
+ reset(): void {
340
+ this._counters.clear()
341
+ this._gauges.clear()
342
+ this._histograms.clear()
343
+ }
344
+ }
345
+
346
+ // ── Singleton ────────────────────────────────────────────────────────────
347
+
348
+ let _instance: MetricsRegistry | null = null
349
+
350
+ /** Get the global MetricsRegistry singleton. */
351
+ export function getMetrics(): MetricsRegistry {
352
+ if (!_instance) {
353
+ _instance = new MetricsRegistry()
354
+ }
355
+ return _instance
356
+ }
357
+
358
+ /** Reset the singleton (useful for testing). */
359
+ export function resetMetrics(): void {
360
+ if (_instance) {
361
+ _instance.reset()
362
+ _instance = null
363
+ }
364
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * OutputStylesLoader — loads custom response persona styles from .mipham/output-styles/.
3
+ *
4
+ * Each .md file in the directory is a named style. The active style's content
5
+ * is injected into the system prompt to shape the AI's tone and communication style.
6
+ *
7
+ * Usage:
8
+ * const loader = new OutputStylesLoader(cwd)
9
+ * loader.list() // ['concise', 'academic', ...]
10
+ * loader.get('concise') // style content or undefined
11
+ * loader.setActive('concise') // switch active style
12
+ * loader.getActiveContent() // active style content (for system prompt)
13
+ */
14
+
15
+ import { readdirSync, readFileSync, existsSync } from 'node:fs'
16
+ import { join, basename, extname } from 'node:path'
17
+
18
+ export class OutputStylesLoader {
19
+ private stylesDir: string
20
+ private activeStyle: string | null = null
21
+
22
+ constructor(cwd: string) {
23
+ this.stylesDir = join(cwd, '.mipham', 'output-styles')
24
+ }
25
+
26
+ /**
27
+ * List all available style names (filename without .md extension).
28
+ */
29
+ list(): string[] {
30
+ if (!existsSync(this.stylesDir)) return []
31
+ try {
32
+ return readdirSync(this.stylesDir)
33
+ .filter((f) => f.endsWith('.md'))
34
+ .map((f) => basename(f, extname(f)))
35
+ } catch {
36
+ return []
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Get the content of a named style. Returns undefined if not found.
42
+ */
43
+ get(name: string): string | undefined {
44
+ if (!existsSync(this.stylesDir)) return undefined
45
+ const filepath = join(this.stylesDir, `${name}.md`)
46
+ if (!existsSync(filepath)) return undefined
47
+ try {
48
+ return readFileSync(filepath, 'utf-8').trim()
49
+ } catch {
50
+ return undefined
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Set the active output style.
56
+ */
57
+ setActive(name: string): boolean {
58
+ const content = this.get(name)
59
+ if (!content) return false
60
+ this.activeStyle = name
61
+ return true
62
+ }
63
+
64
+ /**
65
+ * Clear the active output style (revert to default personality).
66
+ */
67
+ clearActive(): void {
68
+ this.activeStyle = null
69
+ }
70
+
71
+ /**
72
+ * Get the active style name (null if none selected).
73
+ */
74
+ getActiveName(): string | null {
75
+ return this.activeStyle
76
+ }
77
+
78
+ /**
79
+ * Get the active style content for system prompt injection.
80
+ * Returns empty string if no style is active.
81
+ */
82
+ getActiveContent(): string {
83
+ if (!this.activeStyle) return ''
84
+ const content = this.get(this.activeStyle)
85
+ if (!content) return ''
86
+ return [
87
+ `[Output Style: ${this.activeStyle}]`,
88
+ 'Adopt the following communication style in all your responses:',
89
+ '',
90
+ content,
91
+ '',
92
+ '---',
93
+ ].join('\n')
94
+ }
95
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * RulesLoader — path-scoped rules from .mipham/rules/.
3
+ *
4
+ * Rules are markdown files with YAML frontmatter. They are injected into
5
+ * the conversation when the AI touches matching files.
6
+ *
7
+ * Directory structure:
8
+ * .mipham/rules/
9
+ * always.md — always loaded (no paths filter)
10
+ * typescript.md — loaded when touching *.ts files
11
+ * security.md — loaded when touching auth/ or crypto/ paths
12
+ *
13
+ * Frontmatter:
14
+ * ---
15
+ * paths: "apps/cli/src/**\/*.ts"
16
+ * description: TypeScript coding standards
17
+ * ---
18
+ */
19
+
20
+ import { readdirSync, readFileSync, existsSync } from 'node:fs'
21
+ import { join, relative } from 'node:path'
22
+
23
+ interface RuleFile {
24
+ name: string
25
+ paths: string[] // glob patterns, empty = always loaded
26
+ description: string
27
+ content: string
28
+ }
29
+
30
+ export class RulesLoader {
31
+ private rules: RuleFile[] = []
32
+ private rulesDir: string
33
+
34
+ constructor(cwd: string) {
35
+ this.rulesDir = join(cwd, '.mipham', 'rules')
36
+ }
37
+
38
+ /**
39
+ * Load all rules from .mipham/rules/. Call once at startup.
40
+ */
41
+ load(): void {
42
+ this.rules = []
43
+ if (!existsSync(this.rulesDir)) return
44
+
45
+ try {
46
+ const files = readdirSync(this.rulesDir).filter((f) => f.endsWith('.md'))
47
+ for (const file of files) {
48
+ const filepath = join(this.rulesDir, file)
49
+ try {
50
+ const raw = readFileSync(filepath, 'utf-8')
51
+ const { paths, description, content } = this.parseRule(raw, file)
52
+ this.rules.push({ name: file.replace(/\.md$/, ''), paths, description, content })
53
+ } catch {
54
+ // Skip unparseable files
55
+ }
56
+ }
57
+ } catch {
58
+ // Directory read error — rules unavailable
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Get rules that match the given file paths.
64
+ * Rules with no paths filter ("always") are always included.
65
+ */
66
+ getMatchingRules(touchedFiles: string[]): RuleFile[] {
67
+ const matched: RuleFile[] = []
68
+
69
+ for (const rule of this.rules) {
70
+ // Always rules — no paths filter
71
+ if (rule.paths.length === 0) {
72
+ matched.push(rule)
73
+ continue
74
+ }
75
+
76
+ // Check if any touched file matches any rule path pattern
77
+ for (const file of touchedFiles) {
78
+ for (const pattern of rule.paths) {
79
+ if (this.matchPath(file, pattern)) {
80
+ matched.push(rule)
81
+ // Break inner loops — rule already matched
82
+ break
83
+ }
84
+ }
85
+ if (matched.includes(rule)) break
86
+ }
87
+ }
88
+
89
+ return matched
90
+ }
91
+
92
+ /**
93
+ * Build a context block to inject into the conversation.
94
+ */
95
+ buildContextBlock(touchedFiles: string[]): string {
96
+ const matched = this.getMatchingRules(touchedFiles)
97
+ if (matched.length === 0) return ''
98
+
99
+ const blocks = matched.map(
100
+ (r) => `[Rule: ${r.name}]${r.description ? ` — ${r.description}` : ''}\n${r.content}`,
101
+ )
102
+ return `\n<!-- Path-scoped rules matching: ${touchedFiles.join(', ')} -->\n${blocks.join('\n\n')}\n`
103
+ }
104
+
105
+ /**
106
+ * Count loaded rules.
107
+ */
108
+ count(): number {
109
+ return this.rules.length
110
+ }
111
+
112
+ /**
113
+ * List loaded rule names.
114
+ */
115
+ list(): string[] {
116
+ return this.rules.map((r) => r.name)
117
+ }
118
+
119
+ /**
120
+ * Simple glob matching. Supports **, *, and exact file matching.
121
+ * Returns true if file matches pattern.
122
+ */
123
+ private matchPath(file: string, pattern: string): boolean {
124
+ // Convert glob pattern to regex
125
+ let regexStr = pattern
126
+ .replace(/\./g, '\\.')
127
+ .replace(/\*\*/g, '<<GLOBSTAR>>')
128
+ .replace(/\*/g, '[^/]*')
129
+ .replace(/<<GLOBSTAR>>/g, '.*')
130
+
131
+ // If pattern doesn't start with ** or *, anchor to be a suffix match
132
+ if (!pattern.startsWith('**') && !pattern.startsWith('*')) {
133
+ regexStr = regexStr + '$'
134
+ }
135
+
136
+ try {
137
+ return new RegExp(regexStr).test(file)
138
+ } catch {
139
+ // Invalid pattern — fallback to simple includes
140
+ return file.includes(pattern.replace(/\*\*/g, '').replace(/\*/g, ''))
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Parse a rule file — extract frontmatter and body.
146
+ */
147
+ private parseRule(
148
+ raw: string,
149
+ filename: string,
150
+ ): { paths: string[]; description: string; content: string } {
151
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/)
152
+ if (!match) {
153
+ return { paths: [], description: '', content: raw.trim() }
154
+ }
155
+
156
+ const frontmatter = match[1] || ''
157
+ const body = (match[2] || '').trim()
158
+
159
+ const paths: string[] = []
160
+ let description = ''
161
+
162
+ for (const line of frontmatter.split('\n')) {
163
+ const pathMatch = line.match(/^paths:\s*"(.+)"$/)
164
+ if (pathMatch) {
165
+ pathMatch[1]!.split(',').forEach((p) => paths.push(p.trim()))
166
+ }
167
+ const descMatch = line.match(/^description:\s*(.+)$/)
168
+ if (descMatch) {
169
+ description = descMatch[1]!.trim()
170
+ }
171
+ }
172
+
173
+ return { paths, description, content: body }
174
+ }
175
+ }
package/src/index.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import { join } from 'node:path'
2
2
  import { render } from 'ink'
3
3
  import { App } from './ui/app'
4
- import { loadConfig } from './config/loader'
4
+ import { loadConfig, loadInferenceHookConfig } from './config/loader'
5
5
  import { bootstrapProviders } from './providers/bootstrap'
6
6
  import { InstructionsLoader } from './core/instructions'
7
7
  import { loadSessionMemories } from './core/memory/memory-loader'
@@ -18,6 +18,7 @@ import { registerMcpServerTools } from './mcp/registry'
18
18
  import { AgentRegistry } from './agent/agent-registry'
19
19
  import { HookEngine } from './core/hooks'
20
20
  import { ArtifactServer } from './artifacts/server'
21
+ import { getMetrics } from './core/metrics'
21
22
  import { ARTIFACTS_DIR, ARTIFACT_PORT, MIPHAM_DIR } from './shared/constants'
22
23
  import { AgentViewManager } from './agent-view/agent-view-manager'
23
24
  import { AgentViewDashboard } from './agent-view/dashboard'
@@ -32,6 +33,9 @@ interface RunOptions {
32
33
  }
33
34
 
34
35
  export async function runApp(options: RunOptions): Promise<void> {
36
+ // Metrics: count CLI invocation
37
+ getMetrics().cliInvocations.inc()
38
+
35
39
  // Handle `mipham agents` subcommand — launch standalone dashboard
36
40
  const args = process.argv.slice(2)
37
41
  if (args[0] === 'agents') {
@@ -149,6 +153,10 @@ export async function runApp(options: RunOptions): Promise<void> {
149
153
  engine.setAgentViewManager(agentViewManager)
150
154
  engine.setSkillsLoader(skillsLoader)
151
155
 
156
+ // Wire inference hooks (DLP) configuration
157
+ const inferenceHookConfig = loadInferenceHookConfig()
158
+ engine.setInferenceHookConfig(inferenceHookConfig)
159
+
152
160
  // Sync engine permission with config (fix: UI shows "auto" but engine defaulted to bypass-legacy)
153
161
  if (config.permission) {
154
162
  engine.getPermission().setDefaultLevel(config.permission as PermissionLevel)
package/src/mcp/client.ts CHANGED
@@ -151,6 +151,15 @@ export class McpClient {
151
151
  return this.connections.get(name)?.tools || []
152
152
  }
153
153
 
154
+ /** List all currently connected MCP server names. */
155
+ getConnectedServers(): string[] {
156
+ const names: string[] = []
157
+ for (const [name, conn] of this.connections) {
158
+ if (conn.status === 'connected') names.push(name)
159
+ }
160
+ return names
161
+ }
162
+
154
163
  async callTool(
155
164
  serverName: string,
156
165
  toolName: string,