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,237 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ const STORE_VERSION = 1;
5
+ const STORE_FILENAME = "ocuclaw-settings.json";
6
+
7
+ function normalizeLogger(logger) {
8
+ if (!logger || typeof logger !== "object") {
9
+ return console;
10
+ }
11
+ return {
12
+ info: typeof logger.info === "function" ? logger.info.bind(logger) : console.log,
13
+ warn: typeof logger.warn === "function" ? logger.warn.bind(logger) : console.warn,
14
+ error: typeof logger.error === "function" ? logger.error.bind(logger) : console.error,
15
+ debug:
16
+ typeof logger.debug === "function" ? logger.debug.bind(logger) : console.debug,
17
+ };
18
+ }
19
+
20
+ function normalizeTrimmedString(value) {
21
+ if (typeof value !== "string") {
22
+ return "";
23
+ }
24
+ return value.trim();
25
+ }
26
+
27
+ export function normalizeOcuClawSystemPrompt(value) {
28
+ return normalizeTrimmedString(value);
29
+ }
30
+
31
+ export function normalizeOcuClawDefaultModel(value) {
32
+ return normalizeTrimmedString(value);
33
+ }
34
+
35
+ export function normalizeOcuClawDefaultThinking(value) {
36
+ const normalized = normalizeTrimmedString(value).toLowerCase();
37
+ if (
38
+ normalized === "" ||
39
+ normalized === "off" ||
40
+ normalized === "minimal" ||
41
+ normalized === "low" ||
42
+ normalized === "medium" ||
43
+ normalized === "high" ||
44
+ normalized === "xhigh"
45
+ ) {
46
+ return normalized;
47
+ }
48
+ return "";
49
+ }
50
+
51
+ function isStoredSnapshotCanonical(value, snapshot) {
52
+ if (!value || typeof value !== "object") {
53
+ return false;
54
+ }
55
+ return (
56
+ normalizeTrimmedString(value.systemPrompt) === snapshot.systemPrompt &&
57
+ normalizeTrimmedString(value.defaultModel) === snapshot.defaultModel &&
58
+ normalizeOcuClawDefaultThinking(value.defaultThinking) === snapshot.defaultThinking
59
+ );
60
+ }
61
+
62
+ export function normalizeOcuClawSettingsSnapshot(value = {}) {
63
+ return {
64
+ systemPrompt: normalizeOcuClawSystemPrompt(value.systemPrompt),
65
+ defaultModel: normalizeOcuClawDefaultModel(value.defaultModel),
66
+ defaultThinking: normalizeOcuClawDefaultThinking(value.defaultThinking),
67
+ };
68
+ }
69
+
70
+ function hasOwn(obj, key) {
71
+ return !!obj && Object.prototype.hasOwnProperty.call(obj, key);
72
+ }
73
+
74
+ export function createOcuClawSettingsStore(opts = {}) {
75
+ const logger = normalizeLogger(opts.logger);
76
+ const emitDebug = typeof opts.emitDebug === "function" ? opts.emitDebug : () => {};
77
+ const now = typeof opts.now === "function" ? opts.now : () => Date.now();
78
+ const defaults = normalizeOcuClawSettingsSnapshot(opts.defaults || {});
79
+ const statePath =
80
+ typeof opts.statePath === "string" && opts.statePath.trim()
81
+ ? opts.statePath.trim()
82
+ : typeof opts.stateDir === "string" && opts.stateDir.trim()
83
+ ? path.join(opts.stateDir.trim(), STORE_FILENAME)
84
+ : null;
85
+
86
+ function persistSnapshot(snapshot, reason) {
87
+ if (!statePath) {
88
+ emitDebug(
89
+ "settings.loadsave",
90
+ "ocuclaw_settings_persist_skipped",
91
+ "debug",
92
+ null,
93
+ () => ({
94
+ reason,
95
+ systemPromptChars: snapshot.systemPrompt.length,
96
+ defaultModel: snapshot.defaultModel,
97
+ defaultThinking: snapshot.defaultThinking,
98
+ }),
99
+ );
100
+ return;
101
+ }
102
+
103
+ try {
104
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
105
+ fs.writeFileSync(
106
+ statePath,
107
+ JSON.stringify(
108
+ {
109
+ version: STORE_VERSION,
110
+ updatedAtMs: now(),
111
+ settings: snapshot,
112
+ },
113
+ null,
114
+ 2,
115
+ ) + "\n",
116
+ );
117
+ emitDebug(
118
+ "settings.loadsave",
119
+ "ocuclaw_settings_persisted",
120
+ "info",
121
+ null,
122
+ () => ({
123
+ reason,
124
+ statePath,
125
+ systemPromptChars: snapshot.systemPrompt.length,
126
+ defaultModel: snapshot.defaultModel,
127
+ defaultThinking: snapshot.defaultThinking,
128
+ }),
129
+ );
130
+ } catch (err) {
131
+ logger.error(
132
+ `[ocuclaw] failed to persist OcuClaw settings: ${err && err.message ? err.message : err}`,
133
+ );
134
+ emitDebug(
135
+ "settings.loadsave",
136
+ "ocuclaw_settings_persist_failed",
137
+ "warn",
138
+ null,
139
+ () => ({
140
+ reason,
141
+ statePath,
142
+ message: err && err.message ? err.message : String(err),
143
+ }),
144
+ );
145
+ throw err;
146
+ }
147
+ }
148
+
149
+ function loadInitialSnapshot() {
150
+ if (!statePath || !fs.existsSync(statePath)) {
151
+ persistSnapshot(defaults, "seed_defaults");
152
+ return { ...defaults };
153
+ }
154
+
155
+ try {
156
+ const raw = fs.readFileSync(statePath, "utf8");
157
+ const parsed = JSON.parse(raw);
158
+ const storedSettings =
159
+ parsed && parsed.settings && typeof parsed.settings === "object"
160
+ ? parsed.settings
161
+ : null;
162
+ const loaded =
163
+ parsed && parsed.version === STORE_VERSION && storedSettings
164
+ ? normalizeOcuClawSettingsSnapshot(storedSettings)
165
+ : defaults;
166
+ emitDebug(
167
+ "settings.loadsave",
168
+ "ocuclaw_settings_loaded",
169
+ "info",
170
+ null,
171
+ () => ({
172
+ statePath,
173
+ systemPromptChars: loaded.systemPrompt.length,
174
+ defaultModel: loaded.defaultModel,
175
+ defaultThinking: loaded.defaultThinking,
176
+ }),
177
+ );
178
+ if (
179
+ parsed.version !== STORE_VERSION ||
180
+ !isStoredSnapshotCanonical(storedSettings, loaded)
181
+ ) {
182
+ persistSnapshot(loaded, "normalize_loaded_settings");
183
+ }
184
+ return loaded;
185
+ } catch (err) {
186
+ logger.warn(
187
+ `[ocuclaw] failed to load OcuClaw settings, falling back to defaults: ${err && err.message ? err.message : err}`,
188
+ );
189
+ emitDebug(
190
+ "settings.loadsave",
191
+ "ocuclaw_settings_load_failed",
192
+ "warn",
193
+ null,
194
+ () => ({
195
+ statePath,
196
+ message: err && err.message ? err.message : String(err),
197
+ }),
198
+ );
199
+ persistSnapshot(defaults, "rewrite_after_load_failure");
200
+ return { ...defaults };
201
+ }
202
+ }
203
+
204
+ let snapshot = loadInitialSnapshot();
205
+
206
+ return {
207
+ getStatePath() {
208
+ return statePath;
209
+ },
210
+
211
+ getSnapshot() {
212
+ return { ...snapshot };
213
+ },
214
+
215
+ async setSettings(patch = {}) {
216
+ const next = {
217
+ systemPrompt: hasOwn(patch, "systemPrompt")
218
+ ? normalizeOcuClawSystemPrompt(patch.systemPrompt)
219
+ : snapshot.systemPrompt,
220
+ defaultModel: hasOwn(patch, "defaultModel")
221
+ ? normalizeOcuClawDefaultModel(patch.defaultModel)
222
+ : snapshot.defaultModel,
223
+ defaultThinking: hasOwn(patch, "defaultThinking")
224
+ ? normalizeOcuClawDefaultThinking(patch.defaultThinking)
225
+ : snapshot.defaultThinking,
226
+ };
227
+ snapshot = next;
228
+ persistSnapshot(snapshot, "set_settings");
229
+ return {
230
+ status: "accepted",
231
+ settings: { ...snapshot },
232
+ };
233
+ },
234
+ };
235
+ }
236
+
237
+ export default createOcuClawSettingsStore;
@@ -0,0 +1,378 @@
1
+ const V1_TO_INTERNAL = {
2
+ send: "ocuclaw.message.send",
3
+ newSession: "ocuclaw.session.create",
4
+ switchSession: "ocuclaw.session.switch",
5
+ newChat: "ocuclaw.session.reset",
6
+ getSessions: "ocuclaw.session.list",
7
+ getStatus: "ocuclaw.runtime.status.get",
8
+ getModelsCatalog: "ocuclaw.model.catalog.get",
9
+ getSkills: "ocuclaw.skills.catalog.get",
10
+ getSonioxModels: "ocuclaw.voice.soniox.models.get",
11
+ getSessionModelConfig: "ocuclaw.session.config.get",
12
+ setSessionModelConfig: "ocuclaw.session.config.set",
13
+ getEvenAiSettings: "ocuclaw.evenai.settings.get",
14
+ getEvenAiSessions: "ocuclaw.evenai.session.list",
15
+ setEvenAiSettings: "ocuclaw.evenai.settings.set",
16
+ getOcuClawSettings: "ocuclaw.settings.get",
17
+ setOcuClawSettings: "ocuclaw.settings.set",
18
+ slashCommand: "ocuclaw.command.slash",
19
+ approvalResponse: "ocuclaw.approval.resolve",
20
+ subscribeProtocol: "ocuclaw.protocol.tap.subscribe",
21
+ eventDebug: "ocuclaw.debug.event",
22
+ "debug-set": "ocuclaw.debug.config.set",
23
+ "debug-dump": "ocuclaw.debug.events.query",
24
+ resume: "ocuclaw.sync.resume",
25
+ };
26
+
27
+ const RESULT_TO_V1 = {
28
+ "ocuclaw.message.send": "sendAck",
29
+ "ocuclaw.message.send.ack": "sendAck",
30
+ "ocuclaw.session.list.result": "sessions",
31
+ "ocuclaw.session.config.snapshot": "sessionModelConfig",
32
+ "ocuclaw.session.config.set.ack": "sessionModelConfigAck",
33
+ "ocuclaw.evenai.settings.snapshot": "evenAiSettings",
34
+ "ocuclaw.evenai.session.list.result": "evenAiSessions",
35
+ "ocuclaw.evenai.settings.set.ack": "evenAiSettingsAck",
36
+ "ocuclaw.settings.snapshot": "ocuClawSettings",
37
+ "ocuclaw.settings.set.ack": "ocuClawSettingsAck",
38
+ "ocuclaw.model.catalog.snapshot": "modelsCatalog",
39
+ "ocuclaw.skills.catalog.snapshot": "skillsCatalog",
40
+ "ocuclaw.voice.soniox.models.snapshot": "sonioxModels",
41
+ "ocuclaw.approval.resolve.ack": "approvalResponseAck",
42
+ };
43
+
44
+ const EVENT_TO_V1 = {
45
+ "ocuclaw.view.pages.snapshot": "pages",
46
+ "ocuclaw.runtime.status": "status",
47
+ "ocuclaw.activity.update": "activity",
48
+ "ocuclaw.message.stream.delta": "streaming",
49
+ "ocuclaw.session.switch.applied": "sessionSwitched",
50
+ "ocuclaw.sync.resume.ack": "resume-ack",
51
+ "ocuclaw.approval.request": "approval",
52
+ "ocuclaw.approval.resolved": "approvalResolved",
53
+ "ocuclaw.remote.control": "remote-control",
54
+ "ocuclaw.protocol.tap.frame": "protocol",
55
+ };
56
+
57
+ const INTERNAL_TO_V1 = Object.fromEntries(
58
+ Object.entries(V1_TO_INTERNAL).map(([type, op]) => [op, type]),
59
+ );
60
+
61
+ const V1_TO_RESULT = Object.fromEntries(
62
+ Object.entries(RESULT_TO_V1).map(([op, type]) => [type, op]),
63
+ );
64
+
65
+ const V1_TO_EVENT = Object.fromEntries(
66
+ Object.entries(EVENT_TO_V1).map(([event, type]) => [type, event]),
67
+ );
68
+
69
+ function asRecord(value) {
70
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
71
+ return null;
72
+ }
73
+ return value;
74
+ }
75
+
76
+ function frameRequestId(frame) {
77
+ const value = frame.requestId ?? frame.id;
78
+ return typeof value === "string" ? value : null;
79
+ }
80
+
81
+ function framePayload(frame) {
82
+ const { type: _type, ...payload } = frame;
83
+ return payload;
84
+ }
85
+
86
+ function clonePayload(value) {
87
+ return { ...value };
88
+ }
89
+
90
+ function internalOpPayload(op) {
91
+ const payload = clonePayload(op.payload || {});
92
+ delete payload.requestId;
93
+ delete payload.idempotencyKey;
94
+ return payload;
95
+ }
96
+
97
+ function parseV1Inbound(raw) {
98
+ try {
99
+ const parsed = asRecord(JSON.parse(raw));
100
+ if (!parsed || typeof parsed.type !== "string") {
101
+ return null;
102
+ }
103
+ return parsed;
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ function serializeV1Outbound(frame) {
110
+ return JSON.stringify(frame);
111
+ }
112
+
113
+ function parseV2Inbound(raw) {
114
+ try {
115
+ const parsed = asRecord(JSON.parse(raw));
116
+ if (!parsed || typeof parsed.type !== "string") {
117
+ return null;
118
+ }
119
+ if (!parsed.type.startsWith("ocuclaw.")) {
120
+ return null;
121
+ }
122
+ const requestId =
123
+ typeof parsed.requestId === "string" && parsed.requestId.trim()
124
+ ? parsed.requestId.trim()
125
+ : null;
126
+ const idempotencyKey =
127
+ typeof parsed.idempotencyKey === "string" && parsed.idempotencyKey.trim()
128
+ ? parsed.idempotencyKey.trim()
129
+ : null;
130
+ const { type: op, requestId: _requestId, idempotencyKey: _idempotencyKey, ...payload } = parsed;
131
+ return {
132
+ op,
133
+ requestId,
134
+ idempotencyKey,
135
+ payload,
136
+ };
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ function mapV1InboundToInternalOps(frame, _ctx) {
143
+ const op = V1_TO_INTERNAL[frame.type];
144
+ if (!op) {
145
+ return [];
146
+ }
147
+ const idempotencyKey = typeof frame.idempotencyKey === "string" ? frame.idempotencyKey : null;
148
+ return [
149
+ {
150
+ op,
151
+ requestId: frameRequestId(frame),
152
+ idempotencyKey,
153
+ payload: framePayload(frame),
154
+ },
155
+ ];
156
+ }
157
+
158
+ function mapInternalOpToV1Inbound(op, _ctx) {
159
+ const type = INTERNAL_TO_V1[op.op];
160
+ if (!type) {
161
+ return [];
162
+ }
163
+
164
+ const payload = internalOpPayload(op);
165
+ if (type === "send") {
166
+ const id =
167
+ op.requestId ||
168
+ (typeof payload.id === "string" && payload.id.trim() ? payload.id.trim() : null);
169
+ if (id) payload.id = id;
170
+ delete payload.requestId;
171
+ }
172
+
173
+ if (type === "approvalResponse") {
174
+ if (op.requestId && !payload.requestId) {
175
+ payload.requestId = op.requestId;
176
+ }
177
+ }
178
+
179
+ return [{ type, ...payload }];
180
+ }
181
+
182
+ function mapInternalResultToV1Outbound(result, _ctx) {
183
+ const type = RESULT_TO_V1[result.op];
184
+ if (!type) {
185
+ return [];
186
+ }
187
+ if (type !== "sendAck") {
188
+ return [{ type, ...result.result }];
189
+ }
190
+ const payload = clonePayload(result.result || {});
191
+ const id =
192
+ result.requestId ||
193
+ (typeof payload.id === "string" && payload.id.trim() ? payload.id.trim() : null);
194
+ const status =
195
+ typeof payload.status === "string"
196
+ ? payload.status
197
+ : result.ok
198
+ ? "accepted"
199
+ : "rejected";
200
+ const error =
201
+ typeof payload.error === "string" && payload.error
202
+ ? payload.error
203
+ : !result.ok && result.message
204
+ ? result.message
205
+ : undefined;
206
+ const errorCode =
207
+ typeof payload.errorCode === "string" && payload.errorCode
208
+ ? payload.errorCode
209
+ : !result.ok && result.code
210
+ ? result.code
211
+ : undefined;
212
+ return [
213
+ {
214
+ type: "sendAck",
215
+ id,
216
+ status,
217
+ ...(error ? { error } : {}),
218
+ ...(errorCode ? { errorCode } : {}),
219
+ },
220
+ ];
221
+ }
222
+
223
+ function mapInternalEventToV1Outbound(event, _ctx) {
224
+ const type = EVENT_TO_V1[event.event];
225
+ if (!type) {
226
+ return [];
227
+ }
228
+ return [{ type, ...event.payload }];
229
+ }
230
+
231
+ function mapV1OutboundToInternalResult(frame, _ctx) {
232
+ const op = V1_TO_RESULT[frame.type];
233
+ if (!op) {
234
+ return null;
235
+ }
236
+
237
+ const payload = framePayload(frame);
238
+ if (frame.type === "sendAck") {
239
+ return {
240
+ op: "ocuclaw.message.send.ack",
241
+ requestId:
242
+ typeof frame.id === "string" && frame.id.trim() ? frame.id.trim() : null,
243
+ ok: frame.status === "accepted",
244
+ code:
245
+ typeof frame.errorCode === "string" && frame.errorCode.trim()
246
+ ? frame.errorCode.trim()
247
+ : frame.status === "accepted"
248
+ ? "ok"
249
+ : "request_rejected",
250
+ message:
251
+ typeof frame.error === "string" && frame.error.trim()
252
+ ? frame.error.trim()
253
+ : null,
254
+ result: payload,
255
+ };
256
+ }
257
+
258
+ if (frame.type === "approvalResponseAck") {
259
+ return {
260
+ op,
261
+ requestId:
262
+ typeof frame.requestId === "string" && frame.requestId.trim()
263
+ ? frame.requestId.trim()
264
+ : null,
265
+ ok: frame.status === "accepted",
266
+ code:
267
+ typeof frame.code === "string" && frame.code.trim() ? frame.code.trim() : null,
268
+ message:
269
+ typeof frame.message === "string" && frame.message.trim()
270
+ ? frame.message.trim()
271
+ : null,
272
+ result: payload,
273
+ };
274
+ }
275
+
276
+ if (
277
+ frame.type === "sessionModelConfigAck" ||
278
+ frame.type === "evenAiSettingsAck" ||
279
+ frame.type === "ocuClawSettingsAck"
280
+ ) {
281
+ return {
282
+ op,
283
+ requestId: null,
284
+ ok: frame.status === "accepted",
285
+ code: frame.status === "accepted" ? "ok" : "request_rejected",
286
+ message:
287
+ typeof frame.error === "string" && frame.error.trim()
288
+ ? frame.error.trim()
289
+ : null,
290
+ result: payload,
291
+ };
292
+ }
293
+
294
+ return {
295
+ op,
296
+ requestId:
297
+ typeof frame.requestId === "string" && frame.requestId.trim()
298
+ ? frame.requestId.trim()
299
+ : typeof frame.id === "string" && frame.id.trim()
300
+ ? frame.id.trim()
301
+ : null,
302
+ ok: true,
303
+ code: "ok",
304
+ message: null,
305
+ result: payload,
306
+ };
307
+ }
308
+
309
+ function mapV1OutboundToInternalEvent(frame, _ctx) {
310
+ const event = V1_TO_EVENT[frame.type];
311
+ if (!event) {
312
+ return null;
313
+ }
314
+ return {
315
+ event,
316
+ requestId:
317
+ typeof frame.requestId === "string" && frame.requestId.trim()
318
+ ? frame.requestId.trim()
319
+ : null,
320
+ payload: framePayload(frame),
321
+ };
322
+ }
323
+
324
+ function serializeInternalResultToV2Outbound(result) {
325
+ const payload = {
326
+ type: result.op,
327
+ ...(result.requestId ? { requestId: result.requestId } : {}),
328
+ ...(result.result || {}),
329
+ };
330
+ if (result.code && payload.code === undefined) {
331
+ payload.code = result.code;
332
+ }
333
+ if (result.message && payload.message === undefined) {
334
+ payload.message = result.message;
335
+ }
336
+ return JSON.stringify(payload);
337
+ }
338
+
339
+ function serializeInternalEventToV2Outbound(event) {
340
+ return JSON.stringify({
341
+ type: event.event,
342
+ ...(event.requestId ? { requestId: event.requestId } : {}),
343
+ ...(event.payload || {}),
344
+ });
345
+ }
346
+
347
+ function normalizeInternalError(input) {
348
+ if (input instanceof Error) {
349
+ return {
350
+ code: input.name || "internal_error",
351
+ message: input.message || "internal error",
352
+ retryable: false,
353
+ details: {},
354
+ };
355
+ }
356
+ const value = asRecord(input);
357
+ return {
358
+ code: typeof value?.code === "string" ? value.code : "internal_error",
359
+ message: typeof value?.message === "string" ? value.message : "internal error",
360
+ retryable: value?.retryable === true,
361
+ details: asRecord(value?.details) ?? {},
362
+ };
363
+ }
364
+
365
+ export {
366
+ mapInternalEventToV1Outbound,
367
+ mapInternalOpToV1Inbound,
368
+ mapInternalResultToV1Outbound,
369
+ mapV1InboundToInternalOps,
370
+ mapV1OutboundToInternalEvent,
371
+ mapV1OutboundToInternalResult,
372
+ normalizeInternalError,
373
+ parseV1Inbound,
374
+ parseV2Inbound,
375
+ serializeInternalEventToV2Outbound,
376
+ serializeInternalResultToV2Outbound,
377
+ serializeV1Outbound,
378
+ };