@vellumai/assistant 0.4.41 → 0.4.42

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.
@@ -2,12 +2,20 @@ import type { Command } from "commander";
2
2
 
3
3
  import {
4
4
  getAssistantContactMetadata,
5
+ getChannelById,
5
6
  getContact,
6
7
  listContacts,
7
8
  mergeContacts,
8
9
  searchContacts,
10
+ updateChannelStatus,
11
+ upsertContact,
9
12
  } from "../contacts/contact-store.js";
10
- import type { ContactRole } from "../contacts/types.js";
13
+ import type {
14
+ ChannelPolicy,
15
+ ChannelStatus,
16
+ ContactRole,
17
+ ContactType,
18
+ } from "../contacts/types.js";
11
19
  import { initializeDb } from "../memory/db.js";
12
20
  import {
13
21
  createIngressInvite,
@@ -36,6 +44,7 @@ is the source of truth for identity resolution across all channels.
36
44
  Examples:
37
45
  $ assistant contacts list
38
46
  $ assistant contacts get abc-123
47
+ $ assistant contacts upsert --display-name "Alice"
39
48
  $ assistant contacts merge keep-id merge-id
40
49
  $ assistant contacts invites list`,
41
50
  );
@@ -46,6 +55,14 @@ Examples:
46
55
  .option("--role <role>", "Filter by role (default: contact)", "contact")
47
56
  .option("--limit <limit>", "Maximum number of contacts to return")
48
57
  .option("--query <query>", "Search query to filter contacts")
58
+ .option(
59
+ "--channel-address <address>",
60
+ "Search by channel address (email, phone, handle)",
61
+ )
62
+ .option(
63
+ "--channel-type <channelType>",
64
+ "Filter by channel type (email, telegram, sms, voice, whatsapp, slack)",
65
+ )
49
66
  .addHelpText(
50
67
  "after",
51
68
  `
@@ -53,14 +70,20 @@ Lists contacts with optional filtering. The --role flag accepts: contact
53
70
  or guardian (defaults to contact). The --limit flag sets
54
71
  the maximum number of results (defaults to 50).
55
72
 
56
- When --query is provided, a full-text search is performed across contact
57
- names and linked external identifiers (phone numbers, emails, Telegram
58
- usernames). Without --query, returns all contacts matching the role filter.
73
+ When --query, --channel-address, or --channel-type is provided, a search
74
+ is performed. --query does full-text search across contact names and
75
+ linked external identifiers. --channel-address matches phone numbers,
76
+ emails, or handles. --channel-type filters by channel kind. These filters
77
+ can be combined. Without any search params, returns all contacts matching
78
+ the role filter.
59
79
 
60
80
  Examples:
61
81
  $ assistant contacts list
62
82
  $ assistant contacts list --role guardian
63
83
  $ assistant contacts list --query "john" --limit 10
84
+ $ assistant contacts list --channel-address "+15551234567"
85
+ $ assistant contacts list --channel-type telegram
86
+ $ assistant contacts list --query "alice" --channel-type email
64
87
  $ assistant contacts list --role guardian --json`,
65
88
  )
66
89
  .action(
@@ -69,6 +92,8 @@ Examples:
69
92
  role?: string;
70
93
  limit?: string;
71
94
  query?: string;
95
+ channelAddress?: string;
96
+ channelType?: string;
72
97
  },
73
98
  cmd: Command,
74
99
  ) => {
@@ -79,8 +104,16 @@ Examples:
79
104
 
80
105
  const effectiveLimit = limit ?? 50;
81
106
 
82
- const results = opts.query
83
- ? searchContacts({ query: opts.query, role, limit: effectiveLimit })
107
+ const hasSearchParams =
108
+ opts.query || opts.channelAddress || opts.channelType;
109
+ const results = hasSearchParams
110
+ ? searchContacts({
111
+ query: opts.query,
112
+ channelAddress: opts.channelAddress,
113
+ channelType: opts.channelType,
114
+ role,
115
+ limit: effectiveLimit,
116
+ })
84
117
  : listContacts(effectiveLimit, role);
85
118
 
86
119
  writeOutput(cmd, { ok: true, contacts: results });
@@ -166,6 +199,222 @@ Examples:
166
199
  },
167
200
  );
168
201
 
202
+ contacts
203
+ .command("upsert")
204
+ .description("Create or update a contact")
205
+ .requiredOption("--display-name <name>", "Display name for the contact")
206
+ .option("--id <id>", "Contact ID — provide to update an existing contact")
207
+ .option("--notes <notes>", "Free-text notes about the contact")
208
+ .option(
209
+ "--role <role>",
210
+ "Contact role: contact or guardian (default: contact)",
211
+ "contact",
212
+ )
213
+ .option(
214
+ "--contact-type <type>",
215
+ "Contact type: human or assistant (default: human)",
216
+ "human",
217
+ )
218
+ .option(
219
+ "--channels <json>",
220
+ "JSON array of channel objects, each with type, address, and optional isPrimary, externalUserId, externalChatId, status, policy",
221
+ )
222
+ .addHelpText(
223
+ "after",
224
+ `
225
+ Creates a new contact or updates an existing one. When --id is provided and
226
+ matches an existing contact, that contact is updated. When --id is omitted,
227
+ a new contact is created with a generated UUID.
228
+
229
+ The --channels flag accepts a JSON array of channel objects. Each object must
230
+ have "type" (e.g. telegram, sms, voice, email, whatsapp) and "address" fields.
231
+ Optional channel fields: isPrimary (boolean), externalUserId, externalChatId,
232
+ status (active, revoked, blocked), policy (allow, deny).
233
+
234
+ Examples:
235
+ $ assistant contacts upsert --display-name "Alice" --json
236
+ $ assistant contacts upsert --display-name "Alice" --id abc-123 --notes "Updated notes" --json
237
+ $ assistant contacts upsert --display-name "Bob" --role guardian --json
238
+ $ assistant contacts upsert --display-name "Bob" --channels '[{"type":"telegram","address":"12345","externalUserId":"12345","status":"active","policy":"allow"}]' --json`,
239
+ )
240
+ .action(
241
+ async (
242
+ opts: {
243
+ displayName: string;
244
+ id?: string;
245
+ notes?: string;
246
+ role?: string;
247
+ contactType?: string;
248
+ channels?: string;
249
+ },
250
+ cmd: Command,
251
+ ) => {
252
+ try {
253
+ initializeDb();
254
+
255
+ let channels: unknown[] | undefined;
256
+ if (opts.channels) {
257
+ try {
258
+ channels = JSON.parse(opts.channels);
259
+ if (!Array.isArray(channels)) {
260
+ writeOutput(cmd, {
261
+ ok: false,
262
+ error: "--channels must be a JSON array",
263
+ });
264
+ process.exitCode = 1;
265
+ return;
266
+ }
267
+ } catch {
268
+ writeOutput(cmd, {
269
+ ok: false,
270
+ error: `Invalid JSON for --channels: ${opts.channels}`,
271
+ });
272
+ process.exitCode = 1;
273
+ return;
274
+ }
275
+ }
276
+
277
+ const result = upsertContact({
278
+ id: opts.id,
279
+ displayName: opts.displayName,
280
+ notes: opts.notes,
281
+ role: opts.role as ContactRole | undefined,
282
+ contactType: opts.contactType as ContactType | undefined,
283
+ channels: channels as
284
+ | {
285
+ type: string;
286
+ address: string;
287
+ isPrimary?: boolean;
288
+ externalUserId?: string;
289
+ externalChatId?: string;
290
+ status?: ChannelStatus;
291
+ policy?: ChannelPolicy;
292
+ }[]
293
+ | undefined,
294
+ });
295
+
296
+ writeOutput(cmd, { ok: true, contact: result });
297
+ } catch (err) {
298
+ const message = err instanceof Error ? err.message : String(err);
299
+ writeOutput(cmd, { ok: false, error: message });
300
+ process.exitCode = 1;
301
+ }
302
+ },
303
+ );
304
+
305
+ const channelsCmds = contacts
306
+ .command("channels")
307
+ .description("Manage contact channels");
308
+
309
+ channelsCmds.addHelpText(
310
+ "after",
311
+ `
312
+ Channels represent external communication endpoints linked to contacts —
313
+ phone numbers, Telegram IDs, email addresses, etc. Each channel has a
314
+ status (active, pending, revoked, blocked, unverified) and a policy
315
+ (allow, deny, escalate) that controls how the assistant handles messages
316
+ from that channel.
317
+
318
+ Examples:
319
+ $ assistant contacts channels update-status <channelId> --status revoked --reason "No longer needed"
320
+ $ assistant contacts channels update-status <channelId> --policy deny`,
321
+ );
322
+
323
+ channelsCmds
324
+ .command("update-status <channelId>")
325
+ .description("Update a channel's status or policy")
326
+ .option(
327
+ "--status <status>",
328
+ "New channel status: active, revoked, or blocked",
329
+ )
330
+ .option("--policy <policy>", "New channel policy: allow, deny, or escalate")
331
+ .option("--reason <reason>", "Reason for the status change")
332
+ .addHelpText(
333
+ "after",
334
+ `
335
+ Arguments:
336
+ channelId UUID of the contact channel to update
337
+
338
+ Updates the access-control fields on an existing channel. At least one of
339
+ --status or --policy must be provided.
340
+
341
+ When --status is "revoked", --reason is mapped to revokedReason on the
342
+ channel record. When --status is "blocked", --reason is mapped to
343
+ blockedReason. The --reason flag is ignored for other status values.
344
+
345
+ Valid --status values: active, revoked, blocked
346
+ Valid --policy values: allow, deny, escalate
347
+
348
+ Examples:
349
+ $ assistant contacts channels update-status abc-123 --status revoked --reason "No longer needed" --json
350
+ $ assistant contacts channels update-status abc-123 --status blocked --reason "Spam" --json
351
+ $ assistant contacts channels update-status abc-123 --policy deny --json
352
+ $ assistant contacts channels update-status abc-123 --status active --policy allow --json`,
353
+ )
354
+ .action(
355
+ async (
356
+ channelId: string,
357
+ opts: {
358
+ status?: string;
359
+ policy?: string;
360
+ reason?: string;
361
+ },
362
+ cmd: Command,
363
+ ) => {
364
+ try {
365
+ if (!opts.status && !opts.policy) {
366
+ writeOutput(cmd, {
367
+ ok: false,
368
+ error: "At least one of --status or --policy must be provided",
369
+ });
370
+ process.exitCode = 1;
371
+ return;
372
+ }
373
+
374
+ initializeDb();
375
+
376
+ const existing = getChannelById(channelId);
377
+ if (!existing) {
378
+ writeOutput(cmd, {
379
+ ok: false,
380
+ error: `Channel not found: ${channelId}`,
381
+ });
382
+ process.exitCode = 1;
383
+ return;
384
+ }
385
+
386
+ const status = opts.status as ChannelStatus | undefined;
387
+ const policy = opts.policy as ChannelPolicy | undefined;
388
+
389
+ const revokedReason: string | null | undefined =
390
+ status !== undefined
391
+ ? status === "revoked"
392
+ ? (opts.reason ?? null)
393
+ : null
394
+ : undefined;
395
+ const blockedReason: string | null | undefined =
396
+ status !== undefined
397
+ ? status === "blocked"
398
+ ? (opts.reason ?? null)
399
+ : null
400
+ : undefined;
401
+
402
+ const result = updateChannelStatus(channelId, {
403
+ status,
404
+ policy,
405
+ revokedReason,
406
+ blockedReason,
407
+ });
408
+
409
+ writeOutput(cmd, { ok: true, channel: result });
410
+ } catch (err) {
411
+ const message = err instanceof Error ? err.message : String(err);
412
+ writeOutput(cmd, { ok: false, error: message });
413
+ process.exitCode = 1;
414
+ }
415
+ },
416
+ );
417
+
169
418
  const invites = contacts
170
419
  .command("invites")
171
420
  .description("Manage contact invites");
@@ -25,8 +25,17 @@ You are an expert app builder and visual designer. When the user asks you to cre
25
25
 
26
26
  **Make creative decisions on behalf of the user.** They want to be delighted, not consulted. Pick the accent color. Choose between a dark moody aesthetic or a light airy one. Decide if cards should have glassmorphism or layered shadows. Add a background pattern or gradient. These are YOUR decisions as the designer.
27
27
 
28
+ <!-- feature:app-builder-multifile:start -->
29
+
28
30
  **Prefer multi-file TSX projects** for any non-trivial app. They give you component reuse, TypeScript safety, and cleaner organization. Fall back to single-file HTML only for the simplest one-off pages.
29
31
 
32
+ <!-- feature:app-builder-multifile:end -->
33
+ <!-- feature:app-builder-multifile:alt -->
34
+
35
+ **Always build single-file HTML apps.** Write a complete, self-contained HTML document with all CSS in `<style>` and all JavaScript in `<script>`. Do not use multi-file projects or TSX.
36
+
37
+ <!-- feature:app-builder-multifile:alt:end -->
38
+
30
39
  **Only ask questions when the request is genuinely ambiguous** — e.g., "build me an app" with no indication of what kind. Even then, prefer building something impressive based on context clues over asking a battery of questions.
31
40
 
32
41
  **When in doubt, build something impressive** and let the user refine with `app_update`. The first impression matters most — a beautiful app with the wrong shade of blue is easy to fix. A correct but ugly app is hard to come back from.
@@ -69,7 +78,11 @@ Example schema for a project tracker:
69
78
 
70
79
  ### 3. Build the App
71
80
 
72
- Apps are rendered inside a sandboxed WebView on macOS. You can build them in two formats: **multi-file TSX** (preferred) or **single-file HTML** (legacy).
81
+ Apps are rendered inside a sandboxed WebView on macOS.
82
+
83
+ <!-- feature:app-builder-multifile:start -->
84
+
85
+ You can build them in two formats: **multi-file TSX** (preferred) or **single-file HTML** (legacy).
73
86
 
74
87
  #### Multi-file TSX projects (preferred)
75
88
 
@@ -198,10 +211,11 @@ app_file_write(app_id, "src/styles.css", `.app { padding: var(--v-spacing-lg); }
198
211
  - No external fonts, images, or resources — use system fonts and CSS/SVG for visuals
199
212
  - Design for 400-600px width with graceful resizing
200
213
  - The WebView blocks all navigation — links and form `action` attributes won't work
214
+ <!-- feature:app-builder-multifile:end -->
201
215
 
202
- #### Single HTML file (legacy)
216
+ #### Single HTML file
203
217
 
204
- Use this format only for the simplest one-off pages. Write a complete, self-contained HTML document.
218
+ Write a complete, self-contained HTML document.
205
219
 
206
220
  **Technical constraints (single-file):**
207
221