ocuclaw 1.2.4 → 1.3.1
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/README.md +21 -6
- package/dist/config/runtime-config.js +84 -3
- package/dist/domain/activity-status-adapter.js +138 -605
- package/dist/domain/activity-status-arbiter.js +109 -0
- package/dist/domain/activity-status-labels.js +906 -0
- package/dist/domain/code-span-regions.js +103 -0
- package/dist/domain/conversation-state.js +14 -1
- package/dist/domain/debug-store.js +56 -182
- package/dist/domain/glasses-ui-content-summary.js +62 -0
- package/dist/domain/glasses-ui-system-prompt.js +28 -0
- package/dist/domain/message-emoji-allowlist.js +16 -0
- package/dist/domain/message-emoji-filter.js +33 -55
- package/dist/domain/neural-emoji-reactor-system-prompt.js +43 -0
- package/dist/domain/neural-emoji-reactor-tag-config.js +56 -0
- package/dist/domain/neural-pace-modulator-system-prompt.js +32 -0
- package/dist/domain/neural-pace-modulator-tag-config.js +51 -0
- package/dist/domain/tagged-span-parser.js +121 -0
- package/dist/domain/tagged-span-strip.js +38 -0
- package/dist/even-ai/even-ai-endpoint.js +91 -0
- package/dist/even-ai/even-ai-run-waiter.js +14 -0
- package/dist/even-ai/even-ai-settings-store.js +14 -0
- package/dist/gateway/gateway-bridge.js +14 -2
- package/dist/gateway/gateway-timing-ledger.js +457 -0
- package/dist/gateway/openclaw-client.js +462 -38
- package/dist/index.js +28 -1
- package/dist/runtime/downstream-handler.js +754 -83
- package/dist/runtime/ocuclaw-settings-store.js +74 -31
- package/dist/runtime/plugin-version-service.js +23 -0
- package/dist/runtime/protocol-adapter.js +9 -0
- package/dist/runtime/provider-usage-select.js +168 -0
- package/dist/runtime/relay-client-nudge-controller.js +553 -0
- package/dist/runtime/relay-core.js +1293 -225
- package/dist/runtime/relay-health-monitor.js +172 -0
- package/dist/runtime/relay-operation-registry.js +263 -0
- package/dist/runtime/relay-service.js +201 -1
- package/dist/runtime/relay-worker-approval-replay-cache.js +68 -0
- package/dist/runtime/relay-worker-entry.js +32 -0
- package/dist/runtime/relay-worker-health.js +272 -0
- package/dist/runtime/relay-worker-protocol.js +281 -0
- package/dist/runtime/relay-worker-queue.js +202 -0
- package/dist/runtime/relay-worker-supervisor.js +1004 -0
- package/dist/runtime/relay-worker-transport.js +1051 -0
- package/dist/runtime/session-context-service.js +189 -0
- package/dist/runtime/session-service.js +638 -27
- package/dist/runtime/upstream-runtime.js +1167 -60
- package/dist/tools/device-info-tool.js +242 -0
- package/dist/tools/glasses-ui-cron.js +427 -0
- package/dist/tools/glasses-ui-descriptors.js +261 -0
- package/dist/tools/glasses-ui-limits.js +21 -0
- package/dist/tools/glasses-ui-paint-floor.js +99 -0
- package/dist/tools/glasses-ui-recipes.js +581 -0
- package/dist/tools/glasses-ui-surfaces.js +278 -0
- package/dist/tools/glasses-ui-template.js +182 -0
- package/dist/tools/glasses-ui-tool.js +1111 -0
- package/dist/tools/session-title-tool.js +209 -0
- package/dist/version.js +2 -0
- package/openclaw.plugin.json +163 -15
- package/package.json +14 -5
- package/skills/glasses-ui/SKILL.md +156 -0
- package/dist/runtime/downstream-server.js +0 -1891
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
export const SESSION_TITLE_LIMITS = {
|
|
2
|
+
titleMax: 55, // 64 SDK list-item cap minus ~9 chars headroom for "<time> - " prefix
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export const sessionTitleParametersSchema = {
|
|
6
|
+
type: "object",
|
|
7
|
+
required: ["title"],
|
|
8
|
+
properties: {
|
|
9
|
+
title: {
|
|
10
|
+
type: "string",
|
|
11
|
+
minLength: 1,
|
|
12
|
+
maxLength: SESSION_TITLE_LIMITS.titleMax,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
additionalProperties: false,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function validateSessionTitleInput(input) {
|
|
19
|
+
if (!input || typeof input !== "object") {
|
|
20
|
+
return { ok: false, code: "missing_field", message: "input must be an object with a title field" };
|
|
21
|
+
}
|
|
22
|
+
if (!("title" in input)) {
|
|
23
|
+
return { ok: false, code: "missing_field", message: "title field is required" };
|
|
24
|
+
}
|
|
25
|
+
if (typeof input.title !== "string") {
|
|
26
|
+
return { ok: false, code: "invalid_type", message: "title must be a string" };
|
|
27
|
+
}
|
|
28
|
+
const trimmed = input.title.trim();
|
|
29
|
+
if (trimmed.length === 0) {
|
|
30
|
+
return { ok: false, code: "title_empty", message: "title cannot be empty or whitespace-only" };
|
|
31
|
+
}
|
|
32
|
+
if (trimmed.length > SESSION_TITLE_LIMITS.titleMax) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
code: "title_too_long",
|
|
36
|
+
message: `title is ${trimmed.length} chars; max ${SESSION_TITLE_LIMITS.titleMax}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return { ok: true, spec: { title: trimmed } };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const EVEN_AI_DEDICATED_KEY_PREFIX = "ocuclaw:even-ai";
|
|
43
|
+
|
|
44
|
+
function isEvenAiDedicatedKey(sessionKey) {
|
|
45
|
+
if (typeof sessionKey !== "string") return false;
|
|
46
|
+
const normalized = sessionKey.trim().toLowerCase();
|
|
47
|
+
return (
|
|
48
|
+
normalized === EVEN_AI_DEDICATED_KEY_PREFIX ||
|
|
49
|
+
normalized.startsWith(`${EVEN_AI_DEDICATED_KEY_PREFIX}:`)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function gateReason(sessionKey, deps) {
|
|
54
|
+
if (typeof deps.isSessionUserLocked === "function" && deps.isSessionUserLocked(sessionKey)) {
|
|
55
|
+
return "session_user_locked";
|
|
56
|
+
}
|
|
57
|
+
if (
|
|
58
|
+
typeof deps.isNeuralSessionNamesEnabled === "function" &&
|
|
59
|
+
!deps.isNeuralSessionNamesEnabled(sessionKey)
|
|
60
|
+
) {
|
|
61
|
+
return "feature_disabled";
|
|
62
|
+
}
|
|
63
|
+
// Hard guard against the agent titling a session before the user has sent
|
|
64
|
+
// their first real message. The synthetic session-starter prompt the agent
|
|
65
|
+
// sees on /new + the proactive prompt-hook nudge can otherwise tempt the
|
|
66
|
+
// model into titling from a non-user input (observed: titles like "New
|
|
67
|
+
// session"). Real user sends are recorded via dispatchOcuClawUserSend ->
|
|
68
|
+
// sessionService.recordFirstSentUserMessage; the synthetic starter never is.
|
|
69
|
+
if (
|
|
70
|
+
typeof deps.hasRecordedUserMessage === "function" &&
|
|
71
|
+
!deps.hasRecordedUserMessage(sessionKey)
|
|
72
|
+
) {
|
|
73
|
+
return "no_user_message_yet";
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createSessionTitleToolHandler(deps) {
|
|
79
|
+
async function setSessionTitle(params) {
|
|
80
|
+
const validation = validateSessionTitleInput(params);
|
|
81
|
+
if (!validation.ok) {
|
|
82
|
+
const err = new Error(`${validation.code}: ${validation.message}`);
|
|
83
|
+
err.code = validation.code;
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
const sessionKey = deps.peekSessionKey();
|
|
87
|
+
if (typeof sessionKey !== "string" || !sessionKey.trim()) {
|
|
88
|
+
const err = new Error(
|
|
89
|
+
"no_active_session: no OcuClaw session is currently active",
|
|
90
|
+
);
|
|
91
|
+
err.code = "no_active_session";
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
if (isEvenAiDedicatedKey(sessionKey)) {
|
|
95
|
+
const err = new Error(
|
|
96
|
+
"session_not_renamable: the persistent EvenAI session cannot be retitled",
|
|
97
|
+
);
|
|
98
|
+
err.code = "session_not_renamable";
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
const blockedReason = gateReason(sessionKey, deps);
|
|
102
|
+
if (blockedReason) {
|
|
103
|
+
const err = new Error(`${blockedReason}: tool unavailable for this session`);
|
|
104
|
+
err.code = blockedReason;
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
const result = await deps.setSessionTitle(sessionKey, validation.spec.title);
|
|
108
|
+
if (result && result.ok === false) {
|
|
109
|
+
const err = new Error(`${result.code}: ${result.message || "set rejected"}`);
|
|
110
|
+
err.code = result.code;
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
return result || { ok: true };
|
|
114
|
+
}
|
|
115
|
+
return { setSessionTitle };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const TOOL_DESCRIPTION = [
|
|
119
|
+
"Set a short title for the current chat session (shown in the user's glasses session list).",
|
|
120
|
+
"",
|
|
121
|
+
"When: first user turn that names any concrete topic — call eagerly. Later, only when the topic has clearly shifted. Skip greetings, acknowledgments, and no-topic messages.",
|
|
122
|
+
"",
|
|
123
|
+
"Title: 2-5 word noun phrase, ≤55 chars, no trailing punctuation or surrounding quotes.",
|
|
124
|
+
"",
|
|
125
|
+
"Do not announce the rename unless the user explicitly asked to retitle.",
|
|
126
|
+
].join("\n");
|
|
127
|
+
|
|
128
|
+
export function createSessionTitlePromptHook(deps) {
|
|
129
|
+
return function sessionTitleBeforePromptBuild(_event, ctx) {
|
|
130
|
+
const sessionKey = ctx && typeof ctx.sessionKey === "string" ? ctx.sessionKey : null;
|
|
131
|
+
if (!sessionKey) return undefined;
|
|
132
|
+
const title = typeof deps.getSessionTitle === "function" ? deps.getSessionTitle(sessionKey) : null;
|
|
133
|
+
const userLocked =
|
|
134
|
+
typeof deps.isSessionUserLocked === "function" && deps.isSessionUserLocked(sessionKey);
|
|
135
|
+
const featureEnabled =
|
|
136
|
+
typeof deps.isNeuralSessionNamesEnabled === "function"
|
|
137
|
+
? deps.isNeuralSessionNamesEnabled(sessionKey)
|
|
138
|
+
: true;
|
|
139
|
+
|
|
140
|
+
const fragments = [];
|
|
141
|
+
if (title) {
|
|
142
|
+
fragments.push(`Current session title: "${title}".`);
|
|
143
|
+
}
|
|
144
|
+
if (userLocked) {
|
|
145
|
+
fragments.push(
|
|
146
|
+
"The user has set a custom title; do not call set_session_title.",
|
|
147
|
+
);
|
|
148
|
+
} else if (!featureEnabled) {
|
|
149
|
+
fragments.push(
|
|
150
|
+
"Neural Topic Distiller is disabled; do not call set_session_title.",
|
|
151
|
+
);
|
|
152
|
+
} else if (title) {
|
|
153
|
+
fragments.push(
|
|
154
|
+
"Call set_session_title only if the topic has clearly shifted.",
|
|
155
|
+
);
|
|
156
|
+
} else {
|
|
157
|
+
const hasUserMessage =
|
|
158
|
+
typeof deps.hasRecordedUserMessage !== "function" ||
|
|
159
|
+
deps.hasRecordedUserMessage(sessionKey);
|
|
160
|
+
if (hasUserMessage) {
|
|
161
|
+
fragments.push(
|
|
162
|
+
"No session title is set yet. Call set_session_title now if the user's latest message names any concrete topic.",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (fragments.length === 0) return undefined;
|
|
168
|
+
return { appendSystemContext: fragments.join(" ") };
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function registerSessionTitleTool(api, service) {
|
|
173
|
+
if (!api || typeof api.registerTool !== "function") {
|
|
174
|
+
throw new Error("registerSessionTitleTool requires api.registerTool");
|
|
175
|
+
}
|
|
176
|
+
if (!service) {
|
|
177
|
+
throw new Error("registerSessionTitleTool requires the OcuClaw relay service");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const handler = createSessionTitleToolHandler({
|
|
181
|
+
peekSessionKey: () => service.peekSessionKey(),
|
|
182
|
+
setSessionTitle: (sessionKey, title, opts) => service.setSessionTitle(sessionKey, title, opts),
|
|
183
|
+
isSessionUserLocked: (sessionKey) => service.isSessionUserLocked(sessionKey),
|
|
184
|
+
isNeuralSessionNamesEnabled: (sessionKey) => service.isNeuralSessionNamesEnabled(sessionKey),
|
|
185
|
+
hasRecordedUserMessage: (sessionKey) => service.hasRecordedUserMessage(sessionKey),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
api.registerTool({
|
|
189
|
+
name: "set_session_title",
|
|
190
|
+
description: TOOL_DESCRIPTION,
|
|
191
|
+
parameters: sessionTitleParametersSchema,
|
|
192
|
+
async execute(_toolCallId, params) {
|
|
193
|
+
await handler.setSessionTitle(params);
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: "text", text: JSON.stringify({ status: "accepted" }) }],
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (typeof api.on === "function") {
|
|
201
|
+
const hook = createSessionTitlePromptHook({
|
|
202
|
+
getSessionTitle: (sessionKey) => service.getSessionTitle(sessionKey),
|
|
203
|
+
isSessionUserLocked: (sessionKey) => service.isSessionUserLocked(sessionKey),
|
|
204
|
+
isNeuralSessionNamesEnabled: (sessionKey) => service.isNeuralSessionNamesEnabled(sessionKey),
|
|
205
|
+
hasRecordedUserMessage: (sessionKey) => service.hasRecordedUserMessage(sessionKey),
|
|
206
|
+
});
|
|
207
|
+
api.on("before_prompt_build", hook);
|
|
208
|
+
}
|
|
209
|
+
}
|
package/dist/version.js
ADDED
package/openclaw.plugin.json
CHANGED
|
@@ -1,34 +1,144 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "ocuclaw",
|
|
3
|
+
"activation": {
|
|
4
|
+
"onStartup": true
|
|
5
|
+
},
|
|
3
6
|
"name": "OcuClaw",
|
|
4
|
-
"description": "OcuClaw for Even G2 smart glasses
|
|
7
|
+
"description": "OcuClaw for Even Realities G2 smart glasses.",
|
|
8
|
+
"contracts": {
|
|
9
|
+
"tools": [
|
|
10
|
+
"render_glasses_ui",
|
|
11
|
+
"set_session_title",
|
|
12
|
+
"get_evenrealities_device_info"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"skills": ["skills/glasses-ui"],
|
|
16
|
+
"hooks": {
|
|
17
|
+
"allowConversationAccess": true
|
|
18
|
+
},
|
|
19
|
+
"uiHints": {
|
|
20
|
+
"relayToken": {
|
|
21
|
+
"label": "Relay token",
|
|
22
|
+
"help": "Shared secret. Must match the relay server token set in the OcuClaw app inside Even Hub.",
|
|
23
|
+
"placeholder": "user-defined password",
|
|
24
|
+
"sensitive": true
|
|
25
|
+
},
|
|
26
|
+
"sonioxApiKey": {
|
|
27
|
+
"label": "Soniox API key",
|
|
28
|
+
"help": "Optional. Enables Soniox speech-to-text for voice input.",
|
|
29
|
+
"sensitive": true
|
|
30
|
+
},
|
|
31
|
+
"evenAiEnabled": {
|
|
32
|
+
"label": "Enable Even AI",
|
|
33
|
+
"help": "Routes Even AI agent requests through OcuClaw. Requires evenAiToken when enabled."
|
|
34
|
+
},
|
|
35
|
+
"evenAiToken": {
|
|
36
|
+
"label": "Even AI token",
|
|
37
|
+
"help": "Shared secret. Must match the password set in the Even AI Agent Configure section of the Even Realities app. Required when evenAiEnabled is true.",
|
|
38
|
+
"placeholder": "user-defined password",
|
|
39
|
+
"sensitive": true
|
|
40
|
+
},
|
|
41
|
+
"evenAiSystemPrompt": {
|
|
42
|
+
"label": "Even AI system prompt",
|
|
43
|
+
"help": "Optional extra system prompt appended to Even AI runs only.",
|
|
44
|
+
"advanced": true
|
|
45
|
+
},
|
|
46
|
+
"evenAiRoutingMode": {
|
|
47
|
+
"label": "Even AI routing mode",
|
|
48
|
+
"help": "active = current session; background = dedicated background session; background_new = fresh background session per request.",
|
|
49
|
+
"advanced": true
|
|
50
|
+
},
|
|
51
|
+
"wsBind": {
|
|
52
|
+
"label": "WebSocket bind address",
|
|
53
|
+
"help": "Local interface the OcuClaw relay listens on.",
|
|
54
|
+
"advanced": true
|
|
55
|
+
},
|
|
56
|
+
"wsPort": {
|
|
57
|
+
"label": "WebSocket port",
|
|
58
|
+
"help": "Local port the OcuClaw relay listens on.",
|
|
59
|
+
"advanced": true
|
|
60
|
+
},
|
|
61
|
+
"sessionLimit": {
|
|
62
|
+
"label": "Concurrent session limit",
|
|
63
|
+
"help": "Maximum simultaneous client sessions accepted by the relay.",
|
|
64
|
+
"advanced": true
|
|
65
|
+
},
|
|
66
|
+
"debugPayloadMaxBytes": {
|
|
67
|
+
"label": "Debug payload size limit",
|
|
68
|
+
"help": "Maximum byte size for debug payloads recorded for debugctl.",
|
|
69
|
+
"advanced": true
|
|
70
|
+
},
|
|
71
|
+
"debugNoisyPolicies": {
|
|
72
|
+
"label": "Debug noise policies",
|
|
73
|
+
"help": "Per-channel filters that suppress or sample noisy debug events.",
|
|
74
|
+
"advanced": true
|
|
75
|
+
},
|
|
76
|
+
"externalDebugToolsEnabled": {
|
|
77
|
+
"label": "External debug tools",
|
|
78
|
+
"help": "Allow debugctl-style external tools to call debug-set, debug-dump, and remote-control.",
|
|
79
|
+
"advanced": true
|
|
80
|
+
},
|
|
81
|
+
"evenAiRequestTimeoutMs": {
|
|
82
|
+
"label": "Even AI request timeout (ms) (deprecated)",
|
|
83
|
+
"advanced": true
|
|
84
|
+
},
|
|
85
|
+
"evenAiMaxBodyBytes": {
|
|
86
|
+
"label": "Even AI max body size (bytes) (deprecated)",
|
|
87
|
+
"advanced": true
|
|
88
|
+
},
|
|
89
|
+
"evenAiDedupWindowMs": {
|
|
90
|
+
"label": "Even AI dedup window (ms) (deprecated)",
|
|
91
|
+
"advanced": true
|
|
92
|
+
},
|
|
93
|
+
"evenAiDedicatedSessionKey": {
|
|
94
|
+
"label": "Even AI dedicated session key (deprecated)",
|
|
95
|
+
"help": "Deprecated — scheduled for removal in a future release. Internal session key prefix for background routing. Must start with 'ocuclaw:'.",
|
|
96
|
+
"advanced": true
|
|
97
|
+
},
|
|
98
|
+
"renderGlassesUiTimeoutMs": {
|
|
99
|
+
"label": "render_glasses_ui timeout (ms)",
|
|
100
|
+
"help": "How long a render_glasses_ui call waits for a user pick before resolving with { result: \"timeout\" }. Bounds the orphan tool_use corruption window if the gateway dies mid-render. Default 1800000 (30 minutes). Set 0 to disable (infinite wait — pre-2026-05-23 behaviour).",
|
|
101
|
+
"advanced": true
|
|
102
|
+
},
|
|
103
|
+
"freshnessWindowMs": {
|
|
104
|
+
"label": "Activity summary freshness window",
|
|
105
|
+
"help": "Milliseconds an agent summary outranks a tool label (3000-8000).",
|
|
106
|
+
"advanced": true
|
|
107
|
+
}
|
|
108
|
+
},
|
|
5
109
|
"configSchema": {
|
|
6
110
|
"type": "object",
|
|
7
111
|
"additionalProperties": false,
|
|
112
|
+
"required": ["relayToken"],
|
|
8
113
|
"properties": {
|
|
9
114
|
"relayToken": {
|
|
10
115
|
"type": "string",
|
|
11
|
-
"minLength": 1
|
|
116
|
+
"minLength": 1,
|
|
117
|
+
"description": "Shared secret matching the OcuClaw relay token in the Even Hub app. Required."
|
|
12
118
|
},
|
|
13
119
|
"wsBind": {
|
|
14
120
|
"type": "string",
|
|
15
|
-
"default": "127.0.0.1"
|
|
121
|
+
"default": "127.0.0.1",
|
|
122
|
+
"description": "Local interface the relay WebSocket listens on."
|
|
16
123
|
},
|
|
17
124
|
"wsPort": {
|
|
18
125
|
"type": "integer",
|
|
19
126
|
"minimum": 1,
|
|
20
127
|
"maximum": 65535,
|
|
21
|
-
"default": 9000
|
|
128
|
+
"default": 9000,
|
|
129
|
+
"description": "Local port the relay WebSocket listens on."
|
|
22
130
|
},
|
|
23
131
|
"sessionLimit": {
|
|
24
132
|
"type": "integer",
|
|
25
133
|
"minimum": 1,
|
|
26
|
-
"default": 10
|
|
134
|
+
"default": 10,
|
|
135
|
+
"description": "Maximum concurrent client sessions accepted by the relay."
|
|
27
136
|
},
|
|
28
137
|
"debugPayloadMaxBytes": {
|
|
29
138
|
"type": "integer",
|
|
30
139
|
"minimum": 1,
|
|
31
|
-
"default": 2048
|
|
140
|
+
"default": 2048,
|
|
141
|
+
"description": "Maximum byte size for individual debug payloads recorded for debugctl."
|
|
32
142
|
},
|
|
33
143
|
"debugNoisyPolicies": {
|
|
34
144
|
"anyOf": [
|
|
@@ -41,7 +151,8 @@
|
|
|
41
151
|
{
|
|
42
152
|
"type": "null"
|
|
43
153
|
}
|
|
44
|
-
]
|
|
154
|
+
],
|
|
155
|
+
"description": "Per-channel filters that suppress or sample noisy debug events."
|
|
45
156
|
},
|
|
46
157
|
"externalDebugToolsEnabled": {
|
|
47
158
|
"type": "boolean",
|
|
@@ -49,14 +160,18 @@
|
|
|
49
160
|
"description": "Allow debugctl-style external debug tools to use debug-set, debug-dump, and remote-control."
|
|
50
161
|
},
|
|
51
162
|
"sonioxApiKey": {
|
|
52
|
-
"type": "string"
|
|
163
|
+
"type": "string",
|
|
164
|
+
"description": "Optional Soniox API key. Enables Soniox speech-to-text for voice input."
|
|
53
165
|
},
|
|
54
166
|
"evenAiEnabled": {
|
|
55
167
|
"type": "boolean",
|
|
56
|
-
"default": false
|
|
168
|
+
"default": false,
|
|
169
|
+
"description": "Route Even AI agent requests through OcuClaw. When true, evenAiToken is required."
|
|
57
170
|
},
|
|
58
171
|
"evenAiToken": {
|
|
59
|
-
"type": "string"
|
|
172
|
+
"type": "string",
|
|
173
|
+
"minLength": 1,
|
|
174
|
+
"description": "Shared secret matching the password set in the Even AI Agent Configure section of the Even Realities app. Required when evenAiEnabled is true."
|
|
60
175
|
},
|
|
61
176
|
"evenAiSystemPrompt": {
|
|
62
177
|
"type": "string",
|
|
@@ -65,17 +180,23 @@
|
|
|
65
180
|
"evenAiRequestTimeoutMs": {
|
|
66
181
|
"type": "integer",
|
|
67
182
|
"minimum": 1,
|
|
68
|
-
"default": 60000
|
|
183
|
+
"default": 60000,
|
|
184
|
+
"deprecated": true,
|
|
185
|
+
"description": "Deprecated — scheduled for removal in a future release. The built-in default is used when this key is absent. Timeout (ms) for Even AI upstream requests."
|
|
69
186
|
},
|
|
70
187
|
"evenAiMaxBodyBytes": {
|
|
71
188
|
"type": "integer",
|
|
72
189
|
"minimum": 1,
|
|
73
|
-
"default": 65536
|
|
190
|
+
"default": 65536,
|
|
191
|
+
"deprecated": true,
|
|
192
|
+
"description": "Deprecated — scheduled for removal in a future release. The built-in default is used when this key is absent. Maximum response body size (bytes) accepted from the Even AI upstream."
|
|
74
193
|
},
|
|
75
194
|
"evenAiDedupWindowMs": {
|
|
76
195
|
"type": "integer",
|
|
77
196
|
"minimum": 0,
|
|
78
|
-
"default": 500
|
|
197
|
+
"default": 500,
|
|
198
|
+
"deprecated": true,
|
|
199
|
+
"description": "Deprecated — scheduled for removal in a future release. The built-in default is used when this key is absent. Dedup window (ms) for collapsing repeated Even AI requests."
|
|
79
200
|
},
|
|
80
201
|
"evenAiRoutingMode": {
|
|
81
202
|
"type": "string",
|
|
@@ -84,12 +205,39 @@
|
|
|
84
205
|
"background",
|
|
85
206
|
"background_new"
|
|
86
207
|
],
|
|
87
|
-
"default": "active"
|
|
208
|
+
"default": "active",
|
|
209
|
+
"description": "Even AI session routing: active uses the current session, background reuses a dedicated session, background_new starts a fresh session per request."
|
|
88
210
|
},
|
|
89
211
|
"evenAiDedicatedSessionKey": {
|
|
90
212
|
"type": "string",
|
|
91
|
-
"default": "ocuclaw:even-ai"
|
|
213
|
+
"default": "ocuclaw:even-ai",
|
|
214
|
+
"deprecated": true,
|
|
215
|
+
"description": "Deprecated — scheduled for removal in a future release. The built-in default is used when this key is absent. Internal session key prefix for background Even AI routing. Must start with 'ocuclaw:'."
|
|
216
|
+
},
|
|
217
|
+
"renderGlassesUiTimeoutMs": {
|
|
218
|
+
"type": "integer",
|
|
219
|
+
"minimum": 0,
|
|
220
|
+
"default": 1800000,
|
|
221
|
+
"description": "How long render_glasses_ui waits for user interaction before resolving as { result: \"timeout\" }. Bounds the orphan tool_use corruption window if the gateway dies during a pending render. Default 1800000 (30 minutes). 0 disables the timeout (infinite wait)."
|
|
222
|
+
},
|
|
223
|
+
"freshnessWindowMs": {
|
|
224
|
+
"type": "integer",
|
|
225
|
+
"minimum": 3000,
|
|
226
|
+
"maximum": 8000,
|
|
227
|
+
"default": 5000,
|
|
228
|
+
"description": "How long (ms) a fresh agent summary stays preferred over a tool label in the glasses activity status. Clamped 3000-8000."
|
|
92
229
|
}
|
|
230
|
+
},
|
|
231
|
+
"if": {
|
|
232
|
+
"properties": {
|
|
233
|
+
"evenAiEnabled": {
|
|
234
|
+
"const": true
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
"required": ["evenAiEnabled"]
|
|
238
|
+
},
|
|
239
|
+
"then": {
|
|
240
|
+
"required": ["evenAiToken"]
|
|
93
241
|
}
|
|
94
242
|
}
|
|
95
243
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ocuclaw",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"requiresClientVersion": "1.3.1",
|
|
5
|
+
"description": "OcuClaw for Even Realities G2 smart glasses.",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "./dist/index.js",
|
|
7
8
|
"files": [
|
|
8
9
|
"README.md",
|
|
9
10
|
"dist/",
|
|
10
|
-
"openclaw.plugin.json"
|
|
11
|
+
"openclaw.plugin.json",
|
|
12
|
+
"skills/"
|
|
11
13
|
],
|
|
12
14
|
"keywords": [
|
|
13
15
|
"even",
|
|
@@ -24,13 +26,20 @@
|
|
|
24
26
|
"openclaw": {
|
|
25
27
|
"extensions": [
|
|
26
28
|
"./dist/index.js"
|
|
27
|
-
]
|
|
29
|
+
],
|
|
30
|
+
"install": {
|
|
31
|
+
"npmSpec": "ocuclaw",
|
|
32
|
+
"defaultChoice": "npm",
|
|
33
|
+
"minHostVersion": ">=2026.5.20"
|
|
34
|
+
}
|
|
28
35
|
},
|
|
29
36
|
"dependencies": {
|
|
30
37
|
"marked": "^17.0.2",
|
|
38
|
+
"undici": "^6.26.0",
|
|
31
39
|
"ws": "^8.19.0"
|
|
32
40
|
},
|
|
33
41
|
"scripts": {
|
|
34
|
-
"build": "node ./scripts/build.mjs"
|
|
42
|
+
"build": "node ./scripts/build.mjs",
|
|
43
|
+
"prepare": "node ./scripts/build.mjs"
|
|
35
44
|
}
|
|
36
45
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: glasses-ui
|
|
3
|
+
description: Authoring guide for render_glasses_ui surfaces on Even G2 glasses. Load BEFORE building any live/refreshing surface, per-item detail list, or multi-screen flow — covers the capability-tier ladder (system-stats host metrics, http data), the patch/replace/push moves and exit-to-chat policy, recipe recon, per-item {label,body} templates, and worked examples.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Authoring glasses surfaces with `render_glasses_ui`
|
|
8
|
+
|
|
9
|
+
`render_glasses_ui` paints an interactive surface on the user's Even G2 HUD instead of a text reply. The call blocks until the user selects, dismisses, or backs out. This skill is the source of truth for **authoring** surfaces; the tool description is deliberately lean.
|
|
10
|
+
|
|
11
|
+
## Surface kinds
|
|
12
|
+
|
|
13
|
+
| kind | use it for | caps |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `text_surface` | one formatted read-only block | body ≤ 1000 chars; optional `title` ≤ 64 |
|
|
16
|
+
| `list_surface` | a short pickable list, label-only | ≤ 20 items × ≤ 64 chars |
|
|
17
|
+
| `list_with_details_surface` | a pickable list where each item carries a detail body shown as the user scrolls | label ≤ 64, body ≤ 200; total of all bodies ≤ 6144 |
|
|
18
|
+
|
|
19
|
+
`list_with_details` items are `{ label, body }` objects (`label` required, `body` optional). A bare string is treated as a label-only item. Use it when each option needs a 1–2 sentence compare-before-choosing detail — all bodies ship in one call, so the user browses with zero round-trips.
|
|
20
|
+
|
|
21
|
+
## Capability tiers — pick the lowest tier that answers the need
|
|
22
|
+
|
|
23
|
+
The refresh recipe runs at a capability tier. **Always pick the lowest tier that answers the need.**
|
|
24
|
+
|
|
25
|
+
- **L0 — `http` (data APIs).** In-process, SSRF-guarded fetch → template → surface. For pure data: scores, status APIs, quotes, weather. **Not currently enableable on this install:** `http` is gated by `glassesUiLive.httpEnabled`, and that opt-in isn't reachable yet (the plugin's config-schema omits the `glassesUiLive` block — tracked as a separate fix). Until that lands, an `http` refresh is **rejected (refresh disabled)** — do not author one expecting it to run. It is documented here so you know the tier exists and is the right pick once enabled.
|
|
26
|
+
- **L0′ — `system-stats` (host metrics).** Built-in, in-process `node:os` reader (RAM/CPU/load). **No operator gate** — governed only by the master `glassesUiLive.enabled` (on by default). **This is the only refresh tier that runs without operator config today.** Reach for it for anything host-metric.
|
|
27
|
+
- **Reasoning / judgment tier (`agent`, L1/L2) — designed, not yet available.** A future tier where a sandboxed subagent interprets a page or local files on a timer. It is design-gated (credential-isolation + containment spike) and **not built** — there is no `agent` recipe kind. For a surface that needs interpretation *right now*, render it **once from your own turn** (compose the content yourself, render a static `text_surface`/list). A *self-refreshing* reasoning surface needs an agent in the loop, which isn't wired yet; a live reasoning tier is coming.
|
|
28
|
+
|
|
29
|
+
### `system-stats` output fields (the `{{path}}` sources)
|
|
30
|
+
|
|
31
|
+
`memTotalMb`, `memUsedMb`, `memFreeMb`, `memUsedPct`, `cpuPct`, `loadAvg1`. Optional recipe param `sampleWindowMs` (50–1000, default 200) sizes the CPU-sample window.
|
|
32
|
+
|
|
33
|
+
## The four moves
|
|
34
|
+
|
|
35
|
+
`render_glasses_ui` takes an optional `update` telling it how this render relates to the current surface. **Default is `replace`.**
|
|
36
|
+
|
|
37
|
+
| move | what it does | when |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `patch` | edit *some fields* of the current screen; the refresh cron **keeps ticking** | a partial in-place edit |
|
|
40
|
+
| `replace` *(default)* | swap the *whole content* of the current screen in place; **no back-target** | new content, no going back |
|
|
41
|
+
| `push` | stack a *new child screen*; the parent is retained and **its cron pauses** (resumes on Back) | new content, the user can go back |
|
|
42
|
+
| exit to chat | *not a param* — just end your turn with a short text reply; the chat screen takes over and the surface disappears | you're done; respond in chat |
|
|
43
|
+
|
|
44
|
+
**One-line rule:** partial edit → `patch`; new content, no going back → `replace`; new content, can go back → `push`; done → exit to chat.
|
|
45
|
+
|
|
46
|
+
**Exit-to-chat policy:** on a *deliberate* exit (the user backs past the root, or you're finished), the default is to **respond in chat and not reflexively re-surface**. You retain the capability to surface again later in the conversation — just don't bounce a new surface up reflexively. On an *in-stack* Back (depth ≥ 2), the client transparently restores the parent and its cron resumes; you don't need to re-render it.
|
|
47
|
+
|
|
48
|
+
> **Don't preempt yourself.** Omitting `update` means `replace` — it swaps your current surface in place. To drill into a detail *without losing the list*, use `push`. (Back restores the parent **surface** and re-fires its cron; it does not restore list scroll position.)
|
|
49
|
+
|
|
50
|
+
## Live refresh: recipe recon (validate-then-commit)
|
|
51
|
+
|
|
52
|
+
Add a `refresh` block to make a surface self-update: `{ recipe, intervalMs, targets, onError?, maxDurationMs?, maxConsecutiveFailures? }`. `intervalMs` ≥ 1000.
|
|
53
|
+
|
|
54
|
+
When you submit a render carrying `refresh`, the plugin runs an **initial smoke-test tick before the surface commits**. If that tick fails, the call resolves with `result: "recipe_failed"` and a `failureReason` string — **read it, fix the recipe, and retry**; the surface and cron only start on success. This is your recon loop: don't guess twice, read the failure.
|
|
55
|
+
|
|
56
|
+
`targets` maps recipe output → display:
|
|
57
|
+
- `targets.body` — a string template for `text_surface`.
|
|
58
|
+
- `targets.items` — an array of templates for list surfaces; each entry is either a string (label-only) or `{ label, body }`. The **whole array** is emitted each tick. **Labels are templated too**, so keep static labels as plain literals (no `{{…}}`) — only put `{{…}}` in the parts that should change.
|
|
59
|
+
|
|
60
|
+
### Template filters
|
|
61
|
+
|
|
62
|
+
`{{path}}` reads a value (`{{output}}` for the raw recipe output). Chain filters left-to-right:
|
|
63
|
+
|
|
64
|
+
`trim` · `upper` · `lower` · `int` · `round:N` · `percent` (×100, append %) · `truncate:N` · `default:"--"` · `prefix:"+"` · `minus:previous.value` · `plus:previous.value`. Use `previous.*` paths for deltas vs the prior tick.
|
|
65
|
+
|
|
66
|
+
## Worked examples
|
|
67
|
+
|
|
68
|
+
### 1. Live host stats — `system-stats` text_surface (runs today)
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
render_glasses_ui({
|
|
72
|
+
kind: "text_surface",
|
|
73
|
+
title: "Host",
|
|
74
|
+
body: "RAM — CPU —",
|
|
75
|
+
refresh: {
|
|
76
|
+
recipe: { kind: "system-stats" },
|
|
77
|
+
intervalMs: 2000,
|
|
78
|
+
targets: { body: "RAM {{memUsedPct | round:0}}% CPU {{cpuPct | round:0}}% Load {{loadAvg1 | round:2}}" }
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 2. Live host stats — `system-stats` list_with_details (the canonical interactive example, runs today)
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
render_glasses_ui({
|
|
87
|
+
kind: "list_with_details_surface",
|
|
88
|
+
items: [
|
|
89
|
+
{ label: "Memory", body: "—" },
|
|
90
|
+
{ label: "CPU", body: "—" },
|
|
91
|
+
{ label: "Load", body: "—" }
|
|
92
|
+
],
|
|
93
|
+
refresh: {
|
|
94
|
+
recipe: { kind: "system-stats" },
|
|
95
|
+
intervalMs: 2000,
|
|
96
|
+
targets: {
|
|
97
|
+
items: [
|
|
98
|
+
{ label: "Memory", body: "{{memUsedMb}} / {{memTotalMb}} MB ({{memUsedPct | round:0}}%)" },
|
|
99
|
+
{ label: "CPU", body: "{{cpuPct | round:1}}% busy" },
|
|
100
|
+
{ label: "Load", body: "{{loadAvg1 | round:2}} (1-min avg)" }
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
// Labels are static literals (don't re-render); bodies tick every 2s.
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 3. `push` drill-down (don't preempt the list)
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
// User highlights "Memory" and asks for detail → stack a child, keep the live list underneath.
|
|
112
|
+
render_glasses_ui({
|
|
113
|
+
kind: "text_surface",
|
|
114
|
+
title: "Memory detail",
|
|
115
|
+
body: "Used … of … MB …", // compose from what you know, or give the child its own system-stats refresh
|
|
116
|
+
update: "push"
|
|
117
|
+
})
|
|
118
|
+
// The parent list's cron pauses while the child is up; on Back it staleness-resumes.
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 4. `http` data (designed; NOT enableable on this install — shown for completeness)
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
// L0 http — DATA APIs. Requires operator opt-in glassesUiLive.httpEnabled, which is
|
|
125
|
+
// NOT currently reachable (config-schema gap). This will be REJECTED today — prefer system-stats.
|
|
126
|
+
render_glasses_ui({
|
|
127
|
+
kind: "text_surface",
|
|
128
|
+
title: "AAPL",
|
|
129
|
+
body: "—",
|
|
130
|
+
refresh: {
|
|
131
|
+
recipe: { kind: "http", url: "https://api.example.com/quote/AAPL", jsonPath: "$.data" },
|
|
132
|
+
intervalMs: 30000,
|
|
133
|
+
targets: { body: "AAPL {{price}} ({{change | round:1 | prefix:\"+\"}})" }
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Outcomes (the `result` you get back)
|
|
139
|
+
|
|
140
|
+
| result | meaning |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `selected` | user picked a list item; `selected_index` + `selected_text` returned |
|
|
143
|
+
| `back` | user double-tapped above the root; they want to revise — re-render the previous step or pivot |
|
|
144
|
+
| `dismissed` | dismissed at root, or no selection made |
|
|
145
|
+
| `timeout` | no interaction within the window (non-refresh default 30 min; refresh ends at `maxDurationMs`) |
|
|
146
|
+
| `recipe_failed` | refresh only — initial smoke tick failed, the consecutive-failure breaker fired, or `onError:stop`; `failureReason` carries the last error |
|
|
147
|
+
| `glasses_disconnected` | refresh only — the glasses client dropped mid-cron |
|
|
148
|
+
|
|
149
|
+
Refresh results also carry: `ticks: { count, succeeded, failed, lastSuccessAt, lastFailureAt? }`, `lastBody`, `lastItems`, and `failureReason` (on `recipe_failed`).
|
|
150
|
+
|
|
151
|
+
## Quick reference
|
|
152
|
+
|
|
153
|
+
- Pick the **lowest tier**: host metrics → `system-stats`; pure data API → `http` (not enabled yet). Needs interpretation → render once from your turn.
|
|
154
|
+
- `update` default is `replace` (in-place). Use `push` to drill in without losing the parent; `patch` to edit fields while the cron keeps ticking.
|
|
155
|
+
- `intervalMs` ≥ 1000. Read `failureReason` on `recipe_failed` and fix the recipe.
|
|
156
|
+
- After a surface resolves, a short text reply exits to chat; another render replaces; silence lets it linger.
|