@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.
@@ -5,13 +5,13 @@ user-invocable: true
5
5
  metadata: { "vellum": { "emoji": "\ud83d\udc65" } }
6
6
  ---
7
7
 
8
- Manage the user's contacts, relationship graph, access control (trusted contacts), and invite links. This skill covers contact CRUD with multi-channel tracking, controlling who can message the assistant through external channels (Telegram, SMS, voice), and creating/managing invite links that grant access. Use Vellum CLI for read operations where available, and use gateway control-plane `curl` calls for mutating actions.
8
+ Manage the user's contacts, relationship graph, access control (trusted contacts), and invite links. This skill covers contact CRUD with multi-channel tracking, controlling who can message the assistant through external channels (Telegram, SMS, voice), and creating/managing invite links that grant access.
9
9
 
10
10
  ## Prerequisites
11
11
 
12
- - Use the injected `INTERNAL_GATEWAY_BASE_URL` for gateway API calls.
13
- - Use gateway control-plane routes only: this skill calls `/v1/contacts`, `/v1/contact-channels`, `/v1/contacts/invites`, `/v1/channels/readiness`, and `/v1/integrations/telegram/config` on the gateway, never the assistant runtime port directly.
14
- - The bearer token is available as the `$GATEWAY_AUTH_TOKEN` environment variable for control-plane `curl` requests.
12
+ - `assistant contacts` CLI commands call the service layer directly and do not require the assistant to be running.
13
+ - `assistant channels` and `assistant integrations` CLI commands (e.g. `assistant channels readiness`, `assistant integrations telegram config`) require the assistant to be running because they call the gateway.
14
+ - All CLI commands support `--json` for machine-readable output.
15
15
 
16
16
  ## Contact Management
17
17
 
@@ -20,49 +20,37 @@ Manage the user's contacts, relationship graph, access control (trusted contacts
20
20
  Create a new contact or update an existing one in the relationship graph. Use this to track people the user interacts with across channels.
21
21
 
22
22
  ```bash
23
- curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts" \
24
- -H "Content-Type: application/json" \
25
- -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
26
- -d '{
27
- "displayName": "<name>",
28
- "notes": "<free-text notes about this contact>",
29
- "channels": [
30
- {
31
- "type": "<channel_type>",
32
- "address": "<address>",
33
- "isPrimary": true
34
- }
35
- ]
36
- }'
23
+ assistant contacts upsert --display-name "<name>" --notes "<notes>" --channels '<json_array>' --json
37
24
  ```
38
25
 
39
- To update an existing contact, include the `id` field in the request body.
26
+ To update an existing contact, include the `--id` flag.
40
27
 
41
- Required fields:
28
+ Required flags:
42
29
 
43
- - `displayName` -- the contact's name
30
+ - `--display-name` -- the contact's name
44
31
 
45
- Optional fields:
32
+ Optional flags:
46
33
 
47
- - `id` -- contact ID to update (omit to create new, or auto-match by channel address)
48
- - `notes` -- free-text notes about this contact (e.g. relationship, communication preferences, response expectations)
49
- - `channels` -- list of communication channels
34
+ - `--id` -- contact ID to update (omit to create new, or auto-match by channel address)
35
+ - `--notes` -- free-text notes about this contact (e.g. relationship, communication preferences, response expectations)
36
+ - `--role` -- contact role: `contact` or `guardian` (default: `contact`)
37
+ - `--contact-type` -- contact type: `human` or `assistant` (default: `human`)
38
+ - `--channels` -- JSON array of channel objects, each with `type`, `address`, and optional `isPrimary`, `externalUserId`, `externalChatId`, `status`, `policy`
50
39
 
51
40
  ### Search contacts
52
41
 
53
- Search for contacts by name, channel address, or other criteria using the gateway API.
42
+ Search for contacts by name, channel address, or other criteria.
54
43
 
55
44
  ```bash
56
- curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/contacts?query=<search_term>" \
57
- -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
45
+ assistant contacts list --query "<search_term>" --json
58
46
  ```
59
47
 
60
- Optional query parameters:
48
+ Optional flags:
61
49
 
62
- - `query` -- search by display name (partial match)
63
- - `channelAddress` -- search by channel address (email, phone, handle)
64
- - `channelType` -- filter by channel type when searching by address
65
- - `limit` -- maximum results to return (default 50, max 100)
50
+ - `--query` -- search by display name (partial match)
51
+ - `--channel-address` -- search by channel address (email, phone, handle)
52
+ - `--channel-type` -- filter by channel type when searching by address
53
+ - `--limit` -- maximum results to return (default 50, max 100)
66
54
 
67
55
  ### Merge contacts
68
56
 
@@ -74,13 +62,7 @@ When you discover two contacts are the same person (e.g. same person on email an
74
62
  - Deletes the donor contact
75
63
 
76
64
  ```bash
77
- curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/merge" \
78
- -H "Content-Type: application/json" \
79
- -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
80
- -d '{
81
- "keepId": "<surviving_contact_id>",
82
- "mergeId": "<donor_contact_id>"
83
- }'
65
+ assistant contacts merge <surviving_contact_id> <donor_contact_id> --json
84
66
  ```
85
67
 
86
68
  ## Access Control (Trusted Contacts)
@@ -139,25 +121,13 @@ Use this when the user wants to grant someone access to message the assistant. *
139
121
  Ask the user: _"I'll add [name/identifier] on [channel] as an allowed contact. Should I proceed?"_
140
122
 
141
123
  ```bash
142
- curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts" \
143
- -H "Content-Type: application/json" \
144
- -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
145
- -d '{
146
- "displayName": "<display_name>",
147
- "channels": [{
148
- "type": "<channel>",
149
- "address": "<user_id>",
150
- "externalUserId": "<user_id>",
151
- "status": "active",
152
- "policy": "allow"
153
- }]
154
- }'
124
+ assistant contacts upsert --display-name "<display_name>" --channels '[{"type":"<channel>","address":"<user_id>","externalUserId":"<user_id>","status":"active","policy":"allow"}]' --json
155
125
  ```
156
126
 
157
- Required fields:
127
+ Required flags:
158
128
 
159
- - `displayName` -- human-readable name for the contact
160
- - `channels` -- at least one channel entry with:
129
+ - `--display-name` -- human-readable name for the contact
130
+ - `--channels` -- at least one channel entry with:
161
131
  - `type` -- the channel type (e.g., `telegram`, `sms`)
162
132
  - `address` -- the channel-specific identifier
163
133
  - `externalUserId` -- the user's ID on that channel (or `externalChatId` for chat-based channels)
@@ -172,18 +142,15 @@ Use this when the user wants to remove someone's access. **Always confirm with t
172
142
 
173
143
  Ask the user: _"I'll revoke access for [name/identifier]. They will no longer be able to message the assistant. Should I proceed?"_
174
144
 
175
- First, list contacts to find the channel's `id` (each entry in a contact's `channels` array has an `id` field -- visible in `GET /v1/contacts` or `assistant contacts list --json` output), then revoke:
145
+ First, list contacts to find the channel's `id` (each entry in a contact's `channels` array has an `id` field -- visible in `assistant contacts list --json` output), then revoke:
176
146
 
177
147
  **Important**: Before revoking, check the channel's current `status`. If the channel is **blocked**, do not attempt to revoke it -- blocking is stronger than revoking. Inform the user that the contact is already blocked and revoking is not applicable. Only channels with `active` or `pending` status can be revoked.
178
148
 
179
149
  ```bash
180
- curl -s -X PATCH "$INTERNAL_GATEWAY_BASE_URL/v1/contact-channels/<channel_id>" \
181
- -H "Content-Type: application/json" \
182
- -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
183
- -d '{"status": "revoked", "reason": "<optional reason>"}'
150
+ assistant contacts channels update-status <channel_id> --status revoked --reason "<optional reason>" --json
184
151
  ```
185
152
 
186
- Replace `<channel_id>` with the channel's `id` from the contact's `channels` array. The API will return a `409 Conflict` error if the channel is currently blocked.
153
+ Replace `<channel_id>` with the channel's `id` from the contact's `channels` array.
187
154
 
188
155
  ### Block a user
189
156
 
@@ -192,13 +159,10 @@ Use this when the user wants to explicitly block someone. Blocking is stronger t
192
159
  Ask the user: _"I'll block [name/identifier]. They will be permanently denied from messaging the assistant. Should I proceed?"_
193
160
 
194
161
  ```bash
195
- curl -s -X PATCH "$INTERNAL_GATEWAY_BASE_URL/v1/contact-channels/<channel_id>" \
196
- -H "Content-Type: application/json" \
197
- -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
198
- -d '{"status": "blocked", "reason": "<optional reason>"}'
162
+ assistant contacts channels update-status <channel_id> --status blocked --reason "<optional reason>" --json
199
163
  ```
200
164
 
201
- Replace `<channel_id>` with the channel's `id` from the contact's `channels` array (visible in `GET /v1/contacts` or `assistant contacts list --json` output).
165
+ Replace `<channel_id>` with the channel's `id` from the contact's `channels` array (visible in `assistant contacts list --json` output).
202
166
 
203
167
  ## Channel Readiness
204
168
 
@@ -232,14 +196,7 @@ Use this when the guardian wants to invite someone to message the assistant on T
232
196
  **Important**: The shell snippet below emits a `<vellum-sensitive-output>` directive containing the raw invite token. The tool executor automatically strips this directive and replaces the raw token with a placeholder so the LLM never sees it. The placeholder is resolved back to the real token in the final assistant reply.
233
197
 
234
198
  ```bash
235
- INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/invites" \
236
- -H "Content-Type: application/json" \
237
- -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
238
- -d '{
239
- "sourceChannel": "telegram",
240
- "maxUses": 1,
241
- "note": "<optional note, e.g. the person it is for>"
242
- }')
199
+ INVITE_JSON=$(assistant contacts invites create --source-channel telegram --max-uses 1 --note "<optional note, e.g. the person it is for>" --json)
243
200
 
244
201
  INVITE_TOKEN=$(printf '%s' "$INVITE_JSON" | python3 -c "
245
202
  import json, sys
@@ -263,7 +220,11 @@ fi
263
220
  # Prefer backend-provided canonical link when available.
264
221
  if [ -z "$INVITE_URL" ]; then
265
222
  BOT_CONFIG_JSON=$(assistant integrations telegram config --json)
266
- BOT_USERNAME=$(printf '%s' "$BOT_CONFIG_JSON" | tr -d '\n' | sed -n 's/.*"botUsername"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
223
+ BOT_USERNAME=$(printf '%s' "$BOT_CONFIG_JSON" | python3 -c "
224
+ import json, sys
225
+ data = json.load(sys.stdin)
226
+ print(data.get('botUsername', ''), end='')
227
+ ")
267
228
  if [ -z "$BOT_USERNAME" ]; then
268
229
  echo "error:no_share_url_or_bot_username"
269
230
  exit 1
@@ -275,11 +236,11 @@ echo "<vellum-sensitive-output kind=\"invite_code\" value=\"$INVITE_TOKEN\" />"
275
236
  echo "$INVITE_URL"
276
237
  ```
277
238
 
278
- Optional fields:
239
+ Optional flags:
279
240
 
280
- - `maxUses` -- how many times the link can be used (default: 1). Use a higher number for group invites.
281
- - `expiresInMs` -- expiration time in milliseconds from now (e.g., `86400000` for 24 hours). Defaults to 7 days (`604800000`) if omitted.
282
- - `note` -- a human-readable label for the invite (e.g., "For Mom", "Family group").
241
+ - `--max-uses` -- how many times the link can be used (default: 1). Use a higher number for group invites.
242
+ - `--expires-in-ms` -- expiration time in milliseconds from now (e.g., `86400000` for 24 hours). Defaults to 7 days (`604800000`) if omitted.
243
+ - `--note` -- a human-readable label for the invite (e.g., "For Mom", "Family group").
283
244
 
284
245
  The create response contains `{ ok: true, invite: { id, token, share?, ... } }`.
285
246
 
@@ -307,33 +268,21 @@ Use this when the guardian wants to authorize a specific phone number to call th
307
268
  **Important**: The response includes a `voiceCode` field that is only returned at creation time and cannot be retrieved later. Extract and present it clearly.
308
269
 
309
270
  ```bash
310
- INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/invites" \
311
- -H "Content-Type: application/json" \
312
- -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
313
- -d '{
314
- "sourceChannel": "voice",
315
- "expectedExternalUserId": "<phone_number_E164>",
316
- "friendName": "<invitee display name>",
317
- "guardianName": "<guardian display name>",
318
- "maxUses": 1,
319
- "note": "<optional note, e.g. the person it is for>"
320
- }')
321
- printf '%s\n' "$INVITE_JSON"
271
+ assistant contacts invites create --source-channel voice --expected-external-user-id "<phone_E164>" --friend-name "<invitee_name>" --guardian-name "<guardian_name>" --max-uses 1 --note "<optional note, e.g. the person it is for>" --json
322
272
  ```
323
273
 
324
- Required fields:
274
+ Required flags:
325
275
 
326
- - `sourceChannel` -- must be `"voice"`
327
- - `expectedExternalUserId` -- the invitee's phone number in E.164 format (e.g., `+15551234567`)
328
- - `friendName` -- the invitee's display name (e.g., "Mom", "Dr. Smith"). Used during the voice verification call to personalize the experience.
329
- - `guardianName` -- the guardian's display name (e.g., "Alex"). Used during the voice verification call so the invitee knows who invited them.
276
+ - `--source-channel` -- must be `voice`
277
+ - `--expected-external-user-id` -- the invitee's phone number in E.164 format (e.g., `+15551234567`)
278
+ - `--friend-name` -- the invitee's display name (e.g., "Mom", "Dr. Smith"). Used during the voice verification call to personalize the experience.
279
+ - `--guardian-name` -- the guardian's display name (e.g., "Alex"). Used during the voice verification call so the invitee knows who invited them.
330
280
 
331
- Optional fields:
281
+ Optional flags:
332
282
 
333
- - `maxUses` -- how many times the code can be used (default: 1)
334
- - `expiresInMs` -- expiration time in milliseconds from now (e.g., `86400000` for 24 hours). Defaults to 7 days if omitted.
335
- - ~~`voiceCodeDigits`~~ -- always 6 digits; this parameter is accepted but ignored
336
- - `note` -- a human-readable label for the invite (e.g., "For Mom", "Dr. Smith")
283
+ - `--max-uses` -- how many times the code can be used (default: 1)
284
+ - `--expires-in-ms` -- expiration time in milliseconds from now (e.g., `86400000` for 24 hours). Defaults to 7 days if omitted.
285
+ - `--note` -- a human-readable label for the invite (e.g., "For Mom", "Dr. Smith")
337
286
 
338
287
  The create response contains `{ ok: true, invite: { id, voiceCode, expectedExternalUserId, friendName, guardianName, ... } }`.
339
288
 
@@ -366,16 +315,7 @@ Use this when the guardian wants to invite someone to message the assistant via
366
315
  **Before creating the invite**, check channel readiness (see [Channel Readiness](#channel-readiness)). If the `email` channel is not ready, tell the guardian what prerequisites are missing instead of creating an unusable invite.
367
316
 
368
317
  ```bash
369
- INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/invites" \
370
- -H "Content-Type: application/json" \
371
- -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
372
- -d '{
373
- "sourceChannel": "email",
374
- "contactName": "<invitee display name>",
375
- "maxUses": 1,
376
- "note": "<optional note, e.g. the person it is for>"
377
- }')
378
- printf '%s\n' "$INVITE_JSON"
318
+ assistant contacts invites create --source-channel email --contact-name "<invitee_name>" --max-uses 1 --note "<optional note, e.g. the person it is for>" --json
379
319
  ```
380
320
 
381
321
  The response contains `{ ok: true, invite: { id, token, inviteCode, guardianInstruction, channelHandle, ... } }`.
@@ -403,16 +343,7 @@ Use this when the guardian wants to invite someone to message the assistant on W
403
343
  **Before creating the invite**, check channel readiness (see [Channel Readiness](#channel-readiness)). If the `whatsapp` channel is not ready, tell the guardian what prerequisites are missing instead of creating an unusable invite.
404
344
 
405
345
  ```bash
406
- INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/invites" \
407
- -H "Content-Type: application/json" \
408
- -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
409
- -d '{
410
- "sourceChannel": "whatsapp",
411
- "contactName": "<invitee display name>",
412
- "maxUses": 1,
413
- "note": "<optional note, e.g. the person it is for>"
414
- }')
415
- printf '%s\n' "$INVITE_JSON"
346
+ assistant contacts invites create --source-channel whatsapp --contact-name "<invitee_name>" --max-uses 1 --note "<optional note, e.g. the person it is for>" --json
416
347
  ```
417
348
 
418
349
  The response contains `{ ok: true, invite: { id, token, inviteCode, guardianInstruction, channelHandle?, ... } }`.
@@ -442,16 +373,7 @@ Use this when the guardian wants to invite someone to message the assistant via
442
373
  **Before creating the invite**, check channel readiness (see [Channel Readiness](#channel-readiness)). If the `sms` channel is not ready, tell the guardian what prerequisites are missing instead of creating an unusable invite.
443
374
 
444
375
  ```bash
445
- INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/invites" \
446
- -H "Content-Type: application/json" \
447
- -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
448
- -d '{
449
- "sourceChannel": "sms",
450
- "contactName": "<invitee display name>",
451
- "maxUses": 1,
452
- "note": "<optional note, e.g. the person it is for>"
453
- }')
454
- printf '%s\n' "$INVITE_JSON"
376
+ assistant contacts invites create --source-channel sms --contact-name "<invitee_name>" --max-uses 1 --note "<optional note, e.g. the person it is for>" --json
455
377
  ```
456
378
 
457
379
  The response follows the same shape as email and WhatsApp invites (`inviteCode`, `guardianInstruction`, `channelHandle`).
@@ -465,16 +387,7 @@ Use this when the guardian wants to invite someone to message the assistant on S
465
387
  **Before creating the invite**, check channel readiness (see [Channel Readiness](#channel-readiness)). If the `slack` channel is not ready, tell the guardian what prerequisites are missing instead of creating an unusable invite.
466
388
 
467
389
  ```bash
468
- INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/invites" \
469
- -H "Content-Type: application/json" \
470
- -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
471
- -d '{
472
- "sourceChannel": "slack",
473
- "contactName": "<invitee display name>",
474
- "maxUses": 1,
475
- "note": "<optional note, e.g. the person it is for>"
476
- }')
477
- printf '%s\n' "$INVITE_JSON"
390
+ assistant contacts invites create --source-channel slack --contact-name "<invitee_name>" --max-uses 1 --note "<optional note, e.g. the person it is for>" --json
478
391
  ```
479
392
 
480
393
  The response follows the same shape as email, WhatsApp, and SMS invites (`inviteCode`, `guardianInstruction`, `channelHandle`).
@@ -548,11 +461,10 @@ Ask the user: _"I'll revoke the invite [note or ID]. It will no longer be usable
548
461
  First, list invites to find the invite's `id`, then revoke:
549
462
 
550
463
  ```bash
551
- curl -s -X DELETE "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/invites/<invite_id>" \
552
- -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
464
+ assistant contacts invites revoke <invite_id> --json
553
465
  ```
554
466
 
555
- Replace `<invite_id>` with the invite's `id` from the list response. The same revoke endpoint is used for both Telegram and voice invites.
467
+ Replace `<invite_id>` with the invite's `id` from the list response. The same revoke command is used for both Telegram and voice invites.
556
468
 
557
469
  ## Contact Fields
558
470
 
@@ -575,60 +487,60 @@ Each channel has:
575
487
  **All mutating actions (allow, revoke, block, revoke invite) require explicit user confirmation before execution.** This is a safety measure -- modifying who can access the assistant should always be a deliberate choice. Creating an invite (Telegram link, voice invite, email invite, WhatsApp invite, SMS invite, or Slack invite) does not require confirmation since it does not grant access until the invitee redeems it.
576
488
 
577
489
  - Clearly state what action you are about to take and who it affects.
578
- - Wait for the user to confirm before running the curl command.
490
+ - Wait for the user to confirm before running the command.
579
491
  - Report the result after execution.
580
492
 
581
493
  ## Error Handling
582
494
 
583
- - If a request returns `{ ok: false, error: "..." }`, report the error message to the user.
495
+ - If a command returns `{ ok: false, error: "..." }`, report the error message to the user. CLI commands also exit with non-zero status on errors.
584
496
  - Common errors:
585
497
  - `Channel not found` -- the channel ID may be invalid; list contacts to find the correct channel ID.
586
498
  - `Channel already revoked` -- the channel has already been revoked.
587
499
  - `Channel already blocked` -- the channel has already been blocked.
588
500
  - `Cannot revoke a blocked channel` -- the channel is blocked; blocking is stronger than revoking. Tell the user the contact is already blocked.
589
- - `sourceChannel is required for create` -- when creating an invite, always pass `"sourceChannel": "telegram"` for Telegram or `"sourceChannel": "voice"` for voice invites.
590
- - `expectedExternalUserId is required for voice invites` -- voice invites must include the invitee's phone number.
501
+ - `sourceChannel is required for create` -- when creating an invite, always pass `--source-channel`.
502
+ - `expectedExternalUserId is required for voice invites` -- voice invites must include the invitee's phone number via `--expected-external-user-id`.
591
503
  - `expectedExternalUserId must be in E.164 format` -- the phone number must start with `+` followed by country code and number (e.g., `+15551234567`).
592
- - `friendName is required for voice invites` -- voice invites must include the invitee's display name.
593
- - `guardianName is required for voice invites` -- voice invites must include the guardian's display name.
504
+ - `friendName is required for voice invites` -- voice invites must include the invitee's display name via `--friend-name`.
505
+ - `guardianName is required for voice invites` -- voice invites must include the guardian's display name via `--guardian-name`.
594
506
  - `Invite not found or already revoked` -- the invite ID may be invalid or the invite is already revoked.
595
507
 
596
508
  ## Tips
597
509
 
598
- - Use contact search with `channelAddress` to find contacts by their email, phone, or handle.
510
+ - Use contact search with `--channel-address` to find contacts by their email, phone, or handle.
599
511
  - When creating follow-ups, provide a `contact_id` to link the follow-up to a specific contact.
600
512
  - When merging contacts, the surviving contact gains all channels and merged notes from the donor.
601
513
 
602
514
  ## Typical Workflows
603
515
 
604
- **"Who can message me?"** -- List all contacts, present active channels as a formatted list.
516
+ **"Who can message me?"** -- List all contacts with `assistant contacts list --json`, present active channels as a formatted list.
605
517
 
606
- **"Add my friend to Telegram"** -- Ask for their Telegram user ID (numeric) and display name, confirm, then create a contact with a channel entry with `policy: "allow"` and `status: "active"`.
518
+ **"Add my friend to Telegram"** -- Ask for their Telegram user ID (numeric) and display name, confirm, then create a contact with `assistant contacts upsert` including a channel entry with `policy: "allow"` and `status: "active"`.
607
519
 
608
- **"Remove [name]'s access"** -- List contacts to find them, identify the channel to revoke, confirm the revocation, then patch the channel status to `"revoked"`.
520
+ **"Remove [name]'s access"** -- List contacts with `assistant contacts list --json` to find them, identify the channel to revoke, confirm the revocation, then run `assistant contacts channels update-status <channel_id> --status revoked --json`.
609
521
 
610
- **"Block [name]"** -- List contacts to find them, identify the channel to block, confirm the block, then patch the channel status to `"blocked"`.
522
+ **"Block [name]"** -- List contacts with `assistant contacts list --json` to find them, identify the channel to block, confirm the block, then run `assistant contacts channels update-status <channel_id> --status blocked --json`.
611
523
 
612
- **"Show me blocked contacts"** -- List contacts and filter for channels with `status: "blocked"`.
524
+ **"Show me blocked contacts"** -- List contacts with `assistant contacts list --json` and filter for channels with `status: "blocked"`.
613
525
 
614
- **"Create a Telegram invite link"** / **"Invite someone on Telegram"** -- Create an invite with `sourceChannel: "telegram"`, look up the bot username, build the deep link, and present it with sharing instructions.
526
+ **"Create a Telegram invite link"** / **"Invite someone on Telegram"** -- Create an invite with `assistant contacts invites create --source-channel telegram`, look up the bot username, build the deep link, and present it with sharing instructions.
615
527
 
616
- **"Invite someone by email"** / **"Send an email invite"** -- Create an invite with `sourceChannel: "email"`. Present the 6-digit invite code and the assistant's email address. Tell the guardian to share both with the invitee.
528
+ **"Invite someone by email"** / **"Send an email invite"** -- Create an invite with `assistant contacts invites create --source-channel email`. Present the 6-digit invite code and the assistant's email address. Tell the guardian to share both with the invitee.
617
529
 
618
- **"Invite someone on WhatsApp"** -- Create an invite with `sourceChannel: "whatsapp"`. Present the 6-digit invite code. If `channelHandle` is returned, also present the assistant's WhatsApp number; otherwise, tell the guardian to share the code and instruct the invitee to send it to the assistant on WhatsApp.
530
+ **"Invite someone on WhatsApp"** -- Create an invite with `assistant contacts invites create --source-channel whatsapp`. Present the 6-digit invite code. If `channelHandle` is returned, also present the assistant's WhatsApp number; otherwise, tell the guardian to share the code and instruct the invitee to send it to the assistant on WhatsApp.
619
531
 
620
- **"Invite someone via SMS"** / **"Send a text invite"** -- Create an invite with `sourceChannel: "sms"`. Present the 6-digit invite code and the assistant's phone number. Tell the guardian to share both with the invitee.
532
+ **"Invite someone via SMS"** / **"Send a text invite"** -- Create an invite with `assistant contacts invites create --source-channel sms`. Present the 6-digit invite code and the assistant's phone number. Tell the guardian to share both with the invitee.
621
533
 
622
- **"Invite someone on Slack"** -- Create an invite with `sourceChannel: "slack"`. Present the 6-digit invite code and tell the guardian to have the invitee DM the code to the assistant's Slack bot.
534
+ **"Invite someone on Slack"** -- Create an invite with `assistant contacts invites create --source-channel slack`. Present the 6-digit invite code and tell the guardian to have the invitee DM the code to the assistant's Slack bot.
623
535
 
624
- **"Show my invites"** / **"List active invite links"** -- List invites filtered by `sourceChannel=telegram`, present active invites with uses remaining and expiration info. Use the appropriate `--source-channel` value for other channels.
536
+ **"Show my invites"** / **"List active invite links"** -- List invites with `assistant contacts invites list --source-channel telegram --json`, present active invites with uses remaining and expiration info. Use the appropriate `--source-channel` value for other channels.
625
537
 
626
- **"Revoke invite"** / **"Cancel invite link"** -- List invites to identify the target, confirm, then revoke by ID.
538
+ **"Revoke invite"** / **"Cancel invite link"** -- List invites to identify the target, confirm, then revoke with `assistant contacts invites revoke <invite_id> --json`.
627
539
 
628
- **"Create a voice invite for +15551234567"** -- Create a voice invite with `sourceChannel: "voice"` and the given phone number as `expectedExternalUserId`. Present the invite code and instructions: the person must call from that number and enter the code.
540
+ **"Create a voice invite for +15551234567"** -- Create a voice invite with `assistant contacts invites create --source-channel voice --expected-external-user-id "+15551234567" --friend-name "<name>" --guardian-name "<name>"`. Present the invite code and instructions: the person must call from that number and enter the code.
629
541
 
630
- **"Let my mom call in"** / **"Invite someone by phone"** -- Ask for the phone number in E.164 format, create a voice invite, and present the code + calling instructions.
542
+ **"Let my mom call in"** / **"Invite someone by phone"** -- Ask for the phone number in E.164 format, create a voice invite with `assistant contacts invites create --source-channel voice`, and present the code + calling instructions.
631
543
 
632
- **"Show my voice invites"** / **"List phone invites"** -- List invites filtered by `sourceChannel=voice`, present active invites with bound phone number and expiration info.
544
+ **"Show my voice invites"** / **"List phone invites"** -- List invites with `assistant contacts invites list --source-channel voice --json`, present active invites with bound phone number and expiration info.
633
545
 
634
- **"Revoke voice invite"** / **"Cancel the phone invite for +15551234567"** -- List voice invites, identify the target by phone number or note, confirm, then revoke by ID.
546
+ **"Revoke voice invite"** / **"Cancel the phone invite for +15551234567"** -- List voice invites, identify the target by phone number or note, confirm, then revoke with `assistant contacts invites revoke <invite_id> --json`.
@@ -25,6 +25,8 @@ import { parseToolManifestFile } from "../skills/tool-manifest.js";
25
25
  import { computeSkillVersionHash } from "../skills/version-hash.js";
26
26
  import { getLogger } from "../util/logger.js";
27
27
  import { getWorkspaceSkillsDir } from "../util/platform.js";
28
+ import { isAssistantFeatureFlagEnabled } from "./assistant-feature-flags.js";
29
+ import { getConfig } from "./loader.js";
28
30
  import { stripCommentLines } from "./system-prompt.js";
29
31
 
30
32
  const log = getLogger("skills");
@@ -968,6 +970,39 @@ export function loadSkillCatalog(
968
970
  return catalog;
969
971
  }
970
972
 
973
+ /**
974
+ * Process feature-gated sections in skill body markdown.
975
+ *
976
+ * Markers:
977
+ * <!-- feature:<flag-id>:start --> ... <!-- feature:<flag-id>:end -->
978
+ * Content included only when the flag is enabled.
979
+ * <!-- feature:<flag-id>:alt --> ... <!-- feature:<flag-id>:alt:end -->
980
+ * Fallback content included only when the flag is disabled.
981
+ */
982
+ function applyFeatureGatedSections(body: string): string {
983
+ const config = getConfig();
984
+ // Match feature:*:start/end blocks
985
+ const mainRe =
986
+ /<!-- feature:([^:]+):start -->\n?([\s\S]*?)<!-- feature:\1:end -->\n?/g;
987
+ // Match feature:*:alt/alt:end blocks
988
+ const altRe =
989
+ /<!-- feature:([^:]+):alt -->\n?([\s\S]*?)<!-- feature:\1:alt:end -->\n?/g;
990
+
991
+ let result = body;
992
+
993
+ result = result.replace(mainRe, (_match, flagId: string, content: string) => {
994
+ const key = `feature_flags.${flagId}.enabled`;
995
+ return isAssistantFeatureFlagEnabled(key, config) ? content : "";
996
+ });
997
+
998
+ result = result.replace(altRe, (_match, flagId: string, content: string) => {
999
+ const key = `feature_flags.${flagId}.enabled`;
1000
+ return isAssistantFeatureFlagEnabled(key, config) ? "" : content;
1001
+ });
1002
+
1003
+ return result;
1004
+ }
1005
+
971
1006
  function loadSkillDefinition(skill: SkillSummary): SkillLookupResult {
972
1007
  let loaded: SkillDefinition | null;
973
1008
  if (skill.bundled) {
@@ -992,6 +1027,8 @@ function loadSkillDefinition(skill: SkillSummary): SkillLookupResult {
992
1027
  }
993
1028
  // Replace {baseDir} placeholders with the actual skill directory path
994
1029
  loaded.body = loaded.body.replaceAll("{baseDir}", loaded.directoryPath);
1030
+ // Strip feature-gated sections based on assistant feature flags
1031
+ loaded.body = applyFeatureGatedSections(loaded.body);
995
1032
  return { skill: loaded };
996
1033
  }
997
1034
 
@@ -31,6 +31,7 @@ import {
31
31
  getApp,
32
32
  getAppPreview,
33
33
  getAppsDir,
34
+ isMultifileApp,
34
35
  listApps,
35
36
  queryAppRecords,
36
37
  updateApp,
@@ -113,11 +114,11 @@ export function handleAppDataRequest(
113
114
  }
114
115
  }
115
116
 
116
- export function handleAppOpenRequest(
117
+ export async function handleAppOpenRequest(
117
118
  msg: { appId: string },
118
119
  socket: net.Socket,
119
120
  ctx: HandlerContext,
120
- ): void {
121
+ ): Promise<void> {
121
122
  try {
122
123
  const appId = msg.appId;
123
124
  if (!appId) {
@@ -131,13 +132,37 @@ export function handleAppOpenRequest(
131
132
  const app = getApp(appId);
132
133
  if (app) {
133
134
  const surfaceId = `app-open-${uuid()}`;
135
+ let html = app.htmlDefinition;
136
+
137
+ // Multifile apps store source in src/ and compiled output in dist/.
138
+ // Read dist/index.html instead of the (empty) htmlDefinition.
139
+ if (isMultifileApp(app)) {
140
+ const appDir = join(getAppsDir(), appId);
141
+ const distIndex = join(appDir, "dist", "index.html");
142
+ if (!existsSync(distIndex)) {
143
+ // Auto-compile if dist/ is missing (first open or after clean)
144
+ const result = await compileApp(appDir);
145
+ if (!result.ok) {
146
+ log.warn(
147
+ { appId, errors: result.errors },
148
+ "Auto-compile failed on app open",
149
+ );
150
+ }
151
+ }
152
+ if (existsSync(distIndex)) {
153
+ html = readFileSync(distIndex, "utf-8");
154
+ } else {
155
+ html = `<p>App compilation failed. Edit a source file to trigger a rebuild.</p>`;
156
+ }
157
+ }
158
+
134
159
  ctx.send(socket, {
135
160
  type: "ui_surface_show",
136
161
  sessionId: "app-panel",
137
162
  surfaceId,
138
163
  surfaceType: "dynamic_page",
139
164
  title: app.name,
140
- data: { html: app.htmlDefinition, appId: app.id },
165
+ data: { html, appId: app.id },
141
166
  display: "panel",
142
167
  } as UiSurfaceShow);
143
168
  return;
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  process.title = "vellum-daemon";
3
+
3
4
  import * as Sentry from "@sentry/node";
4
5
 
5
6
  import { getLogger } from "../util/logger.js";