@vellumai/assistant 0.4.21 → 0.4.23
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/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +55 -44
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -75
- package/src/__tests__/headless-browser-interactions.test.ts +0 -4
- package/src/__tests__/ipc-snapshot.test.ts +0 -54
- package/src/__tests__/resolve-guardian-trust-class.test.ts +61 -0
- package/src/__tests__/session-init.benchmark.test.ts +0 -4
- package/src/config/system-prompt.ts +1 -0
- package/src/config/templates/BOOTSTRAP.md +21 -31
- package/src/config/templates/SOUL.md +19 -9
- package/src/daemon/computer-use-session.ts +5 -3
- package/src/daemon/daemon-control.ts +3 -0
- package/src/daemon/handlers/browser.ts +2 -48
- package/src/daemon/handlers/config-voice.ts +155 -33
- package/src/daemon/handlers/dictation.ts +361 -214
- package/src/daemon/ipc-contract/browser.ts +4 -74
- package/src/daemon/ipc-contract/surfaces.ts +51 -48
- package/src/daemon/ipc-contract-inventory.json +0 -7
- package/src/daemon/session-agent-loop.ts +2 -1
- package/src/daemon/session-runtime-assembly.ts +477 -247
- package/src/daemon/session-surfaces.ts +5 -3
- package/src/daemon/session-tool-setup.ts +27 -13
- package/src/memory/migrations/102-alter-table-columns.ts +254 -37
- package/src/memory/schema.ts +1227 -1035
- package/src/tools/browser/browser-execution.ts +314 -331
- package/src/tools/browser/browser-handoff.ts +11 -37
- package/src/tools/browser/browser-manager.ts +271 -264
- package/src/tools/browser/browser-screencast.ts +19 -75
|
@@ -251,6 +251,9 @@ function releaseStartupLock(): void {
|
|
|
251
251
|
try { unlinkSync(getStartupLockPath()); } catch { /* already removed */ }
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
// NOTE: startDaemon() is the assistant-side daemon lifecycle manager.
|
|
255
|
+
// It should eventually converge with cli/src/lib/local.ts::startLocalDaemon
|
|
256
|
+
// which is the CLI-side equivalent.
|
|
254
257
|
export async function startDaemon(): Promise<{
|
|
255
258
|
pid: number;
|
|
256
259
|
alreadyRunning: boolean;
|
|
@@ -1,54 +1,8 @@
|
|
|
1
|
-
import { browserManager
|
|
2
|
-
import { defineHandlers
|
|
1
|
+
import { browserManager } from "../../tools/browser/browser-manager.js";
|
|
2
|
+
import { defineHandlers } from "./shared.js";
|
|
3
3
|
|
|
4
4
|
export const browserHandlers = defineHandlers({
|
|
5
5
|
browser_cdp_response: (msg) => {
|
|
6
6
|
browserManager.resolveCDPResponse(msg.sessionId, msg.success, msg.declined);
|
|
7
7
|
},
|
|
8
|
-
|
|
9
|
-
browser_user_click: async (msg) => {
|
|
10
|
-
try {
|
|
11
|
-
const page = await browserManager.getOrCreateSessionPage(msg.sessionId);
|
|
12
|
-
const viewport = await page.evaluate('(() => ({ vw: window.innerWidth, vh: window.innerHeight }))()') as { vw: number; vh: number };
|
|
13
|
-
const scale = Math.min(SCREENCAST_WIDTH / viewport.vw, SCREENCAST_HEIGHT / viewport.vh);
|
|
14
|
-
const pageX = msg.x / scale;
|
|
15
|
-
const pageY = msg.y / scale;
|
|
16
|
-
const options: Record<string, unknown> = {};
|
|
17
|
-
if (msg.button === 'right') options.button = 'right';
|
|
18
|
-
if (msg.doubleClick) options.clickCount = 2;
|
|
19
|
-
await page.mouse.click(pageX, pageY, options);
|
|
20
|
-
} catch (err) {
|
|
21
|
-
log.warn({ err, sessionId: msg.sessionId }, 'Failed to forward user click');
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
|
|
25
|
-
browser_user_scroll: async (msg) => {
|
|
26
|
-
try {
|
|
27
|
-
const page = await browserManager.getOrCreateSessionPage(msg.sessionId);
|
|
28
|
-
await page.mouse.wheel(msg.deltaX, msg.deltaY);
|
|
29
|
-
} catch (err) {
|
|
30
|
-
log.warn({ err, sessionId: msg.sessionId }, 'Failed to forward user scroll');
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
browser_user_keypress: async (msg) => {
|
|
35
|
-
try {
|
|
36
|
-
const page = await browserManager.getOrCreateSessionPage(msg.sessionId);
|
|
37
|
-
const combo = msg.modifiers?.length ? [...msg.modifiers, msg.key].join('+') : msg.key;
|
|
38
|
-
await page.keyboard.press(combo);
|
|
39
|
-
} catch (err) {
|
|
40
|
-
log.warn({ err, sessionId: msg.sessionId }, 'Failed to forward user keypress');
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
|
|
44
|
-
browser_interactive_mode: (msg, socket, ctx) => {
|
|
45
|
-
log.info({ sessionId: msg.sessionId, enabled: msg.enabled }, 'Interactive mode toggled');
|
|
46
|
-
browserManager.setInteractiveMode(msg.sessionId, msg.enabled);
|
|
47
|
-
ctx.send(socket, {
|
|
48
|
-
type: 'browser_interactive_mode_changed',
|
|
49
|
-
sessionId: msg.sessionId,
|
|
50
|
-
surfaceId: msg.surfaceId,
|
|
51
|
-
enabled: msg.enabled,
|
|
52
|
-
});
|
|
53
|
-
},
|
|
54
8
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import * as net from
|
|
1
|
+
import * as net from "node:net";
|
|
2
2
|
|
|
3
|
-
import type { VoiceConfigUpdateRequest } from
|
|
4
|
-
import { defineHandlers, type HandlerContext, log } from
|
|
3
|
+
import type { VoiceConfigUpdateRequest } from "../ipc-contract/settings.js";
|
|
4
|
+
import { defineHandlers, type HandlerContext, log } from "./shared.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Send a client_settings_update message to all connected clients.
|
|
@@ -14,16 +14,16 @@ export function broadcastClientSettingsUpdate(
|
|
|
14
14
|
ctx: HandlerContext,
|
|
15
15
|
): void {
|
|
16
16
|
ctx.broadcast({
|
|
17
|
-
type:
|
|
17
|
+
type: "client_settings_update",
|
|
18
18
|
key,
|
|
19
19
|
value,
|
|
20
20
|
});
|
|
21
|
-
log.info({ key, value },
|
|
21
|
+
log.info({ key, value }, "Broadcast client_settings_update");
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// ── Activation key validation ────────────────────────────────────────
|
|
25
25
|
|
|
26
|
-
const VALID_ACTIVATION_KEYS = [
|
|
26
|
+
const VALID_ACTIVATION_KEYS = ["fn", "ctrl", "fn_shift", "none"] as const;
|
|
27
27
|
export type ActivationKey = (typeof VALID_ACTIVATION_KEYS)[number];
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -31,48 +31,167 @@ export type ActivationKey = (typeof VALID_ACTIVATION_KEYS)[number];
|
|
|
31
31
|
* Case-insensitive matching is applied by the caller.
|
|
32
32
|
*/
|
|
33
33
|
const NATURAL_LANGUAGE_MAP: Record<string, ActivationKey> = {
|
|
34
|
-
fn:
|
|
35
|
-
globe:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
ctrl:
|
|
39
|
-
control:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
fn_shift:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
none:
|
|
47
|
-
off:
|
|
48
|
-
disabled:
|
|
49
|
-
disable:
|
|
34
|
+
fn: "fn",
|
|
35
|
+
globe: "fn",
|
|
36
|
+
"fn key": "fn",
|
|
37
|
+
"globe key": "fn",
|
|
38
|
+
ctrl: "ctrl",
|
|
39
|
+
control: "ctrl",
|
|
40
|
+
"ctrl key": "ctrl",
|
|
41
|
+
"control key": "ctrl",
|
|
42
|
+
fn_shift: "fn_shift",
|
|
43
|
+
"fn+shift": "fn_shift",
|
|
44
|
+
"fn shift": "fn_shift",
|
|
45
|
+
"shift+fn": "fn_shift",
|
|
46
|
+
none: "none",
|
|
47
|
+
off: "none",
|
|
48
|
+
disabled: "none",
|
|
49
|
+
disable: "none",
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
// ── PTTActivator JSON validation ─────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const VALID_KINDS = [
|
|
55
|
+
"modifierOnly",
|
|
56
|
+
"key",
|
|
57
|
+
"modifierKey",
|
|
58
|
+
"mouseButton",
|
|
59
|
+
"none",
|
|
60
|
+
] as const;
|
|
61
|
+
type PTTKind = (typeof VALID_KINDS)[number];
|
|
62
|
+
|
|
63
|
+
interface PTTActivatorPayload {
|
|
64
|
+
kind: PTTKind;
|
|
65
|
+
keyCode?: number | null;
|
|
66
|
+
modifierFlags?: number | null;
|
|
67
|
+
mouseButton?: number | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validate a parsed PTTActivator JSON payload.
|
|
72
|
+
* Returns an error message if invalid, or null if valid.
|
|
73
|
+
*/
|
|
74
|
+
function validatePTTActivator(payload: PTTActivatorPayload): string | null {
|
|
75
|
+
if (!VALID_KINDS.includes(payload.kind)) {
|
|
76
|
+
return `Invalid kind "${payload.kind}". Valid values: ${VALID_KINDS.join(", ")}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Enforce numeric types for fields that the Swift client decodes as numbers.
|
|
80
|
+
// Without this, JS coercion lets string values like "96" pass range checks
|
|
81
|
+
// but the macOS client fails to decode them as UInt16/Int/UInt.
|
|
82
|
+
if (payload.keyCode != null && typeof payload.keyCode !== "number") {
|
|
83
|
+
return `keyCode must be a number, got ${typeof payload.keyCode}`;
|
|
84
|
+
}
|
|
85
|
+
if (
|
|
86
|
+
payload.modifierFlags != null &&
|
|
87
|
+
typeof payload.modifierFlags !== "number"
|
|
88
|
+
) {
|
|
89
|
+
return `modifierFlags must be a number, got ${typeof payload.modifierFlags}`;
|
|
90
|
+
}
|
|
91
|
+
if (payload.mouseButton != null && typeof payload.mouseButton !== "number") {
|
|
92
|
+
return `mouseButton must be a number, got ${typeof payload.mouseButton}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
switch (payload.kind) {
|
|
96
|
+
case "modifierOnly":
|
|
97
|
+
if (payload.modifierFlags == null) {
|
|
98
|
+
return "modifierOnly requires modifierFlags";
|
|
99
|
+
}
|
|
100
|
+
if (payload.keyCode != null || payload.mouseButton != null) {
|
|
101
|
+
return "modifierOnly must not have keyCode or mouseButton";
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
|
|
105
|
+
case "key":
|
|
106
|
+
if (payload.keyCode == null) {
|
|
107
|
+
return "key requires keyCode";
|
|
108
|
+
}
|
|
109
|
+
if (payload.keyCode < 0 || payload.keyCode > 255) {
|
|
110
|
+
return `keyCode must be 0-255, got ${payload.keyCode}`;
|
|
111
|
+
}
|
|
112
|
+
if (payload.mouseButton != null) {
|
|
113
|
+
return "key must not have mouseButton";
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case "modifierKey":
|
|
118
|
+
if (payload.keyCode == null) {
|
|
119
|
+
return "modifierKey requires keyCode";
|
|
120
|
+
}
|
|
121
|
+
if (payload.keyCode < 0 || payload.keyCode > 255) {
|
|
122
|
+
return `keyCode must be 0-255, got ${payload.keyCode}`;
|
|
123
|
+
}
|
|
124
|
+
if (payload.modifierFlags == null) {
|
|
125
|
+
return "modifierKey requires modifierFlags";
|
|
126
|
+
}
|
|
127
|
+
if (payload.mouseButton != null) {
|
|
128
|
+
return "modifierKey must not have mouseButton";
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case "mouseButton":
|
|
133
|
+
if (payload.mouseButton == null) {
|
|
134
|
+
return "mouseButton requires mouseButton field";
|
|
135
|
+
}
|
|
136
|
+
if (payload.mouseButton < 2) {
|
|
137
|
+
return `mouseButton must be >= 2 (left=0, right=1 are reserved), got ${payload.mouseButton}`;
|
|
138
|
+
}
|
|
139
|
+
if (payload.keyCode != null) {
|
|
140
|
+
return "mouseButton must not have keyCode";
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case "none":
|
|
145
|
+
// No required fields
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
52
152
|
/**
|
|
53
153
|
* Validate and normalise a user-provided activation key string.
|
|
54
|
-
* Accepts
|
|
154
|
+
* Accepts legacy enum values, natural-language variants, and PTTActivator JSON.
|
|
55
155
|
* Returns the canonical value on success, or an error message on failure.
|
|
56
156
|
*/
|
|
57
157
|
export function normalizeActivationKey(
|
|
58
158
|
input: string,
|
|
59
|
-
): { ok: true; value:
|
|
60
|
-
const trimmed = input.trim()
|
|
159
|
+
): { ok: true; value: string } | { ok: false; reason: string } {
|
|
160
|
+
const trimmed = input.trim();
|
|
161
|
+
|
|
162
|
+
// Try JSON parse first (PTTActivator payloads start with '{')
|
|
163
|
+
if (trimmed.startsWith("{")) {
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(trimmed) as PTTActivatorPayload;
|
|
166
|
+
const error = validatePTTActivator(parsed);
|
|
167
|
+
if (error) {
|
|
168
|
+
return { ok: false, reason: `Invalid PTTActivator: ${error}` };
|
|
169
|
+
}
|
|
170
|
+
// Pass through the validated JSON as-is
|
|
171
|
+
return { ok: true, value: trimmed };
|
|
172
|
+
} catch {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
reason: `Malformed PTTActivator JSON: ${input}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
61
179
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
180
|
+
// Legacy: direct enum match
|
|
181
|
+
const lower = trimmed.toLowerCase();
|
|
182
|
+
if ((VALID_ACTIVATION_KEYS as readonly string[]).includes(lower)) {
|
|
183
|
+
return { ok: true, value: lower as ActivationKey };
|
|
65
184
|
}
|
|
66
185
|
|
|
67
|
-
//
|
|
68
|
-
const mapped = NATURAL_LANGUAGE_MAP[
|
|
186
|
+
// Legacy: natural-language match
|
|
187
|
+
const mapped = NATURAL_LANGUAGE_MAP[lower];
|
|
69
188
|
if (mapped) {
|
|
70
189
|
return { ok: true, value: mapped };
|
|
71
190
|
}
|
|
72
191
|
|
|
73
192
|
return {
|
|
74
193
|
ok: false,
|
|
75
|
-
reason: `Invalid activation key "${input}". Valid values: fn (Fn/Globe key), ctrl (Control key), fn_shift (Fn+Shift), none (disable PTT).`,
|
|
194
|
+
reason: `Invalid activation key "${input}". Valid values: fn (Fn/Globe key), ctrl (Control key), fn_shift (Fn+Shift), none (disable PTT), or a PTTActivator JSON object.`,
|
|
76
195
|
};
|
|
77
196
|
}
|
|
78
197
|
|
|
@@ -91,8 +210,11 @@ export function handleVoiceConfigUpdate(
|
|
|
91
210
|
return;
|
|
92
211
|
}
|
|
93
212
|
|
|
94
|
-
broadcastClientSettingsUpdate(
|
|
95
|
-
log.info(
|
|
213
|
+
broadcastClientSettingsUpdate("activationKey", result.value, ctx);
|
|
214
|
+
log.info(
|
|
215
|
+
{ activationKey: result.value },
|
|
216
|
+
"Voice config updated: activation key",
|
|
217
|
+
);
|
|
96
218
|
}
|
|
97
219
|
|
|
98
220
|
export const voiceHandlers = defineHandlers({
|