ocsmarttools 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,374 @@
1
+ import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { jsonResult } from "openclaw/plugin-sdk";
3
+ import { resolveSettings } from "../lib/plugin-config.js";
4
+ import { invokeToolViaGateway } from "../lib/invoke.js";
5
+ import type { ResultStore } from "../lib/result-store.js";
6
+ import { shapeToolResult } from "../lib/result-shaper.js";
7
+ import { resolveValueTemplates } from "../lib/refs.js";
8
+ import type { MetricsStore } from "../lib/metrics-store.js";
9
+
10
+ const CONTROL_PLANE = new Set(["gateway", "cron"]);
11
+
12
+ type BatchStepCall = {
13
+ type?: "call";
14
+ id?: string;
15
+ tool: string;
16
+ action?: string;
17
+ args?: Record<string, unknown>;
18
+ sessionKey?: string;
19
+ channel?: string;
20
+ accountId?: string;
21
+ timeoutMs?: number;
22
+ };
23
+
24
+ type BatchStepForEach = {
25
+ type: "foreach";
26
+ id?: string;
27
+ items: unknown;
28
+ as?: string;
29
+ call: BatchStepCall;
30
+ };
31
+
32
+ type BatchStep = BatchStepCall | BatchStepForEach;
33
+
34
+ export const ToolBatchSchema = {
35
+ type: "object",
36
+ additionalProperties: false,
37
+ properties: {
38
+ steps: {
39
+ type: "array",
40
+ description: "Array of call/foreach steps.",
41
+ items: { type: "object" },
42
+ minItems: 1,
43
+ maxItems: 200,
44
+ },
45
+ sessionKey: { type: "string" },
46
+ channel: { type: "string" },
47
+ accountId: { type: "string" },
48
+ maxSteps: { type: "number", minimum: 1, maximum: 200 },
49
+ maxForEach: { type: "number", minimum: 1, maximum: 200 },
50
+ timeoutMs: { type: "number", minimum: 0, maximum: 1800000 },
51
+ },
52
+ required: ["steps"],
53
+ };
54
+
55
+ function normalizeStepId(candidate: unknown, index: number, existing: Set<string>): string {
56
+ const base = typeof candidate === "string" && candidate.trim() ? candidate.trim() : `step_${index + 1}`;
57
+ if (!existing.has(base)) {
58
+ existing.add(base);
59
+ return base;
60
+ }
61
+ let seq = 2;
62
+ while (existing.has(`${base}_${seq}`)) {
63
+ seq += 1;
64
+ }
65
+ const id = `${base}_${seq}`;
66
+ existing.add(id);
67
+ return id;
68
+ }
69
+
70
+ function asRecord(value: unknown): Record<string, unknown> {
71
+ return value && typeof value === "object" && !Array.isArray(value)
72
+ ? (value as Record<string, unknown>)
73
+ : {};
74
+ }
75
+
76
+ function parseSteps(raw: unknown): BatchStep[] {
77
+ if (!Array.isArray(raw)) {
78
+ return [];
79
+ }
80
+ return raw.filter((entry) => entry && typeof entry === "object") as BatchStep[];
81
+ }
82
+
83
+ async function runCall(params: {
84
+ api: OpenClawPluginApi;
85
+ config: Record<string, unknown>;
86
+ settings: ReturnType<typeof resolveSettings>;
87
+ step: BatchStepCall;
88
+ context: Record<string, unknown>;
89
+ defaults: { sessionKey?: string; channel?: string; accountId?: string; timeoutMs?: number };
90
+ timeoutMs?: number;
91
+ signal?: AbortSignal;
92
+ sandboxed?: boolean;
93
+ store?: ResultStore;
94
+ metrics?: MetricsStore;
95
+ }) {
96
+ const { api, config, settings, step, context, defaults, sandboxed } = params;
97
+
98
+ const resolvedTool = resolveValueTemplates(step.tool, context);
99
+ const toolName = typeof resolvedTool === "string" ? resolvedTool.trim() : "";
100
+ if (!toolName) {
101
+ return { ok: false, error: "call step missing tool" };
102
+ }
103
+
104
+ if (toolName === "tool_batch") {
105
+ return { ok: false, error: "tool_batch cannot recursively dispatch itself" };
106
+ }
107
+
108
+ if (settings.requireSandbox && sandboxed === false) {
109
+ return { ok: false, error: "call blocked: sandbox required by plugin mode" };
110
+ }
111
+
112
+ if (settings.denyControlPlane && CONTROL_PLANE.has(toolName)) {
113
+ return { ok: false, error: `call blocked for control-plane tool: ${toolName}` };
114
+ }
115
+
116
+ const resolvedArgs = resolveValueTemplates(step.args ?? {}, context);
117
+ const args = asRecord(resolvedArgs);
118
+
119
+ const resolvedAction = resolveValueTemplates(step.action, context);
120
+ const action = typeof resolvedAction === "string" && resolvedAction.trim() ? resolvedAction.trim() : undefined;
121
+
122
+ const resolvedSessionKey = resolveValueTemplates(step.sessionKey, context);
123
+ const sessionKey =
124
+ typeof resolvedSessionKey === "string" && resolvedSessionKey.trim()
125
+ ? resolvedSessionKey.trim()
126
+ : defaults.sessionKey;
127
+
128
+ const resolvedChannel = resolveValueTemplates(step.channel, context);
129
+ const channel =
130
+ typeof resolvedChannel === "string" && resolvedChannel.trim()
131
+ ? resolvedChannel.trim()
132
+ : defaults.channel;
133
+
134
+ const resolvedAccountId = resolveValueTemplates(step.accountId, context);
135
+ const accountId =
136
+ typeof resolvedAccountId === "string" && resolvedAccountId.trim()
137
+ ? resolvedAccountId.trim()
138
+ : defaults.accountId;
139
+ const resolvedTimeoutMs = resolveValueTemplates(step.timeoutMs, context);
140
+ const timeoutMs =
141
+ typeof resolvedTimeoutMs === "number" && Number.isFinite(resolvedTimeoutMs)
142
+ ? Math.trunc(resolvedTimeoutMs)
143
+ : defaults.timeoutMs ?? settings.invokeTimeoutMs;
144
+
145
+ const invoke = await invokeToolViaGateway(config, {
146
+ tool: toolName,
147
+ action,
148
+ args,
149
+ sessionKey,
150
+ channel,
151
+ accountId,
152
+ timeoutMs,
153
+ signal: params.signal,
154
+ });
155
+
156
+ if (!invoke.ok) {
157
+ params.metrics?.record({
158
+ tool: toolName,
159
+ outcome: invoke.error?.type === "timeout" ? "timeout" : "failure",
160
+ latencyMs: invoke.latencyMs,
161
+ });
162
+ return {
163
+ ok: false,
164
+ status: invoke.status,
165
+ tool: toolName,
166
+ error: invoke.error,
167
+ };
168
+ }
169
+
170
+ const shaped = shapeToolResult({
171
+ toolName,
172
+ value: invoke.result,
173
+ settings,
174
+ store: params.store,
175
+ });
176
+ const jsonChars = (value: unknown): number => {
177
+ try {
178
+ const serialized = JSON.stringify(value);
179
+ return serialized ? serialized.length : 0;
180
+ } catch {
181
+ return 0;
182
+ }
183
+ };
184
+ const isShaped = (value: unknown): boolean => {
185
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
186
+ return false;
187
+ }
188
+ const obj = value as Record<string, unknown>;
189
+ return obj.reduced === true || typeof obj.handle === "string";
190
+ };
191
+ params.metrics?.record({
192
+ tool: toolName,
193
+ outcome: "success",
194
+ latencyMs: invoke.latencyMs,
195
+ shaped: isShaped(shaped),
196
+ originalChars: jsonChars(invoke.result),
197
+ returnedChars: jsonChars(shaped),
198
+ });
199
+
200
+ return {
201
+ ok: true,
202
+ status: invoke.status,
203
+ tool: toolName,
204
+ result: shaped,
205
+ };
206
+ }
207
+
208
+ export function createToolBatchTool(
209
+ api: OpenClawPluginApi,
210
+ options?: { sandboxed?: boolean; store?: ResultStore; metrics?: MetricsStore },
211
+ ): AnyAgentTool {
212
+ return {
213
+ name: "tool_batch",
214
+ label: "Tool Batch",
215
+ description:
216
+ "Run a bounded sequence of tool calls with deterministic call/foreach steps and template refs.",
217
+ parameters: ToolBatchSchema,
218
+ async execute(_toolCallId: string, params: Record<string, unknown>, signal?: AbortSignal) {
219
+ const loaded = api.runtime.config.loadConfig();
220
+ const settings = resolveSettings(api, loaded);
221
+ if (!settings.enabled) {
222
+ return jsonResult({
223
+ ok: false,
224
+ error: "ocsmarttools plugin config is disabled.",
225
+ });
226
+ }
227
+
228
+ const steps = parseSteps(params?.steps);
229
+ if (steps.length === 0) {
230
+ return jsonResult({ ok: false, error: "steps array is required" });
231
+ }
232
+
233
+ const requestedMaxSteps =
234
+ typeof params?.maxSteps === "number" && Number.isFinite(params.maxSteps)
235
+ ? Math.trunc(params.maxSteps)
236
+ : settings.maxSteps;
237
+ const maxSteps = Math.max(1, Math.min(200, requestedMaxSteps));
238
+
239
+ const requestedMaxForEach =
240
+ typeof params?.maxForEach === "number" && Number.isFinite(params.maxForEach)
241
+ ? Math.trunc(params.maxForEach)
242
+ : settings.maxForEach;
243
+ const maxForEach = Math.max(1, Math.min(200, requestedMaxForEach));
244
+
245
+ if (steps.length > maxSteps) {
246
+ return jsonResult({
247
+ ok: false,
248
+ error: `steps exceed maxSteps (${steps.length} > ${maxSteps})`,
249
+ });
250
+ }
251
+
252
+ const defaults = {
253
+ sessionKey:
254
+ typeof params?.sessionKey === "string" && params.sessionKey.trim()
255
+ ? params.sessionKey.trim()
256
+ : undefined,
257
+ channel:
258
+ typeof params?.channel === "string" && params.channel.trim() ? params.channel.trim() : undefined,
259
+ accountId:
260
+ typeof params?.accountId === "string" && params.accountId.trim()
261
+ ? params.accountId.trim()
262
+ : undefined,
263
+ timeoutMs:
264
+ typeof params?.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
265
+ ? Math.trunc(params.timeoutMs)
266
+ : settings.invokeTimeoutMs,
267
+ };
268
+
269
+ const stepOutputs: Record<string, unknown> = {};
270
+ const ordered: Array<{ id: string; output: unknown }> = [];
271
+ const ids = new Set<string>();
272
+
273
+ for (const [index, stepRaw] of steps.entries()) {
274
+ const step = asRecord(stepRaw);
275
+ const type = typeof step.type === "string" ? step.type : "call";
276
+ const stepId = normalizeStepId(step.id, index, ids);
277
+
278
+ const contextBase: Record<string, unknown> = {
279
+ steps: stepOutputs,
280
+ index,
281
+ };
282
+
283
+ if (type === "foreach") {
284
+ const asName = typeof step.as === "string" && step.as.trim() ? step.as.trim() : "item";
285
+ const resolvedItems = resolveValueTemplates(step.items, contextBase);
286
+ if (!Array.isArray(resolvedItems)) {
287
+ stepOutputs[stepId] = {
288
+ ok: false,
289
+ error: "foreach.items must resolve to an array",
290
+ };
291
+ ordered.push({ id: stepId, output: stepOutputs[stepId] });
292
+ continue;
293
+ }
294
+
295
+ const subset = resolvedItems.slice(0, maxForEach);
296
+ const results: unknown[] = [];
297
+ let failures = 0;
298
+
299
+ const callSpec = asRecord(step.call) as BatchStepCall;
300
+
301
+ for (let i = 0; i < subset.length; i += 1) {
302
+ const item = subset[i];
303
+ const context = {
304
+ ...contextBase,
305
+ item,
306
+ index: i,
307
+ vars: {
308
+ [asName]: item,
309
+ },
310
+ };
311
+ const itemResult = await runCall({
312
+ api,
313
+ config: loaded,
314
+ settings,
315
+ step: callSpec,
316
+ context,
317
+ defaults,
318
+ signal: signal as AbortSignal | undefined,
319
+ sandboxed: options?.sandboxed,
320
+ store: options?.store,
321
+ metrics: options?.metrics,
322
+ });
323
+ if (!itemResult.ok) {
324
+ failures += 1;
325
+ }
326
+ results.push(itemResult);
327
+ }
328
+
329
+ stepOutputs[stepId] = {
330
+ ok: failures === 0,
331
+ type: "foreach",
332
+ itemCount: subset.length,
333
+ failures,
334
+ results,
335
+ };
336
+ ordered.push({ id: stepId, output: stepOutputs[stepId] });
337
+ continue;
338
+ }
339
+
340
+ const callStep = step as unknown as BatchStepCall;
341
+ const output = await runCall({
342
+ api,
343
+ config: loaded,
344
+ settings,
345
+ step: callStep,
346
+ context: contextBase,
347
+ defaults,
348
+ signal: signal as AbortSignal | undefined,
349
+ sandboxed: options?.sandboxed,
350
+ store: options?.store,
351
+ metrics: options?.metrics,
352
+ });
353
+ stepOutputs[stepId] = output;
354
+ ordered.push({ id: stepId, output });
355
+ }
356
+
357
+ const failed = ordered.filter((step) => {
358
+ const out = asRecord(step.output);
359
+ return out.ok === false;
360
+ }).length;
361
+
362
+ return jsonResult({
363
+ ok: failed === 0,
364
+ summary: {
365
+ totalSteps: ordered.length,
366
+ failedSteps: failed,
367
+ maxSteps,
368
+ maxForEach,
369
+ },
370
+ steps: ordered,
371
+ });
372
+ },
373
+ } as AnyAgentTool;
374
+ }
@@ -0,0 +1,157 @@
1
+ import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { jsonResult } from "openclaw/plugin-sdk";
3
+ import { resolveSettings } from "../lib/plugin-config.js";
4
+ import { invokeToolViaGateway } from "../lib/invoke.js";
5
+ import type { ResultStore } from "../lib/result-store.js";
6
+ import { shapeToolResult } from "../lib/result-shaper.js";
7
+ import type { MetricsStore } from "../lib/metrics-store.js";
8
+
9
+ const CONTROL_PLANE = new Set(["gateway", "cron"]);
10
+ const INTERNAL_LOOP_TOOLS = new Set(["tool_dispatch", "tool_batch"]);
11
+
12
+ export const ToolDispatchSchema = {
13
+ type: "object",
14
+ additionalProperties: false,
15
+ properties: {
16
+ tool: { type: "string", description: "Tool name to invoke." },
17
+ action: { type: "string", description: "Optional action helper for tools using action patterns." },
18
+ args: { type: "object", description: "Tool arguments object." },
19
+ sessionKey: { type: "string" },
20
+ channel: { type: "string" },
21
+ accountId: { type: "string" },
22
+ timeoutMs: { type: "number", minimum: 0, maximum: 1800000 },
23
+ },
24
+ required: ["tool"],
25
+ };
26
+
27
+ export function createToolDispatchTool(
28
+ api: OpenClawPluginApi,
29
+ options?: { sandboxed?: boolean; store?: ResultStore; metrics?: MetricsStore },
30
+ ): AnyAgentTool {
31
+ const jsonChars = (value: unknown): number => {
32
+ try {
33
+ const serialized = JSON.stringify(value);
34
+ return serialized ? serialized.length : 0;
35
+ } catch {
36
+ return 0;
37
+ }
38
+ };
39
+
40
+ const isShaped = (value: unknown): boolean => {
41
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
42
+ return false;
43
+ }
44
+ const obj = value as Record<string, unknown>;
45
+ return obj.reduced === true || typeof obj.handle === "string";
46
+ };
47
+
48
+ return {
49
+ name: "tool_dispatch",
50
+ label: "Tool Dispatch",
51
+ description:
52
+ "Invoke a single tool through Gateway /tools/invoke with normal OpenClaw policy enforcement.",
53
+ parameters: ToolDispatchSchema,
54
+ async execute(_toolCallId: string, params: Record<string, unknown>, signal?: AbortSignal) {
55
+ const loaded = api.runtime.config.loadConfig();
56
+ const settings = resolveSettings(api, loaded);
57
+ if (!settings.enabled) {
58
+ return jsonResult({
59
+ ok: false,
60
+ error: "ocsmarttools plugin config is disabled.",
61
+ });
62
+ }
63
+ const toolName = typeof params?.tool === "string" ? params.tool.trim() : "";
64
+
65
+ if (!toolName) {
66
+ return jsonResult({ ok: false, error: "tool is required" });
67
+ }
68
+
69
+ if (INTERNAL_LOOP_TOOLS.has(toolName)) {
70
+ return jsonResult({
71
+ ok: false,
72
+ error: `Dispatch blocked for internal orchestration tool: ${toolName}`,
73
+ });
74
+ }
75
+
76
+ if (settings.requireSandbox && options?.sandboxed === false) {
77
+ return jsonResult({
78
+ ok: false,
79
+ error: "Dispatch blocked: plugin requires sandboxed execution in current mode.",
80
+ });
81
+ }
82
+
83
+ if (settings.denyControlPlane && CONTROL_PLANE.has(toolName)) {
84
+ return jsonResult({
85
+ ok: false,
86
+ error: `Dispatch blocked in ${settings.mode} mode for control-plane tool: ${toolName}`,
87
+ });
88
+ }
89
+
90
+ const args = params?.args;
91
+ const safeArgs = args && typeof args === "object" && !Array.isArray(args) ? args : {};
92
+ const action = typeof params?.action === "string" ? params.action.trim() : undefined;
93
+ const sessionKey =
94
+ typeof params?.sessionKey === "string" && params.sessionKey.trim()
95
+ ? params.sessionKey.trim()
96
+ : undefined;
97
+ const channel =
98
+ typeof params?.channel === "string" && params.channel.trim() ? params.channel.trim() : undefined;
99
+ const accountId =
100
+ typeof params?.accountId === "string" && params.accountId.trim()
101
+ ? params.accountId.trim()
102
+ : undefined;
103
+ const requestedTimeoutMs =
104
+ typeof params?.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
105
+ ? Math.trunc(params.timeoutMs)
106
+ : settings.invokeTimeoutMs;
107
+
108
+ const result = await invokeToolViaGateway(loaded, {
109
+ tool: toolName,
110
+ action,
111
+ args: safeArgs as Record<string, unknown>,
112
+ sessionKey,
113
+ channel,
114
+ accountId,
115
+ timeoutMs: requestedTimeoutMs,
116
+ signal: signal as AbortSignal | undefined,
117
+ });
118
+
119
+ if (!result.ok) {
120
+ options?.metrics?.record({
121
+ tool: toolName,
122
+ outcome: result.error?.type === "timeout" ? "timeout" : "failure",
123
+ latencyMs: result.latencyMs,
124
+ });
125
+ return jsonResult({
126
+ ok: false,
127
+ status: result.status,
128
+ error: result.error,
129
+ });
130
+ }
131
+
132
+ const shaped = shapeToolResult({
133
+ toolName,
134
+ value: result.result,
135
+ settings,
136
+ store: options?.store,
137
+ });
138
+ const originalChars = jsonChars(result.result);
139
+ const returnedChars = jsonChars(shaped);
140
+ options?.metrics?.record({
141
+ tool: toolName,
142
+ outcome: "success",
143
+ latencyMs: result.latencyMs,
144
+ shaped: isShaped(shaped),
145
+ originalChars,
146
+ returnedChars,
147
+ });
148
+
149
+ return jsonResult({
150
+ ok: true,
151
+ status: result.status,
152
+ tool: toolName,
153
+ result: shaped,
154
+ });
155
+ },
156
+ } as AnyAgentTool;
157
+ }
@@ -0,0 +1,65 @@
1
+ import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { jsonResult } from "openclaw/plugin-sdk";
3
+ import { resolveSettings } from "../lib/plugin-config.js";
4
+ import type { ResultStore } from "../lib/result-store.js";
5
+ import { shapeToolResult } from "../lib/result-shaper.js";
6
+
7
+ export const ToolResultGetSchema = {
8
+ type: "object",
9
+ additionalProperties: false,
10
+ properties: {
11
+ handle: { type: "string", description: "Handle returned by tool_dispatch/tool_batch for large stored results." },
12
+ maxChars: { type: "number", minimum: 500, maximum: 500000 },
13
+ },
14
+ required: ["handle"],
15
+ };
16
+
17
+ export function createToolResultGetTool(api: OpenClawPluginApi, store: ResultStore): AnyAgentTool {
18
+ return {
19
+ name: "tool_result_get",
20
+ label: "Tool Result Get",
21
+ description: "Retrieve a previously stored large tool result by handle.",
22
+ parameters: ToolResultGetSchema,
23
+ async execute(_toolCallId: string, params: Record<string, unknown>) {
24
+ const loaded = api.runtime.config.loadConfig();
25
+ const settings = resolveSettings(api, loaded);
26
+ if (!settings.enabled) {
27
+ return jsonResult({ ok: false, error: "ocsmarttools plugin config is disabled." });
28
+ }
29
+
30
+ const handle = typeof params.handle === "string" ? params.handle.trim() : "";
31
+ if (!handle) {
32
+ return jsonResult({ ok: false, error: "handle is required" });
33
+ }
34
+
35
+ const lookedUp = store.get(handle);
36
+ if (!lookedUp.ok) {
37
+ return jsonResult({
38
+ ok: false,
39
+ error: `No stored result found for handle: ${handle}`,
40
+ });
41
+ }
42
+
43
+ const maxChars =
44
+ typeof params.maxChars === "number" && Number.isFinite(params.maxChars)
45
+ ? Math.trunc(params.maxChars)
46
+ : settings.maxResultChars;
47
+
48
+ const shaped = shapeToolResult({
49
+ toolName: lookedUp.toolName ?? "tool_result_get",
50
+ value: lookedUp.value,
51
+ settings,
52
+ maxChars,
53
+ allowStore: false,
54
+ });
55
+
56
+ return jsonResult({
57
+ ok: true,
58
+ handle,
59
+ expiresAt: new Date(lookedUp.expiresAt).toISOString(),
60
+ result: shaped,
61
+ });
62
+ },
63
+ } as AnyAgentTool;
64
+ }
65
+
@@ -0,0 +1,72 @@
1
+ import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { jsonResult } from "openclaw/plugin-sdk";
3
+ import { resolveSettings } from "../lib/plugin-config.js";
4
+ import { buildCatalogWithLive, searchCatalog } from "../lib/tool-catalog.js";
5
+
6
+ export const ToolSearchSchema = {
7
+ type: "object",
8
+ additionalProperties: false,
9
+ properties: {
10
+ query: { type: "string", description: "Search query for tool discovery." },
11
+ limit: { type: "number", minimum: 1, maximum: 50 },
12
+ group: { type: "string", description: "Optional group filter, e.g. web, fs, runtime." },
13
+ },
14
+ required: ["query"],
15
+ };
16
+
17
+ export function createToolSearchTool(api: OpenClawPluginApi): AnyAgentTool {
18
+ return {
19
+ name: "tool_search",
20
+ label: "Tool Search",
21
+ description: "Search available tools and return compact metadata.",
22
+ parameters: ToolSearchSchema,
23
+ async execute(_toolCallId, params) {
24
+ const loaded = api.runtime.config.loadConfig();
25
+ const settings = resolveSettings(api, loaded);
26
+ if (!settings.enabled) {
27
+ return jsonResult({
28
+ ok: false,
29
+ error: "ocsmarttools plugin config is disabled.",
30
+ });
31
+ }
32
+ if (!settings.toolSearch.enabled) {
33
+ return jsonResult({
34
+ ok: false,
35
+ error: "tool_search is disabled in plugin config.",
36
+ });
37
+ }
38
+
39
+ const query = typeof params?.query === "string" ? params.query.trim() : "";
40
+ if (!query) {
41
+ return jsonResult({ ok: false, error: "query is required" });
42
+ }
43
+
44
+ const requestedLimit =
45
+ typeof params?.limit === "number" && Number.isFinite(params.limit)
46
+ ? Math.trunc(params.limit)
47
+ : settings.toolSearch.defaultLimit;
48
+ const limit = Math.max(1, Math.min(50, requestedLimit));
49
+
50
+ const group = typeof params?.group === "string" ? params.group.trim() : undefined;
51
+
52
+ const built = await buildCatalogWithLive({
53
+ cfg: loaded,
54
+ useLiveRegistry: settings.toolSearch.useLiveRegistry,
55
+ liveTimeoutMs: settings.toolSearch.liveTimeoutMs,
56
+ });
57
+ const catalog = built.catalog;
58
+ const matches = searchCatalog({ catalog, query, limit, group });
59
+
60
+ return jsonResult({
61
+ ok: true,
62
+ query,
63
+ group: group ?? null,
64
+ catalogSource: built.source,
65
+ liveCount: built.liveCount,
66
+ totalCatalogSize: catalog.length,
67
+ count: matches.length,
68
+ tools: matches,
69
+ });
70
+ },
71
+ } as AnyAgentTool;
72
+ }