ocuclaw 0.1.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.
@@ -0,0 +1,365 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export const DEFAULT_EVEN_AI_ROUTING_MODE = "active";
5
+ export const SUPPORTED_EVEN_AI_ROUTING_MODES = Object.freeze([
6
+ "active",
7
+ "background",
8
+ "background_new",
9
+ ]);
10
+ const ACCEPTED_EVEN_AI_ROUTING_MODES = Object.freeze([
11
+ ...SUPPORTED_EVEN_AI_ROUTING_MODES,
12
+ "dedicated",
13
+ "new",
14
+ "dedicated_shadow",
15
+ "new_shadow",
16
+ ]);
17
+
18
+ const STORE_VERSION = 1;
19
+ const STORE_FILENAME = "even-ai-settings.json";
20
+ const MAX_TRACKED_THROWAWAY_KEYS = 20;
21
+
22
+ function normalizeLogger(logger) {
23
+ if (!logger || typeof logger !== "object") {
24
+ return console;
25
+ }
26
+ return {
27
+ info: typeof logger.info === "function" ? logger.info.bind(logger) : console.log,
28
+ warn: typeof logger.warn === "function" ? logger.warn.bind(logger) : console.warn,
29
+ error: typeof logger.error === "function" ? logger.error.bind(logger) : console.error,
30
+ debug:
31
+ typeof logger.debug === "function" ? logger.debug.bind(logger) : console.debug,
32
+ };
33
+ }
34
+
35
+ function normalizeTrimmedString(value) {
36
+ if (typeof value !== "string") {
37
+ return "";
38
+ }
39
+ return value.trim();
40
+ }
41
+
42
+ export function normalizeEvenAiRoutingMode(value) {
43
+ const normalized = normalizeTrimmedString(value).toLowerCase();
44
+ if (SUPPORTED_EVEN_AI_ROUTING_MODES.includes(normalized)) {
45
+ return normalized;
46
+ }
47
+ if (normalized === "dedicated" || normalized === "dedicated_shadow") {
48
+ return "background";
49
+ }
50
+ if (normalized === "new" || normalized === "new_shadow") {
51
+ return "background_new";
52
+ }
53
+ return DEFAULT_EVEN_AI_ROUTING_MODE;
54
+ }
55
+
56
+ export function normalizeEvenAiSystemPrompt(value) {
57
+ return normalizeTrimmedString(value);
58
+ }
59
+
60
+ export function normalizeEvenAiDefaultModel(value) {
61
+ return normalizeTrimmedString(value);
62
+ }
63
+
64
+ export function normalizeEvenAiDefaultThinking(value) {
65
+ const normalized = normalizeTrimmedString(value).toLowerCase();
66
+ if (!normalized) {
67
+ return "";
68
+ }
69
+ if ([
70
+ "off",
71
+ "minimal",
72
+ "low",
73
+ "medium",
74
+ "high",
75
+ "xhigh",
76
+ ].includes(normalized)) {
77
+ return normalized;
78
+ }
79
+ return "";
80
+ }
81
+
82
+ export function normalizeEvenAiListenEnabled(value) {
83
+ return value === true;
84
+ }
85
+
86
+ function normalizeTrackedThrowawayKeys(value) {
87
+ if (!Array.isArray(value)) {
88
+ return [];
89
+ }
90
+ const normalized = [];
91
+ const seen = new Set();
92
+ for (const rawKey of value) {
93
+ const sessionKey = normalizeTrimmedString(rawKey);
94
+ if (!sessionKey) continue;
95
+ const dedupeKey = sessionKey.toLowerCase();
96
+ if (seen.has(dedupeKey)) continue;
97
+ seen.add(dedupeKey);
98
+ normalized.push(sessionKey);
99
+ if (normalized.length >= MAX_TRACKED_THROWAWAY_KEYS) {
100
+ break;
101
+ }
102
+ }
103
+ return normalized;
104
+ }
105
+
106
+ function arraysEqual(left, right) {
107
+ if (left.length !== right.length) {
108
+ return false;
109
+ }
110
+ for (let i = 0; i < left.length; i += 1) {
111
+ if (left[i] !== right[i]) {
112
+ return false;
113
+ }
114
+ }
115
+ return true;
116
+ }
117
+
118
+ function isStoredSnapshotCanonical(value, snapshot) {
119
+ if (!value || typeof value !== "object") {
120
+ return false;
121
+ }
122
+ if (normalizeTrimmedString(value.routingMode) !== snapshot.routingMode) {
123
+ return false;
124
+ }
125
+ if (normalizeTrimmedString(value.systemPrompt) !== snapshot.systemPrompt) {
126
+ return false;
127
+ }
128
+ if (normalizeTrimmedString(value.defaultModel) !== snapshot.defaultModel) {
129
+ return false;
130
+ }
131
+ if (normalizeEvenAiDefaultThinking(value.defaultThinking) !== snapshot.defaultThinking) {
132
+ return false;
133
+ }
134
+ if (normalizeEvenAiListenEnabled(value.listenEnabled) !== snapshot.listenEnabled) {
135
+ return false;
136
+ }
137
+ if (!Array.isArray(value.trackedThrowawayKeys)) {
138
+ return snapshot.trackedThrowawayKeys.length === 0;
139
+ }
140
+ return arraysEqual(value.trackedThrowawayKeys, snapshot.trackedThrowawayKeys);
141
+ }
142
+
143
+ export function normalizeEvenAiSettingsSnapshot(value = {}) {
144
+ return {
145
+ routingMode: normalizeEvenAiRoutingMode(value.routingMode),
146
+ systemPrompt: normalizeEvenAiSystemPrompt(value.systemPrompt),
147
+ defaultModel: normalizeEvenAiDefaultModel(value.defaultModel),
148
+ defaultThinking: normalizeEvenAiDefaultThinking(value.defaultThinking),
149
+ listenEnabled: normalizeEvenAiListenEnabled(value.listenEnabled),
150
+ trackedThrowawayKeys: normalizeTrackedThrowawayKeys(value.trackedThrowawayKeys),
151
+ };
152
+ }
153
+
154
+ function hasOwn(obj, key) {
155
+ return !!obj && Object.prototype.hasOwnProperty.call(obj, key);
156
+ }
157
+
158
+ export function createEvenAiSettingsStore(opts = {}) {
159
+ const logger = normalizeLogger(opts.logger);
160
+ const emitDebug = typeof opts.emitDebug === "function" ? opts.emitDebug : () => {};
161
+ const now = typeof opts.now === "function" ? opts.now : () => Date.now();
162
+ const defaults = normalizeEvenAiSettingsSnapshot(opts.defaults || {});
163
+ const statePath =
164
+ typeof opts.statePath === "string" && opts.statePath.trim()
165
+ ? opts.statePath.trim()
166
+ : typeof opts.stateDir === "string" && opts.stateDir.trim()
167
+ ? path.join(opts.stateDir.trim(), STORE_FILENAME)
168
+ : null;
169
+
170
+ function persistSnapshot(snapshot, reason) {
171
+ if (!statePath) {
172
+ emitDebug(
173
+ "settings.loadsave",
174
+ "even_ai_settings_persist_skipped",
175
+ "debug",
176
+ null,
177
+ () => ({
178
+ reason,
179
+ routingMode: snapshot.routingMode,
180
+ systemPromptChars: snapshot.systemPrompt.length,
181
+ defaultModel: snapshot.defaultModel,
182
+ defaultThinking: snapshot.defaultThinking,
183
+ listenEnabled: snapshot.listenEnabled,
184
+ trackedThrowawayKeyCount: snapshot.trackedThrowawayKeys.length,
185
+ }),
186
+ );
187
+ return;
188
+ }
189
+
190
+ try {
191
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
192
+ fs.writeFileSync(
193
+ statePath,
194
+ JSON.stringify(
195
+ {
196
+ version: STORE_VERSION,
197
+ updatedAtMs: now(),
198
+ settings: snapshot,
199
+ },
200
+ null,
201
+ 2,
202
+ ) + "\n",
203
+ );
204
+ emitDebug(
205
+ "settings.loadsave",
206
+ "even_ai_settings_persisted",
207
+ "info",
208
+ null,
209
+ () => ({
210
+ reason,
211
+ statePath,
212
+ routingMode: snapshot.routingMode,
213
+ systemPromptChars: snapshot.systemPrompt.length,
214
+ defaultModel: snapshot.defaultModel,
215
+ defaultThinking: snapshot.defaultThinking,
216
+ listenEnabled: snapshot.listenEnabled,
217
+ trackedThrowawayKeyCount: snapshot.trackedThrowawayKeys.length,
218
+ }),
219
+ );
220
+ } catch (err) {
221
+ logger.error(
222
+ `[evenai] failed to persist Even AI settings: ${err && err.message ? err.message : err}`,
223
+ );
224
+ emitDebug(
225
+ "settings.loadsave",
226
+ "even_ai_settings_persist_failed",
227
+ "warn",
228
+ null,
229
+ () => ({
230
+ reason,
231
+ statePath,
232
+ message: err && err.message ? err.message : String(err),
233
+ }),
234
+ );
235
+ throw err;
236
+ }
237
+ }
238
+
239
+ function loadInitialSnapshot() {
240
+ if (!statePath || !fs.existsSync(statePath)) {
241
+ persistSnapshot(defaults, "seed_defaults");
242
+ return { ...defaults };
243
+ }
244
+
245
+ try {
246
+ const raw = fs.readFileSync(statePath, "utf8");
247
+ const parsed = JSON.parse(raw);
248
+ const storedSettings =
249
+ parsed && parsed.settings && typeof parsed.settings === "object"
250
+ ? parsed.settings
251
+ : null;
252
+ const loaded =
253
+ parsed &&
254
+ parsed.version === STORE_VERSION &&
255
+ storedSettings
256
+ ? normalizeEvenAiSettingsSnapshot(storedSettings)
257
+ : defaults;
258
+ emitDebug(
259
+ "settings.loadsave",
260
+ "even_ai_settings_loaded",
261
+ "info",
262
+ null,
263
+ () => ({
264
+ statePath,
265
+ routingMode: loaded.routingMode,
266
+ systemPromptChars: loaded.systemPrompt.length,
267
+ defaultModel: loaded.defaultModel,
268
+ defaultThinking: loaded.defaultThinking,
269
+ listenEnabled: loaded.listenEnabled,
270
+ trackedThrowawayKeyCount: loaded.trackedThrowawayKeys.length,
271
+ }),
272
+ );
273
+ if (
274
+ parsed.version !== STORE_VERSION ||
275
+ !isStoredSnapshotCanonical(storedSettings, loaded)
276
+ ) {
277
+ persistSnapshot(loaded, "normalize_loaded_settings");
278
+ }
279
+ return loaded;
280
+ } catch (err) {
281
+ logger.warn(
282
+ `[evenai] failed to load Even AI settings, falling back to defaults: ${err && err.message ? err.message : err}`,
283
+ );
284
+ emitDebug(
285
+ "settings.loadsave",
286
+ "even_ai_settings_load_failed",
287
+ "warn",
288
+ null,
289
+ () => ({
290
+ statePath,
291
+ message: err && err.message ? err.message : String(err),
292
+ }),
293
+ );
294
+ persistSnapshot(defaults, "rewrite_after_load_failure");
295
+ return { ...defaults };
296
+ }
297
+ }
298
+
299
+ let snapshot = loadInitialSnapshot();
300
+
301
+ function updateTrackedThrowawayKeys(nextKeys, reason) {
302
+ snapshot = {
303
+ ...snapshot,
304
+ trackedThrowawayKeys: normalizeTrackedThrowawayKeys(nextKeys),
305
+ };
306
+ persistSnapshot(snapshot, reason);
307
+ return { ...snapshot };
308
+ }
309
+
310
+ return {
311
+ getStatePath() {
312
+ return statePath;
313
+ },
314
+
315
+ getSnapshot() {
316
+ return { ...snapshot };
317
+ },
318
+
319
+ getTrackedThrowawayKeys() {
320
+ return [...snapshot.trackedThrowawayKeys];
321
+ },
322
+
323
+ recordTrackedThrowawayKey(sessionKey) {
324
+ const normalizedKey = normalizeTrimmedString(sessionKey);
325
+ if (!normalizedKey) {
326
+ return { ...snapshot };
327
+ }
328
+ const dedupeKey = normalizedKey.toLowerCase();
329
+ const nextKeys = [
330
+ normalizedKey,
331
+ ...snapshot.trackedThrowawayKeys.filter((key) => key.toLowerCase() !== dedupeKey),
332
+ ];
333
+ return updateTrackedThrowawayKeys(nextKeys, "record_throwaway_session");
334
+ },
335
+
336
+ async setSettings(patch = {}) {
337
+ const next = {
338
+ routingMode: hasOwn(patch, "routingMode")
339
+ ? normalizeEvenAiRoutingMode(patch.routingMode)
340
+ : snapshot.routingMode,
341
+ systemPrompt: hasOwn(patch, "systemPrompt")
342
+ ? normalizeEvenAiSystemPrompt(patch.systemPrompt)
343
+ : snapshot.systemPrompt,
344
+ defaultModel: hasOwn(patch, "defaultModel")
345
+ ? normalizeEvenAiDefaultModel(patch.defaultModel)
346
+ : snapshot.defaultModel,
347
+ defaultThinking: hasOwn(patch, "defaultThinking")
348
+ ? normalizeEvenAiDefaultThinking(patch.defaultThinking)
349
+ : snapshot.defaultThinking,
350
+ listenEnabled: hasOwn(patch, "listenEnabled")
351
+ ? normalizeEvenAiListenEnabled(patch.listenEnabled)
352
+ : snapshot.listenEnabled,
353
+ trackedThrowawayKeys: [...snapshot.trackedThrowawayKeys],
354
+ };
355
+ snapshot = next;
356
+ persistSnapshot(snapshot, "set_settings");
357
+ return {
358
+ status: "accepted",
359
+ settings: { ...snapshot },
360
+ };
361
+ },
362
+ };
363
+ }
364
+
365
+ export default createEvenAiSettingsStore;
@@ -0,0 +1,175 @@
1
+ function removeListenerCompat(emitter, eventName, listener) {
2
+ if (typeof emitter.off === "function") {
3
+ emitter.off(eventName, listener);
4
+ return;
5
+ }
6
+ if (typeof emitter.removeListener === "function") {
7
+ emitter.removeListener(eventName, listener);
8
+ }
9
+ }
10
+
11
+ function defaultIdempotencyKey() {
12
+ const globalCrypto = globalThis && globalThis.crypto;
13
+ if (globalCrypto && typeof globalCrypto.randomUUID === "function") {
14
+ return globalCrypto.randomUUID();
15
+ }
16
+ return `ocuclaw-${Date.now()}-${Math.random().toString(16).slice(2)}`;
17
+ }
18
+
19
+ function callClientMethod(openclawClient, name, args) {
20
+ const fn = openclawClient && openclawClient[name];
21
+ if (typeof fn !== "function") {
22
+ throw new Error(`Gateway bridge requires openclawClient.${name}()`);
23
+ }
24
+ return fn.apply(openclawClient, args);
25
+ }
26
+
27
+ function callRequestMethod(openclawClient, method, params, requestOpts) {
28
+ const requestFn = openclawClient && openclawClient.request;
29
+ if (typeof requestFn !== "function") {
30
+ throw new Error("Plugin RPC bridge requires openclawClient.request()");
31
+ }
32
+ return requestFn.call(openclawClient, method, params, requestOpts);
33
+ }
34
+
35
+ function buildAgentRequestParams(
36
+ text,
37
+ sessionKey,
38
+ attachment,
39
+ createIdempotencyKey,
40
+ requestOptions,
41
+ ) {
42
+ const params = {
43
+ message: text,
44
+ sessionKey: sessionKey || "main",
45
+ idempotencyKey: createIdempotencyKey(),
46
+ };
47
+ const extraSystemPrompt =
48
+ requestOptions && typeof requestOptions.extraSystemPrompt === "string"
49
+ ? requestOptions.extraSystemPrompt.trim()
50
+ : "";
51
+
52
+ if (extraSystemPrompt) {
53
+ params.extraSystemPrompt = extraSystemPrompt;
54
+ }
55
+
56
+ const thinking =
57
+ requestOptions && typeof requestOptions.thinking === "string"
58
+ ? requestOptions.thinking.trim().toLowerCase()
59
+ : "";
60
+ if (thinking) {
61
+ params.thinking = thinking;
62
+ }
63
+
64
+ if (
65
+ attachment &&
66
+ typeof attachment === "object" &&
67
+ typeof attachment.base64Data === "string" &&
68
+ attachment.base64Data
69
+ ) {
70
+ const normalizedAttachment = {
71
+ type: attachment.kind || "image",
72
+ mimeType: attachment.mimeType || "image/jpeg",
73
+ fileName: attachment.name || "image.jpg",
74
+ content: attachment.base64Data,
75
+ };
76
+ if (typeof attachment.source === "string" && attachment.source) {
77
+ normalizedAttachment.source = attachment.source;
78
+ }
79
+ if (Number.isFinite(attachment.sizeBytes) && attachment.sizeBytes > 0) {
80
+ normalizedAttachment.sizeBytes = Math.floor(attachment.sizeBytes);
81
+ }
82
+ if (Number.isFinite(attachment.widthPx) && attachment.widthPx > 0) {
83
+ normalizedAttachment.widthPx = Math.floor(attachment.widthPx);
84
+ }
85
+ if (Number.isFinite(attachment.heightPx) && attachment.heightPx > 0) {
86
+ normalizedAttachment.heightPx = Math.floor(attachment.heightPx);
87
+ }
88
+ params.attachments = [
89
+ normalizedAttachment,
90
+ ];
91
+ }
92
+
93
+ return params;
94
+ }
95
+
96
+ /**
97
+ * Bridge the relay runtime to OpenClaw structured RPC method calls.
98
+ *
99
+ * This keeps relay-facing bridge semantics unchanged while switching
100
+ * send/approval operations to request-based structured RPC calls.
101
+ *
102
+ * @param {{openclawClient: object, idempotencyKeyFactory?: () => string}} opts
103
+ * @returns {object}
104
+ */
105
+ function createPluginRpcGatewayBridge(opts) {
106
+ const openclawClient = opts && opts.openclawClient;
107
+ const idempotencyKeyFactory =
108
+ opts && typeof opts.idempotencyKeyFactory === "function"
109
+ ? opts.idempotencyKeyFactory
110
+ : defaultIdempotencyKey;
111
+
112
+ if (!openclawClient || typeof openclawClient !== "object") {
113
+ throw new Error("Plugin RPC bridge requires an openclawClient object");
114
+ }
115
+ if (typeof openclawClient.request !== "function") {
116
+ throw new Error("Plugin RPC bridge requires openclawClient.request()");
117
+ }
118
+
119
+ function start() {
120
+ if (typeof openclawClient.start === "function") {
121
+ return openclawClient.start();
122
+ }
123
+ }
124
+
125
+ function stop() {
126
+ if (typeof openclawClient.stop === "function") {
127
+ return openclawClient.stop();
128
+ }
129
+ }
130
+
131
+ function request(method, params, requestOpts) {
132
+ return callRequestMethod(openclawClient, method, params, requestOpts);
133
+ }
134
+
135
+ function sendMessage(text, sessionKey, attachment, requestOptions) {
136
+ return request(
137
+ "agent",
138
+ buildAgentRequestParams(
139
+ text,
140
+ sessionKey,
141
+ attachment,
142
+ idempotencyKeyFactory,
143
+ requestOptions,
144
+ ),
145
+ { expectFinal: false },
146
+ );
147
+ }
148
+
149
+ function resolveApproval(id, decision) {
150
+ return request("exec.approval.resolve", { id, decision });
151
+ }
152
+
153
+ function off(eventName, listener) {
154
+ removeListenerCompat(openclawClient, eventName, listener);
155
+ }
156
+
157
+ function subscribe(eventName, listener) {
158
+ callClientMethod(openclawClient, "on", [eventName, listener]);
159
+ return () => off(eventName, listener);
160
+ }
161
+
162
+ return {
163
+ kind: "plugin-rpc-openclaw-client",
164
+ start,
165
+ stop,
166
+ sendMessage,
167
+ request,
168
+ resolveApproval,
169
+ on: subscribe,
170
+ off,
171
+ rawClient: openclawClient,
172
+ };
173
+ }
174
+
175
+ export { createPluginRpcGatewayBridge };