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.
- package/CHANGELOG.md +35 -0
- package/README.md +169 -0
- package/openclaw.plugin.json +48 -0
- package/package.json +33 -0
- package/src/commands/chat.ts +123 -0
- package/src/commands/cli.ts +130 -0
- package/src/commands/operations.ts +370 -0
- package/src/index.ts +70 -0
- package/src/lib/bootstrap.ts +114 -0
- package/src/lib/invoke.ts +202 -0
- package/src/lib/metrics-store.ts +177 -0
- package/src/lib/plugin-config.ts +143 -0
- package/src/lib/refs.ts +68 -0
- package/src/lib/result-shaper.ts +237 -0
- package/src/lib/result-store.ts +72 -0
- package/src/lib/tool-catalog.ts +339 -0
- package/src/tools/tool-batch.ts +374 -0
- package/src/tools/tool-dispatch.ts +157 -0
- package/src/tools/tool-result-get.ts +65 -0
- package/src/tools/tool-search.ts +72 -0
- package/src/types/openclaw-plugin-sdk.d.ts +78 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_SETTINGS,
|
|
4
|
+
deepCloneConfig,
|
|
5
|
+
ensurePluginEntry,
|
|
6
|
+
mergeUniqueStrings,
|
|
7
|
+
resolveSettings,
|
|
8
|
+
type AdvToolsMode,
|
|
9
|
+
writeConfig,
|
|
10
|
+
} from "../lib/plugin-config.js";
|
|
11
|
+
import type { MetricsStore } from "../lib/metrics-store.js";
|
|
12
|
+
|
|
13
|
+
const ADVTOOLS_TOOL_NAMES = ["tool_search", "tool_dispatch", "tool_batch", "tool_result_get"];
|
|
14
|
+
type ConfigKind = "boolean" | "integer" | "enum";
|
|
15
|
+
type ConfigSpec =
|
|
16
|
+
| { kind: "boolean" }
|
|
17
|
+
| { kind: "integer"; min: number; max: number }
|
|
18
|
+
| { kind: "enum"; values: string[] };
|
|
19
|
+
|
|
20
|
+
const CONFIG_SPECS: Record<string, ConfigSpec> = {
|
|
21
|
+
enabled: { kind: "boolean" },
|
|
22
|
+
mode: { kind: "enum", values: ["safe", "standard"] },
|
|
23
|
+
maxSteps: { kind: "integer", min: 1, max: 200 },
|
|
24
|
+
maxForEach: { kind: "integer", min: 1, max: 200 },
|
|
25
|
+
maxResultChars: { kind: "integer", min: 500, max: 500000 },
|
|
26
|
+
invokeTimeoutMs: { kind: "integer", min: 0, max: 1800000 },
|
|
27
|
+
storeLargeResults: { kind: "boolean" },
|
|
28
|
+
resultStoreTtlSec: { kind: "integer", min: 60, max: 86400 },
|
|
29
|
+
resultSampleItems: { kind: "integer", min: 1, max: 50 },
|
|
30
|
+
requireSandbox: { kind: "boolean" },
|
|
31
|
+
denyControlPlane: { kind: "boolean" },
|
|
32
|
+
"toolSearch.enabled": { kind: "boolean" },
|
|
33
|
+
"toolSearch.defaultLimit": { kind: "integer", min: 1, max: 50 },
|
|
34
|
+
"toolSearch.useLiveRegistry": { kind: "boolean" },
|
|
35
|
+
"toolSearch.liveTimeoutMs": { kind: "integer", min: 250, max: 10000 },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const DEFAULT_BY_KEY: Record<string, boolean | number | string> = {
|
|
39
|
+
enabled: DEFAULT_SETTINGS.enabled,
|
|
40
|
+
mode: DEFAULT_SETTINGS.mode,
|
|
41
|
+
maxSteps: DEFAULT_SETTINGS.maxSteps,
|
|
42
|
+
maxForEach: DEFAULT_SETTINGS.maxForEach,
|
|
43
|
+
maxResultChars: DEFAULT_SETTINGS.maxResultChars,
|
|
44
|
+
invokeTimeoutMs: DEFAULT_SETTINGS.invokeTimeoutMs,
|
|
45
|
+
storeLargeResults: DEFAULT_SETTINGS.storeLargeResults,
|
|
46
|
+
resultStoreTtlSec: DEFAULT_SETTINGS.resultStoreTtlSec,
|
|
47
|
+
resultSampleItems: DEFAULT_SETTINGS.resultSampleItems,
|
|
48
|
+
requireSandbox: DEFAULT_SETTINGS.requireSandbox,
|
|
49
|
+
denyControlPlane: DEFAULT_SETTINGS.denyControlPlane,
|
|
50
|
+
"toolSearch.enabled": DEFAULT_SETTINGS.toolSearch.enabled,
|
|
51
|
+
"toolSearch.defaultLimit": DEFAULT_SETTINGS.toolSearch.defaultLimit,
|
|
52
|
+
"toolSearch.useLiveRegistry": DEFAULT_SETTINGS.toolSearch.useLiveRegistry,
|
|
53
|
+
"toolSearch.liveTimeoutMs": DEFAULT_SETTINGS.toolSearch.liveTimeoutMs,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function asObj(value: unknown): Record<string, unknown> {
|
|
57
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
58
|
+
? (value as Record<string, unknown>)
|
|
59
|
+
: {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseBoolean(valueRaw: string): boolean | null {
|
|
63
|
+
const value = valueRaw.trim().toLowerCase();
|
|
64
|
+
if (["true", "1", "yes", "on"].includes(value)) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (["false", "0", "no", "off"].includes(value)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseConfigValue(key: string, valueRaw: string): { ok: true; value: unknown } | { ok: false; error: string } {
|
|
74
|
+
const spec = CONFIG_SPECS[key];
|
|
75
|
+
if (!spec) {
|
|
76
|
+
return { ok: false, error: `Unknown key: ${key}` };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (spec.kind === "boolean") {
|
|
80
|
+
const parsed = parseBoolean(valueRaw);
|
|
81
|
+
if (parsed === null) {
|
|
82
|
+
return { ok: false, error: `Invalid boolean for ${key}: ${valueRaw}` };
|
|
83
|
+
}
|
|
84
|
+
return { ok: true, value: parsed };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (spec.kind === "enum") {
|
|
88
|
+
if (!spec.values.includes(valueRaw)) {
|
|
89
|
+
return { ok: false, error: `${key} must be one of: ${spec.values.join(", ")}` };
|
|
90
|
+
}
|
|
91
|
+
return { ok: true, value: valueRaw };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const parsed = Number(valueRaw);
|
|
95
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
|
|
96
|
+
return { ok: false, error: `Invalid integer for ${key}: ${valueRaw}` };
|
|
97
|
+
}
|
|
98
|
+
if (parsed < spec.min || parsed > spec.max) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
error: `${key} must be between ${spec.min} and ${spec.max}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return { ok: true, value: parsed };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function setValueAtPath(root: Record<string, unknown>, key: string, value: unknown): void {
|
|
108
|
+
const path = key.split(".");
|
|
109
|
+
if (path.length === 1) {
|
|
110
|
+
root[path[0] as string] = value;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let cursor = root;
|
|
115
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
116
|
+
const part = path[i] as string;
|
|
117
|
+
cursor[part] = asObj(cursor[part]);
|
|
118
|
+
cursor = cursor[part] as Record<string, unknown>;
|
|
119
|
+
}
|
|
120
|
+
cursor[path[path.length - 1] as string] = value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sortedKeys(): string[] {
|
|
124
|
+
return Object.keys(CONFIG_SPECS).sort((a, b) => a.localeCompare(b));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function renderStatus(api: OpenClawPluginApi): string {
|
|
128
|
+
const loaded = api.runtime.config.loadConfig();
|
|
129
|
+
const s = resolveSettings(api, loaded);
|
|
130
|
+
const cfg = loaded as Record<string, unknown>;
|
|
131
|
+
|
|
132
|
+
const tools = asObj(cfg.tools);
|
|
133
|
+
const allow = Array.isArray(tools.allow)
|
|
134
|
+
? tools.allow.filter((v): v is string => typeof v === "string")
|
|
135
|
+
: [];
|
|
136
|
+
|
|
137
|
+
const allowlistedTools = ADVTOOLS_TOOL_NAMES.filter((name) => allow.includes(name));
|
|
138
|
+
|
|
139
|
+
return [
|
|
140
|
+
"OCSmartTools Status",
|
|
141
|
+
`- plugin: ${api.id}`,
|
|
142
|
+
`- mode: ${s.mode}`,
|
|
143
|
+
`- tool_search enabled: ${s.toolSearch.enabled}`,
|
|
144
|
+
`- maxSteps: ${s.maxSteps}`,
|
|
145
|
+
`- maxForEach: ${s.maxForEach}`,
|
|
146
|
+
`- invokeTimeoutMs: ${s.invokeTimeoutMs}`,
|
|
147
|
+
`- storeLargeResults: ${s.storeLargeResults}`,
|
|
148
|
+
`- resultStoreTtlSec: ${s.resultStoreTtlSec}`,
|
|
149
|
+
`- resultSampleItems: ${s.resultSampleItems}`,
|
|
150
|
+
`- requireSandbox: ${s.requireSandbox}`,
|
|
151
|
+
`- denyControlPlane: ${s.denyControlPlane}`,
|
|
152
|
+
`- tools allowlisted (if strict policy): ${allowlistedTools.length ? allowlistedTools.join(", ") : "none"}`,
|
|
153
|
+
].join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function renderHelp(): string {
|
|
157
|
+
return [
|
|
158
|
+
"OCSmartTools Help",
|
|
159
|
+
"",
|
|
160
|
+
"Chat commands:",
|
|
161
|
+
"- /ocsmarttools version: Show installed plugin version",
|
|
162
|
+
"- /ocsmarttools help: Show this help text",
|
|
163
|
+
"- /ocsmarttools status: Show current plugin status and safety flags",
|
|
164
|
+
"- /ocsmarttools stats: Show usage/savings metrics (success/failure/timeout/latency/shaping)",
|
|
165
|
+
"- /ocsmarttools stats reset: Reset metrics window",
|
|
166
|
+
"- /ocsmarttools setup [safe|standard]: Apply recommended defaults (default: standard)",
|
|
167
|
+
"- /ocsmarttools mode <safe|standard>: Switch only the operating mode",
|
|
168
|
+
"- /ocsmarttools config: Show effective plugin config",
|
|
169
|
+
"- /ocsmarttools config keys: List editable config keys",
|
|
170
|
+
"- /ocsmarttools config set <key> <value>: Update one config key",
|
|
171
|
+
"- /ocsmarttools config reset [key]: Reset one key (or all keys) to defaults",
|
|
172
|
+
"",
|
|
173
|
+
"Examples:",
|
|
174
|
+
"- /ocsmarttools config set maxResultChars 120000",
|
|
175
|
+
"- /ocsmarttools config set storeLargeResults true",
|
|
176
|
+
"- /ocsmarttools config reset maxResultChars",
|
|
177
|
+
].join("\n");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function renderVersion(api: OpenClawPluginApi): string {
|
|
181
|
+
return `ocsmarttools version: ${api.version ?? "unknown"}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function formatPct(value: number): string {
|
|
185
|
+
return `${(Math.max(0, Math.min(1, value)) * 100).toFixed(1)}%`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function renderStats(metrics: MetricsStore): string {
|
|
189
|
+
const s = metrics.snapshot();
|
|
190
|
+
const topTools =
|
|
191
|
+
s.topTools.length > 0
|
|
192
|
+
? s.topTools
|
|
193
|
+
.map(
|
|
194
|
+
(t) =>
|
|
195
|
+
` - ${t.tool}: ${t.calls} calls (${t.success} ok, ${t.failure} fail, ${t.timeout} timeout, ${t.shaped} shaped)`,
|
|
196
|
+
)
|
|
197
|
+
.join("\n")
|
|
198
|
+
: " - none";
|
|
199
|
+
|
|
200
|
+
return [
|
|
201
|
+
"OCSmartTools Stats",
|
|
202
|
+
`- windowStart: ${s.startedAt}`,
|
|
203
|
+
`- totalCalls: ${s.totalCalls}`,
|
|
204
|
+
`- success/failure/timeout: ${s.success}/${s.failure}/${s.timeout}`,
|
|
205
|
+
`- successRate: ${formatPct(s.successRate)}`,
|
|
206
|
+
`- timeoutRate: ${formatPct(s.timeoutRate)}`,
|
|
207
|
+
`- avgLatencyMs: ${s.avgLatencyMs.toFixed(1)}`,
|
|
208
|
+
`- p95LatencyApproxMs: ${s.p95LatencyApproxMs}`,
|
|
209
|
+
`- shapedResults: ${s.shapedResults}`,
|
|
210
|
+
`- shapedRateOnSuccess: ${formatPct(s.shapedRateOnSuccess)}`,
|
|
211
|
+
`- charsSaved: ${s.charsSaved.toLocaleString()}`,
|
|
212
|
+
`- charReductionRate: ${formatPct(s.charReductionRate)}`,
|
|
213
|
+
`- estimatedTokensSaved(approx chars/4): ${s.estimatedTokensSaved.toLocaleString()}`,
|
|
214
|
+
"- topTools:",
|
|
215
|
+
topTools,
|
|
216
|
+
].join("\n");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function resetStats(metrics: MetricsStore): string {
|
|
220
|
+
metrics.reset();
|
|
221
|
+
return "OCSmartTools stats reset.";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function renderConfig(api: OpenClawPluginApi): string {
|
|
225
|
+
const loaded = api.runtime.config.loadConfig();
|
|
226
|
+
const s = resolveSettings(api, loaded);
|
|
227
|
+
|
|
228
|
+
return [
|
|
229
|
+
"OCSmartTools Config",
|
|
230
|
+
`- enabled: ${s.enabled}`,
|
|
231
|
+
`- mode: ${s.mode}`,
|
|
232
|
+
`- maxSteps: ${s.maxSteps}`,
|
|
233
|
+
`- maxForEach: ${s.maxForEach}`,
|
|
234
|
+
`- maxResultChars: ${s.maxResultChars}`,
|
|
235
|
+
`- invokeTimeoutMs: ${s.invokeTimeoutMs}`,
|
|
236
|
+
`- storeLargeResults: ${s.storeLargeResults}`,
|
|
237
|
+
`- resultStoreTtlSec: ${s.resultStoreTtlSec}`,
|
|
238
|
+
`- resultSampleItems: ${s.resultSampleItems}`,
|
|
239
|
+
`- requireSandbox: ${s.requireSandbox}`,
|
|
240
|
+
`- denyControlPlane: ${s.denyControlPlane}`,
|
|
241
|
+
`- toolSearch.enabled: ${s.toolSearch.enabled}`,
|
|
242
|
+
`- toolSearch.defaultLimit: ${s.toolSearch.defaultLimit}`,
|
|
243
|
+
`- toolSearch.useLiveRegistry: ${s.toolSearch.useLiveRegistry}`,
|
|
244
|
+
`- toolSearch.liveTimeoutMs: ${s.toolSearch.liveTimeoutMs}`,
|
|
245
|
+
"",
|
|
246
|
+
"Use `/ocsmarttools config keys` to see editable keys.",
|
|
247
|
+
].join("\n");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function renderConfigKeys(): string {
|
|
251
|
+
return [
|
|
252
|
+
"OCSmartTools Config Keys",
|
|
253
|
+
...sortedKeys().map((key) => `- ${key}`),
|
|
254
|
+
].join("\n");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function setConfigKey(
|
|
258
|
+
api: OpenClawPluginApi,
|
|
259
|
+
key: string,
|
|
260
|
+
valueRaw: string,
|
|
261
|
+
): Promise<string> {
|
|
262
|
+
const parsed = parseConfigValue(key, valueRaw);
|
|
263
|
+
if (!parsed.ok) {
|
|
264
|
+
return parsed.error;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const next = deepCloneConfig(api.runtime.config.loadConfig());
|
|
268
|
+
const { entryObj } = ensurePluginEntry(next, api.id);
|
|
269
|
+
entryObj.enabled = true;
|
|
270
|
+
const pluginCfg = asObj(entryObj.config);
|
|
271
|
+
entryObj.config = pluginCfg;
|
|
272
|
+
|
|
273
|
+
setValueAtPath(pluginCfg, key, parsed.value);
|
|
274
|
+
await writeConfig(api, next);
|
|
275
|
+
|
|
276
|
+
return `Config updated: ${key}=${JSON.stringify(parsed.value)}.`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function resetConfig(api: OpenClawPluginApi, key?: string): Promise<string> {
|
|
280
|
+
const next = deepCloneConfig(api.runtime.config.loadConfig());
|
|
281
|
+
const { entryObj } = ensurePluginEntry(next, api.id);
|
|
282
|
+
entryObj.enabled = true;
|
|
283
|
+
const pluginCfg = asObj(entryObj.config);
|
|
284
|
+
entryObj.config = pluginCfg;
|
|
285
|
+
|
|
286
|
+
if (!key) {
|
|
287
|
+
for (const cfgKey of sortedKeys()) {
|
|
288
|
+
setValueAtPath(pluginCfg, cfgKey, DEFAULT_BY_KEY[cfgKey]);
|
|
289
|
+
}
|
|
290
|
+
await writeConfig(api, next);
|
|
291
|
+
return "Config reset to plugin defaults.";
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!(key in CONFIG_SPECS)) {
|
|
295
|
+
return `Unknown key: ${key}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
setValueAtPath(pluginCfg, key, DEFAULT_BY_KEY[key]);
|
|
299
|
+
await writeConfig(api, next);
|
|
300
|
+
return `Config key reset: ${key}=${JSON.stringify(DEFAULT_BY_KEY[key])}.`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function normalizeConfigKey(keyRaw: string): string {
|
|
304
|
+
return keyRaw.trim();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function canEditConfigKey(key: string): boolean {
|
|
308
|
+
return Boolean(CONFIG_SPECS[key]);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function applySetup(api: OpenClawPluginApi, mode: AdvToolsMode): Promise<string> {
|
|
312
|
+
const next = deepCloneConfig(api.runtime.config.loadConfig());
|
|
313
|
+
const root = next as Record<string, unknown>;
|
|
314
|
+
|
|
315
|
+
const { entryObj } = ensurePluginEntry(next, api.id);
|
|
316
|
+
entryObj.enabled = true;
|
|
317
|
+
|
|
318
|
+
const pluginCfg = asObj(entryObj.config);
|
|
319
|
+
entryObj.config = pluginCfg;
|
|
320
|
+
|
|
321
|
+
pluginCfg.enabled = true;
|
|
322
|
+
pluginCfg.mode = mode;
|
|
323
|
+
pluginCfg.maxSteps = DEFAULT_SETTINGS.maxSteps;
|
|
324
|
+
pluginCfg.maxForEach = DEFAULT_SETTINGS.maxForEach;
|
|
325
|
+
pluginCfg.maxResultChars = DEFAULT_SETTINGS.maxResultChars;
|
|
326
|
+
pluginCfg.invokeTimeoutMs = DEFAULT_SETTINGS.invokeTimeoutMs;
|
|
327
|
+
pluginCfg.storeLargeResults = DEFAULT_SETTINGS.storeLargeResults;
|
|
328
|
+
pluginCfg.resultStoreTtlSec = DEFAULT_SETTINGS.resultStoreTtlSec;
|
|
329
|
+
pluginCfg.resultSampleItems = DEFAULT_SETTINGS.resultSampleItems;
|
|
330
|
+
pluginCfg.requireSandbox = mode === "safe";
|
|
331
|
+
pluginCfg.denyControlPlane = true;
|
|
332
|
+
pluginCfg.toolSearch = {
|
|
333
|
+
enabled: true,
|
|
334
|
+
defaultLimit: DEFAULT_SETTINGS.toolSearch.defaultLimit,
|
|
335
|
+
useLiveRegistry: DEFAULT_SETTINGS.toolSearch.useLiveRegistry,
|
|
336
|
+
liveTimeoutMs: DEFAULT_SETTINGS.toolSearch.liveTimeoutMs,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const toolsObj = asObj(root.tools);
|
|
340
|
+
root.tools = toolsObj;
|
|
341
|
+
toolsObj.allow = mergeUniqueStrings(
|
|
342
|
+
Array.isArray(toolsObj.allow) ? toolsObj.allow : [],
|
|
343
|
+
ADVTOOLS_TOOL_NAMES,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
await writeConfig(api, next);
|
|
347
|
+
|
|
348
|
+
return [
|
|
349
|
+
"OCSmartTools setup applied.",
|
|
350
|
+
`- mode: ${mode}`,
|
|
351
|
+
`- ensured tools.allow includes: ${ADVTOOLS_TOOL_NAMES.join(", ")}`,
|
|
352
|
+
"- config written via runtime config writer",
|
|
353
|
+
"If your gateway does not hot-apply this change, run: openclaw gateway restart",
|
|
354
|
+
].join("\n");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export async function updateMode(api: OpenClawPluginApi, mode: AdvToolsMode): Promise<string> {
|
|
358
|
+
const next = deepCloneConfig(api.runtime.config.loadConfig());
|
|
359
|
+
const { entryObj } = ensurePluginEntry(next, api.id);
|
|
360
|
+
entryObj.enabled = true;
|
|
361
|
+
|
|
362
|
+
const pluginCfg = asObj(entryObj.config);
|
|
363
|
+
entryObj.config = pluginCfg;
|
|
364
|
+
pluginCfg.mode = mode;
|
|
365
|
+
pluginCfg.requireSandbox = mode === "safe";
|
|
366
|
+
pluginCfg.denyControlPlane = true;
|
|
367
|
+
|
|
368
|
+
await writeConfig(api, next);
|
|
369
|
+
return `Mode updated to ${mode}.`;
|
|
370
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { registerChatCommands } from "./commands/chat.js";
|
|
3
|
+
import { registerCliCommands } from "./commands/cli.js";
|
|
4
|
+
import { createToolSearchTool } from "./tools/tool-search.js";
|
|
5
|
+
import { createToolDispatchTool } from "./tools/tool-dispatch.js";
|
|
6
|
+
import { createToolBatchTool } from "./tools/tool-batch.js";
|
|
7
|
+
import { ResultStore } from "./lib/result-store.js";
|
|
8
|
+
import { createToolResultGetTool } from "./tools/tool-result-get.js";
|
|
9
|
+
import { autoBootstrap } from "./lib/bootstrap.js";
|
|
10
|
+
import { MetricsStore } from "./lib/metrics-store.js";
|
|
11
|
+
|
|
12
|
+
const plugin = {
|
|
13
|
+
id: "ocsmarttools",
|
|
14
|
+
name: "OCSmartTools",
|
|
15
|
+
description:
|
|
16
|
+
"Provider-agnostic advanced tool helper plugin with deterministic dispatch/batch tools and adaptive large-result handling.",
|
|
17
|
+
register(api: OpenClawPluginApi) {
|
|
18
|
+
const resultStore = new ResultStore();
|
|
19
|
+
const metricsStore = new MetricsStore();
|
|
20
|
+
let pruneTimer: NodeJS.Timeout | null = null;
|
|
21
|
+
|
|
22
|
+
registerChatCommands(api, metricsStore);
|
|
23
|
+
registerCliCommands(api, metricsStore);
|
|
24
|
+
|
|
25
|
+
api.registerTool(createToolSearchTool(api) as AnyAgentTool);
|
|
26
|
+
|
|
27
|
+
api.registerTool(
|
|
28
|
+
(ctx) =>
|
|
29
|
+
createToolDispatchTool(api, {
|
|
30
|
+
sandboxed: ctx.sandboxed,
|
|
31
|
+
store: resultStore,
|
|
32
|
+
metrics: metricsStore,
|
|
33
|
+
}) as AnyAgentTool,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
api.registerTool(
|
|
37
|
+
(ctx) =>
|
|
38
|
+
createToolBatchTool(api, {
|
|
39
|
+
sandboxed: ctx.sandboxed,
|
|
40
|
+
store: resultStore,
|
|
41
|
+
metrics: metricsStore,
|
|
42
|
+
}) as AnyAgentTool,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
api.registerTool(createToolResultGetTool(api, resultStore) as AnyAgentTool);
|
|
46
|
+
|
|
47
|
+
api.registerService({
|
|
48
|
+
id: "ocsmarttools",
|
|
49
|
+
start: async () => {
|
|
50
|
+
const bootstrap = await autoBootstrap(api);
|
|
51
|
+
if (bootstrap.changed) {
|
|
52
|
+
api.logger.info(`[ocsmarttools] auto-bootstrap applied: ${bootstrap.notes.join(", ")}`);
|
|
53
|
+
}
|
|
54
|
+
resultStore.prune();
|
|
55
|
+
pruneTimer = setInterval(() => resultStore.prune(), 60_000);
|
|
56
|
+
api.logger.info("[ocsmarttools] service started");
|
|
57
|
+
},
|
|
58
|
+
stop: () => {
|
|
59
|
+
if (pruneTimer) {
|
|
60
|
+
clearInterval(pruneTimer);
|
|
61
|
+
pruneTimer = null;
|
|
62
|
+
}
|
|
63
|
+
resultStore.clear();
|
|
64
|
+
api.logger.info("[ocsmarttools] service stopped");
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default plugin;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_SETTINGS,
|
|
4
|
+
deepCloneConfig,
|
|
5
|
+
ensurePluginEntry,
|
|
6
|
+
mergeUniqueStrings,
|
|
7
|
+
writeConfig,
|
|
8
|
+
} from "./plugin-config.js";
|
|
9
|
+
|
|
10
|
+
const TOOL_NAMES = ["tool_search", "tool_dispatch", "tool_batch", "tool_result_get"];
|
|
11
|
+
|
|
12
|
+
function asObj(value: unknown): Record<string, unknown> {
|
|
13
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
14
|
+
? (value as Record<string, unknown>)
|
|
15
|
+
: {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function autoBootstrap(api: OpenClawPluginApi): Promise<{ changed: boolean; notes: string[] }> {
|
|
19
|
+
const current = api.runtime.config.loadConfig();
|
|
20
|
+
const next = deepCloneConfig(current);
|
|
21
|
+
const notes: string[] = [];
|
|
22
|
+
let changed = false;
|
|
23
|
+
|
|
24
|
+
const root = next as Record<string, unknown>;
|
|
25
|
+
const { entryObj } = ensurePluginEntry(next, api.id);
|
|
26
|
+
if (entryObj.enabled !== true) {
|
|
27
|
+
entryObj.enabled = true;
|
|
28
|
+
changed = true;
|
|
29
|
+
notes.push("enabled plugin entry");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const pluginCfg = asObj(entryObj.config);
|
|
33
|
+
if (entryObj.config !== pluginCfg) {
|
|
34
|
+
entryObj.config = pluginCfg;
|
|
35
|
+
changed = true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const setDefault = (key: string, value: unknown) => {
|
|
39
|
+
if (pluginCfg[key] === undefined) {
|
|
40
|
+
pluginCfg[key] = value;
|
|
41
|
+
changed = true;
|
|
42
|
+
notes.push(`set default ${key}`);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
setDefault("enabled", true);
|
|
47
|
+
setDefault("mode", DEFAULT_SETTINGS.mode);
|
|
48
|
+
setDefault("maxSteps", DEFAULT_SETTINGS.maxSteps);
|
|
49
|
+
setDefault("maxForEach", DEFAULT_SETTINGS.maxForEach);
|
|
50
|
+
setDefault("maxResultChars", DEFAULT_SETTINGS.maxResultChars);
|
|
51
|
+
setDefault("invokeTimeoutMs", DEFAULT_SETTINGS.invokeTimeoutMs);
|
|
52
|
+
setDefault("storeLargeResults", DEFAULT_SETTINGS.storeLargeResults);
|
|
53
|
+
setDefault("resultStoreTtlSec", DEFAULT_SETTINGS.resultStoreTtlSec);
|
|
54
|
+
setDefault("resultSampleItems", DEFAULT_SETTINGS.resultSampleItems);
|
|
55
|
+
setDefault("requireSandbox", DEFAULT_SETTINGS.requireSandbox);
|
|
56
|
+
setDefault("denyControlPlane", DEFAULT_SETTINGS.denyControlPlane);
|
|
57
|
+
if (pluginCfg.toolSearch === undefined) {
|
|
58
|
+
pluginCfg.toolSearch = {
|
|
59
|
+
enabled: DEFAULT_SETTINGS.toolSearch.enabled,
|
|
60
|
+
defaultLimit: DEFAULT_SETTINGS.toolSearch.defaultLimit,
|
|
61
|
+
useLiveRegistry: DEFAULT_SETTINGS.toolSearch.useLiveRegistry,
|
|
62
|
+
liveTimeoutMs: DEFAULT_SETTINGS.toolSearch.liveTimeoutMs,
|
|
63
|
+
};
|
|
64
|
+
changed = true;
|
|
65
|
+
notes.push("set default toolSearch");
|
|
66
|
+
} else {
|
|
67
|
+
const ts = asObj(pluginCfg.toolSearch);
|
|
68
|
+
if (pluginCfg.toolSearch !== ts) {
|
|
69
|
+
pluginCfg.toolSearch = ts;
|
|
70
|
+
changed = true;
|
|
71
|
+
}
|
|
72
|
+
if (ts.enabled === undefined) {
|
|
73
|
+
ts.enabled = DEFAULT_SETTINGS.toolSearch.enabled;
|
|
74
|
+
changed = true;
|
|
75
|
+
notes.push("set default toolSearch.enabled");
|
|
76
|
+
}
|
|
77
|
+
if (ts.defaultLimit === undefined) {
|
|
78
|
+
ts.defaultLimit = DEFAULT_SETTINGS.toolSearch.defaultLimit;
|
|
79
|
+
changed = true;
|
|
80
|
+
notes.push("set default toolSearch.defaultLimit");
|
|
81
|
+
}
|
|
82
|
+
if (ts.useLiveRegistry === undefined) {
|
|
83
|
+
ts.useLiveRegistry = DEFAULT_SETTINGS.toolSearch.useLiveRegistry;
|
|
84
|
+
changed = true;
|
|
85
|
+
notes.push("set default toolSearch.useLiveRegistry");
|
|
86
|
+
}
|
|
87
|
+
if (ts.liveTimeoutMs === undefined) {
|
|
88
|
+
ts.liveTimeoutMs = DEFAULT_SETTINGS.toolSearch.liveTimeoutMs;
|
|
89
|
+
changed = true;
|
|
90
|
+
notes.push("set default toolSearch.liveTimeoutMs");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// If the user already runs strict allowlist mode, auto-add ocsmarttools tool names.
|
|
95
|
+
const toolsObj = asObj(root.tools);
|
|
96
|
+
root.tools = toolsObj;
|
|
97
|
+
const allowRaw = toolsObj.allow;
|
|
98
|
+
if (Array.isArray(allowRaw)) {
|
|
99
|
+
const merged = mergeUniqueStrings(allowRaw, TOOL_NAMES);
|
|
100
|
+
if (
|
|
101
|
+
merged.length !== allowRaw.length ||
|
|
102
|
+
merged.some((item, index) => allowRaw[index] !== item)
|
|
103
|
+
) {
|
|
104
|
+
toolsObj.allow = merged;
|
|
105
|
+
changed = true;
|
|
106
|
+
notes.push("extended tools.allow with ocsmarttools tools");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (changed) {
|
|
111
|
+
await writeConfig(api, next);
|
|
112
|
+
}
|
|
113
|
+
return { changed, notes };
|
|
114
|
+
}
|