opencode-toolbox 0.5.1 → 0.6.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 (3) hide show
  1. package/README.md +36 -1
  2. package/dist/index.js +494 -94
  3. package/package.json +6 -2
package/README.md CHANGED
@@ -63,7 +63,7 @@ Create `~/.config/opencode/toolbox.jsonc`:
63
63
 
64
64
  ## Usage
65
65
 
66
- The plugin exposes four tools:
66
+ The plugin exposes five tools:
67
67
 
68
68
  ### toolbox_search_bm25
69
69
 
@@ -215,6 +215,41 @@ The plugin automatically creates a `/toolbox-status` slash command on first laun
215
215
 
216
216
  Use it in OpenCode by typing `/toolbox-status` to get a formatted status report.
217
217
 
218
+ ### toolbox_perf
219
+
220
+ Get detailed performance metrics for the toolbox plugin:
221
+
222
+ ```
223
+ toolbox_perf({})
224
+ ```
225
+
226
+ Returns performance data including initialization times, search latencies, and execution stats:
227
+
228
+ ```json
229
+ {
230
+ "init": {
231
+ "duration": 1234.56,
232
+ "serverCount": 6,
233
+ "toolCount": 42
234
+ },
235
+ "timers": {
236
+ "search.bm25": { "count": 15, "total": 45.2, "avg": 3.01, "min": 1.2, "max": 8.5 },
237
+ "search.regex": { "count": 5, "total": 12.1, "avg": 2.42, "min": 1.1, "max": 4.2 },
238
+ "tool.execute": { "count": 10, "total": 892.3, "avg": 89.23, "min": 12.5, "max": 245.8 }
239
+ },
240
+ "indexStats": {
241
+ "documentCount": 42,
242
+ "avgDocLength": 15.3
243
+ },
244
+ "config": {
245
+ "initMode": "eager",
246
+ "connectionTimeout": 5000,
247
+ "requestTimeout": 30000,
248
+ "retryAttempts": 2
249
+ }
250
+ }
251
+ ```
252
+
218
253
  ## Search Modes
219
254
 
220
255
  ### BM25 (Natural Language)
package/dist/index.js CHANGED
@@ -32816,8 +32816,16 @@ var ServerConfigSchema = exports_external2.discriminatedUnion("type", [
32816
32816
  LocalServerConfigSchema,
32817
32817
  RemoteServerConfigSchema
32818
32818
  ]);
32819
+ var ConnectionConfigSchema = exports_external2.object({
32820
+ connectTimeout: exports_external2.number().min(100).max(60000).default(5000),
32821
+ requestTimeout: exports_external2.number().min(100).max(300000).default(30000),
32822
+ retryAttempts: exports_external2.number().min(0).max(10).default(2),
32823
+ retryDelay: exports_external2.number().min(0).max(30000).default(1000)
32824
+ });
32819
32825
  var SettingsConfigSchema = exports_external2.object({
32820
- defaultLimit: exports_external2.number().min(1).max(20).default(5)
32826
+ defaultLimit: exports_external2.number().min(1).max(20).default(5),
32827
+ initMode: exports_external2.enum(["eager", "lazy"]).default("eager"),
32828
+ connection: ConnectionConfigSchema.optional()
32821
32829
  });
32822
32830
  var ConfigSchema = exports_external2.object({
32823
32831
  mcp: exports_external2.record(exports_external2.string(), ServerConfigSchema),
@@ -37627,6 +37635,9 @@ class RemoteMCPClient {
37627
37635
  return this.toolsCache;
37628
37636
  }
37629
37637
  }
37638
+ // src/mcp-client/manager.ts
37639
+ import { EventEmitter } from "events";
37640
+
37630
37641
  // src/catalog/catalog.ts
37631
37642
  function normalizeTool(serverName, tool3) {
37632
37643
  const id = {
@@ -37674,15 +37685,178 @@ function buildSearchableText(serverName, tool3, args) {
37674
37685
  function normalizeTools(serverName, tools) {
37675
37686
  return tools.map((tool3) => normalizeTool(serverName, tool3));
37676
37687
  }
37688
+ // src/profiler/profiler.ts
37689
+ function percentile(sorted, p) {
37690
+ if (sorted.length === 0)
37691
+ return 0;
37692
+ const index = Math.ceil(p / 100 * sorted.length) - 1;
37693
+ return sorted[Math.max(0, index)];
37694
+ }
37695
+ function calculateStats(measurements) {
37696
+ if (measurements.length === 0)
37697
+ return null;
37698
+ const sorted = [...measurements].sort((a, b) => a - b);
37699
+ const total = sorted.reduce((sum, v) => sum + v, 0);
37700
+ return {
37701
+ count: sorted.length,
37702
+ min: sorted[0],
37703
+ max: sorted[sorted.length - 1],
37704
+ avg: total / sorted.length,
37705
+ p50: percentile(sorted, 50),
37706
+ p95: percentile(sorted, 95),
37707
+ p99: percentile(sorted, 99),
37708
+ total
37709
+ };
37710
+ }
37711
+
37712
+ class Profiler {
37713
+ marks = new Map;
37714
+ measures = new Map;
37715
+ serverMetrics = new Map;
37716
+ initStartTime = null;
37717
+ initEndTime = null;
37718
+ initState = "idle";
37719
+ indexBuildTime = null;
37720
+ toolCount = 0;
37721
+ incrementalUpdates = 0;
37722
+ startTime = performance.now();
37723
+ mark(name) {
37724
+ this.marks.set(name, performance.now());
37725
+ }
37726
+ measure(name, startMark) {
37727
+ const start = this.marks.get(startMark);
37728
+ if (start === undefined) {
37729
+ return -1;
37730
+ }
37731
+ const duration5 = performance.now() - start;
37732
+ const existing = this.measures.get(name) || [];
37733
+ existing.push(duration5);
37734
+ this.measures.set(name, existing);
37735
+ return duration5;
37736
+ }
37737
+ record(name, duration5) {
37738
+ const existing = this.measures.get(name) || [];
37739
+ existing.push(duration5);
37740
+ this.measures.set(name, existing);
37741
+ }
37742
+ getStats(name) {
37743
+ const measurements = this.measures.get(name);
37744
+ if (!measurements)
37745
+ return null;
37746
+ return calculateStats(measurements);
37747
+ }
37748
+ initStart() {
37749
+ this.initStartTime = performance.now();
37750
+ this.initState = "initializing";
37751
+ }
37752
+ initComplete(state) {
37753
+ this.initEndTime = performance.now();
37754
+ this.initState = state;
37755
+ }
37756
+ recordServerConnect(name, connectTime, toolCount, status, error92) {
37757
+ this.serverMetrics.set(name, {
37758
+ name,
37759
+ connectTime,
37760
+ toolCount,
37761
+ status,
37762
+ error: error92
37763
+ });
37764
+ }
37765
+ recordIndexBuild(duration5, toolCount) {
37766
+ this.indexBuildTime = duration5;
37767
+ this.toolCount = toolCount;
37768
+ }
37769
+ recordIncrementalUpdate(toolCount) {
37770
+ this.incrementalUpdates++;
37771
+ this.toolCount += toolCount;
37772
+ }
37773
+ getInitState() {
37774
+ return this.initState;
37775
+ }
37776
+ getInitDuration() {
37777
+ if (this.initStartTime === null)
37778
+ return null;
37779
+ if (this.initEndTime !== null) {
37780
+ return this.initEndTime - this.initStartTime;
37781
+ }
37782
+ return performance.now() - this.initStartTime;
37783
+ }
37784
+ export() {
37785
+ return {
37786
+ timestamp: new Date().toISOString(),
37787
+ uptime: performance.now() - this.startTime,
37788
+ initialization: {
37789
+ startTime: this.initStartTime || 0,
37790
+ endTime: this.initEndTime,
37791
+ duration: this.getInitDuration(),
37792
+ state: this.initState,
37793
+ servers: Array.from(this.serverMetrics.values())
37794
+ },
37795
+ indexing: {
37796
+ buildTime: this.indexBuildTime,
37797
+ toolCount: this.toolCount,
37798
+ incrementalUpdates: this.incrementalUpdates
37799
+ },
37800
+ searches: {
37801
+ bm25: this.getStats("search.bm25"),
37802
+ regex: this.getStats("search.regex")
37803
+ },
37804
+ executions: this.getStats("tool.execute")
37805
+ };
37806
+ }
37807
+ reset() {
37808
+ this.marks.clear();
37809
+ this.measures.clear();
37810
+ this.serverMetrics.clear();
37811
+ this.initStartTime = null;
37812
+ this.initEndTime = null;
37813
+ this.initState = "idle";
37814
+ this.indexBuildTime = null;
37815
+ this.toolCount = 0;
37816
+ this.incrementalUpdates = 0;
37817
+ }
37818
+ startTimer(name) {
37819
+ const start = performance.now();
37820
+ return () => {
37821
+ const duration5 = performance.now() - start;
37822
+ this.record(name, duration5);
37823
+ return duration5;
37824
+ };
37825
+ }
37826
+ }
37827
+ var globalProfiler = new Profiler;
37677
37828
  // src/mcp-client/manager.ts
37678
- class MCPManager {
37829
+ var DEFAULT_CONNECTION_CONFIG = {
37830
+ connectTimeout: 5000,
37831
+ requestTimeout: 30000,
37832
+ retryAttempts: 2,
37833
+ retryDelay: 1000
37834
+ };
37835
+ function sleep(ms) {
37836
+ return new Promise((resolve) => setTimeout(resolve, ms));
37837
+ }
37838
+ function withTimeout(promise4, ms, errorMessage) {
37839
+ return Promise.race([
37840
+ promise4,
37841
+ new Promise((_, reject) => setTimeout(() => reject(new Error(errorMessage)), ms))
37842
+ ]);
37843
+ }
37844
+
37845
+ class MCPManager extends EventEmitter {
37679
37846
  servers;
37680
37847
  clients;
37681
37848
  clientFactory;
37849
+ connectionConfig;
37850
+ initState = "idle";
37851
+ initPromise = null;
37852
+ serversPending = 0;
37853
+ serversCompleted = 0;
37682
37854
  constructor(options) {
37855
+ super();
37683
37856
  this.servers = new Map;
37684
37857
  this.clients = new Map;
37685
37858
  this.clientFactory = options?.clientFactory || this.defaultClientFactory.bind(this);
37859
+ this.connectionConfig = { ...DEFAULT_CONNECTION_CONFIG, ...options?.connectionConfig };
37686
37860
  }
37687
37861
  defaultClientFactory(name, config3) {
37688
37862
  if (config3.type === "local") {
@@ -37694,40 +37868,135 @@ class MCPManager {
37694
37868
  }
37695
37869
  }
37696
37870
  async initialize(servers) {
37871
+ if (this.initState !== "idle") {
37872
+ return this.initPromise || Promise.resolve();
37873
+ }
37874
+ globalProfiler.initStart();
37875
+ this.initState = "initializing";
37876
+ this.serversPending = Object.keys(servers).length;
37877
+ this.serversCompleted = 0;
37697
37878
  const promises = Object.entries(servers).map(async ([name, config3]) => {
37698
- return this.connectServer(name, config3);
37879
+ return this.connectServerWithRetry(name, config3);
37699
37880
  });
37700
- await Promise.all(promises);
37881
+ this.initPromise = Promise.all(promises).then(() => {
37882
+ this.finalizeInit();
37883
+ });
37884
+ return this.initPromise;
37885
+ }
37886
+ initializeBackground(servers) {
37887
+ this.initialize(servers).catch(() => {});
37888
+ }
37889
+ finalizeInit() {
37890
+ const allServers = Array.from(this.servers.values());
37891
+ const connected = allServers.filter((s) => s.status === "connected");
37892
+ const failed = allServers.filter((s) => s.status === "error");
37893
+ if (connected.length === allServers.length) {
37894
+ this.initState = "ready";
37895
+ } else if (connected.length > 0) {
37896
+ this.initState = "degraded";
37897
+ } else {
37898
+ this.initState = "degraded";
37899
+ }
37900
+ globalProfiler.initComplete(this.initState);
37901
+ this.emit("init:complete", this.initState);
37902
+ }
37903
+ async connectServerWithRetry(name, config3) {
37904
+ const maxAttempts = this.connectionConfig.retryAttempts + 1;
37905
+ let lastError = null;
37906
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
37907
+ try {
37908
+ await this.connectServer(name, config3);
37909
+ return;
37910
+ } catch (error92) {
37911
+ lastError = error92 instanceof Error ? error92 : new Error(String(error92));
37912
+ if (attempt < maxAttempts) {
37913
+ await sleep(this.connectionConfig.retryDelay);
37914
+ }
37915
+ }
37916
+ }
37917
+ this.servers.set(name, {
37918
+ name,
37919
+ config: config3,
37920
+ tools: [],
37921
+ status: "error",
37922
+ error: lastError?.message || "Connection failed after retries"
37923
+ });
37924
+ globalProfiler.recordServerConnect(name, -1, 0, "error", lastError?.message);
37925
+ this.emit("server:error", name, lastError?.message || "Unknown error");
37926
+ this.checkPartialReady();
37701
37927
  }
37702
37928
  async connectServer(name, config3) {
37929
+ const startTime = performance.now();
37703
37930
  this.servers.set(name, {
37704
37931
  name,
37705
37932
  config: config3,
37706
37933
  tools: [],
37707
37934
  status: "connecting"
37708
37935
  });
37709
- try {
37710
- const client = this.clientFactory(name, config3);
37711
- await client.connect();
37712
- const tools = await client.listTools();
37713
- const catalogTools = normalizeTools(name, tools);
37714
- this.servers.set(name, {
37715
- name,
37716
- config: config3,
37717
- tools: catalogTools,
37718
- status: "connected"
37719
- });
37720
- this.clients.set(name, client);
37721
- } catch (error92) {
37722
- this.servers.set(name, {
37723
- name,
37724
- config: config3,
37725
- tools: [],
37726
- status: "error",
37727
- error: error92 instanceof Error ? error92.message : String(error92)
37728
- });
37936
+ const client = this.clientFactory(name, config3);
37937
+ await withTimeout(client.connect(), this.connectionConfig.connectTimeout, `Connection to ${name} timed out after ${this.connectionConfig.connectTimeout}ms`);
37938
+ const tools = await withTimeout(client.listTools(), this.connectionConfig.requestTimeout, `Listing tools from ${name} timed out after ${this.connectionConfig.requestTimeout}ms`);
37939
+ const catalogTools = normalizeTools(name, tools);
37940
+ const connectTime = performance.now() - startTime;
37941
+ this.servers.set(name, {
37942
+ name,
37943
+ config: config3,
37944
+ tools: catalogTools,
37945
+ status: "connected"
37946
+ });
37947
+ this.clients.set(name, client);
37948
+ globalProfiler.recordServerConnect(name, connectTime, catalogTools.length, "connected");
37949
+ this.emit("server:connected", name, catalogTools);
37950
+ this.checkPartialReady();
37951
+ }
37952
+ checkPartialReady() {
37953
+ this.serversCompleted++;
37954
+ if (this.initState === "initializing") {
37955
+ const connected = Array.from(this.servers.values()).filter((s) => s.status === "connected");
37956
+ if (connected.length === 1 && this.serversCompleted < this.serversPending) {
37957
+ this.initState = "partial";
37958
+ this.emit("init:partial");
37959
+ }
37729
37960
  }
37730
37961
  }
37962
+ isReady() {
37963
+ return this.initState === "ready" || this.initState === "partial" || this.initState === "degraded";
37964
+ }
37965
+ isComplete() {
37966
+ return this.initState === "ready" || this.initState === "degraded";
37967
+ }
37968
+ getInitState() {
37969
+ return this.initState;
37970
+ }
37971
+ async waitForReady() {
37972
+ if (this.isComplete()) {
37973
+ return this.initState;
37974
+ }
37975
+ if (this.initPromise) {
37976
+ await this.initPromise;
37977
+ return this.initState;
37978
+ }
37979
+ return this.initState;
37980
+ }
37981
+ waitForPartial() {
37982
+ if (this.isReady()) {
37983
+ return Promise.resolve();
37984
+ }
37985
+ return new Promise((resolve) => {
37986
+ const onPartial = () => {
37987
+ this.off("init:partial", onPartial);
37988
+ this.off("init:complete", onComplete);
37989
+ resolve();
37990
+ };
37991
+ const onComplete = () => {
37992
+ this.off("init:partial", onPartial);
37993
+ this.off("init:complete", onComplete);
37994
+ resolve();
37995
+ };
37996
+ this.on("init:partial", onPartial);
37997
+ this.on("init:complete", onComplete);
37998
+ });
37999
+ }
37731
38000
  getAllTools() {
37732
38001
  const allTools = [];
37733
38002
  for (const [name, server] of this.servers) {
@@ -37751,7 +38020,7 @@ class MCPManager {
37751
38020
  if (!client) {
37752
38021
  throw new Error(`MCP client not found for server: ${serverName}`);
37753
38022
  }
37754
- return client.callTool(toolName, args);
38023
+ return withTimeout(client.callTool(toolName, args), this.connectionConfig.requestTimeout, `Tool execution timed out after ${this.connectionConfig.requestTimeout}ms`);
37755
38024
  }
37756
38025
  getServer(name) {
37757
38026
  return this.servers.get(name);
@@ -37764,12 +38033,17 @@ class MCPManager {
37764
38033
  await Promise.all(promises);
37765
38034
  this.servers.clear();
37766
38035
  this.clients.clear();
38036
+ this.initState = "idle";
38037
+ this.initPromise = null;
37767
38038
  }
37768
38039
  }
37769
38040
  // src/search/bm25.ts
37770
38041
  function tokenize(text) {
37771
38042
  return text.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((token) => token.length > 0);
37772
38043
  }
38044
+ function yieldToEventLoop() {
38045
+ return new Promise((resolve) => setImmediate(resolve));
38046
+ }
37773
38047
 
37774
38048
  class BM25Index {
37775
38049
  documents;
@@ -37777,6 +38051,7 @@ class BM25Index {
37777
38051
  docLengths;
37778
38052
  avgDocLength = 0;
37779
38053
  totalDocs = 0;
38054
+ totalTokens = 0;
37780
38055
  k1 = 1.2;
37781
38056
  b = 0.75;
37782
38057
  constructor() {
@@ -37785,20 +38060,82 @@ class BM25Index {
37785
38060
  this.docLengths = new Map;
37786
38061
  }
37787
38062
  indexTools(tools) {
37788
- this.documents.clear();
37789
- this.docFreqs.clear();
37790
- this.docLengths.clear();
38063
+ this.clear();
38064
+ this.addToolsBatch(tools);
38065
+ }
38066
+ async indexToolsAsync(tools, chunkSize = 50) {
38067
+ this.clear();
38068
+ await this.addToolsAsync(tools, chunkSize);
38069
+ }
38070
+ addToolsBatch(tools) {
37791
38071
  for (const tool3 of tools) {
37792
- const tokens = tokenize(tool3.searchableText);
37793
- this.documents.set(tool3.idString, { tokens, tool: tool3 });
37794
- this.docLengths.set(tool3.idString, tokens.length);
37795
- const uniqueTokens = new Set(tokens);
37796
- for (const token of uniqueTokens) {
37797
- this.docFreqs.set(token, (this.docFreqs.get(token) || 0) + 1);
38072
+ this.addToolInternal(tool3);
38073
+ }
38074
+ this.recalculateAvgDocLength();
38075
+ }
38076
+ async addToolsAsync(tools, chunkSize = 50) {
38077
+ for (let i = 0;i < tools.length; i += chunkSize) {
38078
+ const chunk = tools.slice(i, i + chunkSize);
38079
+ for (const tool3 of chunk) {
38080
+ this.addToolInternal(tool3);
38081
+ }
38082
+ if (i + chunkSize < tools.length) {
38083
+ await yieldToEventLoop();
37798
38084
  }
37799
38085
  }
37800
- this.totalDocs = this.documents.size;
37801
- this.avgDocLength = Array.from(this.docLengths.values()).reduce((sum, len) => sum + len, 0) / this.totalDocs;
38086
+ this.recalculateAvgDocLength();
38087
+ }
38088
+ addTool(tool3) {
38089
+ this.addToolInternal(tool3);
38090
+ this.recalculateAvgDocLengthIncremental();
38091
+ }
38092
+ addToolInternal(tool3) {
38093
+ if (this.documents.has(tool3.idString)) {
38094
+ return;
38095
+ }
38096
+ const tokens = tokenize(tool3.searchableText);
38097
+ this.documents.set(tool3.idString, { tokens, tool: tool3 });
38098
+ this.docLengths.set(tool3.idString, tokens.length);
38099
+ this.totalTokens += tokens.length;
38100
+ this.totalDocs++;
38101
+ const uniqueTokens = new Set(tokens);
38102
+ for (const token of uniqueTokens) {
38103
+ this.docFreqs.set(token, (this.docFreqs.get(token) || 0) + 1);
38104
+ }
38105
+ }
38106
+ removeTool(idString) {
38107
+ const doc3 = this.documents.get(idString);
38108
+ if (!doc3)
38109
+ return false;
38110
+ const uniqueTokens = new Set(doc3.tokens);
38111
+ for (const token of uniqueTokens) {
38112
+ const freq = this.docFreqs.get(token) || 0;
38113
+ if (freq <= 1) {
38114
+ this.docFreqs.delete(token);
38115
+ } else {
38116
+ this.docFreqs.set(token, freq - 1);
38117
+ }
38118
+ }
38119
+ this.totalTokens -= doc3.tokens.length;
38120
+ this.documents.delete(idString);
38121
+ this.docLengths.delete(idString);
38122
+ this.totalDocs--;
38123
+ this.recalculateAvgDocLengthIncremental();
38124
+ return true;
38125
+ }
38126
+ recalculateAvgDocLength() {
38127
+ if (this.totalDocs === 0) {
38128
+ this.avgDocLength = 0;
38129
+ return;
38130
+ }
38131
+ this.avgDocLength = this.totalTokens / this.totalDocs;
38132
+ }
38133
+ recalculateAvgDocLengthIncremental() {
38134
+ if (this.totalDocs === 0) {
38135
+ this.avgDocLength = 0;
38136
+ return;
38137
+ }
38138
+ this.avgDocLength = this.totalTokens / this.totalDocs;
37802
38139
  }
37803
38140
  search(query, limit = 5) {
37804
38141
  const queryTokens = tokenize(query);
@@ -37854,10 +38191,21 @@ class BM25Index {
37854
38191
  this.docLengths.clear();
37855
38192
  this.avgDocLength = 0;
37856
38193
  this.totalDocs = 0;
38194
+ this.totalTokens = 0;
37857
38195
  }
37858
38196
  get size() {
37859
38197
  return this.totalDocs;
37860
38198
  }
38199
+ has(idString) {
38200
+ return this.documents.has(idString);
38201
+ }
38202
+ getStats() {
38203
+ return {
38204
+ docCount: this.totalDocs,
38205
+ termCount: this.docFreqs.size,
38206
+ avgDocLength: this.avgDocLength
38207
+ };
38208
+ }
37861
38209
  }
37862
38210
  // src/search/regex.ts
37863
38211
  var MAX_REGEX_LENGTH = 200;
@@ -37964,7 +38312,13 @@ Pass arguments as JSON string matching the tool's schema.`;
37964
38312
  var STATUS_DESC = `Get toolbox status including plugin initialization, MCP server connections, and tool counts.
37965
38313
 
37966
38314
  Shows success/total metrics to highlight failures. Use to check if toolbox is working correctly.`;
38315
+ var PERF_DESC = `Get detailed performance metrics for the toolbox plugin.
38316
+
38317
+ Shows initialization times, search latencies, execution stats, and per-server metrics.`;
38318
+ var isTestEnv = !!process.env.BUN_TEST;
37967
38319
  function log(level, message, extra) {
38320
+ if (isTestEnv)
38321
+ return;
37968
38322
  const timestamp = new Date().toISOString();
37969
38323
  const extraStr = extra ? ` ${JSON.stringify(extra)}` : "";
37970
38324
  const line = `${timestamp} [${level.toUpperCase()}] ${message}${extraStr}
@@ -37972,6 +38326,8 @@ function log(level, message, extra) {
37972
38326
  mkdir(LOG_DIR, { recursive: true }).then(() => appendFile(LOG_FILE_PATH, line)).catch(() => {});
37973
38327
  }
37974
38328
  function ensureCommandFile() {
38329
+ if (isTestEnv)
38330
+ return;
37975
38331
  access(COMMAND_FILE_PATH).catch(() => {
37976
38332
  mkdir(COMMAND_DIR, { recursive: true }).then(() => writeFile(COMMAND_FILE_PATH, COMMAND_CONTENT)).then(() => log("info", "Created /toolbox-status command file")).catch(() => {});
37977
38333
  });
@@ -37980,10 +38336,10 @@ var SYSTEM_PROMPT_BASE = `# Extended Toolbox
37980
38336
 
37981
38337
  You have access to an extended toolbox with additional capabilities (web search, time utilities, code search, etc.).
37982
38338
 
37983
- ## Rule
37984
- ALWAYS search before saying "I cannot do that" or "I don't have access to."
37985
- DO NOT try to execute a tool without having the tool's exact tool schema. If you don't have the tool's schema
37986
- then run toolbox_search_* to get them.
38339
+ ## Rules
38340
+ 1. ALWAYS toolbox_search_* before saying "I cannot do that" or "I don't have access to."
38341
+ 2. ALWAYS toolbox_search_* if you think that user wants you to use some tools
38342
+ 3. ALWAYS toolbox_search_* if you think that user may refer specific tool name which is not exist in the context
37987
38343
 
37988
38344
  ## Workflow
37989
38345
  1. Search: toolbox_search_bm25({ text: "what you need" }) or toolbox_search_regex({ pattern: "prefix_.*" })
@@ -38004,13 +38360,12 @@ function generateSystemPrompt(mcpManager) {
38004
38360
  }
38005
38361
  return `${SYSTEM_PROMPT_BASE}
38006
38362
 
38007
- ## Toolbox Schema
38008
- Tool names use \`<server>_<tool>\` format. Pass exact names to toolbox_execute().
38009
- \`\`\`json
38010
- ${JSON.stringify(toolboxSchema, null, 2)}
38011
- \`\`\``;
38363
+ ## Registered MCP Servers
38364
+ ${Object.entries(toolboxSchema).map(([server, tools]) => `- ${server}: ${tools.map((t) => t.split("_").slice(1).join("_")).join(", ")}`).join(`
38365
+ `)}`;
38012
38366
  }
38013
38367
  var ToolboxPlugin = async (ctx) => {
38368
+ const pluginLoadStart = performance.now();
38014
38369
  const { client } = ctx;
38015
38370
  const configPath = process.env.OPENCODE_TOOLBOX_CONFIG || DEFAULT_CONFIG_PATH;
38016
38371
  const configResult = await loadConfig(configPath);
@@ -38020,53 +38375,64 @@ var ToolboxPlugin = async (ctx) => {
38020
38375
  return {};
38021
38376
  }
38022
38377
  const config3 = configResult.data;
38023
- const mcpManager = new MCPManager;
38378
+ const initMode = config3.settings?.initMode || "eager";
38379
+ const connectionConfig = {
38380
+ connectTimeout: config3.settings?.connection?.connectTimeout || 5000,
38381
+ requestTimeout: config3.settings?.connection?.requestTimeout || 30000,
38382
+ retryAttempts: config3.settings?.connection?.retryAttempts || 2,
38383
+ retryDelay: config3.settings?.connection?.retryDelay || 1000
38384
+ };
38385
+ const mcpManager = new MCPManager({ connectionConfig });
38024
38386
  const bm25Index = new BM25Index;
38025
- let initialized = false;
38026
38387
  let searchCount = 0;
38027
38388
  let executionCount = 0;
38028
38389
  let executionSuccessCount = 0;
38029
38390
  ensureCommandFile();
38030
38391
  const serverNames = Object.keys(config3.mcp);
38392
+ const pluginLoadDuration = performance.now() - pluginLoadStart;
38031
38393
  log("info", `Toolbox plugin loaded successfully`, {
38032
38394
  configPath,
38395
+ logPath: LOG_FILE_PATH,
38033
38396
  serverCount: serverNames.length,
38034
- servers: serverNames
38397
+ servers: serverNames,
38398
+ initMode,
38399
+ loadDurationMs: Math.round(pluginLoadDuration * 100) / 100
38400
+ });
38401
+ mcpManager.on("server:connected", (serverName, tools) => {
38402
+ const startTime = performance.now();
38403
+ bm25Index.addToolsBatch(tools);
38404
+ const indexTime = performance.now() - startTime;
38405
+ globalProfiler.recordIncrementalUpdate(tools.length);
38406
+ log("info", `Server ${serverName} connected, indexed ${tools.length} tools in ${indexTime.toFixed(2)}ms`);
38407
+ });
38408
+ mcpManager.on("server:error", (serverName, error92) => {
38409
+ log("warn", `Server ${serverName} failed: ${error92}`);
38410
+ });
38411
+ mcpManager.on("init:complete", (state) => {
38412
+ const duration5 = globalProfiler.getInitDuration();
38413
+ const servers = mcpManager.getAllServers();
38414
+ const connectedServers = servers.filter((s) => s.status === "connected");
38415
+ log("info", `Initialization complete in ${duration5?.toFixed(2)}ms: ${connectedServers.length}/${servers.length} servers, ${bm25Index.size} tools indexed`, {
38416
+ state,
38417
+ totalServers: servers.length,
38418
+ connectedServers: connectedServers.length,
38419
+ totalTools: bm25Index.size
38420
+ });
38035
38421
  });
38422
+ if (initMode === "eager") {
38423
+ mcpManager.initializeBackground(config3.mcp);
38424
+ log("info", "Started eager background initialization");
38425
+ }
38036
38426
  async function ensureInitialized() {
38037
- if (initialized)
38427
+ if (mcpManager.isReady()) {
38038
38428
  return;
38039
- try {
38040
- log("info", "Initializing MCP servers...");
38429
+ }
38430
+ if (initMode === "lazy" && mcpManager.getInitState() === "idle") {
38431
+ log("info", "Starting lazy initialization on first use");
38041
38432
  await mcpManager.initialize(config3.mcp);
38042
- const allTools = mcpManager.getAllCatalogTools();
38043
- bm25Index.indexTools(allTools);
38044
- initialized = true;
38045
- const servers = mcpManager.getAllServers();
38046
- const connectedServers = servers.filter((s) => s.status === "connected");
38047
- const failedServers = servers.filter((s) => s.status === "error");
38048
- const initMsg = `Initialization complete: ${connectedServers.length}/${servers.length} servers connected, ${allTools.length} tools indexed`;
38049
- log("info", initMsg, {
38050
- totalServers: servers.length,
38051
- connectedServers: connectedServers.length,
38052
- failedServers: failedServers.length,
38053
- totalTools: allTools.length,
38054
- servers: servers.map((s) => ({
38055
- name: s.name,
38056
- status: s.status,
38057
- toolCount: s.tools.length,
38058
- error: s.error || null
38059
- }))
38060
- });
38061
- if (failedServers.length > 0) {
38062
- const warnMsg = `${failedServers.length} server(s) failed to connect: ${failedServers.map((s) => s.name).join(", ")}`;
38063
- log("warn", warnMsg);
38064
- }
38065
- } catch (error92) {
38066
- const errorMsg = `Failed to initialize MCP servers: ${error92 instanceof Error ? error92.message : String(error92)}`;
38067
- log("error", errorMsg);
38068
- throw error92;
38433
+ return;
38069
38434
  }
38435
+ await mcpManager.waitForPartial();
38070
38436
  }
38071
38437
  return {
38072
38438
  tool: {
@@ -38077,9 +38443,11 @@ var ToolboxPlugin = async (ctx) => {
38077
38443
  limit: tool.schema.number().optional().describe("Maximum number of results to return (default: 5)")
38078
38444
  },
38079
38445
  async execute(args) {
38446
+ const timer = globalProfiler.startTimer("search.bm25");
38080
38447
  try {
38081
38448
  await ensureInitialized();
38082
38449
  } catch (error92) {
38450
+ timer();
38083
38451
  return JSON.stringify({
38084
38452
  success: false,
38085
38453
  error: `Failed to initialize: ${error92 instanceof Error ? error92.message : String(error92)}`
@@ -38089,11 +38457,13 @@ var ToolboxPlugin = async (ctx) => {
38089
38457
  const searchLimit = args.limit || config3.settings?.defaultLimit || 5;
38090
38458
  const allTools = mcpManager.getAllCatalogTools();
38091
38459
  const results = bm25Index.search(args.text, searchLimit);
38092
- log("info", `BM25 search completed: "${args.text}" -> ${results.length} results`, {
38460
+ const duration5 = timer();
38461
+ log("info", `BM25 search completed: "${args.text}" -> ${results.length} results in ${duration5.toFixed(2)}ms`, {
38093
38462
  searchType: "bm25",
38094
38463
  query: args.text,
38095
38464
  resultsCount: results.length,
38096
- limit: searchLimit
38465
+ limit: searchLimit,
38466
+ durationMs: duration5
38097
38467
  });
38098
38468
  return formatSearchResults(results, allTools);
38099
38469
  }
@@ -38105,9 +38475,11 @@ var ToolboxPlugin = async (ctx) => {
38105
38475
  limit: tool.schema.number().optional().describe("Maximum number of results to return (default: 5)")
38106
38476
  },
38107
38477
  async execute(args) {
38478
+ const timer = globalProfiler.startTimer("search.regex");
38108
38479
  try {
38109
38480
  await ensureInitialized();
38110
38481
  } catch (error92) {
38482
+ timer();
38111
38483
  return JSON.stringify({
38112
38484
  success: false,
38113
38485
  error: `Failed to initialize: ${error92 instanceof Error ? error92.message : String(error92)}`
@@ -38117,6 +38489,7 @@ var ToolboxPlugin = async (ctx) => {
38117
38489
  const searchLimit = args.limit || config3.settings?.defaultLimit || 5;
38118
38490
  const allTools = mcpManager.getAllCatalogTools();
38119
38491
  const result = searchWithRegex(allTools, args.pattern, searchLimit);
38492
+ const duration5 = timer();
38120
38493
  if ("error" in result) {
38121
38494
  log("warn", `Regex search failed: "${args.pattern}" -> ${result.error}`);
38122
38495
  return JSON.stringify({
@@ -38124,11 +38497,12 @@ var ToolboxPlugin = async (ctx) => {
38124
38497
  error: result.error
38125
38498
  });
38126
38499
  }
38127
- log("info", `Regex search completed: "${args.pattern}" -> ${result.length} results`, {
38500
+ log("info", `Regex search completed: "${args.pattern}" -> ${result.length} results in ${duration5.toFixed(2)}ms`, {
38128
38501
  searchType: "regex",
38129
38502
  pattern: args.pattern,
38130
38503
  resultsCount: result.length,
38131
- limit: searchLimit
38504
+ limit: searchLimit,
38505
+ durationMs: duration5
38132
38506
  });
38133
38507
  return formatSearchResults(result, allTools);
38134
38508
  }
@@ -38140,9 +38514,11 @@ var ToolboxPlugin = async (ctx) => {
38140
38514
  arguments: tool.schema.string().optional().describe("JSON-encoded arguments for the tool, matching its schema. Use '{}' or omit for tools with no required arguments.")
38141
38515
  },
38142
38516
  async execute(args) {
38517
+ const timer = globalProfiler.startTimer("tool.execute");
38143
38518
  try {
38144
38519
  await ensureInitialized();
38145
38520
  } catch (error92) {
38521
+ timer();
38146
38522
  return JSON.stringify({
38147
38523
  success: false,
38148
38524
  error: `Failed to initialize: ${error92 instanceof Error ? error92.message : String(error92)}`
@@ -38150,6 +38526,7 @@ var ToolboxPlugin = async (ctx) => {
38150
38526
  }
38151
38527
  const parsed = parseToolName(args.name);
38152
38528
  if (!parsed) {
38529
+ timer();
38153
38530
  log("warn", `Invalid tool name format: ${args.name}`, {
38154
38531
  toolName: args.name
38155
38532
  });
@@ -38163,6 +38540,7 @@ var ToolboxPlugin = async (ctx) => {
38163
38540
  try {
38164
38541
  toolArgs = JSON.parse(args.arguments);
38165
38542
  } catch (error92) {
38543
+ timer();
38166
38544
  log("warn", `Failed to parse arguments as JSON for ${args.name}`, {
38167
38545
  toolName: args.name,
38168
38546
  arguments: args.arguments
@@ -38175,11 +38553,10 @@ var ToolboxPlugin = async (ctx) => {
38175
38553
  }
38176
38554
  executionCount++;
38177
38555
  try {
38178
- const startTime = Date.now();
38179
38556
  const result = await mcpManager.callTool(parsed.serverName, parsed.toolName, toolArgs);
38180
- const duration5 = Date.now() - startTime;
38557
+ const duration5 = timer();
38181
38558
  executionSuccessCount++;
38182
- log("info", `Tool executed successfully: ${args.name}`, {
38559
+ log("info", `Tool executed successfully: ${args.name} in ${duration5.toFixed(2)}ms`, {
38183
38560
  server: parsed.serverName,
38184
38561
  tool: parsed.toolName,
38185
38562
  durationMs: duration5
@@ -38189,11 +38566,13 @@ var ToolboxPlugin = async (ctx) => {
38189
38566
  result
38190
38567
  });
38191
38568
  } catch (error92) {
38569
+ const duration5 = timer();
38192
38570
  const errorMsg = `Tool execution failed: ${error92 instanceof Error ? error92.message : String(error92)}`;
38193
38571
  log("error", errorMsg, {
38194
38572
  server: parsed.serverName,
38195
38573
  tool: parsed.toolName,
38196
- error: errorMsg
38574
+ error: errorMsg,
38575
+ durationMs: duration5
38197
38576
  });
38198
38577
  return JSON.stringify({
38199
38578
  success: false,
@@ -38206,7 +38585,7 @@ var ToolboxPlugin = async (ctx) => {
38206
38585
  description: STATUS_DESC,
38207
38586
  args: {},
38208
38587
  async execute() {
38209
- if (!initialized) {
38588
+ if (!mcpManager.isReady()) {
38210
38589
  try {
38211
38590
  await ensureInitialized();
38212
38591
  } catch (error92) {
@@ -38222,9 +38601,13 @@ var ToolboxPlugin = async (ctx) => {
38222
38601
  const failedServers = servers.filter((s) => s.status === "error");
38223
38602
  const connectingServers = servers.filter((s) => s.status === "connecting");
38224
38603
  const totalTools = mcpManager.getAllCatalogTools().length;
38604
+ const initDuration = globalProfiler.getInitDuration();
38225
38605
  const status = {
38226
38606
  plugin: {
38227
- initialized: true,
38607
+ initialized: mcpManager.isComplete(),
38608
+ initState: mcpManager.getInitState(),
38609
+ initMode,
38610
+ initDurationMs: initDuration ? Math.round(initDuration) : null,
38228
38611
  configPath,
38229
38612
  uptime: process.uptime(),
38230
38613
  searches: searchCount,
@@ -38248,7 +38631,7 @@ var ToolboxPlugin = async (ctx) => {
38248
38631
  },
38249
38632
  tools: {
38250
38633
  total: totalTools,
38251
- available: totalTools,
38634
+ indexed: bm25Index.size,
38252
38635
  serversWithTools: servers.filter((s) => s.tools.length > 0).length
38253
38636
  },
38254
38637
  health: {
@@ -38263,15 +38646,32 @@ var ToolboxPlugin = async (ctx) => {
38263
38646
  });
38264
38647
  return JSON.stringify(status, null, 2);
38265
38648
  }
38649
+ }),
38650
+ toolbox_perf: tool({
38651
+ description: PERF_DESC,
38652
+ args: {},
38653
+ async execute() {
38654
+ const report = globalProfiler.export();
38655
+ return JSON.stringify({
38656
+ ...report,
38657
+ indexStats: bm25Index.getStats(),
38658
+ config: {
38659
+ initMode,
38660
+ connectionTimeout: connectionConfig.connectTimeout,
38661
+ requestTimeout: connectionConfig.requestTimeout,
38662
+ retryAttempts: connectionConfig.retryAttempts
38663
+ }
38664
+ }, null, 2);
38665
+ }
38266
38666
  })
38267
38667
  },
38268
38668
  "experimental.chat.system.transform": async (_input, output) => {
38269
- try {
38270
- await ensureInitialized();
38271
- output.system.push(generateSystemPrompt(mcpManager));
38272
- } catch {
38273
- output.system.push(SYSTEM_PROMPT_BASE);
38669
+ if (!mcpManager.isReady() && initMode === "eager") {
38670
+ try {
38671
+ await mcpManager.waitForPartial();
38672
+ } catch {}
38274
38673
  }
38674
+ output.system.push(generateSystemPrompt(mcpManager));
38275
38675
  }
38276
38676
  };
38277
38677
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-toolbox",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Tool Search Tool Plugin for OpenCode - search and execute tools from MCP servers on-demand",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -16,7 +16,11 @@
16
16
  "dev": "bun --hot src/index.ts",
17
17
  "test": "bun test",
18
18
  "test:coverage": "bun test --coverage",
19
- "typecheck": "tsc --noEmit"
19
+ "typecheck": "tsc --noEmit",
20
+ "bench": "bun run bench:search && bun run bench:init && bun run bench:concurrent",
21
+ "bench:search": "bun run bench/search.bench.ts",
22
+ "bench:init": "bun run bench/init.bench.ts",
23
+ "bench:concurrent": "bun run bench/concurrent.bench.ts"
20
24
  },
21
25
  "files": [
22
26
  "dist",