@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.
@@ -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, SCREENCAST_HEIGHT,SCREENCAST_WIDTH } from '../../tools/browser/browser-manager.js';
2
- import { defineHandlers,log } from './shared.js';
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 'node:net';
1
+ import * as net from "node:net";
2
2
 
3
- import type { VoiceConfigUpdateRequest } from '../ipc-contract/settings.js';
4
- import { defineHandlers, type HandlerContext, log } from './shared.js';
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: 'client_settings_update',
17
+ type: "client_settings_update",
18
18
  key,
19
19
  value,
20
20
  });
21
- log.info({ key, value }, 'Broadcast client_settings_update');
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 = ['fn', 'ctrl', 'fn_shift', 'none'] as const;
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: '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',
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 both canonical enum values and natural-language variants.
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: ActivationKey } | { ok: false; reason: string } {
60
- const trimmed = input.trim().toLowerCase();
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
- // Direct enum match
63
- if ((VALID_ACTIVATION_KEYS as readonly string[]).includes(trimmed)) {
64
- return { ok: true, value: trimmed as ActivationKey };
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
- // Natural-language match
68
- const mapped = NATURAL_LANGUAGE_MAP[trimmed];
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('activationKey', result.value, ctx);
95
- log.info({ activationKey: result.value }, 'Voice config updated: activation key');
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({