@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.cjs CHANGED
@@ -2034,7 +2034,14 @@ var init_Connector = __esm({
2034
2034
  }
2035
2035
  const startTime = Date.now();
2036
2036
  this.requestCount++;
2037
- const url2 = endpoint.startsWith("http") ? endpoint : `${this.baseURL}${endpoint}`;
2037
+ let url2;
2038
+ if (endpoint.startsWith("http")) {
2039
+ url2 = endpoint;
2040
+ } else {
2041
+ const base = (this.baseURL ?? "").replace(/\/+$/, "");
2042
+ const path6 = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
2043
+ url2 = `${base}${path6}`;
2044
+ }
2038
2045
  const timeout = options?.timeout ?? this.config.timeout ?? exports.DEFAULT_CONNECTOR_TIMEOUT;
2039
2046
  if (this.config.logging?.enabled) {
2040
2047
  this.logRequest(url2, options);
@@ -18437,6 +18444,9 @@ var BasePluginNextGen = class {
18437
18444
  }
18438
18445
  };
18439
18446
 
18447
+ // src/core/context-nextgen/AgentContextNextGen.ts
18448
+ init_Connector();
18449
+
18440
18450
  // src/domain/entities/Memory.ts
18441
18451
  function isTaskAwareScope(scope) {
18442
18452
  return typeof scope === "object" && scope !== null && "type" in scope;
@@ -19375,13 +19385,18 @@ Values are immediately visible - no retrieval needed.
19375
19385
  - \`high\`: Keep longer. Important state.
19376
19386
  - \`critical\`: Never auto-evicted.
19377
19387
 
19388
+ **UI Display:** Set \`showInUI: true\` in context_set to display the entry in the user's side panel.
19389
+ Values shown in the UI support the same rich markdown formatting as the chat window
19390
+ (see formatting instructions above). Use this for dashboards, progress displays, and results the user should see.
19391
+
19378
19392
  **Tools:** context_set, context_delete, context_list`;
19379
19393
  var contextSetDefinition = {
19380
19394
  type: "function",
19381
19395
  function: {
19382
19396
  name: "context_set",
19383
19397
  description: `Store or update a key-value pair in live context.
19384
- Value appears directly in context - no retrieval needed.`,
19398
+ Value appears directly in context - no retrieval needed.
19399
+ Set showInUI to true to also display the entry in the user's side panel.`,
19385
19400
  parameters: {
19386
19401
  type: "object",
19387
19402
  properties: {
@@ -19392,6 +19407,10 @@ Value appears directly in context - no retrieval needed.`,
19392
19407
  type: "string",
19393
19408
  enum: ["low", "normal", "high", "critical"],
19394
19409
  description: 'Eviction priority. Default: "normal"'
19410
+ },
19411
+ showInUI: {
19412
+ type: "boolean",
19413
+ 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"
19395
19414
  }
19396
19415
  },
19397
19416
  required: ["key", "description", "value"]
@@ -19432,6 +19451,7 @@ var InContextMemoryPluginNextGen = class {
19432
19451
  _destroyed = false;
19433
19452
  _tokenCache = null;
19434
19453
  _instructionsTokenCache = null;
19454
+ _notifyTimer = null;
19435
19455
  constructor(config = {}) {
19436
19456
  this.config = { ...DEFAULT_CONFIG, ...config };
19437
19457
  }
@@ -19472,15 +19492,18 @@ var InContextMemoryPluginNextGen = class {
19472
19492
  return a.updatedAt - b.updatedAt;
19473
19493
  });
19474
19494
  let freed = 0;
19495
+ let evicted = false;
19475
19496
  for (const entry of evictable) {
19476
19497
  if (freed >= targetTokensToFree) break;
19477
19498
  const entryTokens = this.estimator.estimateTokens(this.formatEntry(entry));
19478
19499
  this.entries.delete(entry.key);
19479
19500
  freed += entryTokens;
19501
+ evicted = true;
19480
19502
  }
19481
19503
  this._tokenCache = null;
19482
19504
  const content = await this.getContent();
19483
19505
  const after = content ? this.estimator.estimateTokens(content) : 0;
19506
+ if (evicted) this.notifyEntriesChanged();
19484
19507
  return Math.max(0, before - after);
19485
19508
  }
19486
19509
  getTools() {
@@ -19492,6 +19515,7 @@ var InContextMemoryPluginNextGen = class {
19492
19515
  }
19493
19516
  destroy() {
19494
19517
  if (this._destroyed) return;
19518
+ if (this._notifyTimer) clearTimeout(this._notifyTimer);
19495
19519
  this.entries.clear();
19496
19520
  this._destroyed = true;
19497
19521
  this._tokenCache = null;
@@ -19509,6 +19533,7 @@ var InContextMemoryPluginNextGen = class {
19509
19533
  this.entries.set(entry.key, entry);
19510
19534
  }
19511
19535
  this._tokenCache = null;
19536
+ this.notifyEntriesChanged();
19512
19537
  }
19513
19538
  // ============================================================================
19514
19539
  // Entry Management
@@ -19516,19 +19541,21 @@ var InContextMemoryPluginNextGen = class {
19516
19541
  /**
19517
19542
  * Store or update a key-value pair
19518
19543
  */
19519
- set(key, description, value, priority) {
19544
+ set(key, description, value, priority, showInUI) {
19520
19545
  this.assertNotDestroyed();
19521
19546
  const entry = {
19522
19547
  key,
19523
19548
  description,
19524
19549
  value,
19525
19550
  updatedAt: Date.now(),
19526
- priority: priority ?? this.config.defaultPriority
19551
+ priority: priority ?? this.config.defaultPriority,
19552
+ showInUI: showInUI ?? false
19527
19553
  };
19528
19554
  this.entries.set(key, entry);
19529
19555
  this.enforceMaxEntries();
19530
19556
  this.enforceTokenLimit();
19531
19557
  this._tokenCache = null;
19558
+ this.notifyEntriesChanged();
19532
19559
  }
19533
19560
  /**
19534
19561
  * Get a value by key
@@ -19550,7 +19577,10 @@ var InContextMemoryPluginNextGen = class {
19550
19577
  delete(key) {
19551
19578
  this.assertNotDestroyed();
19552
19579
  const deleted = this.entries.delete(key);
19553
- if (deleted) this._tokenCache = null;
19580
+ if (deleted) {
19581
+ this._tokenCache = null;
19582
+ this.notifyEntriesChanged();
19583
+ }
19554
19584
  return deleted;
19555
19585
  }
19556
19586
  /**
@@ -19562,7 +19592,8 @@ var InContextMemoryPluginNextGen = class {
19562
19592
  key: e.key,
19563
19593
  description: e.description,
19564
19594
  priority: e.priority,
19565
- updatedAt: e.updatedAt
19595
+ updatedAt: e.updatedAt,
19596
+ showInUI: e.showInUI ?? false
19566
19597
  }));
19567
19598
  }
19568
19599
  /**
@@ -19572,6 +19603,7 @@ var InContextMemoryPluginNextGen = class {
19572
19603
  this.assertNotDestroyed();
19573
19604
  this.entries.clear();
19574
19605
  this._tokenCache = null;
19606
+ this.notifyEntriesChanged();
19575
19607
  }
19576
19608
  // ============================================================================
19577
19609
  // Private Helpers
@@ -19635,6 +19667,20 @@ ${valueStr}
19635
19667
  return a.updatedAt - b.updatedAt;
19636
19668
  });
19637
19669
  }
19670
+ /**
19671
+ * Debounced notification when entries change.
19672
+ * Calls config.onEntriesChanged with all current entries.
19673
+ */
19674
+ notifyEntriesChanged() {
19675
+ if (!this.config.onEntriesChanged) return;
19676
+ if (this._notifyTimer) clearTimeout(this._notifyTimer);
19677
+ this._notifyTimer = setTimeout(() => {
19678
+ this._notifyTimer = null;
19679
+ if (!this._destroyed && this.config.onEntriesChanged) {
19680
+ this.config.onEntriesChanged(Array.from(this.entries.values()));
19681
+ }
19682
+ }, 100);
19683
+ }
19638
19684
  assertNotDestroyed() {
19639
19685
  if (this._destroyed) {
19640
19686
  throw new Error("InContextMemoryPluginNextGen is destroyed");
@@ -19651,16 +19697,18 @@ ${valueStr}
19651
19697
  args.key,
19652
19698
  args.description,
19653
19699
  args.value,
19654
- args.priority
19700
+ args.priority,
19701
+ args.showInUI
19655
19702
  );
19656
19703
  return {
19657
19704
  success: true,
19658
19705
  key: args.key,
19659
- message: `Stored "${args.key}" in live context`
19706
+ showInUI: args.showInUI ?? false,
19707
+ message: `Stored "${args.key}" in live context${args.showInUI ? " (visible in UI)" : ""}`
19660
19708
  };
19661
19709
  },
19662
19710
  permission: { scope: "always", riskLevel: "low" },
19663
- describeCall: (args) => `set ${args.key}`
19711
+ describeCall: (args) => `set ${args.key}${args.showInUI ? " [UI]" : ""}`
19664
19712
  };
19665
19713
  }
19666
19714
  createContextDeleteTool() {
@@ -20972,6 +21020,10 @@ var AgentContextNextGen = class _AgentContextNextGen extends eventemitter3.Event
20972
21020
  _sessionId = null;
20973
21021
  /** Agent ID */
20974
21022
  _agentId;
21023
+ /** User ID for multi-user scenarios */
21024
+ _userId;
21025
+ /** Allowed connector names (when agent is restricted to a subset) */
21026
+ _allowedConnectors;
20975
21027
  /** Storage backend */
20976
21028
  _storage;
20977
21029
  /** Destroyed flag */
@@ -21008,6 +21060,8 @@ var AgentContextNextGen = class _AgentContextNextGen extends eventemitter3.Event
21008
21060
  };
21009
21061
  this._systemPrompt = config.systemPrompt;
21010
21062
  this._agentId = this._config.agentId;
21063
+ this._userId = config.userId;
21064
+ this._allowedConnectors = config.connectors;
21011
21065
  this._storage = config.storage;
21012
21066
  this._compactionStrategy = config.compactionStrategy ?? StrategyRegistry.create(this._config.strategy);
21013
21067
  this._tools = new ToolManager(
@@ -21019,6 +21073,7 @@ var AgentContextNextGen = class _AgentContextNextGen extends eventemitter3.Event
21019
21073
  }
21020
21074
  }
21021
21075
  this.initializePlugins(config.plugins);
21076
+ this.syncToolContext();
21022
21077
  }
21023
21078
  /**
21024
21079
  * Initialize plugins based on feature flags.
@@ -21063,6 +21118,62 @@ var AgentContextNextGen = class _AgentContextNextGen extends eventemitter3.Event
21063
21118
  );
21064
21119
  }
21065
21120
  }
21121
+ /**
21122
+ * Sync identity fields and connector registry to ToolContext.
21123
+ * Merges with existing ToolContext to preserve other fields (memory, signal, taskId).
21124
+ *
21125
+ * Connector registry resolution order:
21126
+ * 1. If `connectors` (allowed names) is set → filtered view of global registry
21127
+ * 2. If access policy + userId → scoped view via Connector.scoped()
21128
+ * 3. Otherwise → full global registry
21129
+ */
21130
+ syncToolContext() {
21131
+ const existing = this._tools.getToolContext();
21132
+ this._tools.setToolContext({
21133
+ ...existing,
21134
+ agentId: this._agentId,
21135
+ userId: this._userId,
21136
+ connectorRegistry: this.buildConnectorRegistry()
21137
+ });
21138
+ }
21139
+ /**
21140
+ * Build the connector registry appropriate for this agent's config.
21141
+ */
21142
+ buildConnectorRegistry() {
21143
+ if (this._allowedConnectors?.length) {
21144
+ const allowedSet = new Set(this._allowedConnectors);
21145
+ const base = this._userId && exports.Connector.getAccessPolicy() ? exports.Connector.scoped({ userId: this._userId }) : exports.Connector.asRegistry();
21146
+ return {
21147
+ get: (name) => {
21148
+ if (!allowedSet.has(name)) {
21149
+ const available = this._allowedConnectors.filter((n) => base.has(n)).join(", ") || "none";
21150
+ throw new Error(`Connector '${name}' not found. Available: ${available}`);
21151
+ }
21152
+ return base.get(name);
21153
+ },
21154
+ has: (name) => allowedSet.has(name) && base.has(name),
21155
+ list: () => base.list().filter((n) => allowedSet.has(n)),
21156
+ listAll: () => base.listAll().filter((c) => allowedSet.has(c.name)),
21157
+ size: () => base.listAll().filter((c) => allowedSet.has(c.name)).length,
21158
+ getDescriptionsForTools: () => {
21159
+ const connectors = base.listAll().filter((c) => allowedSet.has(c.name));
21160
+ if (connectors.length === 0) return "No connectors registered yet.";
21161
+ return connectors.map((c) => ` - "${c.name}": ${c.displayName} - ${c.config.description || "No description"}`).join("\n");
21162
+ },
21163
+ getInfo: () => {
21164
+ const info = {};
21165
+ for (const c of base.listAll().filter((c2) => allowedSet.has(c2.name))) {
21166
+ info[c.name] = { displayName: c.displayName, description: c.config.description || "", baseURL: c.baseURL };
21167
+ }
21168
+ return info;
21169
+ }
21170
+ };
21171
+ }
21172
+ if (this._userId && exports.Connector.getAccessPolicy()) {
21173
+ return exports.Connector.scoped({ userId: this._userId });
21174
+ }
21175
+ return exports.Connector.asRegistry();
21176
+ }
21066
21177
  // ============================================================================
21067
21178
  // Public Properties
21068
21179
  // ============================================================================
@@ -21078,6 +21189,24 @@ var AgentContextNextGen = class _AgentContextNextGen extends eventemitter3.Event
21078
21189
  get agentId() {
21079
21190
  return this._agentId;
21080
21191
  }
21192
+ /** Get the current user ID */
21193
+ get userId() {
21194
+ return this._userId;
21195
+ }
21196
+ /** Set user ID. Automatically updates ToolContext for all tool executions. */
21197
+ set userId(value) {
21198
+ this._userId = value;
21199
+ this.syncToolContext();
21200
+ }
21201
+ /** Get the allowed connector names (undefined = all visible connectors) */
21202
+ get connectors() {
21203
+ return this._allowedConnectors;
21204
+ }
21205
+ /** Set allowed connector names. Updates ToolContext.connectorRegistry. */
21206
+ set connectors(value) {
21207
+ this._allowedConnectors = value;
21208
+ this.syncToolContext();
21209
+ }
21081
21210
  /** Get/set system prompt */
21082
21211
  get systemPrompt() {
21083
21212
  return this._systemPrompt;
@@ -21991,6 +22120,7 @@ ${content}`);
21991
22120
  metadata: {
21992
22121
  savedAt: Date.now(),
21993
22122
  agentId: this._agentId,
22123
+ userId: this._userId,
21994
22124
  model: this._config.model
21995
22125
  }
21996
22126
  };
@@ -24953,6 +25083,8 @@ var BaseAgent = class extends eventemitter3.EventEmitter {
24953
25083
  const contextConfig = {
24954
25084
  model: config.model,
24955
25085
  agentId: config.name,
25086
+ userId: config.userId,
25087
+ connectors: config.connectors,
24956
25088
  // Include storage and sessionId if session config is provided
24957
25089
  storage: config.session?.storage,
24958
25090
  // Thread tool execution timeout to ToolManager
@@ -25109,6 +25241,30 @@ var BaseAgent = class extends eventemitter3.EventEmitter {
25109
25241
  get context() {
25110
25242
  return this._agentContext;
25111
25243
  }
25244
+ /**
25245
+ * Get the current user ID. Delegates to AgentContextNextGen.
25246
+ */
25247
+ get userId() {
25248
+ return this._agentContext.userId;
25249
+ }
25250
+ /**
25251
+ * Set user ID at runtime. Automatically updates ToolContext for all tool executions.
25252
+ */
25253
+ set userId(value) {
25254
+ this._agentContext.userId = value;
25255
+ }
25256
+ /**
25257
+ * Get the allowed connector names (undefined = all visible connectors).
25258
+ */
25259
+ get connectors() {
25260
+ return this._agentContext.connectors;
25261
+ }
25262
+ /**
25263
+ * Restrict this agent to a subset of connectors. Updates ToolContext.connectorRegistry.
25264
+ */
25265
+ set connectors(value) {
25266
+ this._agentContext.connectors = value;
25267
+ }
25112
25268
  /**
25113
25269
  * Permission management. Returns ToolPermissionManager for approval control.
25114
25270
  */
@@ -25160,9 +25316,10 @@ var BaseAgent = class extends eventemitter3.EventEmitter {
25160
25316
  * always sees up-to-date tool descriptions.
25161
25317
  */
25162
25318
  getEnabledToolDefinitions() {
25319
+ const toolContext = this._agentContext.tools.getToolContext();
25163
25320
  return this._agentContext.tools.getEnabled().map((tool) => {
25164
25321
  if (tool.descriptionFactory) {
25165
- const dynamicDescription = tool.descriptionFactory();
25322
+ const dynamicDescription = tool.descriptionFactory(toolContext);
25166
25323
  return {
25167
25324
  ...tool.definition,
25168
25325
  function: {
@@ -26208,6 +26365,7 @@ var Agent = class _Agent extends BaseAgent {
26208
26365
  * const agent = Agent.create({
26209
26366
  * connector: 'openai', // or Connector instance
26210
26367
  * model: 'gpt-4',
26368
+ * userId: 'user-123', // flows to all tool executions automatically
26211
26369
  * instructions: 'You are a helpful assistant',
26212
26370
  * tools: [myTool]
26213
26371
  * });
@@ -41785,6 +41943,32 @@ function filterProtectedHeaders(headers) {
41785
41943
  }
41786
41944
  return filtered;
41787
41945
  }
41946
+ function normalizeBody(body) {
41947
+ if (typeof body === "string") {
41948
+ try {
41949
+ return JSON.parse(body);
41950
+ } catch {
41951
+ return body;
41952
+ }
41953
+ }
41954
+ return body;
41955
+ }
41956
+ function detectAPIError(data) {
41957
+ if (!data || typeof data !== "object") return null;
41958
+ const obj = data;
41959
+ if (obj.ok === false && typeof obj.error === "string") {
41960
+ return obj.error;
41961
+ }
41962
+ if (obj.success === false) {
41963
+ if (typeof obj.error === "string") return obj.error;
41964
+ if (typeof obj.message === "string") return obj.message;
41965
+ }
41966
+ if (obj.error && typeof obj.error === "object") {
41967
+ const err = obj.error;
41968
+ if (typeof err.message === "string") return err.message;
41969
+ }
41970
+ return null;
41971
+ }
41788
41972
  var ConnectorTools = class {
41789
41973
  /** Registry of service-specific tool factories */
41790
41974
  static factories = /* @__PURE__ */ new Map();
@@ -42038,7 +42222,7 @@ var ConnectorTools = class {
42038
42222
  },
42039
42223
  body: {
42040
42224
  type: "object",
42041
- 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.'
42225
+ 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.'
42042
42226
  },
42043
42227
  queryParams: {
42044
42228
  type: "object",
@@ -42053,7 +42237,8 @@ var ConnectorTools = class {
42053
42237
  }
42054
42238
  }
42055
42239
  },
42056
- execute: async (args) => {
42240
+ execute: async (args, context) => {
42241
+ const effectiveUserId = context?.userId ?? userId;
42057
42242
  let url2 = args.endpoint;
42058
42243
  if (args.queryParams && Object.keys(args.queryParams).length > 0) {
42059
42244
  const params = new URLSearchParams();
@@ -42066,7 +42251,8 @@ var ConnectorTools = class {
42066
42251
  let bodyStr;
42067
42252
  if (args.body) {
42068
42253
  try {
42069
- bodyStr = safeStringify2(args.body);
42254
+ const normalized = normalizeBody(args.body);
42255
+ bodyStr = safeStringify2(normalized);
42070
42256
  } catch (e) {
42071
42257
  return {
42072
42258
  success: false,
@@ -42085,7 +42271,7 @@ var ConnectorTools = class {
42085
42271
  },
42086
42272
  body: bodyStr
42087
42273
  },
42088
- userId
42274
+ effectiveUserId
42089
42275
  );
42090
42276
  const text = await response.text();
42091
42277
  let data;
@@ -42094,11 +42280,12 @@ var ConnectorTools = class {
42094
42280
  } catch {
42095
42281
  data = text;
42096
42282
  }
42283
+ const apiError = detectAPIError(data);
42097
42284
  return {
42098
- success: response.ok,
42285
+ success: response.ok && !apiError,
42099
42286
  status: response.status,
42100
- data: response.ok ? data : void 0,
42101
- error: response.ok ? void 0 : typeof data === "string" ? data : safeStringify2(data)
42287
+ data: response.ok && !apiError ? data : void 0,
42288
+ error: apiError ? apiError : response.ok ? void 0 : typeof data === "string" ? data : safeStringify2(data)
42102
42289
  };
42103
42290
  } catch (error) {
42104
42291
  return {
@@ -45321,6 +45508,8 @@ __export(tools_exports, {
45321
45508
  createSpeechToTextTool: () => createSpeechToTextTool,
45322
45509
  createTextToSpeechTool: () => createTextToSpeechTool,
45323
45510
  createVideoTools: () => createVideoTools,
45511
+ createWebScrapeTool: () => createWebScrapeTool,
45512
+ createWebSearchTool: () => createWebSearchTool,
45324
45513
  createWriteFileTool: () => createWriteFileTool,
45325
45514
  developerTools: () => developerTools,
45326
45515
  editFile: () => editFile,
@@ -45350,9 +45539,6 @@ __export(tools_exports, {
45350
45539
  toolRegistry: () => toolRegistry,
45351
45540
  validatePath: () => validatePath,
45352
45541
  webFetch: () => webFetch,
45353
- webFetchJS: () => webFetchJS,
45354
- webScrape: () => webScrape,
45355
- webSearch: () => webSearch,
45356
45542
  writeFile: () => writeFile5
45357
45543
  });
45358
45544
  var DEFAULT_FILESYSTEM_CONFIG = {
@@ -47075,6 +47261,111 @@ The tool returns a result object with:
47075
47261
  }
47076
47262
  };
47077
47263
 
47264
+ // src/tools/web/createWebSearchTool.ts
47265
+ init_Logger();
47266
+ var searchLogger = exports.logger.child({ component: "webSearch" });
47267
+ function createWebSearchTool(connector) {
47268
+ return {
47269
+ definition: {
47270
+ type: "function",
47271
+ function: {
47272
+ name: "web_search",
47273
+ description: `Search the web and get relevant results with snippets.
47274
+
47275
+ RETURNS:
47276
+ An array of search results, each containing:
47277
+ - title: Page title
47278
+ - url: Direct URL to the page
47279
+ - snippet: Short description/excerpt from the page
47280
+ - position: Search ranking position (1, 2, 3...)
47281
+
47282
+ USE CASES:
47283
+ - Find current information on any topic
47284
+ - Research multiple sources
47285
+ - Discover relevant websites
47286
+ - Find URLs to fetch with web_fetch tool
47287
+
47288
+ WORKFLOW PATTERN:
47289
+ 1. Use web_search to find relevant URLs
47290
+ 2. Use web_fetch to get full content from top results
47291
+ 3. Process and summarize the information
47292
+
47293
+ EXAMPLE:
47294
+ {
47295
+ "query": "latest AI developments 2026",
47296
+ "numResults": 5
47297
+ }`,
47298
+ parameters: {
47299
+ type: "object",
47300
+ properties: {
47301
+ query: {
47302
+ type: "string",
47303
+ description: "The search query string. Be specific for better results."
47304
+ },
47305
+ numResults: {
47306
+ type: "number",
47307
+ description: "Number of results to return (default: 10, max: 100)."
47308
+ },
47309
+ country: {
47310
+ type: "string",
47311
+ description: 'Country/region code for localized results (e.g., "us", "gb", "de")'
47312
+ },
47313
+ language: {
47314
+ type: "string",
47315
+ description: 'Language code for results (e.g., "en", "fr", "de")'
47316
+ }
47317
+ },
47318
+ required: ["query"]
47319
+ }
47320
+ },
47321
+ blocking: true,
47322
+ timeout: 15e3
47323
+ },
47324
+ execute: async (args) => {
47325
+ const numResults = args.numResults || 10;
47326
+ searchLogger.debug({ connectorName: connector.name }, "Executing search with connector");
47327
+ try {
47328
+ const searchProvider = SearchProvider.create({ connector: connector.name });
47329
+ const response = await searchProvider.search(args.query, {
47330
+ numResults,
47331
+ country: args.country,
47332
+ language: args.language
47333
+ });
47334
+ if (response.success) {
47335
+ searchLogger.debug({
47336
+ provider: response.provider,
47337
+ count: response.count
47338
+ }, "Search completed successfully");
47339
+ } else {
47340
+ searchLogger.warn({
47341
+ provider: response.provider,
47342
+ error: response.error
47343
+ }, "Search failed");
47344
+ }
47345
+ return {
47346
+ success: response.success,
47347
+ query: response.query,
47348
+ provider: response.provider,
47349
+ results: response.results,
47350
+ count: response.count,
47351
+ error: response.error
47352
+ };
47353
+ } catch (error) {
47354
+ searchLogger.error({ error: error.message, connectorName: connector.name }, "Search threw exception");
47355
+ return {
47356
+ success: false,
47357
+ query: args.query,
47358
+ provider: connector.name,
47359
+ results: [],
47360
+ count: 0,
47361
+ error: error.message || "Unknown error"
47362
+ };
47363
+ }
47364
+ },
47365
+ describeCall: (args) => `"${args.query}"${args.numResults ? ` (${args.numResults} results)` : ""}`
47366
+ };
47367
+ }
47368
+
47078
47369
  // src/tools/web/contentDetector.ts
47079
47370
  function detectContentQuality(html, text, $) {
47080
47371
  const issues = [];
@@ -47149,7 +47440,7 @@ function detectContentQuality(html, text, $) {
47149
47440
  }
47150
47441
  let suggestion;
47151
47442
  if (requiresJS && score < 50) {
47152
- suggestion = "Content quality is low. This appears to be a JavaScript-rendered site. Use the web_fetch_js tool for better results.";
47443
+ suggestion = "Content quality is low. This appears to be a JavaScript-rendered site. Use a scraping service connector for better results.";
47153
47444
  } else if (score < 30) {
47154
47445
  suggestion = "Content extraction failed or page has errors. Check the URL and try again.";
47155
47446
  }
@@ -47258,7 +47549,7 @@ The tool analyzes the fetched content and returns a quality score (0-100):
47258
47549
  - 50-79: Moderate quality, some content extracted
47259
47550
  - 0-49: Low quality, likely needs JavaScript or has errors
47260
47551
 
47261
- If the quality score is low or requiresJS is true, the tool will suggest using 'web_fetch_js' instead.
47552
+ If the quality score is low or requiresJS is true, consider using a scraping service connector for better results.
47262
47553
 
47263
47554
  RETURNS:
47264
47555
  {
@@ -47422,460 +47713,109 @@ With custom user agent:
47422
47713
  }
47423
47714
  };
47424
47715
 
47425
- // src/tools/web/webFetchJS.ts
47426
- var puppeteerModule = null;
47427
- var browserInstance = null;
47428
- async function loadPuppeteer() {
47429
- if (!puppeteerModule) {
47430
- try {
47431
- puppeteerModule = await import('puppeteer');
47432
- } catch (error) {
47433
- throw new Error("Puppeteer not installed");
47434
- }
47435
- }
47436
- return puppeteerModule;
47437
- }
47438
- async function getBrowser() {
47439
- if (!browserInstance) {
47440
- const puppeteer = await loadPuppeteer();
47441
- browserInstance = await puppeteer.launch({
47442
- headless: true,
47443
- args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]
47444
- });
47445
- process.on("exit", async () => {
47446
- if (browserInstance) {
47447
- await browserInstance.close();
47448
- }
47449
- });
47450
- }
47451
- return browserInstance;
47452
- }
47453
- var webFetchJS = {
47454
- definition: {
47455
- type: "function",
47456
- function: {
47457
- name: "web_fetch_js",
47458
- description: `Fetch and extract content from JavaScript-rendered websites using a headless browser (Puppeteer).
47459
-
47460
- USE THIS TOOL WHEN:
47461
- - The web_fetch tool returned a low quality score (<50)
47462
- - The web_fetch tool suggested using JavaScript rendering
47463
- - You know the website is built with React/Vue/Angular/Next.js
47464
- - Content loads dynamically via JavaScript
47465
- - The page requires interaction (though this tool doesn't support interaction yet)
47466
-
47467
- HOW IT WORKS:
47468
- - Launches a headless Chrome browser
47469
- - Navigates to the URL
47470
- - Waits for JavaScript to execute and content to load
47471
- - Extracts the rendered HTML and text content
47472
- - Optionally captures a screenshot
47473
-
47474
- CAPABILITIES:
47475
- - Executes all JavaScript on the page
47476
- - Waits for network to be idle (all resources loaded)
47477
- - Can wait for specific CSS selectors to appear
47478
- - Handles React, Vue, Angular, Next.js, and other SPAs
47479
- - Returns content after full JavaScript execution
47480
-
47481
- LIMITATIONS:
47482
- - Slower than web_fetch (typically 3-10 seconds vs <1 second)
47483
- - Uses more system resources (runs a full browser)
47484
- - May still fail on sites with aggressive bot detection
47485
- - Requires puppeteer to be installed (npm install puppeteer)
47486
-
47487
- PERFORMANCE:
47488
- - First call: Slower (launches browser ~1-2s)
47489
- - Subsequent calls: Faster (reuses browser instance)
47490
-
47491
- RETURNS:
47492
- {
47493
- success: boolean,
47494
- url: string,
47495
- title: string,
47496
- content: string, // Clean markdown (converted via Readability + Turndown)
47497
- screenshot: string, // Base64 PNG screenshot (if requested)
47498
- loadTime: number, // Time taken in milliseconds
47499
- excerpt: string, // Short summary excerpt (if extracted)
47500
- byline: string, // Author info (if extracted)
47501
- wasTruncated: boolean, // True if content was truncated
47502
- error: string // Error message if failed
47503
- }
47504
-
47505
- EXAMPLES:
47506
- Basic usage:
47507
- {
47508
- url: "https://react-app.com/page"
47509
- }
47510
-
47511
- Wait for specific content:
47512
- {
47513
- url: "https://app.com/dashboard",
47514
- waitForSelector: "#main-content", // Wait for this element
47515
- timeout: 20000
47716
+ // src/tools/web/createWebScrapeTool.ts
47717
+ init_Logger();
47718
+ var scrapeLogger = exports.logger.child({ component: "webScrape" });
47719
+ var DEFAULT_MIN_QUALITY = 50;
47720
+ function stripBase64DataUris(content) {
47721
+ if (!content) return content;
47722
+ let cleaned = content.replace(/!\[[^\]]*\]\(data:[^)]+\)/g, "[image removed]");
47723
+ cleaned = cleaned.replace(/url\(['"]?data:[^)]+['"]?\)/gi, "url([data-uri-removed])");
47724
+ cleaned = cleaned.replace(/data:(?:image|font|application)\/[^;]+;base64,[A-Za-z0-9+/=]{100,}/g, "[base64-data-removed]");
47725
+ return cleaned;
47516
47726
  }
47517
-
47518
- With screenshot:
47519
- {
47520
- url: "https://site.com",
47521
- takeScreenshot: true
47522
- }`,
47523
- parameters: {
47524
- type: "object",
47525
- properties: {
47526
- url: {
47527
- type: "string",
47528
- description: "The URL to fetch. Must start with http:// or https://"
47529
- },
47530
- waitForSelector: {
47531
- type: "string",
47532
- description: 'Optional CSS selector to wait for before extracting content. Example: "#main-content" or ".article-body"'
47533
- },
47534
- timeout: {
47535
- type: "number",
47536
- description: "Max wait time in milliseconds (default: 15000)"
47537
- },
47538
- takeScreenshot: {
47539
- type: "boolean",
47540
- description: "Whether to capture a screenshot of the page (default: false). Screenshot returned as base64 PNG."
47541
- }
47542
- },
47543
- required: ["url"]
47544
- }
47545
- },
47546
- blocking: true,
47547
- timeout: 3e4
47548
- // Allow extra time for browser operations
47549
- },
47550
- execute: async (args) => {
47551
- let page = null;
47727
+ function createWebScrapeTool(connector) {
47728
+ async function tryNative(args, startTime, attemptedMethods) {
47729
+ attemptedMethods.push("native");
47730
+ scrapeLogger.debug({ url: args.url }, "Trying native fetch");
47552
47731
  try {
47553
- const browser = await getBrowser();
47554
- page = await browser.newPage();
47555
- await page.setViewport({ width: 1280, height: 800 });
47556
- await page.setUserAgent(
47557
- "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"
47558
- );
47559
- const startTime = Date.now();
47560
- await page.goto(args.url, {
47561
- waitUntil: "networkidle2",
47562
- // Wait until network is mostly idle
47563
- timeout: args.timeout || 15e3
47732
+ const result = await webFetch.execute({
47733
+ url: args.url,
47734
+ timeout: args.timeout || 1e4
47564
47735
  });
47565
- if (args.waitForSelector) {
47566
- await page.waitForSelector(args.waitForSelector, {
47567
- timeout: args.timeout || 15e3
47568
- });
47569
- }
47570
- const html = await page.content();
47571
- const browserTitle = await page.title();
47572
- const loadTime = Date.now() - startTime;
47573
- let screenshot;
47574
- if (args.takeScreenshot) {
47575
- const buffer = await page.screenshot({
47576
- type: "png",
47577
- fullPage: false
47578
- // Just viewport
47579
- });
47580
- screenshot = buffer.toString("base64");
47581
- }
47582
- await page.close();
47583
- const mdResult = await htmlToMarkdown(html, args.url);
47584
- const title = browserTitle || mdResult.title || "Untitled";
47736
+ const cleanContent = stripBase64DataUris(result.content);
47585
47737
  return {
47586
- success: true,
47738
+ success: result.success,
47587
47739
  url: args.url,
47588
- title,
47589
- content: mdResult.markdown,
47590
- screenshot,
47591
- loadTime,
47592
- excerpt: mdResult.excerpt,
47593
- byline: mdResult.byline,
47594
- wasReadabilityUsed: mdResult.wasReadabilityUsed,
47595
- wasTruncated: mdResult.wasTruncated
47740
+ finalUrl: args.url,
47741
+ method: "native",
47742
+ title: result.title,
47743
+ content: cleanContent,
47744
+ qualityScore: result.qualityScore,
47745
+ durationMs: Date.now() - startTime,
47746
+ attemptedMethods,
47747
+ error: result.error
47596
47748
  };
47597
47749
  } catch (error) {
47598
- if (page) {
47599
- try {
47600
- await page.close();
47601
- } catch {
47602
- }
47603
- }
47604
- if (error.message === "Puppeteer not installed") {
47605
- return {
47606
- success: false,
47607
- url: args.url,
47608
- title: "",
47609
- content: "",
47610
- loadTime: 0,
47611
- error: "Puppeteer is not installed",
47612
- suggestion: "Install Puppeteer with: npm install puppeteer (note: downloads ~50MB Chrome binary)"
47613
- };
47614
- }
47615
47750
  return {
47616
47751
  success: false,
47617
47752
  url: args.url,
47753
+ method: "native",
47618
47754
  title: "",
47619
47755
  content: "",
47620
- loadTime: 0,
47756
+ durationMs: Date.now() - startTime,
47757
+ attemptedMethods,
47621
47758
  error: error.message
47622
47759
  };
47623
47760
  }
47624
47761
  }
47625
- };
47626
-
47627
- // src/tools/web/webSearch.ts
47628
- init_Logger();
47629
-
47630
- // src/tools/web/searchProviders/serper.ts
47631
- async function searchWithSerper(query, numResults, apiKey) {
47632
- const response = await fetch("https://google.serper.dev/search", {
47633
- method: "POST",
47634
- headers: {
47635
- "X-API-KEY": apiKey,
47636
- "Content-Type": "application/json"
47637
- },
47638
- body: JSON.stringify({
47639
- q: query,
47640
- num: numResults
47641
- })
47642
- });
47643
- if (!response.ok) {
47644
- throw new Error(`Serper API error: ${response.status} ${response.statusText}`);
47645
- }
47646
- const data = await response.json();
47647
- if (!data.organic || !Array.isArray(data.organic)) {
47648
- throw new Error("Invalid response from Serper API");
47649
- }
47650
- return data.organic.slice(0, numResults).map((result, index) => ({
47651
- title: result.title || "Untitled",
47652
- url: result.link || "",
47653
- snippet: result.snippet || "",
47654
- position: index + 1
47655
- }));
47656
- }
47657
-
47658
- // src/tools/web/searchProviders/brave.ts
47659
- async function searchWithBrave(query, numResults, apiKey) {
47660
- const url2 = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${numResults}`;
47661
- const response = await fetch(url2, {
47662
- headers: {
47663
- "X-Subscription-Token": apiKey,
47664
- Accept: "application/json"
47665
- }
47666
- });
47667
- if (!response.ok) {
47668
- throw new Error(`Brave API error: ${response.status} ${response.statusText}`);
47669
- }
47670
- const data = await response.json();
47671
- if (!data.web?.results || !Array.isArray(data.web.results)) {
47672
- throw new Error("Invalid response from Brave API");
47673
- }
47674
- return data.web.results.slice(0, numResults).map((result, index) => ({
47675
- title: result.title || "Untitled",
47676
- url: result.url || "",
47677
- snippet: result.description || "",
47678
- position: index + 1
47679
- }));
47680
- }
47681
-
47682
- // src/tools/web/searchProviders/tavily.ts
47683
- async function searchWithTavily(query, numResults, apiKey) {
47684
- const response = await fetch("https://api.tavily.com/search", {
47685
- method: "POST",
47686
- headers: {
47687
- "Content-Type": "application/json"
47688
- },
47689
- body: JSON.stringify({
47690
- api_key: apiKey,
47691
- query,
47692
- max_results: numResults,
47693
- search_depth: "basic",
47694
- // 'basic' or 'advanced'
47695
- include_answer: false,
47696
- include_raw_content: false
47697
- })
47698
- });
47699
- if (!response.ok) {
47700
- throw new Error(`Tavily API error: ${response.status} ${response.statusText}`);
47701
- }
47702
- const data = await response.json();
47703
- if (!data.results || !Array.isArray(data.results)) {
47704
- throw new Error("Invalid response from Tavily API");
47705
- }
47706
- return data.results.slice(0, numResults).map((result, index) => ({
47707
- title: result.title || "Untitled",
47708
- url: result.url || "",
47709
- snippet: result.content || "",
47710
- position: index + 1
47711
- }));
47712
- }
47713
-
47714
- // src/tools/web/webSearch.ts
47715
- var searchLogger = exports.logger.child({ component: "webSearch" });
47716
- var SEARCH_SERVICE_TYPES = ["serper", "brave-search", "tavily", "rapidapi-search"];
47717
- var webSearch = {
47718
- definition: {
47719
- type: "function",
47720
- function: {
47721
- name: "web_search",
47722
- description: `Search the web and get relevant results with snippets.
47723
-
47724
- RETURNS:
47725
- An array of search results, each containing:
47726
- - title: Page title
47727
- - url: Direct URL to the page
47728
- - snippet: Short description/excerpt from the page
47729
- - position: Search ranking position (1, 2, 3...)
47730
-
47731
- USE CASES:
47732
- - Find current information on any topic
47733
- - Research multiple sources
47734
- - Discover relevant websites
47735
- - Find URLs to fetch with web_fetch tool
47736
-
47737
- WORKFLOW PATTERN:
47738
- 1. Use web_search to find relevant URLs
47739
- 2. Use web_fetch to get full content from top results
47740
- 3. Process and summarize the information
47741
-
47742
- EXAMPLE:
47743
- {
47744
- "query": "latest AI developments 2026",
47745
- "numResults": 5
47746
- }`,
47747
- parameters: {
47748
- type: "object",
47749
- properties: {
47750
- query: {
47751
- type: "string",
47752
- description: "The search query string. Be specific for better results."
47753
- },
47754
- numResults: {
47755
- type: "number",
47756
- description: "Number of results to return (default: 10, max: 100)."
47757
- },
47758
- country: {
47759
- type: "string",
47760
- description: 'Country/region code for localized results (e.g., "us", "gb", "de")'
47761
- },
47762
- language: {
47763
- type: "string",
47764
- description: 'Language code for results (e.g., "en", "fr", "de")'
47765
- }
47766
- },
47767
- required: ["query"]
47768
- }
47769
- },
47770
- blocking: true,
47771
- timeout: 15e3
47772
- },
47773
- execute: async (args) => {
47774
- const numResults = args.numResults || 10;
47775
- const connector = findConnectorByServiceTypes(SEARCH_SERVICE_TYPES);
47776
- if (connector) {
47777
- return await executeWithConnector(connector.name, args, numResults);
47778
- }
47779
- return await executeWithEnvVar(args, numResults);
47780
- },
47781
- describeCall: (args) => `"${args.query}"${args.numResults ? ` (${args.numResults} results)` : ""}`
47782
- };
47783
- async function executeWithConnector(connectorName, args, numResults) {
47784
- searchLogger.debug({ connectorName }, "Executing search with connector");
47785
- try {
47786
- const searchProvider = SearchProvider.create({ connector: connectorName });
47787
- const response = await searchProvider.search(args.query, {
47788
- numResults,
47789
- country: args.country,
47790
- language: args.language
47791
- });
47792
- if (response.success) {
47793
- searchLogger.debug({
47794
- provider: response.provider,
47795
- count: response.count
47796
- }, "Search completed successfully");
47797
- } else {
47798
- searchLogger.warn({
47799
- provider: response.provider,
47800
- error: response.error
47801
- }, "Search failed");
47802
- }
47803
- return {
47804
- success: response.success,
47805
- query: response.query,
47806
- provider: response.provider,
47807
- results: response.results,
47808
- count: response.count,
47809
- error: response.error
47810
- };
47811
- } catch (error) {
47812
- searchLogger.error({ error: error.message, connectorName }, "Search threw exception");
47813
- return {
47814
- success: false,
47815
- query: args.query,
47816
- provider: connectorName,
47817
- results: [],
47818
- count: 0,
47819
- error: error.message || "Unknown error"
47820
- };
47821
- }
47822
- }
47823
- async function executeWithEnvVar(args, numResults) {
47824
- const providers = [
47825
- { name: "serper", key: process.env.SERPER_API_KEY, fn: searchWithSerper },
47826
- { name: "brave", key: process.env.BRAVE_API_KEY, fn: searchWithBrave },
47827
- { name: "tavily", key: process.env.TAVILY_API_KEY, fn: searchWithTavily }
47828
- ];
47829
- for (const provider of providers) {
47830
- if (provider.key) {
47831
- searchLogger.debug({ provider: provider.name }, "Using environment variable fallback");
47832
- try {
47833
- const results = await provider.fn(args.query, numResults, provider.key);
47834
- return {
47835
- success: true,
47836
- query: args.query,
47837
- provider: provider.name,
47838
- results,
47839
- count: results.length
47840
- };
47841
- } catch (error) {
47842
- searchLogger.warn({ provider: provider.name, error: error.message }, "Provider failed, trying next");
47843
- }
47762
+ async function tryAPI(args, startTime, attemptedMethods) {
47763
+ attemptedMethods.push(`api:${connector.name}`);
47764
+ scrapeLogger.debug({ url: args.url, connectorName: connector.name }, "Trying external API");
47765
+ try {
47766
+ const provider = ScrapeProvider.create({ connector: connector.name });
47767
+ const options = {
47768
+ timeout: args.timeout,
47769
+ waitForSelector: args.waitForSelector,
47770
+ includeHtml: args.includeHtml,
47771
+ includeMarkdown: args.includeMarkdown,
47772
+ includeLinks: args.includeLinks
47773
+ };
47774
+ const result = await provider.scrape(args.url, options);
47775
+ const rawContent = result.result?.content || "";
47776
+ const rawMarkdown = result.result?.markdown;
47777
+ const cleanContent = stripBase64DataUris(rawContent);
47778
+ const cleanMarkdown = rawMarkdown ? stripBase64DataUris(rawMarkdown) : void 0;
47779
+ const isDuplicate = !!cleanMarkdown && cleanContent === cleanMarkdown;
47780
+ return {
47781
+ success: result.success,
47782
+ url: args.url,
47783
+ finalUrl: result.finalUrl,
47784
+ method: result.provider,
47785
+ title: result.result?.title || "",
47786
+ content: cleanContent,
47787
+ html: result.result?.html,
47788
+ markdown: isDuplicate ? void 0 : cleanMarkdown,
47789
+ metadata: result.result?.metadata,
47790
+ links: result.result?.links,
47791
+ qualityScore: result.success ? 90 : 0,
47792
+ durationMs: Date.now() - startTime,
47793
+ attemptedMethods,
47794
+ error: result.error
47795
+ };
47796
+ } catch (error) {
47797
+ return {
47798
+ success: false,
47799
+ url: args.url,
47800
+ method: "api",
47801
+ title: "",
47802
+ content: "",
47803
+ durationMs: Date.now() - startTime,
47804
+ attemptedMethods,
47805
+ error: error.message
47806
+ };
47844
47807
  }
47845
47808
  }
47846
47809
  return {
47847
- success: false,
47848
- query: args.query,
47849
- provider: "none",
47850
- results: [],
47851
- count: 0,
47852
- 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."
47853
- };
47854
- }
47855
-
47856
- // src/tools/web/webScrape.ts
47857
- init_Logger();
47858
- var scrapeLogger = exports.logger.child({ component: "webScrape" });
47859
- function stripBase64DataUris(content) {
47860
- if (!content) return content;
47861
- let cleaned = content.replace(/!\[[^\]]*\]\(data:[^)]+\)/g, "[image removed]");
47862
- cleaned = cleaned.replace(/url\(['"]?data:[^)]+['"]?\)/gi, "url([data-uri-removed])");
47863
- cleaned = cleaned.replace(/data:(?:image|font|application)\/[^;]+;base64,[A-Za-z0-9+/=]{100,}/g, "[base64-data-removed]");
47864
- return cleaned;
47865
- }
47866
- var SCRAPE_SERVICE_TYPES = ["zenrows", "jina-reader", "firecrawl", "scrapingbee"];
47867
- var DEFAULT_MIN_QUALITY = 50;
47868
- var webScrape = {
47869
- definition: {
47870
- type: "function",
47871
- function: {
47872
- name: "web_scrape",
47873
- description: `Scrape any URL with automatic fallback - guaranteed to work on most sites.
47810
+ definition: {
47811
+ type: "function",
47812
+ function: {
47813
+ name: "web_scrape",
47814
+ description: `Scrape any URL with automatic fallback - guaranteed to work on most sites.
47874
47815
 
47875
47816
  Automatically tries multiple methods in sequence:
47876
47817
  1. Native fetch - Fast (~1s), works for blogs/docs/articles
47877
- 2. JS rendering - Handles React/Vue/Angular SPAs
47878
- 3. External API - Handles bot protection, CAPTCHAs (if configured)
47818
+ 2. External API - Handles bot protection, CAPTCHAs, SPAs (if configured)
47879
47819
 
47880
47820
  RETURNS:
47881
47821
  {
@@ -47910,46 +47850,64 @@ For JS-heavy sites:
47910
47850
  "url": "https://spa-app.com",
47911
47851
  "waitForSelector": ".main-content"
47912
47852
  }`,
47913
- parameters: {
47914
- type: "object",
47915
- properties: {
47916
- url: {
47917
- type: "string",
47918
- description: "URL to scrape. Must start with http:// or https://"
47919
- },
47920
- timeout: {
47921
- type: "number",
47922
- description: "Timeout in milliseconds (default: 30000)"
47923
- },
47924
- includeHtml: {
47925
- type: "boolean",
47926
- description: "Include raw HTML in response (default: false)"
47927
- },
47928
- includeMarkdown: {
47929
- type: "boolean",
47930
- description: "Include markdown conversion (default: false)"
47931
- },
47932
- includeLinks: {
47933
- type: "boolean",
47934
- description: "Extract and include links (default: false)"
47853
+ parameters: {
47854
+ type: "object",
47855
+ properties: {
47856
+ url: {
47857
+ type: "string",
47858
+ description: "URL to scrape. Must start with http:// or https://"
47859
+ },
47860
+ timeout: {
47861
+ type: "number",
47862
+ description: "Timeout in milliseconds (default: 30000)"
47863
+ },
47864
+ includeHtml: {
47865
+ type: "boolean",
47866
+ description: "Include raw HTML in response (default: false)"
47867
+ },
47868
+ includeMarkdown: {
47869
+ type: "boolean",
47870
+ description: "Include markdown conversion (default: false)"
47871
+ },
47872
+ includeLinks: {
47873
+ type: "boolean",
47874
+ description: "Extract and include links (default: false)"
47875
+ },
47876
+ waitForSelector: {
47877
+ type: "string",
47878
+ description: "CSS selector to wait for before scraping (for JS-heavy sites)"
47879
+ }
47935
47880
  },
47936
- waitForSelector: {
47937
- type: "string",
47938
- description: "CSS selector to wait for before scraping (for JS-heavy sites)"
47939
- }
47940
- },
47941
- required: ["url"]
47942
- }
47881
+ required: ["url"]
47882
+ }
47883
+ },
47884
+ blocking: true,
47885
+ timeout: 6e4
47943
47886
  },
47944
- blocking: true,
47945
- timeout: 6e4
47946
- },
47947
- execute: async (args) => {
47948
- const startTime = Date.now();
47949
- const attemptedMethods = [];
47950
- try {
47951
- new URL(args.url);
47952
- } catch {
47887
+ execute: async (args) => {
47888
+ const startTime = Date.now();
47889
+ const attemptedMethods = [];
47890
+ try {
47891
+ new URL(args.url);
47892
+ } catch {
47893
+ return {
47894
+ success: false,
47895
+ url: args.url,
47896
+ method: "none",
47897
+ title: "",
47898
+ content: "",
47899
+ durationMs: Date.now() - startTime,
47900
+ attemptedMethods: [],
47901
+ error: "Invalid URL format"
47902
+ };
47903
+ }
47904
+ const native = await tryNative(args, startTime, attemptedMethods);
47905
+ if (native.success && (native.qualityScore ?? 0) >= DEFAULT_MIN_QUALITY) {
47906
+ return native;
47907
+ }
47908
+ const api = await tryAPI(args, startTime, attemptedMethods);
47909
+ if (api.success) return api;
47910
+ if (native.success) return native;
47953
47911
  return {
47954
47912
  success: false,
47955
47913
  url: args.url,
@@ -47957,232 +47915,119 @@ For JS-heavy sites:
47957
47915
  title: "",
47958
47916
  content: "",
47959
47917
  durationMs: Date.now() - startTime,
47960
- attemptedMethods: [],
47961
- error: "Invalid URL format"
47918
+ attemptedMethods,
47919
+ error: "All scraping methods failed. Site may have bot protection."
47962
47920
  };
47963
- }
47964
- const native = await tryNative(args, startTime, attemptedMethods);
47965
- if (native.success && (native.qualityScore ?? 0) >= DEFAULT_MIN_QUALITY) {
47966
- return native;
47967
- }
47968
- const js = await tryJS(args, startTime, attemptedMethods);
47969
- if (js.success && (js.qualityScore ?? 0) >= DEFAULT_MIN_QUALITY) {
47970
- return js;
47971
- }
47972
- const connector = findConnectorByServiceTypes(SCRAPE_SERVICE_TYPES);
47973
- if (connector) {
47974
- const api = await tryAPI(connector.name, args, startTime, attemptedMethods);
47975
- if (api.success) return api;
47976
- }
47977
- if (js.success) return js;
47978
- if (native.success) return native;
47979
- return {
47980
- success: false,
47981
- url: args.url,
47982
- method: "none",
47983
- title: "",
47984
- content: "",
47985
- durationMs: Date.now() - startTime,
47986
- attemptedMethods,
47987
- error: "All scraping methods failed. Site may have bot protection."
47988
- };
47989
- },
47990
- describeCall: (args) => args.url
47991
- };
47992
- async function tryNative(args, startTime, attemptedMethods) {
47993
- attemptedMethods.push("native");
47994
- scrapeLogger.debug({ url: args.url }, "Trying native fetch");
47995
- try {
47996
- const result = await webFetch.execute({
47997
- url: args.url,
47998
- timeout: args.timeout || 1e4
47999
- });
48000
- const cleanContent = stripBase64DataUris(result.content);
48001
- return {
48002
- success: result.success,
48003
- url: args.url,
48004
- finalUrl: args.url,
48005
- method: "native",
48006
- title: result.title,
48007
- content: cleanContent,
48008
- // Native method already returns markdown-like content — no separate markdown field needed
48009
- // (would just duplicate content and waste tokens)
48010
- qualityScore: result.qualityScore,
48011
- durationMs: Date.now() - startTime,
48012
- attemptedMethods,
48013
- error: result.error
48014
- };
48015
- } catch (error) {
48016
- return {
48017
- success: false,
48018
- url: args.url,
48019
- method: "native",
48020
- title: "",
48021
- content: "",
48022
- durationMs: Date.now() - startTime,
48023
- attemptedMethods,
48024
- error: error.message
48025
- };
48026
- }
47921
+ },
47922
+ describeCall: (args) => args.url
47923
+ };
48027
47924
  }
48028
- async function tryJS(args, startTime, attemptedMethods) {
48029
- attemptedMethods.push("js");
48030
- scrapeLogger.debug({ url: args.url }, "Trying JS rendering");
48031
- try {
48032
- const result = await webFetchJS.execute({
48033
- url: args.url,
48034
- timeout: args.timeout || 15e3,
48035
- waitForSelector: args.waitForSelector
48036
- });
48037
- const cleanContent = stripBase64DataUris(result.content);
48038
- return {
48039
- success: result.success,
48040
- url: args.url,
48041
- finalUrl: args.url,
48042
- method: "js",
48043
- title: result.title,
48044
- content: cleanContent,
48045
- // JS method already returns markdown-like content — no separate markdown field needed
48046
- qualityScore: result.success ? 80 : 0,
48047
- durationMs: Date.now() - startTime,
48048
- attemptedMethods,
48049
- error: result.error
48050
- };
48051
- } catch (error) {
48052
- return {
48053
- success: false,
48054
- url: args.url,
48055
- method: "js",
48056
- title: "",
48057
- content: "",
48058
- durationMs: Date.now() - startTime,
48059
- attemptedMethods,
48060
- error: error.message
48061
- };
47925
+
47926
+ // src/tools/web/register.ts
47927
+ var SEARCH_SERVICE_TYPES = ["serper", "brave-search", "tavily", "rapidapi-search"];
47928
+ var SCRAPE_SERVICE_TYPES = ["zenrows", "jina-reader", "firecrawl", "scrapingbee"];
47929
+ function registerWebTools() {
47930
+ for (const st of SEARCH_SERVICE_TYPES) {
47931
+ ConnectorTools.registerService(st, (connector) => [
47932
+ createWebSearchTool(connector)
47933
+ ]);
48062
47934
  }
48063
- }
48064
- async function tryAPI(connectorName, args, startTime, attemptedMethods) {
48065
- attemptedMethods.push(`api:${connectorName}`);
48066
- scrapeLogger.debug({ url: args.url, connectorName }, "Trying external API");
48067
- try {
48068
- const provider = ScrapeProvider.create({ connector: connectorName });
48069
- const options = {
48070
- timeout: args.timeout,
48071
- waitForSelector: args.waitForSelector,
48072
- includeHtml: args.includeHtml,
48073
- includeMarkdown: args.includeMarkdown,
48074
- includeLinks: args.includeLinks
48075
- };
48076
- const result = await provider.scrape(args.url, options);
48077
- const rawContent = result.result?.content || "";
48078
- const rawMarkdown = result.result?.markdown;
48079
- const cleanContent = stripBase64DataUris(rawContent);
48080
- const cleanMarkdown = rawMarkdown ? stripBase64DataUris(rawMarkdown) : void 0;
48081
- const isDuplicate = !!cleanMarkdown && cleanContent === cleanMarkdown;
48082
- return {
48083
- success: result.success,
48084
- url: args.url,
48085
- finalUrl: result.finalUrl,
48086
- method: result.provider,
48087
- title: result.result?.title || "",
48088
- content: cleanContent,
48089
- html: result.result?.html,
48090
- // Keep raw HTML as-is (only used if explicitly requested)
48091
- markdown: isDuplicate ? void 0 : cleanMarkdown,
48092
- metadata: result.result?.metadata,
48093
- links: result.result?.links,
48094
- qualityScore: result.success ? 90 : 0,
48095
- durationMs: Date.now() - startTime,
48096
- attemptedMethods,
48097
- error: result.error
48098
- };
48099
- } catch (error) {
48100
- return {
48101
- success: false,
48102
- url: args.url,
48103
- method: "api",
48104
- title: "",
48105
- content: "",
48106
- durationMs: Date.now() - startTime,
48107
- attemptedMethods,
48108
- error: error.message
48109
- };
47935
+ for (const st of SCRAPE_SERVICE_TYPES) {
47936
+ ConnectorTools.registerService(st, (connector) => [
47937
+ createWebScrapeTool(connector)
47938
+ ]);
48110
47939
  }
48111
47940
  }
48112
47941
 
47942
+ // src/tools/web/index.ts
47943
+ registerWebTools();
47944
+
48113
47945
  // src/tools/code/executeJavaScript.ts
48114
47946
  init_Connector();
48115
- function generateDescription() {
48116
- const connectors = exports.Connector.listAll();
48117
- const connectorList = connectors.length > 0 ? connectors.map((c) => {
48118
- const authType = c.config.auth?.type || "none";
48119
- return ` \u2022 "${c.name}": ${c.displayName}
48120
- ${c.config.description || "No description"}
48121
- Base URL: ${c.baseURL}
48122
- Auth: ${authType}`;
48123
- }).join("\n\n") : " No connectors registered.";
48124
- return `Execute JavaScript code in a secure sandbox with authenticated API access.
48125
-
48126
- AVAILABLE APIS:
48127
-
48128
- 1. authenticatedFetch(url, options, connectorName, userId?)
48129
- Makes authenticated API calls using the connector's configured auth scheme.
48130
- Auth headers are added automatically - DO NOT set Authorization header.
47947
+ var DEFAULT_TIMEOUT = 1e4;
47948
+ var DEFAULT_MAX_TIMEOUT = 3e4;
47949
+ function formatConnectorEntry(c) {
47950
+ const parts = [];
47951
+ const serviceOrVendor = c.serviceType ?? c.vendor ?? void 0;
47952
+ if (serviceOrVendor) parts.push(`Service: ${serviceOrVendor}`);
47953
+ if (c.config.description) parts.push(c.config.description);
47954
+ if (c.baseURL) parts.push(`URL: ${c.baseURL}`);
47955
+ const details = parts.map((p) => ` ${p}`).join("\n");
47956
+ return ` \u2022 "${c.name}" (${c.displayName})
47957
+ ${details}`;
47958
+ }
47959
+ function generateDescription(context, maxTimeout) {
47960
+ const registry = context?.connectorRegistry ?? exports.Connector.asRegistry();
47961
+ const connectors = registry.listAll();
47962
+ const connectorList = connectors.length > 0 ? connectors.map(formatConnectorEntry).join("\n\n") : " No connectors registered.";
47963
+ const timeoutSec = Math.round(maxTimeout / 1e3);
47964
+ return `Execute JavaScript code in a secure sandbox with authenticated API access to external services.
47965
+
47966
+ Use this tool when you need to:
47967
+ - Call external APIs (GitHub, Slack, Stripe, etc.) using registered connectors
47968
+ - Process, transform, or compute data that requires programmatic logic
47969
+ - Chain multiple API calls or perform complex data manipulation
47970
+ - Do anything that plain text generation cannot accomplish
47971
+
47972
+ SANDBOX API:
47973
+
47974
+ 1. authenticatedFetch(url, options, connectorName)
47975
+ Makes authenticated HTTP requests using the connector's credentials.
47976
+ The current user's identity (userId) is automatically included \u2014 no need to pass it.
47977
+ Auth headers are added automatically \u2014 DO NOT set Authorization header manually.
48131
47978
 
48132
47979
  Parameters:
48133
- \u2022 url: Full URL or relative path (uses connector's baseURL)
47980
+ \u2022 url: Full URL or path relative to the connector's base URL
48134
47981
  - Full: "https://api.github.com/user/repos"
48135
- - Relative: "/user/repos" (appended to connector's baseURL)
48136
- \u2022 options: Standard fetch options { method, body, headers }
48137
- \u2022 connectorName: One of the registered connectors below
48138
- \u2022 userId: (optional) For multi-tenant apps with per-user tokens
47982
+ - Relative: "/user/repos" (resolved against connector's base URL)
47983
+ \u2022 options: Standard fetch options { method, headers, body }
47984
+ - For POST/PUT: set body to JSON.stringify(data) and headers to { 'Content-Type': 'application/json' }
47985
+ \u2022 connectorName: Name of a registered connector (see list below)
48139
47986
 
48140
47987
  Returns: Promise<Response>
48141
- \u2022 response.ok - true if status 200-299
48142
- \u2022 response.status - HTTP status code
48143
- \u2022 response.json() - parse JSON body
48144
- \u2022 response.text() - get text body
47988
+ \u2022 response.ok \u2014 true if status 200-299
47989
+ \u2022 response.status \u2014 HTTP status code
47990
+ \u2022 await response.json() \u2014 parse JSON body
47991
+ \u2022 await response.text() \u2014 get text body
48145
47992
 
48146
- Auth Schemes (handled automatically per connector):
48147
- \u2022 Bearer tokens (GitHub, Slack, Stripe)
48148
- \u2022 Bot tokens (Discord)
48149
- \u2022 Basic auth (Twilio, Zendesk)
48150
- \u2022 Custom headers (Shopify uses X-Shopify-Access-Token)
47993
+ 2. fetch(url, options) \u2014 Standard fetch without authentication
48151
47994
 
48152
- 2. connectors.list() - List available connector names
48153
- 3. connectors.get(name) - Get connector info { displayName, description, baseURL }
48154
- 4. fetch(url, options) - Standard fetch (no auth)
47995
+ 3. connectors.list() \u2014 Array of available connector names
47996
+ 4. connectors.get(name) \u2014 Connector info: { displayName, description, baseURL, serviceType }
48155
47997
 
48156
- INPUT/OUTPUT:
48157
- \u2022 input - data passed to your code via the "input" parameter
48158
- \u2022 output - SET THIS variable to return your result
47998
+ VARIABLES:
47999
+ \u2022 input \u2014 data passed via the "input" parameter (default: {})
48000
+ \u2022 output \u2014 SET THIS to return your result to the caller
48159
48001
 
48160
- UTILITIES: console.log/error/warn, Buffer, JSON, Math, Date, Promise
48002
+ 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
48161
48003
 
48162
48004
  REGISTERED CONNECTORS:
48163
48005
  ${connectorList}
48164
48006
 
48165
- EXAMPLE:
48166
- (async () => {
48167
- const response = await authenticatedFetch(
48168
- '/user/repos',
48169
- { method: 'GET' },
48170
- 'github'
48171
- );
48007
+ EXAMPLES:
48172
48008
 
48173
- if (!response.ok) {
48174
- throw new Error(\`API error: \${response.status}\`);
48175
- }
48009
+ // GET request
48010
+ const resp = await authenticatedFetch('/user/repos', { method: 'GET' }, 'github');
48011
+ const repos = await resp.json();
48012
+ output = repos.map(r => r.full_name);
48176
48013
 
48177
- const repos = await response.json();
48178
- console.log(\`Found \${repos.length} repositories\`);
48014
+ // POST request with JSON body
48015
+ const resp = await authenticatedFetch('/chat.postMessage', {
48016
+ method: 'POST',
48017
+ headers: { 'Content-Type': 'application/json' },
48018
+ body: JSON.stringify({ channel: '#general', text: 'Hello!' })
48019
+ }, 'slack');
48020
+ output = await resp.json();
48179
48021
 
48180
- output = repos;
48181
- })();
48022
+ // Data processing (no API needed)
48023
+ const items = input.data;
48024
+ output = items.filter(i => i.score > 0.8).sort((a, b) => b.score - a.score);
48182
48025
 
48183
- SECURITY: 10s timeout, no file system, no require/import.`;
48026
+ LIMITS: ${timeoutSec}s max timeout, no file system access, no require/import.`;
48184
48027
  }
48185
- function createExecuteJavaScriptTool() {
48028
+ function createExecuteJavaScriptTool(options) {
48029
+ const maxTimeout = options?.maxTimeout ?? DEFAULT_MAX_TIMEOUT;
48030
+ const defaultTimeout = options?.defaultTimeout ?? DEFAULT_TIMEOUT;
48186
48031
  return {
48187
48032
  definition: {
48188
48033
  type: "function",
@@ -48195,32 +48040,40 @@ function createExecuteJavaScriptTool() {
48195
48040
  properties: {
48196
48041
  code: {
48197
48042
  type: "string",
48198
- description: 'JavaScript code to execute. MUST set the "output" variable. Wrap in async IIFE for async operations.'
48043
+ 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 () => { ... })().'
48199
48044
  },
48200
48045
  input: {
48201
- description: 'Optional input data available as "input" variable in your code'
48046
+ description: 'Optional data available as the "input" variable in your code. Can be any JSON value.'
48202
48047
  },
48203
48048
  timeout: {
48204
48049
  type: "number",
48205
- description: "Execution timeout in milliseconds (default: 10000, max: 30000)"
48050
+ description: `Execution timeout in milliseconds. Default: ${defaultTimeout}ms, max: ${maxTimeout}ms. Increase for slow API calls or multiple sequential requests.`
48206
48051
  }
48207
48052
  },
48208
48053
  required: ["code"]
48209
48054
  }
48210
48055
  },
48211
48056
  blocking: true,
48212
- timeout: 35e3
48213
- // Tool timeout (slightly more than max code timeout)
48057
+ timeout: maxTimeout + 5e3
48058
+ // Tool-level timeout slightly above max code timeout
48214
48059
  },
48215
- // Dynamic description - evaluated each time tool definitions are sent to LLM
48216
- // This ensures the connector list is always current
48217
- descriptionFactory: generateDescription,
48218
- execute: async (args) => {
48060
+ // Dynamic description regenerated each time tool definitions are sent to LLM.
48061
+ // Receives ToolContext so connector list is scoped to current userId.
48062
+ descriptionFactory: (context) => generateDescription(context, maxTimeout),
48063
+ execute: async (args, context) => {
48219
48064
  const logs = [];
48220
48065
  const startTime = Date.now();
48221
48066
  try {
48222
- const timeout = Math.min(args.timeout || 1e4, 3e4);
48223
- const result = await executeInVM(args.code, args.input, timeout, logs);
48067
+ const timeout = Math.min(Math.max(args.timeout || defaultTimeout, 0), maxTimeout);
48068
+ const registry = context?.connectorRegistry ?? exports.Connector.asRegistry();
48069
+ const result = await executeInVM(
48070
+ args.code,
48071
+ args.input,
48072
+ timeout,
48073
+ logs,
48074
+ context?.userId,
48075
+ registry
48076
+ );
48224
48077
  return {
48225
48078
  success: true,
48226
48079
  result,
@@ -48240,31 +48093,36 @@ function createExecuteJavaScriptTool() {
48240
48093
  };
48241
48094
  }
48242
48095
  var executeJavaScript = createExecuteJavaScriptTool();
48243
- async function executeInVM(code, input, timeout, logs) {
48096
+ async function executeInVM(code, input, timeout, logs, userId, registry) {
48244
48097
  const sandbox = {
48245
48098
  // Input/output
48246
- input: input || {},
48099
+ input: input ?? {},
48247
48100
  output: null,
48248
- // Console (captured)
48101
+ // Console (captured) — stringify objects for readable logs
48249
48102
  console: {
48250
- log: (...args) => logs.push(args.map((a) => String(a)).join(" ")),
48251
- error: (...args) => logs.push("ERROR: " + args.map((a) => String(a)).join(" ")),
48252
- warn: (...args) => logs.push("WARN: " + args.map((a) => String(a)).join(" "))
48103
+ log: (...args) => logs.push(args.map((a) => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ")),
48104
+ error: (...args) => logs.push("ERROR: " + args.map((a) => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ")),
48105
+ warn: (...args) => logs.push("WARN: " + args.map((a) => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" "))
48253
48106
  },
48254
- // Authenticated fetch
48255
- authenticatedFetch,
48256
- // Standard fetch
48107
+ // Authenticated fetch — userId auto-injected from ToolContext.
48108
+ // Only connectors visible in the scoped registry are accessible.
48109
+ authenticatedFetch: (url2, options, connectorName) => {
48110
+ registry.get(connectorName);
48111
+ return authenticatedFetch(url2, options, connectorName, userId);
48112
+ },
48113
+ // Standard fetch (no auth)
48257
48114
  fetch: globalThis.fetch,
48258
- // Connector info
48115
+ // Connector info (userId-scoped)
48259
48116
  connectors: {
48260
- list: () => exports.Connector.list(),
48117
+ list: () => registry.list(),
48261
48118
  get: (name) => {
48262
48119
  try {
48263
- const connector = exports.Connector.get(name);
48120
+ const connector = registry.get(name);
48264
48121
  return {
48265
48122
  displayName: connector.displayName,
48266
48123
  description: connector.config.description || "",
48267
- baseURL: connector.baseURL
48124
+ baseURL: connector.baseURL,
48125
+ serviceType: connector.serviceType
48268
48126
  };
48269
48127
  } catch {
48270
48128
  return null;
@@ -48281,14 +48139,22 @@ async function executeInVM(code, input, timeout, logs) {
48281
48139
  clearTimeout,
48282
48140
  clearInterval,
48283
48141
  Promise,
48284
- // Array/Object
48142
+ // Built-in types
48285
48143
  Array,
48286
48144
  Object,
48287
48145
  String,
48288
48146
  Number,
48289
- Boolean
48147
+ Boolean,
48148
+ RegExp,
48149
+ Map,
48150
+ Set,
48151
+ Error,
48152
+ URL,
48153
+ URLSearchParams,
48154
+ TextEncoder,
48155
+ TextDecoder
48290
48156
  };
48291
- const context = vm__namespace.createContext(sandbox);
48157
+ const vmContext = vm__namespace.createContext(sandbox);
48292
48158
  const wrappedCode = code.trim().startsWith("(async") ? code : `
48293
48159
  (async () => {
48294
48160
  ${code}
@@ -48296,7 +48162,7 @@ async function executeInVM(code, input, timeout, logs) {
48296
48162
  })()
48297
48163
  `;
48298
48164
  const script = new vm__namespace.Script(wrappedCode);
48299
- const resultPromise = script.runInContext(context, {
48165
+ const resultPromise = script.runInContext(vmContext, {
48300
48166
  timeout,
48301
48167
  displayErrors: true
48302
48168
  });
@@ -48752,7 +48618,7 @@ function createTextToSpeechTool(connector, storage, userId) {
48752
48618
  }
48753
48619
 
48754
48620
  // src/tools/multimedia/speechToText.ts
48755
- function createSpeechToTextTool(connector, storage, _userId) {
48621
+ function createSpeechToTextTool(connector, storage) {
48756
48622
  const vendor = connector.vendor;
48757
48623
  const handler = storage ?? getMediaStorage();
48758
48624
  const vendorModels = vendor ? getSTTModelsByVendor(vendor) : [];
@@ -49013,7 +48879,8 @@ EXAMPLES:
49013
48879
  riskLevel: "low",
49014
48880
  approvalMessage: `Search files in a GitHub repository via ${connector.displayName}`
49015
48881
  },
49016
- execute: async (args) => {
48882
+ execute: async (args, context) => {
48883
+ const effectiveUserId = context?.userId ?? userId;
49017
48884
  const resolved = resolveRepository(args.repository, connector);
49018
48885
  if (!resolved.success) {
49019
48886
  return { success: false, error: resolved.error };
@@ -49025,14 +48892,14 @@ EXAMPLES:
49025
48892
  const repoInfo = await githubFetch(
49026
48893
  connector,
49027
48894
  `/repos/${owner}/${repo}`,
49028
- { userId }
48895
+ { userId: effectiveUserId }
49029
48896
  );
49030
48897
  ref = repoInfo.default_branch;
49031
48898
  }
49032
48899
  const tree = await githubFetch(
49033
48900
  connector,
49034
48901
  `/repos/${owner}/${repo}/git/trees/${ref}?recursive=1`,
49035
- { userId }
48902
+ { userId: effectiveUserId }
49036
48903
  );
49037
48904
  const matching = tree.tree.filter(
49038
48905
  (entry) => entry.type === "blob" && matchGlobPattern2(args.pattern, entry.path)
@@ -49123,7 +48990,8 @@ EXAMPLES:
49123
48990
  riskLevel: "low",
49124
48991
  approvalMessage: `Search code in a GitHub repository via ${connector.displayName}`
49125
48992
  },
49126
- execute: async (args) => {
48993
+ execute: async (args, context) => {
48994
+ const effectiveUserId = context?.userId ?? userId;
49127
48995
  const resolved = resolveRepository(args.repository, connector);
49128
48996
  if (!resolved.success) {
49129
48997
  return { success: false, error: resolved.error };
@@ -49140,7 +49008,7 @@ EXAMPLES:
49140
49008
  connector,
49141
49009
  `/search/code`,
49142
49010
  {
49143
- userId,
49011
+ userId: effectiveUserId,
49144
49012
  // Request text-match fragments
49145
49013
  accept: "application/vnd.github.text-match+json",
49146
49014
  queryParams: { q, per_page: perPage }
@@ -49227,7 +49095,8 @@ NOTE: Files larger than 1MB are fetched via the Git Blob API. Very large files (
49227
49095
  riskLevel: "low",
49228
49096
  approvalMessage: `Read a file from a GitHub repository via ${connector.displayName}`
49229
49097
  },
49230
- execute: async (args) => {
49098
+ execute: async (args, context) => {
49099
+ const effectiveUserId = context?.userId ?? userId;
49231
49100
  const resolved = resolveRepository(args.repository, connector);
49232
49101
  if (!resolved.success) {
49233
49102
  return { success: false, error: resolved.error };
@@ -49241,7 +49110,7 @@ NOTE: Files larger than 1MB are fetched via the Git Blob API. Very large files (
49241
49110
  const contentResp = await githubFetch(
49242
49111
  connector,
49243
49112
  `/repos/${owner}/${repo}/contents/${args.path}${refParam}`,
49244
- { userId }
49113
+ { userId: effectiveUserId }
49245
49114
  );
49246
49115
  if (contentResp.type !== "file") {
49247
49116
  return {
@@ -49258,7 +49127,7 @@ NOTE: Files larger than 1MB are fetched via the Git Blob API. Very large files (
49258
49127
  const blob = await githubFetch(
49259
49128
  connector,
49260
49129
  contentResp.git_url,
49261
- { userId }
49130
+ { userId: effectiveUserId }
49262
49131
  );
49263
49132
  fileContent = Buffer.from(blob.content, "base64").toString("utf-8");
49264
49133
  fileSize = blob.size;
@@ -49345,7 +49214,8 @@ EXAMPLES:
49345
49214
  riskLevel: "low",
49346
49215
  approvalMessage: `Get pull request details from GitHub via ${connector.displayName}`
49347
49216
  },
49348
- execute: async (args) => {
49217
+ execute: async (args, context) => {
49218
+ const effectiveUserId = context?.userId ?? userId;
49349
49219
  const resolved = resolveRepository(args.repository, connector);
49350
49220
  if (!resolved.success) {
49351
49221
  return { success: false, error: resolved.error };
@@ -49355,7 +49225,7 @@ EXAMPLES:
49355
49225
  const pr = await githubFetch(
49356
49226
  connector,
49357
49227
  `/repos/${owner}/${repo}/pulls/${args.pull_number}`,
49358
- { userId }
49228
+ { userId: effectiveUserId }
49359
49229
  );
49360
49230
  return {
49361
49231
  success: true,
@@ -49431,7 +49301,8 @@ NOTE: Very large diffs may be truncated by GitHub. Patch content may be absent f
49431
49301
  riskLevel: "low",
49432
49302
  approvalMessage: `Get PR changed files from GitHub via ${connector.displayName}`
49433
49303
  },
49434
- execute: async (args) => {
49304
+ execute: async (args, context) => {
49305
+ const effectiveUserId = context?.userId ?? userId;
49435
49306
  const resolved = resolveRepository(args.repository, connector);
49436
49307
  if (!resolved.success) {
49437
49308
  return { success: false, error: resolved.error };
@@ -49442,7 +49313,7 @@ NOTE: Very large diffs may be truncated by GitHub. Patch content may be absent f
49442
49313
  connector,
49443
49314
  `/repos/${owner}/${repo}/pulls/${args.pull_number}/files`,
49444
49315
  {
49445
- userId,
49316
+ userId: effectiveUserId,
49446
49317
  queryParams: { per_page: 100 }
49447
49318
  }
49448
49319
  );
@@ -49513,7 +49384,8 @@ EXAMPLES:
49513
49384
  riskLevel: "low",
49514
49385
  approvalMessage: `Get PR comments and reviews from GitHub via ${connector.displayName}`
49515
49386
  },
49516
- execute: async (args) => {
49387
+ execute: async (args, context) => {
49388
+ const effectiveUserId = context?.userId ?? userId;
49517
49389
  const resolved = resolveRepository(args.repository, connector);
49518
49390
  if (!resolved.success) {
49519
49391
  return { success: false, error: resolved.error };
@@ -49521,7 +49393,7 @@ EXAMPLES:
49521
49393
  const { owner, repo } = resolved.repo;
49522
49394
  try {
49523
49395
  const basePath = `/repos/${owner}/${repo}`;
49524
- const queryOpts = { userId, queryParams: { per_page: 100 } };
49396
+ const queryOpts = { userId: effectiveUserId, queryParams: { per_page: 100 } };
49525
49397
  const [reviewComments, reviews, issueComments] = await Promise.all([
49526
49398
  githubFetch(
49527
49399
  connector,
@@ -49648,7 +49520,8 @@ EXAMPLES:
49648
49520
  riskLevel: "medium",
49649
49521
  approvalMessage: `Create a pull request on GitHub via ${connector.displayName}`
49650
49522
  },
49651
- execute: async (args) => {
49523
+ execute: async (args, context) => {
49524
+ const effectiveUserId = context?.userId ?? userId;
49652
49525
  const resolved = resolveRepository(args.repository, connector);
49653
49526
  if (!resolved.success) {
49654
49527
  return { success: false, error: resolved.error };
@@ -49660,7 +49533,7 @@ EXAMPLES:
49660
49533
  `/repos/${owner}/${repo}/pulls`,
49661
49534
  {
49662
49535
  method: "POST",
49663
- userId,
49536
+ userId: effectiveUserId,
49664
49537
  body: {
49665
49538
  title: args.title,
49666
49539
  body: args.body,
@@ -49798,37 +49671,6 @@ var toolRegistry = [
49798
49671
  description: "Fetch and extract text content from a web page URL.",
49799
49672
  tool: webFetch,
49800
49673
  safeByDefault: true
49801
- },
49802
- {
49803
- name: "web_fetch_js",
49804
- exportName: "webFetchJS",
49805
- displayName: "Web Fetch Js",
49806
- category: "web",
49807
- description: "Fetch and extract content from JavaScript-rendered websites using a headless browser (Puppeteer).",
49808
- tool: webFetchJS,
49809
- safeByDefault: true
49810
- },
49811
- {
49812
- name: "web_scrape",
49813
- exportName: "webScrape",
49814
- displayName: "Web Scrape",
49815
- category: "web",
49816
- description: "Scrape any URL with automatic fallback - guaranteed to work on most sites.",
49817
- tool: webScrape,
49818
- safeByDefault: true,
49819
- requiresConnector: true,
49820
- connectorServiceTypes: ["zenrows"]
49821
- },
49822
- {
49823
- name: "web_search",
49824
- exportName: "webSearch",
49825
- displayName: "Web Search",
49826
- category: "web",
49827
- description: "Search the web and get relevant results with snippets.",
49828
- tool: webSearch,
49829
- safeByDefault: true,
49830
- requiresConnector: true,
49831
- connectorServiceTypes: ["serper", "brave-search", "tavily", "rapidapi-websearch"]
49832
49674
  }
49833
49675
  ];
49834
49676
  function getAllBuiltInTools() {