ocuclaw 1.3.2 → 1.3.4
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 +29 -1
- package/dist/config/runtime-config-session-title-model.test.js +0 -3
- package/dist/config/runtime-config.js +22 -33
- package/dist/domain/activity-status-adapter.js +0 -7
- package/dist/domain/activity-status-arbiter.js +3 -27
- package/dist/domain/activity-status-labels.js +8 -38
- package/dist/domain/code-span-regions.js +4 -24
- package/dist/domain/constant-time-equal.js +9 -0
- package/dist/domain/constant-time-equal.test.js +28 -0
- package/dist/domain/conversation-state.js +27 -138
- package/dist/domain/debug-bundle-cache.js +52 -0
- package/dist/domain/debug-bundle-format.js +60 -0
- package/dist/domain/debug-bundle-preview.js +123 -0
- package/dist/domain/debug-bundle-redaction.js +182 -0
- package/dist/domain/debug-bundle-save.js +11 -0
- package/dist/domain/debug-bundle-zip.js +15 -0
- package/dist/domain/debug-bundle.js +97 -0
- package/dist/domain/debug-store.js +6 -17
- package/dist/domain/debug-upload-preset.js +27 -0
- package/dist/domain/glasses-display-system-prompt.js +0 -5
- package/dist/domain/glasses-display-system-prompt.test.js +1 -1
- package/dist/domain/glasses-ui-content-summary.js +0 -6
- package/dist/domain/glasses-ui-system-prompt.test.js +1 -2
- package/dist/domain/message-emoji-allowlist.js +0 -7
- package/dist/domain/message-emoji-filter.js +3 -9
- package/dist/domain/neural-emoji-reactor-tag-config.js +3 -3
- package/dist/domain/prompt-channel-fragments.js +1 -10
- package/dist/domain/tagged-span-parser.js +3 -26
- package/dist/domain/tagged-span-strip.js +0 -7
- package/dist/even-ai/even-ai-endpoint.js +77 -24
- package/dist/even-ai/even-ai-run-waiter.js +0 -1
- package/dist/even-ai/even-ai-settings-store.js +11 -0
- package/dist/gateway/gateway-bridge.js +8 -9
- package/dist/gateway/gateway-timing-ledger.js +8 -6
- package/dist/gateway/openclaw-client.js +97 -297
- package/dist/gateway/sanitize-connect-reason.js +10 -0
- package/dist/gateway/sanitize-connect-reason.test.js +34 -0
- package/dist/index.js +3 -3
- package/dist/runtime/channel-two-hook.js +1 -6
- package/dist/runtime/container-env.js +1 -5
- package/dist/runtime/debug-bundle-handler.js +159 -0
- package/dist/runtime/display-toggle-states.js +6 -17
- package/dist/runtime/downstream-handler.js +682 -508
- package/dist/runtime/glasses-backpressure-latch.js +93 -0
- package/dist/runtime/ocuclaw-settings-store.js +10 -1
- package/dist/runtime/openclaw-host-version.js +5 -0
- package/dist/runtime/plugin-version-service.js +13 -6
- package/dist/runtime/provider-usage-select.js +0 -6
- package/dist/runtime/register-session-title-distiller.js +14 -16
- package/dist/runtime/relay-core.js +657 -271
- package/dist/runtime/relay-service.js +40 -36
- package/dist/runtime/relay-worker-approval-replay-cache.js +1 -1
- package/dist/runtime/relay-worker-entry.js +1 -2
- package/dist/runtime/relay-worker-health.js +2 -10
- package/dist/runtime/relay-worker-protocol.js +6 -1
- package/dist/runtime/relay-worker-supervisor.js +109 -39
- package/dist/runtime/relay-worker-transport.js +157 -15
- package/dist/runtime/session-context-service.js +5 -45
- package/dist/runtime/session-service.js +157 -175
- package/dist/runtime/session-title-distiller-budget.js +1 -5
- package/dist/runtime/session-title-distiller-helpers.js +14 -24
- package/dist/runtime/session-title-distiller.js +109 -122
- package/dist/runtime/session-title-record.js +0 -6
- package/dist/runtime/stable-prompt-snapshot.js +3 -14
- package/dist/runtime/upstream-runtime.js +600 -103
- package/dist/tools/device-info-tool.js +4 -21
- package/dist/tools/glasses-ui-cron.js +58 -63
- package/dist/tools/glasses-ui-descriptors.js +4 -33
- package/dist/tools/glasses-ui-limits.js +0 -13
- package/dist/tools/glasses-ui-paint-floor.js +22 -34
- package/dist/tools/glasses-ui-recipes.js +92 -101
- package/dist/tools/glasses-ui-surfaces.js +295 -100
- package/dist/tools/glasses-ui-template.js +7 -22
- package/dist/tools/glasses-ui-tool-description.test.js +2 -2
- package/dist/tools/glasses-ui-tool.js +475 -331
- package/dist/tools/glasses-ui-voicemail.js +242 -0
- package/dist/tools/glasses-ui-wake.js +195 -0
- package/dist/tools/session-title-tool.js +2 -7
- package/dist/tools/session-title-tool.test.js +1 -1
- package/dist/version.js +3 -2
- package/openclaw.plugin.json +60 -13
- package/package.json +3 -2
- package/skills/glasses-ui/SKILL.md +19 -3
- package/dist/runtime/protocol-adapter.js +0 -387
package/README.md
CHANGED
|
@@ -46,6 +46,18 @@ openclaw config set plugins.entries.ocuclaw.config.evenAiToken "your-even-ai-tok
|
|
|
46
46
|
|
|
47
47
|
> **Note:** When `evenAiEnabled` is `true`, `evenAiToken` is required. Config validation will reject the change if you enable Even AI without setting the token.
|
|
48
48
|
|
|
49
|
+
Optional Even AI tuning (only used when `evenAiEnabled` is `true`):
|
|
50
|
+
|
|
51
|
+
- `evenAiRoutingMode`: `active` routes through the current session (default), `background` reuses a dedicated background session, `background_new` starts a fresh background session per request.
|
|
52
|
+
- `evenAiSystemPrompt`: Extra system prompt appended to Even AI runs only.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
openclaw config set plugins.entries.ocuclaw.config.evenAiRoutingMode "active"
|
|
56
|
+
openclaw config set plugins.entries.ocuclaw.config.evenAiSystemPrompt "your-extra-prompt"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> **Note:** These two seed the Even AI settings on first boot. If you use the OcuClaw glasses client or phone WebUI, the in-app Even AI settings editor takes over afterward, and later changes to these config keys won't affect live behaviour unless the stored settings are reset. For deployments that use **only** the direct Even Realities Even AI pathway — never launching the OcuClaw client — these keys are the only way to configure routing mode and system prompt. `evenAiSystemPrompt` has no glasses-side editor, so set it via the phone WebUI or config.
|
|
60
|
+
|
|
49
61
|
Advanced optional settings:
|
|
50
62
|
|
|
51
63
|
```bash
|
|
@@ -53,8 +65,24 @@ openclaw config set plugins.entries.ocuclaw.config.wsBind "127.0.0.1"
|
|
|
53
65
|
# wsPort default is 9000; on Windows that port is often reserved by WinNAT, so the
|
|
54
66
|
# setup assistant uses 47800. Pick any free port in 30000-49151 if you override it.
|
|
55
67
|
openclaw config set plugins.entries.ocuclaw.config.wsPort 47800 --strict-json
|
|
56
|
-
|
|
68
|
+
# Recent sessions fetched for the WebUI switcher/search list (default 80). Glasses
|
|
69
|
+
# clamp to their own item-count cap, so this only widens the WebUI list.
|
|
70
|
+
openclaw config set plugins.entries.ocuclaw.config.sessionLimit 80 --strict-json
|
|
71
|
+
# Optional model override ("provider/model") for the background session-title
|
|
72
|
+
# distiller. This is a lightweight background task, so a small, fast, inexpensive
|
|
73
|
+
# model is a good choice (e.g. Anthropic's Haiku) — it keeps title generation off
|
|
74
|
+
# your main model's tokens and latency. Leave unset to use your normal model.
|
|
75
|
+
openclaw config set plugins.entries.ocuclaw.config.sessionTitleModel "anthropic/claude-haiku-4-5"
|
|
76
|
+
# How long render_glasses_ui waits for a user pick before resolving { result: "timeout" }.
|
|
77
|
+
# Default 1800000 (30 minutes); 0 disables the timeout (infinite wait).
|
|
78
|
+
openclaw config set plugins.entries.ocuclaw.config.renderGlassesUiTimeoutMs 1800000 --strict-json
|
|
79
|
+
# How long (ms) a fresh agent summary outranks a tool label in the glasses activity
|
|
80
|
+
# status. Default 5000, clamped to 3000-8000.
|
|
81
|
+
openclaw config set plugins.entries.ocuclaw.config.freshnessWindowMs 5000 --strict-json
|
|
82
|
+
# Debug tooling (only relevant when externalDebugToolsEnabled is true):
|
|
57
83
|
openclaw config set plugins.entries.ocuclaw.config.externalDebugToolsEnabled true --strict-json
|
|
84
|
+
# Per-channel filters that suppress or sample noisy debug events.
|
|
85
|
+
openclaw config set plugins.entries.ocuclaw.config.debugNoisyPolicies '{}' --strict-json
|
|
58
86
|
```
|
|
59
87
|
|
|
60
88
|
Run `openclaw plugins inspect ocuclaw` to see all settings with their descriptions and defaults.
|
|
@@ -2,9 +2,6 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { createRuntimeConfig } from "./runtime-config.ts";
|
|
4
4
|
|
|
5
|
-
// Minimal valid config: relayToken (pluginConfig) + a gateway auth token
|
|
6
|
-
// (openclawConfig) — createRuntimeConfig throws without a resolvable
|
|
7
|
-
// gatewayUrl/gatewayToken (gatewayUrl defaults to ws://127.0.0.1:18789).
|
|
8
5
|
const base = { relayToken: "tok" };
|
|
9
6
|
const openclawConfig = { gateway: { auth: { token: "gw" } } };
|
|
10
7
|
|
|
@@ -18,6 +18,15 @@ function parseIntOrDefault(value, defaultValue) {
|
|
|
18
18
|
return parsed;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function clampInt(value, min, max, fallback) {
|
|
22
|
+
const n = Number(value);
|
|
23
|
+
if (!Number.isFinite(n)) return fallback;
|
|
24
|
+
const rounded = Math.floor(n);
|
|
25
|
+
if (rounded < min) return min;
|
|
26
|
+
if (rounded > max) return max;
|
|
27
|
+
return rounded;
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
function parseEvenAiRoutingMode(value) {
|
|
22
31
|
return normalizeEvenAiRoutingMode(value);
|
|
23
32
|
}
|
|
@@ -97,19 +106,6 @@ function resolveDebugNoisyPolicies(pluginValue, envValue) {
|
|
|
97
106
|
return parseJsonOrUndefined(envValue, "debugNoisyPolicies");
|
|
98
107
|
}
|
|
99
108
|
|
|
100
|
-
// Supported live-refresh LLM backends — both are HTTP API backends. The two
|
|
101
|
-
// CLI-spawn backends were removed: codex-cli because Codex's read-only sandbox
|
|
102
|
-
// still permits filesystem reads (an agent prompt could exfil ~/.aws/credentials,
|
|
103
|
-
// ~/.ssh/*, etc. via stdout into the glasses surface), and claude-cli to remove
|
|
104
|
-
// the plugin's last child_process spawn so the OpenClaw installer's static
|
|
105
|
-
// dangerous-code scanner passes without --dangerously-force-unsafe-install. (The
|
|
106
|
-
// scanner can't see that --tools "" made the CLI tool-less; it just sees a spawn.
|
|
107
|
-
// This is not an exploit claim about claude-cli — it removes spawn surface and
|
|
108
|
-
// clears the static block.) The proper agentic tier — delegating ticks to the
|
|
109
|
-
// native OpenClaw runtime instead of spawning a CLI — is tracked separately (the
|
|
110
|
-
// glasses-ui L1/L2 delegation redesign) and blocked on request-scoped
|
|
111
|
-
// api.runtime.subagent.run. Operators who want Claude/Codex point an *-api
|
|
112
|
-
// backend at the provider endpoint (key resolved via the host modelAuth).
|
|
113
109
|
const GLASSES_UI_LIVE_BACKENDS = new Set([
|
|
114
110
|
"anthropic-api",
|
|
115
111
|
"openai-compat",
|
|
@@ -122,8 +118,7 @@ const GLASSES_UI_LIVE_DEFAULT_MODEL = {
|
|
|
122
118
|
|
|
123
119
|
function resolveGlassesUiLive(value) {
|
|
124
120
|
const raw = isObject(value) ? value : {};
|
|
125
|
-
|
|
126
|
-
// "codex-cli" from an operator's pre-removal config) coerce to this default.
|
|
121
|
+
|
|
127
122
|
const tickBackend = GLASSES_UI_LIVE_BACKENDS.has(raw.tickBackend)
|
|
128
123
|
? raw.tickBackend
|
|
129
124
|
: "anthropic-api";
|
|
@@ -136,20 +131,13 @@ function resolveGlassesUiLive(value) {
|
|
|
136
131
|
tickApiBaseUrl,
|
|
137
132
|
allowAgentModelOverride: parseBool(raw.allowAgentModelOverride, false),
|
|
138
133
|
tickMaxOutputTokens: parseIntOrDefault(raw.tickMaxOutputTokens, 200),
|
|
139
|
-
|
|
140
|
-
// schedule. The recipe executor blocks loopback/RFC1918/link-local
|
|
141
|
-
// destinations and resolves hostnames through an SSRF-safe dispatcher,
|
|
142
|
-
// but the capability — "the plugin's gateway host can fetch arbitrary
|
|
143
|
-
// public URLs the agent chooses" — is itself worth an operator opt-in.
|
|
144
|
-
// Default to disabled; set
|
|
145
|
-
// plugins.entries.ocuclaw.config.glassesUiLive.httpEnabled = true to
|
|
146
|
-
// enable. The dispatcher protection still applies once enabled.
|
|
134
|
+
|
|
147
135
|
httpEnabled: parseBool(raw.httpEnabled, false),
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
136
|
+
|
|
137
|
+
httpAllowHosts: Array.isArray(raw.httpAllowHosts)
|
|
138
|
+
? raw.httpAllowHosts.filter((h) => typeof h === "string")
|
|
139
|
+
: [],
|
|
140
|
+
|
|
153
141
|
llmEnabled: parseBool(raw.llmEnabled, false),
|
|
154
142
|
maxConcurrentSurfacesPerHost: parseIntOrDefault(raw.maxConcurrentSurfacesPerHost, 4),
|
|
155
143
|
};
|
|
@@ -206,12 +194,9 @@ export function createRuntimeConfig(opts = {}) {
|
|
|
206
194
|
relayToken,
|
|
207
195
|
wsBind: pickString(pluginConfig.wsBind, "127.0.0.1"),
|
|
208
196
|
wsPort: parseIntOrDefault(pickValue(pluginConfig.wsPort), 9000),
|
|
209
|
-
sessionLimit: parseIntOrDefault(pickValue(pluginConfig.sessionLimit),
|
|
197
|
+
sessionLimit: parseIntOrDefault(pickValue(pluginConfig.sessionLimit), 80),
|
|
210
198
|
sonioxApiKey: pickString(pluginConfig.sonioxApiKey),
|
|
211
|
-
|
|
212
|
-
pickValue(pluginConfig.debugPayloadMaxBytes),
|
|
213
|
-
2048,
|
|
214
|
-
),
|
|
199
|
+
cartesiaApiKey: pickString(pluginConfig.cartesiaApiKey),
|
|
215
200
|
debugNoisyPolicies: resolveDebugNoisyPolicies(
|
|
216
201
|
pluginConfig.debugNoisyPolicies,
|
|
217
202
|
undefined,
|
|
@@ -220,6 +205,10 @@ export function createRuntimeConfig(opts = {}) {
|
|
|
220
205
|
pluginConfig.externalDebugToolsEnabled,
|
|
221
206
|
false,
|
|
222
207
|
),
|
|
208
|
+
allowDebugUpload: parseBool(pluginConfig.allowDebugUpload, false),
|
|
209
|
+
debugUploadMaxZipBytes: clampInt(pluginConfig.debugUploadMaxZipBytes, 100_000, 4_300_000, 4_000_000),
|
|
210
|
+
debugUploadCapturePreset: Array.isArray(pluginConfig.debugUploadCapturePreset) ? pluginConfig.debugUploadCapturePreset : undefined,
|
|
211
|
+
debugBundleSaveDir: pluginConfig.debugBundleSaveDir || "",
|
|
223
212
|
evenAiEnabled,
|
|
224
213
|
evenAiToken,
|
|
225
214
|
evenAiSystemPrompt: pickString(pluginConfig.evenAiSystemPrompt),
|
|
@@ -251,7 +251,6 @@ function createActivityStatusAdapter(opts) {
|
|
|
251
251
|
const now =
|
|
252
252
|
typeof options.now === "function" ? options.now : () => Date.now();
|
|
253
253
|
|
|
254
|
-
/** @type {Map<string, {seq: number, toolStartCount: number, currentActivityId: string|null, toolContextByActivityId: Map<string, {label: string, detail: string|null, category: string|null, intent: string|null}>}>} */
|
|
255
254
|
const runStates = new Map();
|
|
256
255
|
|
|
257
256
|
function getRunState(runKey) {
|
|
@@ -381,8 +380,6 @@ function createActivityStatusAdapter(opts) {
|
|
|
381
380
|
detail = resolvedThinking.detail;
|
|
382
381
|
}
|
|
383
382
|
|
|
384
|
-
// Agent-authored summaries get a clamp-only shortLabel when the
|
|
385
|
-
// 64-char header budget needs it; generic "Thinking..." never does.
|
|
386
383
|
if (label && isExplanatoryThinkingLabel(label) && label.length > SHORT_LABEL_MAX_CHARS) {
|
|
387
384
|
shortLabel = label;
|
|
388
385
|
}
|
|
@@ -505,10 +502,6 @@ function createActivityStatusAdapter(opts) {
|
|
|
505
502
|
result.candidateAtMs = now();
|
|
506
503
|
result.freshnessWindowMs = freshnessWindowMs;
|
|
507
504
|
|
|
508
|
-
// Association ids (optional, normalized): trim, or drop when empty. The
|
|
509
|
-
// {...activity} spread already copies them, but normalize at the contract
|
|
510
|
-
// boundary so untrusted callers can't leak whitespace/empty ids. See
|
|
511
|
-
// transport spec docs/superpowers/specs/2026-06-01-...-redesign-design.md §4/§11.
|
|
512
505
|
if (typeof activity.toolCallId === "string" && activity.toolCallId.trim()) {
|
|
513
506
|
result.toolCallId = activity.toolCallId.trim();
|
|
514
507
|
} else {
|
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
// Pure ladder-ranking logic for the activity-status arbiter (Plan 2 core).
|
|
2
|
-
//
|
|
3
|
-
// LEAF MODULE: it may import small shared leaf helpers, but nothing it imports
|
|
4
|
-
// may import it back. The CJS build (scripts/build.mjs#emitCjsFile) appends
|
|
5
|
-
// `module.exports = {...}` at EOF and converts imports to eager in-place
|
|
6
|
-
// requires, so a bidirectional import would resolve to `{}` mid-cycle and
|
|
7
|
-
// crash at call time. Keep this module a strict leaf.
|
|
8
|
-
|
|
9
1
|
const RANK_INTERVENTION = "intervention";
|
|
10
2
|
const RANK_GENERATED_SUMMARY = "generated_summary";
|
|
11
3
|
const RANK_TOOL = "tool";
|
|
12
4
|
const RANK_GENERIC_THINKING = "generic_thinking";
|
|
13
5
|
const RANK_QUIET = "quiet";
|
|
14
6
|
|
|
15
|
-
// Ladder order, highest first. Matches spec §5.
|
|
16
7
|
const RANKS = [
|
|
17
8
|
RANK_INTERVENTION,
|
|
18
9
|
RANK_GENERATED_SUMMARY,
|
|
@@ -30,16 +21,6 @@ function isInterventionSignal(s) {
|
|
|
30
21
|
);
|
|
31
22
|
}
|
|
32
23
|
|
|
33
|
-
// Classify an already-resolved activity into its ladder rank.
|
|
34
|
-
// The branch order IS the ladder order. `intervention` and `tool` are
|
|
35
|
-
// rankable regardless of includeThinking; the two thinking-derived ranks
|
|
36
|
-
// (`generated_summary` and `generic_thinking`) are both gated by
|
|
37
|
-
// `includeThinking === true` (spec §7.4: `includeThinking = false` →
|
|
38
|
-
// no `generated_summary` override and no `generic_thinking` fallback).
|
|
39
|
-
//
|
|
40
|
-
// NOTE: the guard here enforces spec §7.4 directly — callers may pass
|
|
41
|
-
// any value for `thinkingSummarySource` regardless of `includeThinking`
|
|
42
|
-
// and will always get the correct answer.
|
|
43
24
|
function classifyRank(signals) {
|
|
44
25
|
const s = signals || {};
|
|
45
26
|
if (isInterventionSignal(s)) return RANK_INTERVENTION;
|
|
@@ -57,8 +38,6 @@ function classifyRank(signals) {
|
|
|
57
38
|
return RANK_QUIET;
|
|
58
39
|
}
|
|
59
40
|
|
|
60
|
-
// Exact generic stems that must never outrank a tool, even from a
|
|
61
|
-
// summary/bold source. Compared against the normalized whole label.
|
|
62
41
|
const GENERIC_SUMMARY_DENYLIST = new Set([
|
|
63
42
|
"thinking",
|
|
64
43
|
"working",
|
|
@@ -77,14 +56,11 @@ function normalizeSummaryLabel(label) {
|
|
|
77
56
|
if (typeof label !== "string") return "";
|
|
78
57
|
return label
|
|
79
58
|
.replace(/\*\*/g, "")
|
|
80
|
-
.replace(/[.…]+$/g, "")
|
|
81
|
-
.trim()
|
|
59
|
+
.replace(/[.…]+$/g, "")
|
|
60
|
+
.trim()
|
|
82
61
|
.toLowerCase();
|
|
83
62
|
}
|
|
84
63
|
|
|
85
|
-
// A summary may outrank a tool only if it is an authored, concise, specific
|
|
86
|
-
// signal: source ∈ {summary, bold}, non-empty, not a generic stem, and not a
|
|
87
|
-
// bare verb with no object.
|
|
88
64
|
function evaluateSummaryEligibility(thinkingSummarySource, label) {
|
|
89
65
|
if (thinkingSummarySource !== "summary" && thinkingSummarySource !== "bold") {
|
|
90
66
|
return false;
|
|
@@ -92,7 +68,7 @@ function evaluateSummaryEligibility(thinkingSummarySource, label) {
|
|
|
92
68
|
const normalized = normalizeSummaryLabel(label);
|
|
93
69
|
if (!normalized) return false;
|
|
94
70
|
if (GENERIC_SUMMARY_DENYLIST.has(normalized)) return false;
|
|
95
|
-
|
|
71
|
+
|
|
96
72
|
if (!/\s/.test(normalized)) return false;
|
|
97
73
|
return true;
|
|
98
74
|
}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
const DEFAULT_MAX_LABEL_CHARS = 120;
|
|
4
|
-
// No plugin-side preview clipping below the overall label budget: the client's
|
|
5
|
-
// display-width clip (pretext pixel truncation on glasses, Compose ellipsis on
|
|
6
|
-
// phone) is the final physical backstop — see the activity-status redesign spec
|
|
7
|
-
// §8.2. The old 30-char TOOL_PREVIEW_CHARS cap pre-truncated previews well under
|
|
8
|
-
// the glasses line's real pixel capacity, leaving ~90-170px of dead space.
|
|
9
4
|
|
|
10
5
|
const REDACT_QUERY_KEYS = "(token|access_token|api_key|key|password|secret)";
|
|
11
6
|
|
|
@@ -126,7 +121,7 @@ function filenameFromPath(pathValue) {
|
|
|
126
121
|
const normalized = cleaned.replace(/[;,)]+$/g, "");
|
|
127
122
|
if (!normalized) return null;
|
|
128
123
|
if (isNullishToken(normalized)) return null;
|
|
129
|
-
|
|
124
|
+
|
|
130
125
|
if (/\$[({]?[A-Za-z_][A-Za-z0-9_]*[)}]?/.test(normalized) || /\$\(.+\)/.test(normalized)) {
|
|
131
126
|
return null;
|
|
132
127
|
}
|
|
@@ -277,14 +272,14 @@ function unwrapShellCommand(raw) {
|
|
|
277
272
|
let payload = match[2].trim();
|
|
278
273
|
const quote = payload.charAt(0);
|
|
279
274
|
if (quote === '"' || quote === "'") {
|
|
280
|
-
if (payload.length < 2 || !payload.endsWith(quote)) break;
|
|
275
|
+
if (payload.length < 2 || !payload.endsWith(quote)) break;
|
|
281
276
|
payload = payload.slice(1, -1);
|
|
282
277
|
if (quote === '"') payload = payload.replace(/\\(["\\$`])/g, "$1");
|
|
283
278
|
}
|
|
284
279
|
if (!payload.trim()) break;
|
|
285
280
|
cmd = payload.trim();
|
|
286
281
|
}
|
|
287
|
-
|
|
282
|
+
|
|
288
283
|
const cdMatch = cmd.match(/^cd\s+[^;&|]+&&\s*([\s\S]+)$/);
|
|
289
284
|
if (cdMatch) cmd = cdMatch[1].trim();
|
|
290
285
|
return cmd;
|
|
@@ -295,11 +290,6 @@ const SHELL_KEYWORDS = new Set([
|
|
|
295
290
|
"while", "for", "until", "case", "esac", "{", "}", "(", ")", "!", "time",
|
|
296
291
|
]);
|
|
297
292
|
|
|
298
|
-
// Split into command-position token lists: one list per &&/||/;/|/
|
|
299
|
-
// newline segment, with shell keywords, [ ... ] tests, and leading
|
|
300
|
-
// VAR=value assignments stripped. Quoted separators are not shell-parsed
|
|
301
|
-
// (deterministic v1); the head token of the first segment -- which drives
|
|
302
|
-
// classification -- is unaffected by that limitation.
|
|
303
293
|
function commandSegments(command) {
|
|
304
294
|
const out = [];
|
|
305
295
|
for (const rawSeg of String(command || "").split(/&&|\|\||[;|\n]/)) {
|
|
@@ -347,7 +337,7 @@ function classifyExecSegment(tokens) {
|
|
|
347
337
|
if (fileName) return execResult(`Reading ${fileName}...`, "filesystem", "fs.read", fileName, "file");
|
|
348
338
|
}
|
|
349
339
|
if (bin === "sed") {
|
|
350
|
-
|
|
340
|
+
|
|
351
341
|
const inPlace = rest.some(
|
|
352
342
|
(t) => t === "-i" || t.startsWith("-i.") || t === "--in-place" || t.startsWith("--in-place="),
|
|
353
343
|
);
|
|
@@ -376,8 +366,7 @@ function classifyExecSegment(tokens) {
|
|
|
376
366
|
return execResult("Searching files...", "search", "search.files", null, "phrase");
|
|
377
367
|
}
|
|
378
368
|
if (bin === "git") {
|
|
379
|
-
|
|
380
|
-
// commit`) so the flag's ARGUMENT is never mislabeled as the subcommand.
|
|
369
|
+
|
|
381
370
|
let sub = null;
|
|
382
371
|
for (let index = 0; index < rest.length; index += 1) {
|
|
383
372
|
const token = rest[index];
|
|
@@ -423,7 +412,6 @@ function labelFromExecCommand(command) {
|
|
|
423
412
|
}
|
|
424
413
|
const raw = unwrapShellCommand(original);
|
|
425
414
|
|
|
426
|
-
// 1. agent-browser (substring, unchanged behavior)
|
|
427
415
|
if (raw.includes("agent-browser")) {
|
|
428
416
|
const query = extractBrowserQueryFromCommand(raw);
|
|
429
417
|
if (query) {
|
|
@@ -442,9 +430,6 @@ function labelFromExecCommand(command) {
|
|
|
442
430
|
|
|
443
431
|
const segments = commandSegments(raw);
|
|
444
432
|
|
|
445
|
-
// 2. curl/wget LEADING the command (first segment only — a trailing
|
|
446
|
-
// `&& curl …` must not outrank the leading command's first-token arm;
|
|
447
|
-
// pipelines ending in curl still resolve via the URL fallback below)
|
|
448
433
|
if (segments.length > 0) {
|
|
449
434
|
const leadBin = path.basename(segments[0][0]);
|
|
450
435
|
if (leadBin === "curl" || leadBin === "wget") {
|
|
@@ -456,8 +441,6 @@ function labelFromExecCommand(command) {
|
|
|
456
441
|
}
|
|
457
442
|
}
|
|
458
443
|
|
|
459
|
-
// 3. redirects + mktemp (unchanged relative order, BEFORE the read arms —
|
|
460
|
-
// preserves the pinned mktemp test)
|
|
461
444
|
const appendMatch = raw.match(/(?:^|\s)>>\s*([^\s]+)/);
|
|
462
445
|
if (appendMatch) {
|
|
463
446
|
const fileName = filenameFromPath(appendMatch[1]);
|
|
@@ -485,7 +468,6 @@ function labelFromExecCommand(command) {
|
|
|
485
468
|
}
|
|
486
469
|
}
|
|
487
470
|
|
|
488
|
-
// 4. find/grep/rg piped into wc → counting
|
|
489
471
|
if (
|
|
490
472
|
segments.length >= 2 &&
|
|
491
473
|
["find", "grep", "rg"].includes(path.basename(segments[0][0])) &&
|
|
@@ -494,13 +476,11 @@ function labelFromExecCommand(command) {
|
|
|
494
476
|
return execResult("Counting matches...", "search", "search.files");
|
|
495
477
|
}
|
|
496
478
|
|
|
497
|
-
// 5. first-token arms, first matching segment wins
|
|
498
479
|
for (const tokens of segments) {
|
|
499
480
|
const hit = classifyExecSegment(tokens);
|
|
500
481
|
if (hit) return hit;
|
|
501
482
|
}
|
|
502
483
|
|
|
503
|
-
// 6. bare-URL substring fallback (deliberately demoted below first-token arms)
|
|
504
484
|
if (/https?:\/\//i.test(raw)) {
|
|
505
485
|
const url = extractFirstUrl(raw);
|
|
506
486
|
const host = hostFromUrl(url);
|
|
@@ -508,7 +488,6 @@ function labelFromExecCommand(command) {
|
|
|
508
488
|
return execResult("Fetching data...", "network", "network.fetch");
|
|
509
489
|
}
|
|
510
490
|
|
|
511
|
-
// 7. raw fallback on the UNWRAPPED inner command
|
|
512
491
|
return execResult(`Running: ${sanitizeText(raw, DEFAULT_MAX_LABEL_CHARS)}`, "terminal", "terminal.exec");
|
|
513
492
|
}
|
|
514
493
|
|
|
@@ -819,8 +798,6 @@ function mapToolLabel(toolName, activityPath, args, options) {
|
|
|
819
798
|
const SHORT_LABEL_MAX_CHARS = 64;
|
|
820
799
|
const SHORT_LABEL_TARGET_CHARS = 42;
|
|
821
800
|
|
|
822
|
-
// Per-intent verb palettes (spec section 6.3, user-approved 2026-06-06). Applied
|
|
823
|
-
// ONLY to deterministic fallback labels — never to agent-authored summaries.
|
|
824
801
|
const VERB_PALETTES = {
|
|
825
802
|
"search.web": ["researching", "looking up", "searching for"],
|
|
826
803
|
"fs.read": ["reading", "checking", "opening"],
|
|
@@ -852,9 +829,6 @@ function fnv1aHash(text) {
|
|
|
852
829
|
return hash >>> 0;
|
|
853
830
|
}
|
|
854
831
|
|
|
855
|
-
// Deterministic header-safe short label, or null when the intent has no
|
|
856
|
-
// palette (fixed-phrase arms ARE their own short form; emit-when-differs
|
|
857
|
-
// in the adapter keeps them off the wire).
|
|
858
832
|
function buildShortLabel(input) {
|
|
859
833
|
const obj = isObject(input) ? input : null;
|
|
860
834
|
const intent = obj ? asString(obj.intent) : null;
|
|
@@ -869,17 +843,13 @@ function buildShortLabel(input) {
|
|
|
869
843
|
if (!verbs) return null;
|
|
870
844
|
let subject = obj && obj.subject ? String(obj.subject).trim() : "";
|
|
871
845
|
if (!subject || subjectKind === "fixed") return null;
|
|
872
|
-
|
|
873
|
-
// redaction patterns' length floors (e.g. sk- + 16 chars) would let a
|
|
874
|
-
// partial token escape the adapter's emission-time sanitizeText.
|
|
846
|
+
|
|
875
847
|
subject = redactSecrets(subject).trim();
|
|
876
848
|
if (!subject) return null;
|
|
877
849
|
if (subjectKind === "query") subject = trimQueryForShortLabel(subject);
|
|
878
850
|
const verb = verbs[fnv1aHash(stabilityKey) % verbs.length];
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
// (no mixed "…..." glyph run).
|
|
882
|
-
const budgetForSubject = SHORT_LABEL_TARGET_CHARS - verb.length - 4; // " " + "..."
|
|
851
|
+
|
|
852
|
+
const budgetForSubject = SHORT_LABEL_TARGET_CHARS - verb.length - 4;
|
|
883
853
|
if (subject.length > budgetForSubject) {
|
|
884
854
|
subject = subject.slice(0, Math.max(budgetForSubject, 8)).trimEnd();
|
|
885
855
|
}
|
|
@@ -1,28 +1,11 @@
|
|
|
1
|
-
// Markdown code-region scanner for the tagged-span grammar passes.
|
|
2
|
-
// Leaf module by design: no imports (CJS emitter import-cycle hazard).
|
|
3
|
-
//
|
|
4
|
-
// Computes [start, end) regions of text covered by markdown code so that
|
|
5
|
-
// tagged-span grammar (<emoji:…>, <dwell>, <skim>) quoted inside backticks
|
|
6
|
-
// or fenced blocks is treated as literal text instead of live tags.
|
|
7
|
-
//
|
|
8
|
-
// Streaming-partial semantics: an unclosed fence runs to end-of-text
|
|
9
|
-
// (CommonMark), while an unclosed inline backtick stays literal until its
|
|
10
|
-
// closer arrives — the cumulative re-parse on the next flush re-interprets,
|
|
11
|
-
// matching how the markdown pass already behaves on partial text.
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @param {string} text
|
|
15
|
-
* @returns {Array<[number, number]>} sorted, non-overlapping [start, end) regions
|
|
16
|
-
*/
|
|
17
1
|
export function computeCodeSpanRegions(text) {
|
|
18
2
|
if (typeof text !== "string" || !text) return [];
|
|
19
3
|
const n = text.length;
|
|
20
4
|
const regions = [];
|
|
21
5
|
|
|
22
|
-
// --- Pass 1: fenced code blocks (line-oriented, ``` or ~~~) ---
|
|
23
6
|
const FENCE_OPEN_RE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
24
7
|
const FENCE_CLOSE_RE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
|
|
25
|
-
let fence = null;
|
|
8
|
+
let fence = null;
|
|
26
9
|
let lineStart = 0;
|
|
27
10
|
while (lineStart < n) {
|
|
28
11
|
const nl = text.indexOf("\n", lineStart);
|
|
@@ -30,7 +13,7 @@ export function computeCodeSpanRegions(text) {
|
|
|
30
13
|
const line = text.slice(lineStart, lineEnd);
|
|
31
14
|
if (!fence) {
|
|
32
15
|
const open = FENCE_OPEN_RE.exec(line);
|
|
33
|
-
|
|
16
|
+
|
|
34
17
|
if (open && !(open[1][0] === "`" && open[2].includes("`"))) {
|
|
35
18
|
fence = { char: open[1][0], len: open[1].length, start: lineStart };
|
|
36
19
|
}
|
|
@@ -53,9 +36,6 @@ export function computeCodeSpanRegions(text) {
|
|
|
53
36
|
return false;
|
|
54
37
|
};
|
|
55
38
|
|
|
56
|
-
// --- Pass 2: inline backtick code spans outside fences ---
|
|
57
|
-
// CommonMark: a run of N backticks closes only on the next run of exactly
|
|
58
|
-
// N backticks, and a code span cannot cross a blank line.
|
|
59
39
|
let i = 0;
|
|
60
40
|
while (i < n) {
|
|
61
41
|
if (text[i] !== "`" || inFence(i)) {
|
|
@@ -81,7 +61,7 @@ export function computeCodeSpanRegions(text) {
|
|
|
81
61
|
continue;
|
|
82
62
|
}
|
|
83
63
|
if (ch === "\n") {
|
|
84
|
-
|
|
64
|
+
|
|
85
65
|
let p = k + 1;
|
|
86
66
|
while (p < n && (text[p] === " " || text[p] === "\t")) p += 1;
|
|
87
67
|
if (p < n && text[p] === "\n") break scan;
|
|
@@ -90,7 +70,7 @@ export function computeCodeSpanRegions(text) {
|
|
|
90
70
|
}
|
|
91
71
|
|
|
92
72
|
if (close === -1) {
|
|
93
|
-
|
|
73
|
+
|
|
94
74
|
i = runEnd;
|
|
95
75
|
continue;
|
|
96
76
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function constantTimeEqual(a, b) {
|
|
4
|
+
if (typeof a !== "string" || typeof b !== "string") return false;
|
|
5
|
+
if (a.length === 0 || b.length === 0) return false;
|
|
6
|
+
const da = createHash("sha256").update(a, "utf8").digest();
|
|
7
|
+
const db = createHash("sha256").update(b, "utf8").digest();
|
|
8
|
+
return timingSafeEqual(da, db);
|
|
9
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { constantTimeEqual } from "./constant-time-equal.ts";
|
|
4
|
+
|
|
5
|
+
test("constantTimeEqual: equal non-empty strings compare equal", () => {
|
|
6
|
+
assert.equal(constantTimeEqual("s3cr3t-token", "s3cr3t-token"), true);
|
|
7
|
+
assert.equal(constantTimeEqual("a", "a"), true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("constantTimeEqual: any difference compares unequal", () => {
|
|
11
|
+
assert.equal(constantTimeEqual("s3cr3t-token", "s3cr3t-toke"), false);
|
|
12
|
+
assert.equal(constantTimeEqual("s3cr3t-token", "s3cr3t-tokeN"), false);
|
|
13
|
+
assert.equal(constantTimeEqual("abc", "xbc"), false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("constantTimeEqual: length mismatch never throws (hashed to fixed width)", () => {
|
|
17
|
+
assert.doesNotThrow(() => constantTimeEqual("short", "a much much longer candidate value"));
|
|
18
|
+
assert.equal(constantTimeEqual("short", "a much much longer candidate value"), false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("constantTimeEqual: non-string or empty inputs are always false", () => {
|
|
22
|
+
assert.equal(constantTimeEqual("", ""), false);
|
|
23
|
+
assert.equal(constantTimeEqual("x", ""), false);
|
|
24
|
+
assert.equal(constantTimeEqual(null, "x"), false);
|
|
25
|
+
assert.equal(constantTimeEqual("x", undefined), false);
|
|
26
|
+
assert.equal(constantTimeEqual(undefined, undefined), false);
|
|
27
|
+
assert.equal(constantTimeEqual(123, 123), false);
|
|
28
|
+
});
|