@everworker/oneringai 0.1.4 → 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/dist/index.js CHANGED
@@ -2003,7 +2003,14 @@ var init_Connector = __esm({
2003
2003
  }
2004
2004
  const startTime = Date.now();
2005
2005
  this.requestCount++;
2006
- const url2 = endpoint.startsWith("http") ? endpoint : `${this.baseURL}${endpoint}`;
2006
+ let url2;
2007
+ if (endpoint.startsWith("http")) {
2008
+ url2 = endpoint;
2009
+ } else {
2010
+ const base = (this.baseURL ?? "").replace(/\/+$/, "");
2011
+ const path6 = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
2012
+ url2 = `${base}${path6}`;
2013
+ }
2007
2014
  const timeout = options?.timeout ?? this.config.timeout ?? DEFAULT_CONNECTOR_TIMEOUT;
2008
2015
  if (this.config.logging?.enabled) {
2009
2016
  this.logRequest(url2, options);
@@ -18406,6 +18413,9 @@ var BasePluginNextGen = class {
18406
18413
  }
18407
18414
  };
18408
18415
 
18416
+ // src/core/context-nextgen/AgentContextNextGen.ts
18417
+ init_Connector();
18418
+
18409
18419
  // src/domain/entities/Memory.ts
18410
18420
  function isTaskAwareScope(scope) {
18411
18421
  return typeof scope === "object" && scope !== null && "type" in scope;
@@ -19344,13 +19354,18 @@ Values are immediately visible - no retrieval needed.
19344
19354
  - \`high\`: Keep longer. Important state.
19345
19355
  - \`critical\`: Never auto-evicted.
19346
19356
 
19357
+ **UI Display:** Set \`showInUI: true\` in context_set to display the entry in the user's side panel.
19358
+ Values shown in the UI support the same rich markdown formatting as the chat window
19359
+ (see formatting instructions above). Use this for dashboards, progress displays, and results the user should see.
19360
+
19347
19361
  **Tools:** context_set, context_delete, context_list`;
19348
19362
  var contextSetDefinition = {
19349
19363
  type: "function",
19350
19364
  function: {
19351
19365
  name: "context_set",
19352
19366
  description: `Store or update a key-value pair in live context.
19353
- Value appears directly in context - no retrieval needed.`,
19367
+ Value appears directly in context - no retrieval needed.
19368
+ Set showInUI to true to also display the entry in the user's side panel.`,
19354
19369
  parameters: {
19355
19370
  type: "object",
19356
19371
  properties: {
@@ -19361,6 +19376,10 @@ Value appears directly in context - no retrieval needed.`,
19361
19376
  type: "string",
19362
19377
  enum: ["low", "normal", "high", "critical"],
19363
19378
  description: 'Eviction priority. Default: "normal"'
19379
+ },
19380
+ showInUI: {
19381
+ type: "boolean",
19382
+ description: "If true, display this entry in the user's side panel with full rich markdown rendering \u2014 same capabilities as the chat window (code blocks, tables, LaTeX, Mermaid diagrams, Vega-Lite charts, mindmaps, etc. \u2014 see formatting instructions in system prompt). Use this for dashboards, status displays, and structured results the user should see. Default: false"
19364
19383
  }
19365
19384
  },
19366
19385
  required: ["key", "description", "value"]
@@ -19401,6 +19420,7 @@ var InContextMemoryPluginNextGen = class {
19401
19420
  _destroyed = false;
19402
19421
  _tokenCache = null;
19403
19422
  _instructionsTokenCache = null;
19423
+ _notifyTimer = null;
19404
19424
  constructor(config = {}) {
19405
19425
  this.config = { ...DEFAULT_CONFIG, ...config };
19406
19426
  }
@@ -19441,15 +19461,18 @@ var InContextMemoryPluginNextGen = class {
19441
19461
  return a.updatedAt - b.updatedAt;
19442
19462
  });
19443
19463
  let freed = 0;
19464
+ let evicted = false;
19444
19465
  for (const entry of evictable) {
19445
19466
  if (freed >= targetTokensToFree) break;
19446
19467
  const entryTokens = this.estimator.estimateTokens(this.formatEntry(entry));
19447
19468
  this.entries.delete(entry.key);
19448
19469
  freed += entryTokens;
19470
+ evicted = true;
19449
19471
  }
19450
19472
  this._tokenCache = null;
19451
19473
  const content = await this.getContent();
19452
19474
  const after = content ? this.estimator.estimateTokens(content) : 0;
19475
+ if (evicted) this.notifyEntriesChanged();
19453
19476
  return Math.max(0, before - after);
19454
19477
  }
19455
19478
  getTools() {
@@ -19461,6 +19484,7 @@ var InContextMemoryPluginNextGen = class {
19461
19484
  }
19462
19485
  destroy() {
19463
19486
  if (this._destroyed) return;
19487
+ if (this._notifyTimer) clearTimeout(this._notifyTimer);
19464
19488
  this.entries.clear();
19465
19489
  this._destroyed = true;
19466
19490
  this._tokenCache = null;
@@ -19478,6 +19502,7 @@ var InContextMemoryPluginNextGen = class {
19478
19502
  this.entries.set(entry.key, entry);
19479
19503
  }
19480
19504
  this._tokenCache = null;
19505
+ this.notifyEntriesChanged();
19481
19506
  }
19482
19507
  // ============================================================================
19483
19508
  // Entry Management
@@ -19485,19 +19510,21 @@ var InContextMemoryPluginNextGen = class {
19485
19510
  /**
19486
19511
  * Store or update a key-value pair
19487
19512
  */
19488
- set(key, description, value, priority) {
19513
+ set(key, description, value, priority, showInUI) {
19489
19514
  this.assertNotDestroyed();
19490
19515
  const entry = {
19491
19516
  key,
19492
19517
  description,
19493
19518
  value,
19494
19519
  updatedAt: Date.now(),
19495
- priority: priority ?? this.config.defaultPriority
19520
+ priority: priority ?? this.config.defaultPriority,
19521
+ showInUI: showInUI ?? false
19496
19522
  };
19497
19523
  this.entries.set(key, entry);
19498
19524
  this.enforceMaxEntries();
19499
19525
  this.enforceTokenLimit();
19500
19526
  this._tokenCache = null;
19527
+ this.notifyEntriesChanged();
19501
19528
  }
19502
19529
  /**
19503
19530
  * Get a value by key
@@ -19519,7 +19546,10 @@ var InContextMemoryPluginNextGen = class {
19519
19546
  delete(key) {
19520
19547
  this.assertNotDestroyed();
19521
19548
  const deleted = this.entries.delete(key);
19522
- if (deleted) this._tokenCache = null;
19549
+ if (deleted) {
19550
+ this._tokenCache = null;
19551
+ this.notifyEntriesChanged();
19552
+ }
19523
19553
  return deleted;
19524
19554
  }
19525
19555
  /**
@@ -19531,7 +19561,8 @@ var InContextMemoryPluginNextGen = class {
19531
19561
  key: e.key,
19532
19562
  description: e.description,
19533
19563
  priority: e.priority,
19534
- updatedAt: e.updatedAt
19564
+ updatedAt: e.updatedAt,
19565
+ showInUI: e.showInUI ?? false
19535
19566
  }));
19536
19567
  }
19537
19568
  /**
@@ -19541,6 +19572,7 @@ var InContextMemoryPluginNextGen = class {
19541
19572
  this.assertNotDestroyed();
19542
19573
  this.entries.clear();
19543
19574
  this._tokenCache = null;
19575
+ this.notifyEntriesChanged();
19544
19576
  }
19545
19577
  // ============================================================================
19546
19578
  // Private Helpers
@@ -19604,6 +19636,20 @@ ${valueStr}
19604
19636
  return a.updatedAt - b.updatedAt;
19605
19637
  });
19606
19638
  }
19639
+ /**
19640
+ * Debounced notification when entries change.
19641
+ * Calls config.onEntriesChanged with all current entries.
19642
+ */
19643
+ notifyEntriesChanged() {
19644
+ if (!this.config.onEntriesChanged) return;
19645
+ if (this._notifyTimer) clearTimeout(this._notifyTimer);
19646
+ this._notifyTimer = setTimeout(() => {
19647
+ this._notifyTimer = null;
19648
+ if (!this._destroyed && this.config.onEntriesChanged) {
19649
+ this.config.onEntriesChanged(Array.from(this.entries.values()));
19650
+ }
19651
+ }, 100);
19652
+ }
19607
19653
  assertNotDestroyed() {
19608
19654
  if (this._destroyed) {
19609
19655
  throw new Error("InContextMemoryPluginNextGen is destroyed");
@@ -19620,16 +19666,18 @@ ${valueStr}
19620
19666
  args.key,
19621
19667
  args.description,
19622
19668
  args.value,
19623
- args.priority
19669
+ args.priority,
19670
+ args.showInUI
19624
19671
  );
19625
19672
  return {
19626
19673
  success: true,
19627
19674
  key: args.key,
19628
- message: `Stored "${args.key}" in live context`
19675
+ showInUI: args.showInUI ?? false,
19676
+ message: `Stored "${args.key}" in live context${args.showInUI ? " (visible in UI)" : ""}`
19629
19677
  };
19630
19678
  },
19631
19679
  permission: { scope: "always", riskLevel: "low" },
19632
- describeCall: (args) => `set ${args.key}`
19680
+ describeCall: (args) => `set ${args.key}${args.showInUI ? " [UI]" : ""}`
19633
19681
  };
19634
19682
  }
19635
19683
  createContextDeleteTool() {
@@ -20941,6 +20989,10 @@ var AgentContextNextGen = class _AgentContextNextGen extends EventEmitter {
20941
20989
  _sessionId = null;
20942
20990
  /** Agent ID */
20943
20991
  _agentId;
20992
+ /** User ID for multi-user scenarios */
20993
+ _userId;
20994
+ /** Allowed connector names (when agent is restricted to a subset) */
20995
+ _allowedConnectors;
20944
20996
  /** Storage backend */
20945
20997
  _storage;
20946
20998
  /** Destroyed flag */
@@ -20977,6 +21029,8 @@ var AgentContextNextGen = class _AgentContextNextGen extends EventEmitter {
20977
21029
  };
20978
21030
  this._systemPrompt = config.systemPrompt;
20979
21031
  this._agentId = this._config.agentId;
21032
+ this._userId = config.userId;
21033
+ this._allowedConnectors = config.connectors;
20980
21034
  this._storage = config.storage;
20981
21035
  this._compactionStrategy = config.compactionStrategy ?? StrategyRegistry.create(this._config.strategy);
20982
21036
  this._tools = new ToolManager(
@@ -20988,6 +21042,7 @@ var AgentContextNextGen = class _AgentContextNextGen extends EventEmitter {
20988
21042
  }
20989
21043
  }
20990
21044
  this.initializePlugins(config.plugins);
21045
+ this.syncToolContext();
20991
21046
  }
20992
21047
  /**
20993
21048
  * Initialize plugins based on feature flags.
@@ -21032,6 +21087,62 @@ var AgentContextNextGen = class _AgentContextNextGen extends EventEmitter {
21032
21087
  );
21033
21088
  }
21034
21089
  }
21090
+ /**
21091
+ * Sync identity fields and connector registry to ToolContext.
21092
+ * Merges with existing ToolContext to preserve other fields (memory, signal, taskId).
21093
+ *
21094
+ * Connector registry resolution order:
21095
+ * 1. If `connectors` (allowed names) is set → filtered view of global registry
21096
+ * 2. If access policy + userId → scoped view via Connector.scoped()
21097
+ * 3. Otherwise → full global registry
21098
+ */
21099
+ syncToolContext() {
21100
+ const existing = this._tools.getToolContext();
21101
+ this._tools.setToolContext({
21102
+ ...existing,
21103
+ agentId: this._agentId,
21104
+ userId: this._userId,
21105
+ connectorRegistry: this.buildConnectorRegistry()
21106
+ });
21107
+ }
21108
+ /**
21109
+ * Build the connector registry appropriate for this agent's config.
21110
+ */
21111
+ buildConnectorRegistry() {
21112
+ if (this._allowedConnectors?.length) {
21113
+ const allowedSet = new Set(this._allowedConnectors);
21114
+ const base = this._userId && Connector.getAccessPolicy() ? Connector.scoped({ userId: this._userId }) : Connector.asRegistry();
21115
+ return {
21116
+ get: (name) => {
21117
+ if (!allowedSet.has(name)) {
21118
+ const available = this._allowedConnectors.filter((n) => base.has(n)).join(", ") || "none";
21119
+ throw new Error(`Connector '${name}' not found. Available: ${available}`);
21120
+ }
21121
+ return base.get(name);
21122
+ },
21123
+ has: (name) => allowedSet.has(name) && base.has(name),
21124
+ list: () => base.list().filter((n) => allowedSet.has(n)),
21125
+ listAll: () => base.listAll().filter((c) => allowedSet.has(c.name)),
21126
+ size: () => base.listAll().filter((c) => allowedSet.has(c.name)).length,
21127
+ getDescriptionsForTools: () => {
21128
+ const connectors = base.listAll().filter((c) => allowedSet.has(c.name));
21129
+ if (connectors.length === 0) return "No connectors registered yet.";
21130
+ return connectors.map((c) => ` - "${c.name}": ${c.displayName} - ${c.config.description || "No description"}`).join("\n");
21131
+ },
21132
+ getInfo: () => {
21133
+ const info = {};
21134
+ for (const c of base.listAll().filter((c2) => allowedSet.has(c2.name))) {
21135
+ info[c.name] = { displayName: c.displayName, description: c.config.description || "", baseURL: c.baseURL };
21136
+ }
21137
+ return info;
21138
+ }
21139
+ };
21140
+ }
21141
+ if (this._userId && Connector.getAccessPolicy()) {
21142
+ return Connector.scoped({ userId: this._userId });
21143
+ }
21144
+ return Connector.asRegistry();
21145
+ }
21035
21146
  // ============================================================================
21036
21147
  // Public Properties
21037
21148
  // ============================================================================
@@ -21047,6 +21158,24 @@ var AgentContextNextGen = class _AgentContextNextGen extends EventEmitter {
21047
21158
  get agentId() {
21048
21159
  return this._agentId;
21049
21160
  }
21161
+ /** Get the current user ID */
21162
+ get userId() {
21163
+ return this._userId;
21164
+ }
21165
+ /** Set user ID. Automatically updates ToolContext for all tool executions. */
21166
+ set userId(value) {
21167
+ this._userId = value;
21168
+ this.syncToolContext();
21169
+ }
21170
+ /** Get the allowed connector names (undefined = all visible connectors) */
21171
+ get connectors() {
21172
+ return this._allowedConnectors;
21173
+ }
21174
+ /** Set allowed connector names. Updates ToolContext.connectorRegistry. */
21175
+ set connectors(value) {
21176
+ this._allowedConnectors = value;
21177
+ this.syncToolContext();
21178
+ }
21050
21179
  /** Get/set system prompt */
21051
21180
  get systemPrompt() {
21052
21181
  return this._systemPrompt;
@@ -21960,6 +22089,7 @@ ${content}`);
21960
22089
  metadata: {
21961
22090
  savedAt: Date.now(),
21962
22091
  agentId: this._agentId,
22092
+ userId: this._userId,
21963
22093
  model: this._config.model
21964
22094
  }
21965
22095
  };
@@ -24922,6 +25052,8 @@ var BaseAgent = class extends EventEmitter {
24922
25052
  const contextConfig = {
24923
25053
  model: config.model,
24924
25054
  agentId: config.name,
25055
+ userId: config.userId,
25056
+ connectors: config.connectors,
24925
25057
  // Include storage and sessionId if session config is provided
24926
25058
  storage: config.session?.storage,
24927
25059
  // Thread tool execution timeout to ToolManager
@@ -25078,6 +25210,30 @@ var BaseAgent = class extends EventEmitter {
25078
25210
  get context() {
25079
25211
  return this._agentContext;
25080
25212
  }
25213
+ /**
25214
+ * Get the current user ID. Delegates to AgentContextNextGen.
25215
+ */
25216
+ get userId() {
25217
+ return this._agentContext.userId;
25218
+ }
25219
+ /**
25220
+ * Set user ID at runtime. Automatically updates ToolContext for all tool executions.
25221
+ */
25222
+ set userId(value) {
25223
+ this._agentContext.userId = value;
25224
+ }
25225
+ /**
25226
+ * Get the allowed connector names (undefined = all visible connectors).
25227
+ */
25228
+ get connectors() {
25229
+ return this._agentContext.connectors;
25230
+ }
25231
+ /**
25232
+ * Restrict this agent to a subset of connectors. Updates ToolContext.connectorRegistry.
25233
+ */
25234
+ set connectors(value) {
25235
+ this._agentContext.connectors = value;
25236
+ }
25081
25237
  /**
25082
25238
  * Permission management. Returns ToolPermissionManager for approval control.
25083
25239
  */
@@ -25129,9 +25285,10 @@ var BaseAgent = class extends EventEmitter {
25129
25285
  * always sees up-to-date tool descriptions.
25130
25286
  */
25131
25287
  getEnabledToolDefinitions() {
25288
+ const toolContext = this._agentContext.tools.getToolContext();
25132
25289
  return this._agentContext.tools.getEnabled().map((tool) => {
25133
25290
  if (tool.descriptionFactory) {
25134
- const dynamicDescription = tool.descriptionFactory();
25291
+ const dynamicDescription = tool.descriptionFactory(toolContext);
25135
25292
  return {
25136
25293
  ...tool.definition,
25137
25294
  function: {
@@ -26177,6 +26334,7 @@ var Agent = class _Agent extends BaseAgent {
26177
26334
  * const agent = Agent.create({
26178
26335
  * connector: 'openai', // or Connector instance
26179
26336
  * model: 'gpt-4',
26337
+ * userId: 'user-123', // flows to all tool executions automatically
26180
26338
  * instructions: 'You are a helpful assistant',
26181
26339
  * tools: [myTool]
26182
26340
  * });
@@ -41754,6 +41912,32 @@ function filterProtectedHeaders(headers) {
41754
41912
  }
41755
41913
  return filtered;
41756
41914
  }
41915
+ function normalizeBody(body) {
41916
+ if (typeof body === "string") {
41917
+ try {
41918
+ return JSON.parse(body);
41919
+ } catch {
41920
+ return body;
41921
+ }
41922
+ }
41923
+ return body;
41924
+ }
41925
+ function detectAPIError(data) {
41926
+ if (!data || typeof data !== "object") return null;
41927
+ const obj = data;
41928
+ if (obj.ok === false && typeof obj.error === "string") {
41929
+ return obj.error;
41930
+ }
41931
+ if (obj.success === false) {
41932
+ if (typeof obj.error === "string") return obj.error;
41933
+ if (typeof obj.message === "string") return obj.message;
41934
+ }
41935
+ if (obj.error && typeof obj.error === "object") {
41936
+ const err = obj.error;
41937
+ if (typeof err.message === "string") return err.message;
41938
+ }
41939
+ return null;
41940
+ }
41757
41941
  var ConnectorTools = class {
41758
41942
  /** Registry of service-specific tool factories */
41759
41943
  static factories = /* @__PURE__ */ new Map();
@@ -42007,7 +42191,7 @@ var ConnectorTools = class {
42007
42191
  },
42008
42192
  body: {
42009
42193
  type: "object",
42010
- description: 'JSON request body for POST/PUT/PATCH requests. ALWAYS use this for sending data (e.g., {"channel": "C123", "text": "hello"}) instead of query string parameters.'
42194
+ description: 'JSON request body for POST/PUT/PATCH requests. MUST be a JSON object (NOT a string). Example: {"channel": "C123", "text": "hello"}. Do NOT stringify this \u2014 pass it as a raw JSON object. Do NOT use query string parameters for POST data.'
42011
42195
  },
42012
42196
  queryParams: {
42013
42197
  type: "object",
@@ -42022,7 +42206,8 @@ var ConnectorTools = class {
42022
42206
  }
42023
42207
  }
42024
42208
  },
42025
- execute: async (args) => {
42209
+ execute: async (args, context) => {
42210
+ const effectiveUserId = context?.userId ?? userId;
42026
42211
  let url2 = args.endpoint;
42027
42212
  if (args.queryParams && Object.keys(args.queryParams).length > 0) {
42028
42213
  const params = new URLSearchParams();
@@ -42035,7 +42220,8 @@ var ConnectorTools = class {
42035
42220
  let bodyStr;
42036
42221
  if (args.body) {
42037
42222
  try {
42038
- bodyStr = safeStringify2(args.body);
42223
+ const normalized = normalizeBody(args.body);
42224
+ bodyStr = safeStringify2(normalized);
42039
42225
  } catch (e) {
42040
42226
  return {
42041
42227
  success: false,
@@ -42054,7 +42240,7 @@ var ConnectorTools = class {
42054
42240
  },
42055
42241
  body: bodyStr
42056
42242
  },
42057
- userId
42243
+ effectiveUserId
42058
42244
  );
42059
42245
  const text = await response.text();
42060
42246
  let data;
@@ -42063,11 +42249,12 @@ var ConnectorTools = class {
42063
42249
  } catch {
42064
42250
  data = text;
42065
42251
  }
42252
+ const apiError = detectAPIError(data);
42066
42253
  return {
42067
- success: response.ok,
42254
+ success: response.ok && !apiError,
42068
42255
  status: response.status,
42069
- data: response.ok ? data : void 0,
42070
- error: response.ok ? void 0 : typeof data === "string" ? data : safeStringify2(data)
42256
+ data: response.ok && !apiError ? data : void 0,
42257
+ error: apiError ? apiError : response.ok ? void 0 : typeof data === "string" ? data : safeStringify2(data)
42071
42258
  };
42072
42259
  } catch (error) {
42073
42260
  return {
@@ -45290,6 +45477,8 @@ __export(tools_exports, {
45290
45477
  createSpeechToTextTool: () => createSpeechToTextTool,
45291
45478
  createTextToSpeechTool: () => createTextToSpeechTool,
45292
45479
  createVideoTools: () => createVideoTools,
45480
+ createWebScrapeTool: () => createWebScrapeTool,
45481
+ createWebSearchTool: () => createWebSearchTool,
45293
45482
  createWriteFileTool: () => createWriteFileTool,
45294
45483
  developerTools: () => developerTools,
45295
45484
  editFile: () => editFile,
@@ -45319,9 +45508,6 @@ __export(tools_exports, {
45319
45508
  toolRegistry: () => toolRegistry,
45320
45509
  validatePath: () => validatePath,
45321
45510
  webFetch: () => webFetch,
45322
- webFetchJS: () => webFetchJS,
45323
- webScrape: () => webScrape,
45324
- webSearch: () => webSearch,
45325
45511
  writeFile: () => writeFile5
45326
45512
  });
45327
45513
  var DEFAULT_FILESYSTEM_CONFIG = {
@@ -47044,6 +47230,111 @@ The tool returns a result object with:
47044
47230
  }
47045
47231
  };
47046
47232
 
47233
+ // src/tools/web/createWebSearchTool.ts
47234
+ init_Logger();
47235
+ var searchLogger = logger.child({ component: "webSearch" });
47236
+ function createWebSearchTool(connector) {
47237
+ return {
47238
+ definition: {
47239
+ type: "function",
47240
+ function: {
47241
+ name: "web_search",
47242
+ description: `Search the web and get relevant results with snippets.
47243
+
47244
+ RETURNS:
47245
+ An array of search results, each containing:
47246
+ - title: Page title
47247
+ - url: Direct URL to the page
47248
+ - snippet: Short description/excerpt from the page
47249
+ - position: Search ranking position (1, 2, 3...)
47250
+
47251
+ USE CASES:
47252
+ - Find current information on any topic
47253
+ - Research multiple sources
47254
+ - Discover relevant websites
47255
+ - Find URLs to fetch with web_fetch tool
47256
+
47257
+ WORKFLOW PATTERN:
47258
+ 1. Use web_search to find relevant URLs
47259
+ 2. Use web_fetch to get full content from top results
47260
+ 3. Process and summarize the information
47261
+
47262
+ EXAMPLE:
47263
+ {
47264
+ "query": "latest AI developments 2026",
47265
+ "numResults": 5
47266
+ }`,
47267
+ parameters: {
47268
+ type: "object",
47269
+ properties: {
47270
+ query: {
47271
+ type: "string",
47272
+ description: "The search query string. Be specific for better results."
47273
+ },
47274
+ numResults: {
47275
+ type: "number",
47276
+ description: "Number of results to return (default: 10, max: 100)."
47277
+ },
47278
+ country: {
47279
+ type: "string",
47280
+ description: 'Country/region code for localized results (e.g., "us", "gb", "de")'
47281
+ },
47282
+ language: {
47283
+ type: "string",
47284
+ description: 'Language code for results (e.g., "en", "fr", "de")'
47285
+ }
47286
+ },
47287
+ required: ["query"]
47288
+ }
47289
+ },
47290
+ blocking: true,
47291
+ timeout: 15e3
47292
+ },
47293
+ execute: async (args) => {
47294
+ const numResults = args.numResults || 10;
47295
+ searchLogger.debug({ connectorName: connector.name }, "Executing search with connector");
47296
+ try {
47297
+ const searchProvider = SearchProvider.create({ connector: connector.name });
47298
+ const response = await searchProvider.search(args.query, {
47299
+ numResults,
47300
+ country: args.country,
47301
+ language: args.language
47302
+ });
47303
+ if (response.success) {
47304
+ searchLogger.debug({
47305
+ provider: response.provider,
47306
+ count: response.count
47307
+ }, "Search completed successfully");
47308
+ } else {
47309
+ searchLogger.warn({
47310
+ provider: response.provider,
47311
+ error: response.error
47312
+ }, "Search failed");
47313
+ }
47314
+ return {
47315
+ success: response.success,
47316
+ query: response.query,
47317
+ provider: response.provider,
47318
+ results: response.results,
47319
+ count: response.count,
47320
+ error: response.error
47321
+ };
47322
+ } catch (error) {
47323
+ searchLogger.error({ error: error.message, connectorName: connector.name }, "Search threw exception");
47324
+ return {
47325
+ success: false,
47326
+ query: args.query,
47327
+ provider: connector.name,
47328
+ results: [],
47329
+ count: 0,
47330
+ error: error.message || "Unknown error"
47331
+ };
47332
+ }
47333
+ },
47334
+ describeCall: (args) => `"${args.query}"${args.numResults ? ` (${args.numResults} results)` : ""}`
47335
+ };
47336
+ }
47337
+
47047
47338
  // src/tools/web/contentDetector.ts
47048
47339
  function detectContentQuality(html, text, $) {
47049
47340
  const issues = [];
@@ -47118,7 +47409,7 @@ function detectContentQuality(html, text, $) {
47118
47409
  }
47119
47410
  let suggestion;
47120
47411
  if (requiresJS && score < 50) {
47121
- suggestion = "Content quality is low. This appears to be a JavaScript-rendered site. Use the web_fetch_js tool for better results.";
47412
+ suggestion = "Content quality is low. This appears to be a JavaScript-rendered site. Use a scraping service connector for better results.";
47122
47413
  } else if (score < 30) {
47123
47414
  suggestion = "Content extraction failed or page has errors. Check the URL and try again.";
47124
47415
  }
@@ -47227,7 +47518,7 @@ The tool analyzes the fetched content and returns a quality score (0-100):
47227
47518
  - 50-79: Moderate quality, some content extracted
47228
47519
  - 0-49: Low quality, likely needs JavaScript or has errors
47229
47520
 
47230
- If the quality score is low or requiresJS is true, the tool will suggest using 'web_fetch_js' instead.
47521
+ If the quality score is low or requiresJS is true, consider using a scraping service connector for better results.
47231
47522
 
47232
47523
  RETURNS:
47233
47524
  {
@@ -47391,460 +47682,109 @@ With custom user agent:
47391
47682
  }
47392
47683
  };
47393
47684
 
47394
- // src/tools/web/webFetchJS.ts
47395
- var puppeteerModule = null;
47396
- var browserInstance = null;
47397
- async function loadPuppeteer() {
47398
- if (!puppeteerModule) {
47399
- try {
47400
- puppeteerModule = await import('puppeteer');
47401
- } catch (error) {
47402
- throw new Error("Puppeteer not installed");
47403
- }
47404
- }
47405
- return puppeteerModule;
47406
- }
47407
- async function getBrowser() {
47408
- if (!browserInstance) {
47409
- const puppeteer = await loadPuppeteer();
47410
- browserInstance = await puppeteer.launch({
47411
- headless: true,
47412
- args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]
47413
- });
47414
- process.on("exit", async () => {
47415
- if (browserInstance) {
47416
- await browserInstance.close();
47417
- }
47418
- });
47419
- }
47420
- return browserInstance;
47421
- }
47422
- var webFetchJS = {
47423
- definition: {
47424
- type: "function",
47425
- function: {
47426
- name: "web_fetch_js",
47427
- description: `Fetch and extract content from JavaScript-rendered websites using a headless browser (Puppeteer).
47428
-
47429
- USE THIS TOOL WHEN:
47430
- - The web_fetch tool returned a low quality score (<50)
47431
- - The web_fetch tool suggested using JavaScript rendering
47432
- - You know the website is built with React/Vue/Angular/Next.js
47433
- - Content loads dynamically via JavaScript
47434
- - The page requires interaction (though this tool doesn't support interaction yet)
47435
-
47436
- HOW IT WORKS:
47437
- - Launches a headless Chrome browser
47438
- - Navigates to the URL
47439
- - Waits for JavaScript to execute and content to load
47440
- - Extracts the rendered HTML and text content
47441
- - Optionally captures a screenshot
47442
-
47443
- CAPABILITIES:
47444
- - Executes all JavaScript on the page
47445
- - Waits for network to be idle (all resources loaded)
47446
- - Can wait for specific CSS selectors to appear
47447
- - Handles React, Vue, Angular, Next.js, and other SPAs
47448
- - Returns content after full JavaScript execution
47449
-
47450
- LIMITATIONS:
47451
- - Slower than web_fetch (typically 3-10 seconds vs <1 second)
47452
- - Uses more system resources (runs a full browser)
47453
- - May still fail on sites with aggressive bot detection
47454
- - Requires puppeteer to be installed (npm install puppeteer)
47455
-
47456
- PERFORMANCE:
47457
- - First call: Slower (launches browser ~1-2s)
47458
- - Subsequent calls: Faster (reuses browser instance)
47459
-
47460
- RETURNS:
47461
- {
47462
- success: boolean,
47463
- url: string,
47464
- title: string,
47465
- content: string, // Clean markdown (converted via Readability + Turndown)
47466
- screenshot: string, // Base64 PNG screenshot (if requested)
47467
- loadTime: number, // Time taken in milliseconds
47468
- excerpt: string, // Short summary excerpt (if extracted)
47469
- byline: string, // Author info (if extracted)
47470
- wasTruncated: boolean, // True if content was truncated
47471
- error: string // Error message if failed
47472
- }
47473
-
47474
- EXAMPLES:
47475
- Basic usage:
47476
- {
47477
- url: "https://react-app.com/page"
47478
- }
47479
-
47480
- Wait for specific content:
47481
- {
47482
- url: "https://app.com/dashboard",
47483
- waitForSelector: "#main-content", // Wait for this element
47484
- timeout: 20000
47685
+ // src/tools/web/createWebScrapeTool.ts
47686
+ init_Logger();
47687
+ var scrapeLogger = logger.child({ component: "webScrape" });
47688
+ var DEFAULT_MIN_QUALITY = 50;
47689
+ function stripBase64DataUris(content) {
47690
+ if (!content) return content;
47691
+ let cleaned = content.replace(/!\[[^\]]*\]\(data:[^)]+\)/g, "[image removed]");
47692
+ cleaned = cleaned.replace(/url\(['"]?data:[^)]+['"]?\)/gi, "url([data-uri-removed])");
47693
+ cleaned = cleaned.replace(/data:(?:image|font|application)\/[^;]+;base64,[A-Za-z0-9+/=]{100,}/g, "[base64-data-removed]");
47694
+ return cleaned;
47485
47695
  }
47486
-
47487
- With screenshot:
47488
- {
47489
- url: "https://site.com",
47490
- takeScreenshot: true
47491
- }`,
47492
- parameters: {
47493
- type: "object",
47494
- properties: {
47495
- url: {
47496
- type: "string",
47497
- description: "The URL to fetch. Must start with http:// or https://"
47498
- },
47499
- waitForSelector: {
47500
- type: "string",
47501
- description: 'Optional CSS selector to wait for before extracting content. Example: "#main-content" or ".article-body"'
47502
- },
47503
- timeout: {
47504
- type: "number",
47505
- description: "Max wait time in milliseconds (default: 15000)"
47506
- },
47507
- takeScreenshot: {
47508
- type: "boolean",
47509
- description: "Whether to capture a screenshot of the page (default: false). Screenshot returned as base64 PNG."
47510
- }
47511
- },
47512
- required: ["url"]
47513
- }
47514
- },
47515
- blocking: true,
47516
- timeout: 3e4
47517
- // Allow extra time for browser operations
47518
- },
47519
- execute: async (args) => {
47520
- let page = null;
47696
+ function createWebScrapeTool(connector) {
47697
+ async function tryNative(args, startTime, attemptedMethods) {
47698
+ attemptedMethods.push("native");
47699
+ scrapeLogger.debug({ url: args.url }, "Trying native fetch");
47521
47700
  try {
47522
- const browser = await getBrowser();
47523
- page = await browser.newPage();
47524
- await page.setViewport({ width: 1280, height: 800 });
47525
- await page.setUserAgent(
47526
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
47527
- );
47528
- const startTime = Date.now();
47529
- await page.goto(args.url, {
47530
- waitUntil: "networkidle2",
47531
- // Wait until network is mostly idle
47532
- timeout: args.timeout || 15e3
47701
+ const result = await webFetch.execute({
47702
+ url: args.url,
47703
+ timeout: args.timeout || 1e4
47533
47704
  });
47534
- if (args.waitForSelector) {
47535
- await page.waitForSelector(args.waitForSelector, {
47536
- timeout: args.timeout || 15e3
47537
- });
47538
- }
47539
- const html = await page.content();
47540
- const browserTitle = await page.title();
47541
- const loadTime = Date.now() - startTime;
47542
- let screenshot;
47543
- if (args.takeScreenshot) {
47544
- const buffer = await page.screenshot({
47545
- type: "png",
47546
- fullPage: false
47547
- // Just viewport
47548
- });
47549
- screenshot = buffer.toString("base64");
47550
- }
47551
- await page.close();
47552
- const mdResult = await htmlToMarkdown(html, args.url);
47553
- const title = browserTitle || mdResult.title || "Untitled";
47705
+ const cleanContent = stripBase64DataUris(result.content);
47554
47706
  return {
47555
- success: true,
47707
+ success: result.success,
47556
47708
  url: args.url,
47557
- title,
47558
- content: mdResult.markdown,
47559
- screenshot,
47560
- loadTime,
47561
- excerpt: mdResult.excerpt,
47562
- byline: mdResult.byline,
47563
- wasReadabilityUsed: mdResult.wasReadabilityUsed,
47564
- wasTruncated: mdResult.wasTruncated
47709
+ finalUrl: args.url,
47710
+ method: "native",
47711
+ title: result.title,
47712
+ content: cleanContent,
47713
+ qualityScore: result.qualityScore,
47714
+ durationMs: Date.now() - startTime,
47715
+ attemptedMethods,
47716
+ error: result.error
47565
47717
  };
47566
47718
  } catch (error) {
47567
- if (page) {
47568
- try {
47569
- await page.close();
47570
- } catch {
47571
- }
47572
- }
47573
- if (error.message === "Puppeteer not installed") {
47574
- return {
47575
- success: false,
47576
- url: args.url,
47577
- title: "",
47578
- content: "",
47579
- loadTime: 0,
47580
- error: "Puppeteer is not installed",
47581
- suggestion: "Install Puppeteer with: npm install puppeteer (note: downloads ~50MB Chrome binary)"
47582
- };
47583
- }
47584
47719
  return {
47585
47720
  success: false,
47586
47721
  url: args.url,
47722
+ method: "native",
47587
47723
  title: "",
47588
47724
  content: "",
47589
- loadTime: 0,
47725
+ durationMs: Date.now() - startTime,
47726
+ attemptedMethods,
47590
47727
  error: error.message
47591
47728
  };
47592
47729
  }
47593
47730
  }
47594
- };
47595
-
47596
- // src/tools/web/webSearch.ts
47597
- init_Logger();
47598
-
47599
- // src/tools/web/searchProviders/serper.ts
47600
- async function searchWithSerper(query, numResults, apiKey) {
47601
- const response = await fetch("https://google.serper.dev/search", {
47602
- method: "POST",
47603
- headers: {
47604
- "X-API-KEY": apiKey,
47605
- "Content-Type": "application/json"
47606
- },
47607
- body: JSON.stringify({
47608
- q: query,
47609
- num: numResults
47610
- })
47611
- });
47612
- if (!response.ok) {
47613
- throw new Error(`Serper API error: ${response.status} ${response.statusText}`);
47614
- }
47615
- const data = await response.json();
47616
- if (!data.organic || !Array.isArray(data.organic)) {
47617
- throw new Error("Invalid response from Serper API");
47618
- }
47619
- return data.organic.slice(0, numResults).map((result, index) => ({
47620
- title: result.title || "Untitled",
47621
- url: result.link || "",
47622
- snippet: result.snippet || "",
47623
- position: index + 1
47624
- }));
47625
- }
47626
-
47627
- // src/tools/web/searchProviders/brave.ts
47628
- async function searchWithBrave(query, numResults, apiKey) {
47629
- const url2 = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${numResults}`;
47630
- const response = await fetch(url2, {
47631
- headers: {
47632
- "X-Subscription-Token": apiKey,
47633
- Accept: "application/json"
47634
- }
47635
- });
47636
- if (!response.ok) {
47637
- throw new Error(`Brave API error: ${response.status} ${response.statusText}`);
47638
- }
47639
- const data = await response.json();
47640
- if (!data.web?.results || !Array.isArray(data.web.results)) {
47641
- throw new Error("Invalid response from Brave API");
47642
- }
47643
- return data.web.results.slice(0, numResults).map((result, index) => ({
47644
- title: result.title || "Untitled",
47645
- url: result.url || "",
47646
- snippet: result.description || "",
47647
- position: index + 1
47648
- }));
47649
- }
47650
-
47651
- // src/tools/web/searchProviders/tavily.ts
47652
- async function searchWithTavily(query, numResults, apiKey) {
47653
- const response = await fetch("https://api.tavily.com/search", {
47654
- method: "POST",
47655
- headers: {
47656
- "Content-Type": "application/json"
47657
- },
47658
- body: JSON.stringify({
47659
- api_key: apiKey,
47660
- query,
47661
- max_results: numResults,
47662
- search_depth: "basic",
47663
- // 'basic' or 'advanced'
47664
- include_answer: false,
47665
- include_raw_content: false
47666
- })
47667
- });
47668
- if (!response.ok) {
47669
- throw new Error(`Tavily API error: ${response.status} ${response.statusText}`);
47670
- }
47671
- const data = await response.json();
47672
- if (!data.results || !Array.isArray(data.results)) {
47673
- throw new Error("Invalid response from Tavily API");
47674
- }
47675
- return data.results.slice(0, numResults).map((result, index) => ({
47676
- title: result.title || "Untitled",
47677
- url: result.url || "",
47678
- snippet: result.content || "",
47679
- position: index + 1
47680
- }));
47681
- }
47682
-
47683
- // src/tools/web/webSearch.ts
47684
- var searchLogger = logger.child({ component: "webSearch" });
47685
- var SEARCH_SERVICE_TYPES = ["serper", "brave-search", "tavily", "rapidapi-search"];
47686
- var webSearch = {
47687
- definition: {
47688
- type: "function",
47689
- function: {
47690
- name: "web_search",
47691
- description: `Search the web and get relevant results with snippets.
47692
-
47693
- RETURNS:
47694
- An array of search results, each containing:
47695
- - title: Page title
47696
- - url: Direct URL to the page
47697
- - snippet: Short description/excerpt from the page
47698
- - position: Search ranking position (1, 2, 3...)
47699
-
47700
- USE CASES:
47701
- - Find current information on any topic
47702
- - Research multiple sources
47703
- - Discover relevant websites
47704
- - Find URLs to fetch with web_fetch tool
47705
-
47706
- WORKFLOW PATTERN:
47707
- 1. Use web_search to find relevant URLs
47708
- 2. Use web_fetch to get full content from top results
47709
- 3. Process and summarize the information
47710
-
47711
- EXAMPLE:
47712
- {
47713
- "query": "latest AI developments 2026",
47714
- "numResults": 5
47715
- }`,
47716
- parameters: {
47717
- type: "object",
47718
- properties: {
47719
- query: {
47720
- type: "string",
47721
- description: "The search query string. Be specific for better results."
47722
- },
47723
- numResults: {
47724
- type: "number",
47725
- description: "Number of results to return (default: 10, max: 100)."
47726
- },
47727
- country: {
47728
- type: "string",
47729
- description: 'Country/region code for localized results (e.g., "us", "gb", "de")'
47730
- },
47731
- language: {
47732
- type: "string",
47733
- description: 'Language code for results (e.g., "en", "fr", "de")'
47734
- }
47735
- },
47736
- required: ["query"]
47737
- }
47738
- },
47739
- blocking: true,
47740
- timeout: 15e3
47741
- },
47742
- execute: async (args) => {
47743
- const numResults = args.numResults || 10;
47744
- const connector = findConnectorByServiceTypes(SEARCH_SERVICE_TYPES);
47745
- if (connector) {
47746
- return await executeWithConnector(connector.name, args, numResults);
47747
- }
47748
- return await executeWithEnvVar(args, numResults);
47749
- },
47750
- describeCall: (args) => `"${args.query}"${args.numResults ? ` (${args.numResults} results)` : ""}`
47751
- };
47752
- async function executeWithConnector(connectorName, args, numResults) {
47753
- searchLogger.debug({ connectorName }, "Executing search with connector");
47754
- try {
47755
- const searchProvider = SearchProvider.create({ connector: connectorName });
47756
- const response = await searchProvider.search(args.query, {
47757
- numResults,
47758
- country: args.country,
47759
- language: args.language
47760
- });
47761
- if (response.success) {
47762
- searchLogger.debug({
47763
- provider: response.provider,
47764
- count: response.count
47765
- }, "Search completed successfully");
47766
- } else {
47767
- searchLogger.warn({
47768
- provider: response.provider,
47769
- error: response.error
47770
- }, "Search failed");
47771
- }
47772
- return {
47773
- success: response.success,
47774
- query: response.query,
47775
- provider: response.provider,
47776
- results: response.results,
47777
- count: response.count,
47778
- error: response.error
47779
- };
47780
- } catch (error) {
47781
- searchLogger.error({ error: error.message, connectorName }, "Search threw exception");
47782
- return {
47783
- success: false,
47784
- query: args.query,
47785
- provider: connectorName,
47786
- results: [],
47787
- count: 0,
47788
- error: error.message || "Unknown error"
47789
- };
47790
- }
47791
- }
47792
- async function executeWithEnvVar(args, numResults) {
47793
- const providers = [
47794
- { name: "serper", key: process.env.SERPER_API_KEY, fn: searchWithSerper },
47795
- { name: "brave", key: process.env.BRAVE_API_KEY, fn: searchWithBrave },
47796
- { name: "tavily", key: process.env.TAVILY_API_KEY, fn: searchWithTavily }
47797
- ];
47798
- for (const provider of providers) {
47799
- if (provider.key) {
47800
- searchLogger.debug({ provider: provider.name }, "Using environment variable fallback");
47801
- try {
47802
- const results = await provider.fn(args.query, numResults, provider.key);
47803
- return {
47804
- success: true,
47805
- query: args.query,
47806
- provider: provider.name,
47807
- results,
47808
- count: results.length
47809
- };
47810
- } catch (error) {
47811
- searchLogger.warn({ provider: provider.name, error: error.message }, "Provider failed, trying next");
47812
- }
47731
+ async function tryAPI(args, startTime, attemptedMethods) {
47732
+ attemptedMethods.push(`api:${connector.name}`);
47733
+ scrapeLogger.debug({ url: args.url, connectorName: connector.name }, "Trying external API");
47734
+ try {
47735
+ const provider = ScrapeProvider.create({ connector: connector.name });
47736
+ const options = {
47737
+ timeout: args.timeout,
47738
+ waitForSelector: args.waitForSelector,
47739
+ includeHtml: args.includeHtml,
47740
+ includeMarkdown: args.includeMarkdown,
47741
+ includeLinks: args.includeLinks
47742
+ };
47743
+ const result = await provider.scrape(args.url, options);
47744
+ const rawContent = result.result?.content || "";
47745
+ const rawMarkdown = result.result?.markdown;
47746
+ const cleanContent = stripBase64DataUris(rawContent);
47747
+ const cleanMarkdown = rawMarkdown ? stripBase64DataUris(rawMarkdown) : void 0;
47748
+ const isDuplicate = !!cleanMarkdown && cleanContent === cleanMarkdown;
47749
+ return {
47750
+ success: result.success,
47751
+ url: args.url,
47752
+ finalUrl: result.finalUrl,
47753
+ method: result.provider,
47754
+ title: result.result?.title || "",
47755
+ content: cleanContent,
47756
+ html: result.result?.html,
47757
+ markdown: isDuplicate ? void 0 : cleanMarkdown,
47758
+ metadata: result.result?.metadata,
47759
+ links: result.result?.links,
47760
+ qualityScore: result.success ? 90 : 0,
47761
+ durationMs: Date.now() - startTime,
47762
+ attemptedMethods,
47763
+ error: result.error
47764
+ };
47765
+ } catch (error) {
47766
+ return {
47767
+ success: false,
47768
+ url: args.url,
47769
+ method: "api",
47770
+ title: "",
47771
+ content: "",
47772
+ durationMs: Date.now() - startTime,
47773
+ attemptedMethods,
47774
+ error: error.message
47775
+ };
47813
47776
  }
47814
47777
  }
47815
47778
  return {
47816
- success: false,
47817
- query: args.query,
47818
- provider: "none",
47819
- results: [],
47820
- count: 0,
47821
- error: "No search provider configured. Set up a search connector (serper, brave-search, tavily) or set SERPER_API_KEY, BRAVE_API_KEY, or TAVILY_API_KEY environment variable."
47822
- };
47823
- }
47824
-
47825
- // src/tools/web/webScrape.ts
47826
- init_Logger();
47827
- var scrapeLogger = logger.child({ component: "webScrape" });
47828
- function stripBase64DataUris(content) {
47829
- if (!content) return content;
47830
- let cleaned = content.replace(/!\[[^\]]*\]\(data:[^)]+\)/g, "[image removed]");
47831
- cleaned = cleaned.replace(/url\(['"]?data:[^)]+['"]?\)/gi, "url([data-uri-removed])");
47832
- cleaned = cleaned.replace(/data:(?:image|font|application)\/[^;]+;base64,[A-Za-z0-9+/=]{100,}/g, "[base64-data-removed]");
47833
- return cleaned;
47834
- }
47835
- var SCRAPE_SERVICE_TYPES = ["zenrows", "jina-reader", "firecrawl", "scrapingbee"];
47836
- var DEFAULT_MIN_QUALITY = 50;
47837
- var webScrape = {
47838
- definition: {
47839
- type: "function",
47840
- function: {
47841
- name: "web_scrape",
47842
- description: `Scrape any URL with automatic fallback - guaranteed to work on most sites.
47779
+ definition: {
47780
+ type: "function",
47781
+ function: {
47782
+ name: "web_scrape",
47783
+ description: `Scrape any URL with automatic fallback - guaranteed to work on most sites.
47843
47784
 
47844
47785
  Automatically tries multiple methods in sequence:
47845
47786
  1. Native fetch - Fast (~1s), works for blogs/docs/articles
47846
- 2. JS rendering - Handles React/Vue/Angular SPAs
47847
- 3. External API - Handles bot protection, CAPTCHAs (if configured)
47787
+ 2. External API - Handles bot protection, CAPTCHAs, SPAs (if configured)
47848
47788
 
47849
47789
  RETURNS:
47850
47790
  {
@@ -47879,46 +47819,64 @@ For JS-heavy sites:
47879
47819
  "url": "https://spa-app.com",
47880
47820
  "waitForSelector": ".main-content"
47881
47821
  }`,
47882
- parameters: {
47883
- type: "object",
47884
- properties: {
47885
- url: {
47886
- type: "string",
47887
- description: "URL to scrape. Must start with http:// or https://"
47888
- },
47889
- timeout: {
47890
- type: "number",
47891
- description: "Timeout in milliseconds (default: 30000)"
47892
- },
47893
- includeHtml: {
47894
- type: "boolean",
47895
- description: "Include raw HTML in response (default: false)"
47896
- },
47897
- includeMarkdown: {
47898
- type: "boolean",
47899
- description: "Include markdown conversion (default: false)"
47900
- },
47901
- includeLinks: {
47902
- type: "boolean",
47903
- description: "Extract and include links (default: false)"
47822
+ parameters: {
47823
+ type: "object",
47824
+ properties: {
47825
+ url: {
47826
+ type: "string",
47827
+ description: "URL to scrape. Must start with http:// or https://"
47828
+ },
47829
+ timeout: {
47830
+ type: "number",
47831
+ description: "Timeout in milliseconds (default: 30000)"
47832
+ },
47833
+ includeHtml: {
47834
+ type: "boolean",
47835
+ description: "Include raw HTML in response (default: false)"
47836
+ },
47837
+ includeMarkdown: {
47838
+ type: "boolean",
47839
+ description: "Include markdown conversion (default: false)"
47840
+ },
47841
+ includeLinks: {
47842
+ type: "boolean",
47843
+ description: "Extract and include links (default: false)"
47844
+ },
47845
+ waitForSelector: {
47846
+ type: "string",
47847
+ description: "CSS selector to wait for before scraping (for JS-heavy sites)"
47848
+ }
47904
47849
  },
47905
- waitForSelector: {
47906
- type: "string",
47907
- description: "CSS selector to wait for before scraping (for JS-heavy sites)"
47908
- }
47909
- },
47910
- required: ["url"]
47911
- }
47850
+ required: ["url"]
47851
+ }
47852
+ },
47853
+ blocking: true,
47854
+ timeout: 6e4
47912
47855
  },
47913
- blocking: true,
47914
- timeout: 6e4
47915
- },
47916
- execute: async (args) => {
47917
- const startTime = Date.now();
47918
- const attemptedMethods = [];
47919
- try {
47920
- new URL(args.url);
47921
- } catch {
47856
+ execute: async (args) => {
47857
+ const startTime = Date.now();
47858
+ const attemptedMethods = [];
47859
+ try {
47860
+ new URL(args.url);
47861
+ } catch {
47862
+ return {
47863
+ success: false,
47864
+ url: args.url,
47865
+ method: "none",
47866
+ title: "",
47867
+ content: "",
47868
+ durationMs: Date.now() - startTime,
47869
+ attemptedMethods: [],
47870
+ error: "Invalid URL format"
47871
+ };
47872
+ }
47873
+ const native = await tryNative(args, startTime, attemptedMethods);
47874
+ if (native.success && (native.qualityScore ?? 0) >= DEFAULT_MIN_QUALITY) {
47875
+ return native;
47876
+ }
47877
+ const api = await tryAPI(args, startTime, attemptedMethods);
47878
+ if (api.success) return api;
47879
+ if (native.success) return native;
47922
47880
  return {
47923
47881
  success: false,
47924
47882
  url: args.url,
@@ -47926,232 +47884,119 @@ For JS-heavy sites:
47926
47884
  title: "",
47927
47885
  content: "",
47928
47886
  durationMs: Date.now() - startTime,
47929
- attemptedMethods: [],
47930
- error: "Invalid URL format"
47887
+ attemptedMethods,
47888
+ error: "All scraping methods failed. Site may have bot protection."
47931
47889
  };
47932
- }
47933
- const native = await tryNative(args, startTime, attemptedMethods);
47934
- if (native.success && (native.qualityScore ?? 0) >= DEFAULT_MIN_QUALITY) {
47935
- return native;
47936
- }
47937
- const js = await tryJS(args, startTime, attemptedMethods);
47938
- if (js.success && (js.qualityScore ?? 0) >= DEFAULT_MIN_QUALITY) {
47939
- return js;
47940
- }
47941
- const connector = findConnectorByServiceTypes(SCRAPE_SERVICE_TYPES);
47942
- if (connector) {
47943
- const api = await tryAPI(connector.name, args, startTime, attemptedMethods);
47944
- if (api.success) return api;
47945
- }
47946
- if (js.success) return js;
47947
- if (native.success) return native;
47948
- return {
47949
- success: false,
47950
- url: args.url,
47951
- method: "none",
47952
- title: "",
47953
- content: "",
47954
- durationMs: Date.now() - startTime,
47955
- attemptedMethods,
47956
- error: "All scraping methods failed. Site may have bot protection."
47957
- };
47958
- },
47959
- describeCall: (args) => args.url
47960
- };
47961
- async function tryNative(args, startTime, attemptedMethods) {
47962
- attemptedMethods.push("native");
47963
- scrapeLogger.debug({ url: args.url }, "Trying native fetch");
47964
- try {
47965
- const result = await webFetch.execute({
47966
- url: args.url,
47967
- timeout: args.timeout || 1e4
47968
- });
47969
- const cleanContent = stripBase64DataUris(result.content);
47970
- return {
47971
- success: result.success,
47972
- url: args.url,
47973
- finalUrl: args.url,
47974
- method: "native",
47975
- title: result.title,
47976
- content: cleanContent,
47977
- // Native method already returns markdown-like content — no separate markdown field needed
47978
- // (would just duplicate content and waste tokens)
47979
- qualityScore: result.qualityScore,
47980
- durationMs: Date.now() - startTime,
47981
- attemptedMethods,
47982
- error: result.error
47983
- };
47984
- } catch (error) {
47985
- return {
47986
- success: false,
47987
- url: args.url,
47988
- method: "native",
47989
- title: "",
47990
- content: "",
47991
- durationMs: Date.now() - startTime,
47992
- attemptedMethods,
47993
- error: error.message
47994
- };
47995
- }
47890
+ },
47891
+ describeCall: (args) => args.url
47892
+ };
47996
47893
  }
47997
- async function tryJS(args, startTime, attemptedMethods) {
47998
- attemptedMethods.push("js");
47999
- scrapeLogger.debug({ url: args.url }, "Trying JS rendering");
48000
- try {
48001
- const result = await webFetchJS.execute({
48002
- url: args.url,
48003
- timeout: args.timeout || 15e3,
48004
- waitForSelector: args.waitForSelector
48005
- });
48006
- const cleanContent = stripBase64DataUris(result.content);
48007
- return {
48008
- success: result.success,
48009
- url: args.url,
48010
- finalUrl: args.url,
48011
- method: "js",
48012
- title: result.title,
48013
- content: cleanContent,
48014
- // JS method already returns markdown-like content — no separate markdown field needed
48015
- qualityScore: result.success ? 80 : 0,
48016
- durationMs: Date.now() - startTime,
48017
- attemptedMethods,
48018
- error: result.error
48019
- };
48020
- } catch (error) {
48021
- return {
48022
- success: false,
48023
- url: args.url,
48024
- method: "js",
48025
- title: "",
48026
- content: "",
48027
- durationMs: Date.now() - startTime,
48028
- attemptedMethods,
48029
- error: error.message
48030
- };
47894
+
47895
+ // src/tools/web/register.ts
47896
+ var SEARCH_SERVICE_TYPES = ["serper", "brave-search", "tavily", "rapidapi-search"];
47897
+ var SCRAPE_SERVICE_TYPES = ["zenrows", "jina-reader", "firecrawl", "scrapingbee"];
47898
+ function registerWebTools() {
47899
+ for (const st of SEARCH_SERVICE_TYPES) {
47900
+ ConnectorTools.registerService(st, (connector) => [
47901
+ createWebSearchTool(connector)
47902
+ ]);
48031
47903
  }
48032
- }
48033
- async function tryAPI(connectorName, args, startTime, attemptedMethods) {
48034
- attemptedMethods.push(`api:${connectorName}`);
48035
- scrapeLogger.debug({ url: args.url, connectorName }, "Trying external API");
48036
- try {
48037
- const provider = ScrapeProvider.create({ connector: connectorName });
48038
- const options = {
48039
- timeout: args.timeout,
48040
- waitForSelector: args.waitForSelector,
48041
- includeHtml: args.includeHtml,
48042
- includeMarkdown: args.includeMarkdown,
48043
- includeLinks: args.includeLinks
48044
- };
48045
- const result = await provider.scrape(args.url, options);
48046
- const rawContent = result.result?.content || "";
48047
- const rawMarkdown = result.result?.markdown;
48048
- const cleanContent = stripBase64DataUris(rawContent);
48049
- const cleanMarkdown = rawMarkdown ? stripBase64DataUris(rawMarkdown) : void 0;
48050
- const isDuplicate = !!cleanMarkdown && cleanContent === cleanMarkdown;
48051
- return {
48052
- success: result.success,
48053
- url: args.url,
48054
- finalUrl: result.finalUrl,
48055
- method: result.provider,
48056
- title: result.result?.title || "",
48057
- content: cleanContent,
48058
- html: result.result?.html,
48059
- // Keep raw HTML as-is (only used if explicitly requested)
48060
- markdown: isDuplicate ? void 0 : cleanMarkdown,
48061
- metadata: result.result?.metadata,
48062
- links: result.result?.links,
48063
- qualityScore: result.success ? 90 : 0,
48064
- durationMs: Date.now() - startTime,
48065
- attemptedMethods,
48066
- error: result.error
48067
- };
48068
- } catch (error) {
48069
- return {
48070
- success: false,
48071
- url: args.url,
48072
- method: "api",
48073
- title: "",
48074
- content: "",
48075
- durationMs: Date.now() - startTime,
48076
- attemptedMethods,
48077
- error: error.message
48078
- };
47904
+ for (const st of SCRAPE_SERVICE_TYPES) {
47905
+ ConnectorTools.registerService(st, (connector) => [
47906
+ createWebScrapeTool(connector)
47907
+ ]);
48079
47908
  }
48080
47909
  }
48081
47910
 
47911
+ // src/tools/web/index.ts
47912
+ registerWebTools();
47913
+
48082
47914
  // src/tools/code/executeJavaScript.ts
48083
47915
  init_Connector();
48084
- function generateDescription() {
48085
- const connectors = Connector.listAll();
48086
- const connectorList = connectors.length > 0 ? connectors.map((c) => {
48087
- const authType = c.config.auth?.type || "none";
48088
- return ` \u2022 "${c.name}": ${c.displayName}
48089
- ${c.config.description || "No description"}
48090
- Base URL: ${c.baseURL}
48091
- Auth: ${authType}`;
48092
- }).join("\n\n") : " No connectors registered.";
48093
- return `Execute JavaScript code in a secure sandbox with authenticated API access.
48094
-
48095
- AVAILABLE APIS:
48096
-
48097
- 1. authenticatedFetch(url, options, connectorName, userId?)
48098
- Makes authenticated API calls using the connector's configured auth scheme.
48099
- Auth headers are added automatically - DO NOT set Authorization header.
47916
+ var DEFAULT_TIMEOUT = 1e4;
47917
+ var DEFAULT_MAX_TIMEOUT = 3e4;
47918
+ function formatConnectorEntry(c) {
47919
+ const parts = [];
47920
+ const serviceOrVendor = c.serviceType ?? c.vendor ?? void 0;
47921
+ if (serviceOrVendor) parts.push(`Service: ${serviceOrVendor}`);
47922
+ if (c.config.description) parts.push(c.config.description);
47923
+ if (c.baseURL) parts.push(`URL: ${c.baseURL}`);
47924
+ const details = parts.map((p) => ` ${p}`).join("\n");
47925
+ return ` \u2022 "${c.name}" (${c.displayName})
47926
+ ${details}`;
47927
+ }
47928
+ function generateDescription(context, maxTimeout) {
47929
+ const registry = context?.connectorRegistry ?? Connector.asRegistry();
47930
+ const connectors = registry.listAll();
47931
+ const connectorList = connectors.length > 0 ? connectors.map(formatConnectorEntry).join("\n\n") : " No connectors registered.";
47932
+ const timeoutSec = Math.round(maxTimeout / 1e3);
47933
+ return `Execute JavaScript code in a secure sandbox with authenticated API access to external services.
47934
+
47935
+ Use this tool when you need to:
47936
+ - Call external APIs (GitHub, Slack, Stripe, etc.) using registered connectors
47937
+ - Process, transform, or compute data that requires programmatic logic
47938
+ - Chain multiple API calls or perform complex data manipulation
47939
+ - Do anything that plain text generation cannot accomplish
47940
+
47941
+ SANDBOX API:
47942
+
47943
+ 1. authenticatedFetch(url, options, connectorName)
47944
+ Makes authenticated HTTP requests using the connector's credentials.
47945
+ The current user's identity (userId) is automatically included \u2014 no need to pass it.
47946
+ Auth headers are added automatically \u2014 DO NOT set Authorization header manually.
48100
47947
 
48101
47948
  Parameters:
48102
- \u2022 url: Full URL or relative path (uses connector's baseURL)
47949
+ \u2022 url: Full URL or path relative to the connector's base URL
48103
47950
  - Full: "https://api.github.com/user/repos"
48104
- - Relative: "/user/repos" (appended to connector's baseURL)
48105
- \u2022 options: Standard fetch options { method, body, headers }
48106
- \u2022 connectorName: One of the registered connectors below
48107
- \u2022 userId: (optional) For multi-tenant apps with per-user tokens
47951
+ - Relative: "/user/repos" (resolved against connector's base URL)
47952
+ \u2022 options: Standard fetch options { method, headers, body }
47953
+ - For POST/PUT: set body to JSON.stringify(data) and headers to { 'Content-Type': 'application/json' }
47954
+ \u2022 connectorName: Name of a registered connector (see list below)
48108
47955
 
48109
47956
  Returns: Promise<Response>
48110
- \u2022 response.ok - true if status 200-299
48111
- \u2022 response.status - HTTP status code
48112
- \u2022 response.json() - parse JSON body
48113
- \u2022 response.text() - get text body
47957
+ \u2022 response.ok \u2014 true if status 200-299
47958
+ \u2022 response.status \u2014 HTTP status code
47959
+ \u2022 await response.json() \u2014 parse JSON body
47960
+ \u2022 await response.text() \u2014 get text body
48114
47961
 
48115
- Auth Schemes (handled automatically per connector):
48116
- \u2022 Bearer tokens (GitHub, Slack, Stripe)
48117
- \u2022 Bot tokens (Discord)
48118
- \u2022 Basic auth (Twilio, Zendesk)
48119
- \u2022 Custom headers (Shopify uses X-Shopify-Access-Token)
47962
+ 2. fetch(url, options) \u2014 Standard fetch without authentication
48120
47963
 
48121
- 2. connectors.list() - List available connector names
48122
- 3. connectors.get(name) - Get connector info { displayName, description, baseURL }
48123
- 4. fetch(url, options) - Standard fetch (no auth)
47964
+ 3. connectors.list() \u2014 Array of available connector names
47965
+ 4. connectors.get(name) \u2014 Connector info: { displayName, description, baseURL, serviceType }
48124
47966
 
48125
- INPUT/OUTPUT:
48126
- \u2022 input - data passed to your code via the "input" parameter
48127
- \u2022 output - SET THIS variable to return your result
47967
+ VARIABLES:
47968
+ \u2022 input \u2014 data passed via the "input" parameter (default: {})
47969
+ \u2022 output \u2014 SET THIS to return your result to the caller
48128
47970
 
48129
- UTILITIES: console.log/error/warn, Buffer, JSON, Math, Date, Promise
47971
+ GLOBALS: console.log/error/warn, JSON, Math, Date, Buffer, Promise, Array, Object, String, Number, Boolean, setTimeout, setInterval, URL, URLSearchParams, RegExp, Map, Set, Error, TextEncoder, TextDecoder
48130
47972
 
48131
47973
  REGISTERED CONNECTORS:
48132
47974
  ${connectorList}
48133
47975
 
48134
- EXAMPLE:
48135
- (async () => {
48136
- const response = await authenticatedFetch(
48137
- '/user/repos',
48138
- { method: 'GET' },
48139
- 'github'
48140
- );
47976
+ EXAMPLES:
48141
47977
 
48142
- if (!response.ok) {
48143
- throw new Error(\`API error: \${response.status}\`);
48144
- }
47978
+ // GET request
47979
+ const resp = await authenticatedFetch('/user/repos', { method: 'GET' }, 'github');
47980
+ const repos = await resp.json();
47981
+ output = repos.map(r => r.full_name);
48145
47982
 
48146
- const repos = await response.json();
48147
- console.log(\`Found \${repos.length} repositories\`);
47983
+ // POST request with JSON body
47984
+ const resp = await authenticatedFetch('/chat.postMessage', {
47985
+ method: 'POST',
47986
+ headers: { 'Content-Type': 'application/json' },
47987
+ body: JSON.stringify({ channel: '#general', text: 'Hello!' })
47988
+ }, 'slack');
47989
+ output = await resp.json();
48148
47990
 
48149
- output = repos;
48150
- })();
47991
+ // Data processing (no API needed)
47992
+ const items = input.data;
47993
+ output = items.filter(i => i.score > 0.8).sort((a, b) => b.score - a.score);
48151
47994
 
48152
- SECURITY: 10s timeout, no file system, no require/import.`;
47995
+ LIMITS: ${timeoutSec}s max timeout, no file system access, no require/import.`;
48153
47996
  }
48154
- function createExecuteJavaScriptTool() {
47997
+ function createExecuteJavaScriptTool(options) {
47998
+ const maxTimeout = options?.maxTimeout ?? DEFAULT_MAX_TIMEOUT;
47999
+ const defaultTimeout = options?.defaultTimeout ?? DEFAULT_TIMEOUT;
48155
48000
  return {
48156
48001
  definition: {
48157
48002
  type: "function",
@@ -48164,32 +48009,40 @@ function createExecuteJavaScriptTool() {
48164
48009
  properties: {
48165
48010
  code: {
48166
48011
  type: "string",
48167
- description: 'JavaScript code to execute. MUST set the "output" variable. Wrap in async IIFE for async operations.'
48012
+ description: 'JavaScript code to execute. Set the "output" variable with your result. Code is auto-wrapped in async IIFE \u2014 you can use await directly. For explicit async control, wrap in (async () => { ... })().'
48168
48013
  },
48169
48014
  input: {
48170
- description: 'Optional input data available as "input" variable in your code'
48015
+ description: 'Optional data available as the "input" variable in your code. Can be any JSON value.'
48171
48016
  },
48172
48017
  timeout: {
48173
48018
  type: "number",
48174
- description: "Execution timeout in milliseconds (default: 10000, max: 30000)"
48019
+ description: `Execution timeout in milliseconds. Default: ${defaultTimeout}ms, max: ${maxTimeout}ms. Increase for slow API calls or multiple sequential requests.`
48175
48020
  }
48176
48021
  },
48177
48022
  required: ["code"]
48178
48023
  }
48179
48024
  },
48180
48025
  blocking: true,
48181
- timeout: 35e3
48182
- // Tool timeout (slightly more than max code timeout)
48026
+ timeout: maxTimeout + 5e3
48027
+ // Tool-level timeout slightly above max code timeout
48183
48028
  },
48184
- // Dynamic description - evaluated each time tool definitions are sent to LLM
48185
- // This ensures the connector list is always current
48186
- descriptionFactory: generateDescription,
48187
- execute: async (args) => {
48029
+ // Dynamic description regenerated each time tool definitions are sent to LLM.
48030
+ // Receives ToolContext so connector list is scoped to current userId.
48031
+ descriptionFactory: (context) => generateDescription(context, maxTimeout),
48032
+ execute: async (args, context) => {
48188
48033
  const logs = [];
48189
48034
  const startTime = Date.now();
48190
48035
  try {
48191
- const timeout = Math.min(args.timeout || 1e4, 3e4);
48192
- const result = await executeInVM(args.code, args.input, timeout, logs);
48036
+ const timeout = Math.min(Math.max(args.timeout || defaultTimeout, 0), maxTimeout);
48037
+ const registry = context?.connectorRegistry ?? Connector.asRegistry();
48038
+ const result = await executeInVM(
48039
+ args.code,
48040
+ args.input,
48041
+ timeout,
48042
+ logs,
48043
+ context?.userId,
48044
+ registry
48045
+ );
48193
48046
  return {
48194
48047
  success: true,
48195
48048
  result,
@@ -48209,31 +48062,36 @@ function createExecuteJavaScriptTool() {
48209
48062
  };
48210
48063
  }
48211
48064
  var executeJavaScript = createExecuteJavaScriptTool();
48212
- async function executeInVM(code, input, timeout, logs) {
48065
+ async function executeInVM(code, input, timeout, logs, userId, registry) {
48213
48066
  const sandbox = {
48214
48067
  // Input/output
48215
- input: input || {},
48068
+ input: input ?? {},
48216
48069
  output: null,
48217
- // Console (captured)
48070
+ // Console (captured) — stringify objects for readable logs
48218
48071
  console: {
48219
- log: (...args) => logs.push(args.map((a) => String(a)).join(" ")),
48220
- error: (...args) => logs.push("ERROR: " + args.map((a) => String(a)).join(" ")),
48221
- warn: (...args) => logs.push("WARN: " + args.map((a) => String(a)).join(" "))
48072
+ log: (...args) => logs.push(args.map((a) => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ")),
48073
+ error: (...args) => logs.push("ERROR: " + args.map((a) => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ")),
48074
+ warn: (...args) => logs.push("WARN: " + args.map((a) => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" "))
48222
48075
  },
48223
- // Authenticated fetch
48224
- authenticatedFetch,
48225
- // Standard fetch
48076
+ // Authenticated fetch — userId auto-injected from ToolContext.
48077
+ // Only connectors visible in the scoped registry are accessible.
48078
+ authenticatedFetch: (url2, options, connectorName) => {
48079
+ registry.get(connectorName);
48080
+ return authenticatedFetch(url2, options, connectorName, userId);
48081
+ },
48082
+ // Standard fetch (no auth)
48226
48083
  fetch: globalThis.fetch,
48227
- // Connector info
48084
+ // Connector info (userId-scoped)
48228
48085
  connectors: {
48229
- list: () => Connector.list(),
48086
+ list: () => registry.list(),
48230
48087
  get: (name) => {
48231
48088
  try {
48232
- const connector = Connector.get(name);
48089
+ const connector = registry.get(name);
48233
48090
  return {
48234
48091
  displayName: connector.displayName,
48235
48092
  description: connector.config.description || "",
48236
- baseURL: connector.baseURL
48093
+ baseURL: connector.baseURL,
48094
+ serviceType: connector.serviceType
48237
48095
  };
48238
48096
  } catch {
48239
48097
  return null;
@@ -48250,14 +48108,22 @@ async function executeInVM(code, input, timeout, logs) {
48250
48108
  clearTimeout,
48251
48109
  clearInterval,
48252
48110
  Promise,
48253
- // Array/Object
48111
+ // Built-in types
48254
48112
  Array,
48255
48113
  Object,
48256
48114
  String,
48257
48115
  Number,
48258
- Boolean
48116
+ Boolean,
48117
+ RegExp,
48118
+ Map,
48119
+ Set,
48120
+ Error,
48121
+ URL,
48122
+ URLSearchParams,
48123
+ TextEncoder,
48124
+ TextDecoder
48259
48125
  };
48260
- const context = vm.createContext(sandbox);
48126
+ const vmContext = vm.createContext(sandbox);
48261
48127
  const wrappedCode = code.trim().startsWith("(async") ? code : `
48262
48128
  (async () => {
48263
48129
  ${code}
@@ -48265,7 +48131,7 @@ async function executeInVM(code, input, timeout, logs) {
48265
48131
  })()
48266
48132
  `;
48267
48133
  const script = new vm.Script(wrappedCode);
48268
- const resultPromise = script.runInContext(context, {
48134
+ const resultPromise = script.runInContext(vmContext, {
48269
48135
  timeout,
48270
48136
  displayErrors: true
48271
48137
  });
@@ -48721,7 +48587,7 @@ function createTextToSpeechTool(connector, storage, userId) {
48721
48587
  }
48722
48588
 
48723
48589
  // src/tools/multimedia/speechToText.ts
48724
- function createSpeechToTextTool(connector, storage, _userId) {
48590
+ function createSpeechToTextTool(connector, storage) {
48725
48591
  const vendor = connector.vendor;
48726
48592
  const handler = storage ?? getMediaStorage();
48727
48593
  const vendorModels = vendor ? getSTTModelsByVendor(vendor) : [];
@@ -48982,7 +48848,8 @@ EXAMPLES:
48982
48848
  riskLevel: "low",
48983
48849
  approvalMessage: `Search files in a GitHub repository via ${connector.displayName}`
48984
48850
  },
48985
- execute: async (args) => {
48851
+ execute: async (args, context) => {
48852
+ const effectiveUserId = context?.userId ?? userId;
48986
48853
  const resolved = resolveRepository(args.repository, connector);
48987
48854
  if (!resolved.success) {
48988
48855
  return { success: false, error: resolved.error };
@@ -48994,14 +48861,14 @@ EXAMPLES:
48994
48861
  const repoInfo = await githubFetch(
48995
48862
  connector,
48996
48863
  `/repos/${owner}/${repo}`,
48997
- { userId }
48864
+ { userId: effectiveUserId }
48998
48865
  );
48999
48866
  ref = repoInfo.default_branch;
49000
48867
  }
49001
48868
  const tree = await githubFetch(
49002
48869
  connector,
49003
48870
  `/repos/${owner}/${repo}/git/trees/${ref}?recursive=1`,
49004
- { userId }
48871
+ { userId: effectiveUserId }
49005
48872
  );
49006
48873
  const matching = tree.tree.filter(
49007
48874
  (entry) => entry.type === "blob" && matchGlobPattern2(args.pattern, entry.path)
@@ -49092,7 +48959,8 @@ EXAMPLES:
49092
48959
  riskLevel: "low",
49093
48960
  approvalMessage: `Search code in a GitHub repository via ${connector.displayName}`
49094
48961
  },
49095
- execute: async (args) => {
48962
+ execute: async (args, context) => {
48963
+ const effectiveUserId = context?.userId ?? userId;
49096
48964
  const resolved = resolveRepository(args.repository, connector);
49097
48965
  if (!resolved.success) {
49098
48966
  return { success: false, error: resolved.error };
@@ -49109,7 +48977,7 @@ EXAMPLES:
49109
48977
  connector,
49110
48978
  `/search/code`,
49111
48979
  {
49112
- userId,
48980
+ userId: effectiveUserId,
49113
48981
  // Request text-match fragments
49114
48982
  accept: "application/vnd.github.text-match+json",
49115
48983
  queryParams: { q, per_page: perPage }
@@ -49196,7 +49064,8 @@ NOTE: Files larger than 1MB are fetched via the Git Blob API. Very large files (
49196
49064
  riskLevel: "low",
49197
49065
  approvalMessage: `Read a file from a GitHub repository via ${connector.displayName}`
49198
49066
  },
49199
- execute: async (args) => {
49067
+ execute: async (args, context) => {
49068
+ const effectiveUserId = context?.userId ?? userId;
49200
49069
  const resolved = resolveRepository(args.repository, connector);
49201
49070
  if (!resolved.success) {
49202
49071
  return { success: false, error: resolved.error };
@@ -49210,7 +49079,7 @@ NOTE: Files larger than 1MB are fetched via the Git Blob API. Very large files (
49210
49079
  const contentResp = await githubFetch(
49211
49080
  connector,
49212
49081
  `/repos/${owner}/${repo}/contents/${args.path}${refParam}`,
49213
- { userId }
49082
+ { userId: effectiveUserId }
49214
49083
  );
49215
49084
  if (contentResp.type !== "file") {
49216
49085
  return {
@@ -49227,7 +49096,7 @@ NOTE: Files larger than 1MB are fetched via the Git Blob API. Very large files (
49227
49096
  const blob = await githubFetch(
49228
49097
  connector,
49229
49098
  contentResp.git_url,
49230
- { userId }
49099
+ { userId: effectiveUserId }
49231
49100
  );
49232
49101
  fileContent = Buffer.from(blob.content, "base64").toString("utf-8");
49233
49102
  fileSize = blob.size;
@@ -49314,7 +49183,8 @@ EXAMPLES:
49314
49183
  riskLevel: "low",
49315
49184
  approvalMessage: `Get pull request details from GitHub via ${connector.displayName}`
49316
49185
  },
49317
- execute: async (args) => {
49186
+ execute: async (args, context) => {
49187
+ const effectiveUserId = context?.userId ?? userId;
49318
49188
  const resolved = resolveRepository(args.repository, connector);
49319
49189
  if (!resolved.success) {
49320
49190
  return { success: false, error: resolved.error };
@@ -49324,7 +49194,7 @@ EXAMPLES:
49324
49194
  const pr = await githubFetch(
49325
49195
  connector,
49326
49196
  `/repos/${owner}/${repo}/pulls/${args.pull_number}`,
49327
- { userId }
49197
+ { userId: effectiveUserId }
49328
49198
  );
49329
49199
  return {
49330
49200
  success: true,
@@ -49400,7 +49270,8 @@ NOTE: Very large diffs may be truncated by GitHub. Patch content may be absent f
49400
49270
  riskLevel: "low",
49401
49271
  approvalMessage: `Get PR changed files from GitHub via ${connector.displayName}`
49402
49272
  },
49403
- execute: async (args) => {
49273
+ execute: async (args, context) => {
49274
+ const effectiveUserId = context?.userId ?? userId;
49404
49275
  const resolved = resolveRepository(args.repository, connector);
49405
49276
  if (!resolved.success) {
49406
49277
  return { success: false, error: resolved.error };
@@ -49411,7 +49282,7 @@ NOTE: Very large diffs may be truncated by GitHub. Patch content may be absent f
49411
49282
  connector,
49412
49283
  `/repos/${owner}/${repo}/pulls/${args.pull_number}/files`,
49413
49284
  {
49414
- userId,
49285
+ userId: effectiveUserId,
49415
49286
  queryParams: { per_page: 100 }
49416
49287
  }
49417
49288
  );
@@ -49482,7 +49353,8 @@ EXAMPLES:
49482
49353
  riskLevel: "low",
49483
49354
  approvalMessage: `Get PR comments and reviews from GitHub via ${connector.displayName}`
49484
49355
  },
49485
- execute: async (args) => {
49356
+ execute: async (args, context) => {
49357
+ const effectiveUserId = context?.userId ?? userId;
49486
49358
  const resolved = resolveRepository(args.repository, connector);
49487
49359
  if (!resolved.success) {
49488
49360
  return { success: false, error: resolved.error };
@@ -49490,7 +49362,7 @@ EXAMPLES:
49490
49362
  const { owner, repo } = resolved.repo;
49491
49363
  try {
49492
49364
  const basePath = `/repos/${owner}/${repo}`;
49493
- const queryOpts = { userId, queryParams: { per_page: 100 } };
49365
+ const queryOpts = { userId: effectiveUserId, queryParams: { per_page: 100 } };
49494
49366
  const [reviewComments, reviews, issueComments] = await Promise.all([
49495
49367
  githubFetch(
49496
49368
  connector,
@@ -49617,7 +49489,8 @@ EXAMPLES:
49617
49489
  riskLevel: "medium",
49618
49490
  approvalMessage: `Create a pull request on GitHub via ${connector.displayName}`
49619
49491
  },
49620
- execute: async (args) => {
49492
+ execute: async (args, context) => {
49493
+ const effectiveUserId = context?.userId ?? userId;
49621
49494
  const resolved = resolveRepository(args.repository, connector);
49622
49495
  if (!resolved.success) {
49623
49496
  return { success: false, error: resolved.error };
@@ -49629,7 +49502,7 @@ EXAMPLES:
49629
49502
  `/repos/${owner}/${repo}/pulls`,
49630
49503
  {
49631
49504
  method: "POST",
49632
- userId,
49505
+ userId: effectiveUserId,
49633
49506
  body: {
49634
49507
  title: args.title,
49635
49508
  body: args.body,
@@ -49767,37 +49640,6 @@ var toolRegistry = [
49767
49640
  description: "Fetch and extract text content from a web page URL.",
49768
49641
  tool: webFetch,
49769
49642
  safeByDefault: true
49770
- },
49771
- {
49772
- name: "web_fetch_js",
49773
- exportName: "webFetchJS",
49774
- displayName: "Web Fetch Js",
49775
- category: "web",
49776
- description: "Fetch and extract content from JavaScript-rendered websites using a headless browser (Puppeteer).",
49777
- tool: webFetchJS,
49778
- safeByDefault: true
49779
- },
49780
- {
49781
- name: "web_scrape",
49782
- exportName: "webScrape",
49783
- displayName: "Web Scrape",
49784
- category: "web",
49785
- description: "Scrape any URL with automatic fallback - guaranteed to work on most sites.",
49786
- tool: webScrape,
49787
- safeByDefault: true,
49788
- requiresConnector: true,
49789
- connectorServiceTypes: ["zenrows"]
49790
- },
49791
- {
49792
- name: "web_search",
49793
- exportName: "webSearch",
49794
- displayName: "Web Search",
49795
- category: "web",
49796
- description: "Search the web and get relevant results with snippets.",
49797
- tool: webSearch,
49798
- safeByDefault: true,
49799
- requiresConnector: true,
49800
- connectorServiceTypes: ["serper", "brave-search", "tavily", "rapidapi-websearch"]
49801
49643
  }
49802
49644
  ];
49803
49645
  function getAllBuiltInTools() {