@vellumai/assistant 0.3.3 → 0.3.5
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/Dockerfile +2 -0
- package/README.md +45 -18
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +13 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
- package/src/__tests__/approval-message-composer.test.ts +253 -0
- package/src/__tests__/call-domain.test.ts +12 -2
- package/src/__tests__/call-orchestrator.test.ts +391 -1
- package/src/__tests__/call-routes-http.test.ts +27 -2
- package/src/__tests__/channel-approval-routes.test.ts +397 -135
- package/src/__tests__/channel-approvals.test.ts +99 -3
- package/src/__tests__/channel-delivery-store.test.ts +30 -4
- package/src/__tests__/channel-guardian.test.ts +261 -22
- package/src/__tests__/channel-readiness-service.test.ts +257 -0
- package/src/__tests__/config-schema.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +636 -0
- package/src/__tests__/dictation-mode-detection.test.ts +63 -0
- package/src/__tests__/entity-search.test.ts +615 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
- package/src/__tests__/handlers-twilio-config.test.ts +480 -0
- package/src/__tests__/ipc-snapshot.test.ts +63 -0
- package/src/__tests__/messaging-send-tool.test.ts +65 -0
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
- package/src/__tests__/run-orchestrator.test.ts +22 -0
- package/src/__tests__/secret-scanner.test.ts +223 -0
- package/src/__tests__/session-runtime-assembly.test.ts +85 -1
- package/src/__tests__/shell-parser-property.test.ts +357 -2
- package/src/__tests__/sms-messaging-provider.test.ts +125 -0
- package/src/__tests__/system-prompt.test.ts +25 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
- package/src/__tests__/twilio-routes.test.ts +39 -3
- package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +68 -0
- package/src/__tests__/web-search.test.ts +1 -1
- package/src/__tests__/work-item-output.test.ts +110 -0
- package/src/calls/call-domain.ts +8 -5
- package/src/calls/call-orchestrator.ts +85 -22
- package/src/calls/twilio-config.ts +17 -11
- package/src/calls/twilio-rest.ts +276 -0
- package/src/calls/twilio-routes.ts +39 -1
- package/src/cli/map.ts +6 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
- package/src/commands/cc-command-registry.ts +14 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
- package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
- package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
- package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
- package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
- package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
- package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
- package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
- package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
- package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
- package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
- package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
- package/src/config/bundled-skills/messaging/SKILL.md +24 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/twitter/SKILL.md +19 -3
- package/src/config/defaults.ts +2 -1
- package/src/config/schema.ts +9 -3
- package/src/config/skills.ts +5 -32
- package/src/config/system-prompt.ts +40 -0
- package/src/config/templates/IDENTITY.md +2 -2
- package/src/config/user-reference.ts +29 -0
- package/src/config/vellum-skills/catalog.json +58 -0
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +76 -6
- package/src/daemon/auth-manager.ts +103 -0
- package/src/daemon/computer-use-session.ts +8 -1
- package/src/daemon/config-watcher.ts +253 -0
- package/src/daemon/handlers/config.ts +819 -22
- package/src/daemon/handlers/dictation.ts +182 -0
- package/src/daemon/handlers/identity.ts +14 -23
- package/src/daemon/handlers/index.ts +2 -0
- package/src/daemon/handlers/sessions.ts +2 -0
- package/src/daemon/handlers/shared.ts +3 -0
- package/src/daemon/handlers/skills.ts +6 -7
- package/src/daemon/handlers/work-items.ts +15 -7
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +114 -4
- package/src/daemon/ipc-handler.ts +87 -0
- package/src/daemon/lifecycle.ts +18 -4
- package/src/daemon/ride-shotgun-handler.ts +11 -1
- package/src/daemon/server.ts +111 -504
- package/src/daemon/session-agent-loop.ts +10 -15
- package/src/daemon/session-runtime-assembly.ts +115 -44
- package/src/daemon/session-tool-setup.ts +2 -0
- package/src/daemon/session.ts +19 -2
- package/src/inbound/public-ingress-urls.ts +3 -3
- package/src/memory/channel-guardian-store.ts +2 -1
- package/src/memory/db-connection.ts +28 -0
- package/src/memory/db-init.ts +1163 -0
- package/src/memory/db.ts +2 -2007
- package/src/memory/embedding-backend.ts +79 -11
- package/src/memory/indexer.ts +2 -0
- package/src/memory/job-handlers/media-processing.ts +100 -0
- package/src/memory/job-utils.ts +64 -4
- package/src/memory/jobs-store.ts +2 -1
- package/src/memory/jobs-worker.ts +11 -1
- package/src/memory/media-store.ts +759 -0
- package/src/memory/recall-cache.ts +107 -0
- package/src/memory/retriever.ts +36 -2
- package/src/memory/schema-migration.ts +984 -0
- package/src/memory/schema.ts +99 -0
- package/src/memory/search/entity.ts +208 -25
- package/src/memory/search/ranking.ts +6 -1
- package/src/memory/search/types.ts +26 -0
- package/src/messaging/provider-types.ts +2 -0
- package/src/messaging/providers/sms/adapter.ts +204 -0
- package/src/messaging/providers/sms/client.ts +93 -0
- package/src/messaging/providers/sms/types.ts +7 -0
- package/src/permissions/checker.ts +16 -2
- package/src/permissions/prompter.ts +14 -3
- package/src/permissions/trust-store.ts +7 -0
- package/src/runtime/approval-message-composer.ts +143 -0
- package/src/runtime/channel-approvals.ts +29 -7
- package/src/runtime/channel-guardian-service.ts +44 -18
- package/src/runtime/channel-readiness-service.ts +292 -0
- package/src/runtime/channel-readiness-types.ts +29 -0
- package/src/runtime/gateway-client.ts +2 -1
- package/src/runtime/http-server.ts +65 -28
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-routes.ts +237 -103
- package/src/runtime/routes/run-routes.ts +7 -1
- package/src/runtime/run-orchestrator.ts +43 -3
- package/src/security/secret-scanner.ts +218 -0
- package/src/skills/frontmatter.ts +63 -0
- package/src/skills/slash-commands.ts +23 -0
- package/src/skills/vellum-catalog-remote.ts +107 -0
- package/src/tools/assets/materialize.ts +2 -2
- package/src/tools/browser/auto-navigate.ts +132 -24
- package/src/tools/browser/browser-manager.ts +67 -61
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/claude-code/claude-code.ts +55 -3
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/execution-target.ts +11 -1
- package/src/tools/executor.ts +10 -2
- package/src/tools/network/web-search.ts +1 -1
- package/src/tools/skills/vellum-catalog.ts +61 -156
- package/src/tools/terminal/parser.ts +21 -5
- package/src/tools/types.ts +2 -0
- package/src/twitter/router.ts +1 -1
- package/src/util/platform.ts +43 -1
- package/src/util/retry.ts +4 -4
|
@@ -12,7 +12,7 @@ You are helping your user create a Slack App and OAuth credentials so the Messag
|
|
|
12
12
|
|
|
13
13
|
## Prerequisites
|
|
14
14
|
|
|
15
|
-
Before starting, check that `ingress.publicBaseUrl` is configured (
|
|
15
|
+
Before starting, check that `ingress.publicBaseUrl` is configured (`INGRESS_PUBLIC_BASE_URL` env var or workspace config). If it is not set, load and execute the **public-ingress** skill first (`skill_load` with `skill: "public-ingress"`) to set up an ngrok tunnel and persist the public URL. The OAuth redirect URI depends on this value.
|
|
16
16
|
|
|
17
17
|
## Before You Start
|
|
18
18
|
|
|
@@ -89,7 +89,7 @@ Tell the user: "Permissions configured! Now let's set up the redirect URL and ge
|
|
|
89
89
|
|
|
90
90
|
Navigate to the "OAuth & Permissions" page if not already there.
|
|
91
91
|
|
|
92
|
-
The redirect URL must point to the gateway's OAuth callback endpoint. Determine the URL by reading the `ingress.publicBaseUrl` value from the assistant's workspace config
|
|
92
|
+
The redirect URL must point to the gateway's OAuth callback endpoint. Determine the URL by reading the `ingress.publicBaseUrl` value from the assistant's workspace config or the `INGRESS_PUBLIC_BASE_URL` environment variable. The callback path is `/webhooks/oauth/callback`.
|
|
93
93
|
|
|
94
94
|
In the "Redirect URLs" section:
|
|
95
95
|
1. Click "Add New Redirect URL"
|
|
@@ -98,7 +98,7 @@ In the "Redirect URLs" section:
|
|
|
98
98
|
|
|
99
99
|
Take a `browser_snapshot` to confirm.
|
|
100
100
|
|
|
101
|
-
Tell the user: "Redirect URL configured. Make sure your tunnel is running and `ingress.publicBaseUrl` is set
|
|
101
|
+
Tell the user: "Redirect URL configured. Make sure your tunnel is running and `ingress.publicBaseUrl` is set so the callback can reach the gateway. You can always check or update this from the Settings page."
|
|
102
102
|
|
|
103
103
|
## Step 5: Extract Client ID and Client Secret
|
|
104
104
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "SMS Setup"
|
|
3
|
+
description: "Set up and troubleshoot SMS messaging with guided Twilio configuration, compliance, and verification"
|
|
4
|
+
user-invocable: true
|
|
5
|
+
metadata: {"vellum": {"emoji": "\ud83d\udce8"}}
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are helping your user set up SMS messaging. This skill orchestrates Twilio setup, SMS-specific compliance, and end-to-end testing through a conversational flow.
|
|
9
|
+
|
|
10
|
+
## Step 1: Check Channel Readiness
|
|
11
|
+
|
|
12
|
+
First, check the current SMS channel readiness state by sending the `channel_readiness` IPC message:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"type": "channel_readiness",
|
|
17
|
+
"action": "get",
|
|
18
|
+
"channel": "sms"
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Inspect the `channel_readiness_response`. The response contains `snapshots` with each channel's readiness state.
|
|
23
|
+
|
|
24
|
+
- If the SMS channel shows `ready: true` and all `localChecks` pass, skip to Step 3.
|
|
25
|
+
- If any local checks fail, proceed to Step 2 to fix the baseline.
|
|
26
|
+
|
|
27
|
+
## Step 2: Establish Baseline (Twilio Setup)
|
|
28
|
+
|
|
29
|
+
If SMS baseline is not ready (missing credentials, phone number, or ingress), load the `twilio-setup` skill to walk the user through the basics:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
skill_load skill=twilio-setup
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Tell the user: *"SMS needs Twilio configured first. I've loaded the Twilio setup guide — let's walk through it."*
|
|
36
|
+
|
|
37
|
+
After twilio-setup completes, re-check readiness:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"type": "channel_readiness",
|
|
42
|
+
"action": "refresh",
|
|
43
|
+
"channel": "sms"
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If baseline is still not ready, report the specific failures and ask the user to address them before continuing.
|
|
48
|
+
|
|
49
|
+
## Step 3: Remote Compliance Check
|
|
50
|
+
|
|
51
|
+
Once baseline is ready, run a full readiness check including remote (Twilio API) checks:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"type": "channel_readiness",
|
|
56
|
+
"action": "refresh",
|
|
57
|
+
"channel": "sms",
|
|
58
|
+
"includeRemote": true
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Examine the remote check results:
|
|
63
|
+
- If all remote checks pass, proceed to Step 4.
|
|
64
|
+
- If compliance issues are found (e.g., toll-free verification needed), guide the user through the compliance flow:
|
|
65
|
+
1. Check compliance status using the `twilio_config` IPC with `action: "sms_compliance_status"` (if available).
|
|
66
|
+
2. If toll-free verification is needed, collect user information and submit via `twilio_config` with `action: "sms_submit_tollfree_verification"`.
|
|
67
|
+
3. Report verification status and next steps.
|
|
68
|
+
|
|
69
|
+
**Note:** Compliance actions (sms_compliance_status, sms_submit_tollfree_verification, etc.) may not be available yet. If the IPC action is not recognized, tell the user: *"Compliance automation isn't available yet. You may need to check Twilio Console manually for toll-free verification status."*
|
|
70
|
+
|
|
71
|
+
### Data Collection for Verification (Individual-First)
|
|
72
|
+
|
|
73
|
+
When collecting information for toll-free verification:
|
|
74
|
+
- Assume the user is an **individual / sole proprietor** by default
|
|
75
|
+
- Do NOT ask for EIN, business registration number, or business registration authority
|
|
76
|
+
- Explain that Twilio labels some fields as "business" fields even for individual submitters
|
|
77
|
+
- Only collect what's required: business name (can be personal name), website (can be personal site), notification email, use case, message samples, opt-in info
|
|
78
|
+
- If Twilio rejects the submission requiring business registration, explain the situation and guide through the fallback path
|
|
79
|
+
|
|
80
|
+
## Step 4: Test Send
|
|
81
|
+
|
|
82
|
+
Run a test SMS to verify end-to-end delivery:
|
|
83
|
+
|
|
84
|
+
Tell the user: *"Let's send a test SMS to verify everything works. What phone number should I send the test to?"*
|
|
85
|
+
|
|
86
|
+
After the user provides a number, send a test message using the messaging tools:
|
|
87
|
+
- Use `messaging_send` with `platform: "sms"`, `conversation_id: "<phone number>"`, and a test message like "Test SMS from your Vellum assistant."
|
|
88
|
+
- Report the result honestly:
|
|
89
|
+
- If the send succeeds: *"The message was accepted by Twilio. Note: 'accepted' means Twilio received it for delivery, not that it reached the handset yet. Delivery can take a few seconds to a few minutes."*
|
|
90
|
+
- If the send fails: report the error and suggest troubleshooting steps
|
|
91
|
+
|
|
92
|
+
## Step 5: Final Status Report
|
|
93
|
+
|
|
94
|
+
After completing (or skipping) the test, present a clear summary:
|
|
95
|
+
|
|
96
|
+
**If everything passed:**
|
|
97
|
+
*"SMS is ready! Here's your setup status:"*
|
|
98
|
+
- Twilio credentials: configured
|
|
99
|
+
- Phone number: {number}
|
|
100
|
+
- Ingress: configured
|
|
101
|
+
- Compliance: {status}
|
|
102
|
+
- Test send: {result}
|
|
103
|
+
|
|
104
|
+
**If there are blockers:**
|
|
105
|
+
*"SMS setup is partially complete. Here's what still needs attention:"*
|
|
106
|
+
- List each blocker with the specific next action
|
|
107
|
+
|
|
108
|
+
## Troubleshooting
|
|
109
|
+
|
|
110
|
+
If the user returns to this skill after initial setup:
|
|
111
|
+
1. Always start with Step 1 (readiness check) to assess current state
|
|
112
|
+
2. Skip steps that are already complete
|
|
113
|
+
3. Focus on the specific issue the user is experiencing
|
|
114
|
+
|
|
115
|
+
Common issues:
|
|
116
|
+
- **"Messages not delivering"** — Check compliance status, verify the number isn't flagged
|
|
117
|
+
- **"Twilio error on send"** — Check credentials, phone number assignment, and ingress
|
|
118
|
+
- **"Trial account limitations"** — Explain that trial accounts can only send to verified numbers
|
|
@@ -103,12 +103,17 @@ Before reporting success, confirm the guardian binding was actually created. Sen
|
|
|
103
103
|
|
|
104
104
|
### Step 8: Report Success
|
|
105
105
|
|
|
106
|
+
First, retrieve the bot identity by sending a `telegram_config` IPC message with `action: "get"` and reading the `botUsername` field from the response.
|
|
107
|
+
|
|
106
108
|
Summarize what was done:
|
|
109
|
+
- Bot identity: @{botUsername}
|
|
107
110
|
- Bot verified and credentials stored securely via daemon
|
|
108
111
|
- Webhook registration: handled automatically by the gateway
|
|
109
112
|
- Bot commands registered: /new, /guardian_verify
|
|
110
|
-
- Guardian identity verified
|
|
113
|
+
- Guardian identity: {verified | not configured}
|
|
114
|
+
- Guardian verification status: {verified via challenge | skipped}
|
|
111
115
|
- Routing configuration validated
|
|
116
|
+
- To re-check guardian status later, send `guardian_verification` with `action: "status"` and `channel: "telegram"`
|
|
112
117
|
|
|
113
118
|
The gateway automatically detects credentials from the vault, reconciles the Telegram webhook registration, and begins accepting Telegram webhooks shortly. In single-assistant mode, routing is automatically configured — no manual environment variable configuration or webhook registration is needed. If the webhook secret changes later, the gateway's credential watcher will automatically re-register the webhook. If the ingress URL changes (e.g., tunnel restart), the assistant daemon triggers an immediate internal reconcile so the webhook re-registers automatically without a gateway restart.
|
|
114
119
|
|
|
@@ -18,6 +18,27 @@ This skill manages the full Twilio lifecycle:
|
|
|
18
18
|
|
|
19
19
|
All operations go through the `twilio_config` IPC handler on the daemon, which validates inputs, stores credentials securely, and manages phone number state.
|
|
20
20
|
|
|
21
|
+
### Multi-Assistant Setups
|
|
22
|
+
|
|
23
|
+
In a multi-assistant environment (multiple assistants sharing the same daemon), some `twilio_config` actions are **assistant-scoped** while others are **global** (shared across all assistants):
|
|
24
|
+
|
|
25
|
+
**Global actions** (ignore `assistantId` — credentials are shared across all assistants):
|
|
26
|
+
- `set_credentials` — Stores Account SID and Auth Token in global secure storage (`credential:twilio:*` keys). All assistants share the same Twilio account credentials.
|
|
27
|
+
- `clear_credentials` — Removes the globally stored Account SID and Auth Token. This affects all assistants.
|
|
28
|
+
|
|
29
|
+
**Assistant-scoped actions** (use `assistantId` to scope phone number configuration per assistant):
|
|
30
|
+
- `get` — Returns the phone number assigned to the specified assistant (falls back to the legacy global number if no per-assistant mapping exists).
|
|
31
|
+
- `assign_number` — Assigns a phone number to a specific assistant via the per-assistant mapping.
|
|
32
|
+
- `provision_number` — Provisions a new number and assigns it to the specified assistant.
|
|
33
|
+
- `list_numbers` — Lists all phone numbers on the shared Twilio account (uses global credentials).
|
|
34
|
+
|
|
35
|
+
Include `assistantId` in assistant-scoped actions whenever:
|
|
36
|
+
- Multiple assistants share the same Twilio account but use different phone numbers
|
|
37
|
+
- You want to ensure configuration changes only affect a specific assistant
|
|
38
|
+
- The user has explicitly selected or referenced a particular assistant
|
|
39
|
+
|
|
40
|
+
All IPC examples below include the optional `assistantId` field in assistant-scoped actions. Omit it in single-assistant setups. For global actions (`set_credentials`, `clear_credentials`), the `assistantId` field is accepted but ignored.
|
|
41
|
+
|
|
21
42
|
## Step 1: Check Current Configuration
|
|
22
43
|
|
|
23
44
|
First, check whether Twilio is already configured by sending the `twilio_config` IPC message with `action: "get"`:
|
|
@@ -25,7 +46,8 @@ First, check whether Twilio is already configured by sending the `twilio_config`
|
|
|
25
46
|
```json
|
|
26
47
|
{
|
|
27
48
|
"type": "twilio_config",
|
|
28
|
-
"action": "get"
|
|
49
|
+
"action": "get",
|
|
50
|
+
"assistantId": "<optional — omit for single-assistant setups>"
|
|
29
51
|
}
|
|
30
52
|
```
|
|
31
53
|
|
|
@@ -62,6 +84,8 @@ After both credentials are collected, retrieve them from secure storage and pass
|
|
|
62
84
|
|
|
63
85
|
Both `accountSid` and `authToken` are required — the daemon validates the credentials against the Twilio API before storing them. If credentials are invalid, the daemon returns an error. Tell the user and ask them to re-enter via the secure prompt.
|
|
64
86
|
|
|
87
|
+
**Note:** `set_credentials` is a global operation — credentials are stored once and shared across all assistants. The `assistantId` field is accepted but ignored.
|
|
88
|
+
|
|
65
89
|
## Step 3: Get a Phone Number
|
|
66
90
|
|
|
67
91
|
The assistant needs a phone number to make calls and send SMS. There are two paths:
|
|
@@ -75,7 +99,8 @@ If the user wants to buy a new number through Twilio, send:
|
|
|
75
99
|
"type": "twilio_config",
|
|
76
100
|
"action": "provision_number",
|
|
77
101
|
"areaCode": "415",
|
|
78
|
-
"country": "US"
|
|
102
|
+
"country": "US",
|
|
103
|
+
"assistantId": "<optional — omit for single-assistant setups>"
|
|
79
104
|
}
|
|
80
105
|
```
|
|
81
106
|
|
|
@@ -100,7 +125,8 @@ If the user already has a Twilio phone number, first list available numbers:
|
|
|
100
125
|
```json
|
|
101
126
|
{
|
|
102
127
|
"type": "twilio_config",
|
|
103
|
-
"action": "list_numbers"
|
|
128
|
+
"action": "list_numbers",
|
|
129
|
+
"assistantId": "<optional — omit for single-assistant setups>"
|
|
104
130
|
}
|
|
105
131
|
```
|
|
106
132
|
|
|
@@ -112,7 +138,8 @@ Then assign the chosen number:
|
|
|
112
138
|
{
|
|
113
139
|
"type": "twilio_config",
|
|
114
140
|
"action": "assign_number",
|
|
115
|
-
"phoneNumber": "+14155551234"
|
|
141
|
+
"phoneNumber": "+14155551234",
|
|
142
|
+
"assistantId": "<optional — omit for single-assistant setups>"
|
|
116
143
|
}
|
|
117
144
|
```
|
|
118
145
|
|
|
@@ -132,7 +159,8 @@ Then assign it through the IPC:
|
|
|
132
159
|
{
|
|
133
160
|
"type": "twilio_config",
|
|
134
161
|
"action": "assign_number",
|
|
135
|
-
"phoneNumber": "+14155551234"
|
|
162
|
+
"phoneNumber": "+14155551234",
|
|
163
|
+
"assistantId": "<optional — omit for single-assistant setups>"
|
|
136
164
|
}
|
|
137
165
|
```
|
|
138
166
|
|
|
@@ -171,6 +199,46 @@ Confirm:
|
|
|
171
199
|
|
|
172
200
|
Tell the user: **"Twilio is configured. Your assistant's phone number is {phoneNumber}. This number is used for both voice calls and SMS messaging."**
|
|
173
201
|
|
|
202
|
+
## Step 5.5: Guardian Verification (SMS)
|
|
203
|
+
|
|
204
|
+
Now link the user's phone number as the trusted SMS guardian for this assistant. Tell the user: "Now let's verify your guardian identity for SMS. This links your phone number as the trusted guardian for SMS messaging."
|
|
205
|
+
|
|
206
|
+
1. Send the `guardian_verification` IPC message with `action: "create_challenge"` and `channel: "sms"`:
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"type": "guardian_verification",
|
|
211
|
+
"action": "create_challenge",
|
|
212
|
+
"channel": "sms",
|
|
213
|
+
"assistantId": "<optional — omit for single-assistant setups>"
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
2. The daemon returns a `guardian_verification_response` with `success: true`, `secret`, and `instruction`. Display the instruction to the user. It will look like: "Send `/guardian_verify <secret>` to your bot via SMS within 10 minutes."
|
|
218
|
+
|
|
219
|
+
3. Wait for the user to confirm they have sent the verification code via SMS to the assistant's phone number.
|
|
220
|
+
|
|
221
|
+
4. Check verification status by sending `guardian_verification` with `action: "status"` and `channel: "sms"`:
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{
|
|
225
|
+
"type": "guardian_verification",
|
|
226
|
+
"action": "status",
|
|
227
|
+
"channel": "sms",
|
|
228
|
+
"assistantId": "<optional — omit for single-assistant setups>"
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
5. If `bound` is `true`: "Guardian verified! Your phone number is now the trusted SMS guardian."
|
|
233
|
+
|
|
234
|
+
6. If `bound` is `false` and the user claims they sent the code: "The verification doesn't appear to have succeeded. Let's generate a new challenge." Repeat from substep 1.
|
|
235
|
+
|
|
236
|
+
**Note:** Guardian verification is optional but recommended. If the user declines or wants to skip, proceed to Step 6 without blocking.
|
|
237
|
+
|
|
238
|
+
To re-check guardian status later, send `guardian_verification` with `action: "status"` and `channel: "sms"`.
|
|
239
|
+
|
|
240
|
+
Report the guardian verification result: **"Guardian identity: {verified | not configured}."**
|
|
241
|
+
|
|
174
242
|
## Step 6: Enable Features
|
|
175
243
|
|
|
176
244
|
Now that Twilio is configured, the user can enable the features that depend on it:
|
|
@@ -194,7 +262,9 @@ If the user wants to disconnect Twilio, send:
|
|
|
194
262
|
}
|
|
195
263
|
```
|
|
196
264
|
|
|
197
|
-
This removes the stored Account SID and Auth Token.
|
|
265
|
+
This removes the stored Account SID and Auth Token. Phone number assignments are preserved. Voice calls and SMS will stop working until credentials are reconfigured.
|
|
266
|
+
|
|
267
|
+
**Note:** `clear_credentials` is a global operation — it removes credentials for all assistants, not just the current one. The `assistantId` field is accepted but ignored. In multi-assistant setups, warn the user that clearing credentials will affect all assistants sharing this Twilio account.
|
|
198
268
|
|
|
199
269
|
## Troubleshooting
|
|
200
270
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages daemon-level authentication: session token lifecycle,
|
|
3
|
+
* per-socket auth state, and auth timeouts.
|
|
4
|
+
*/
|
|
5
|
+
import * as net from 'node:net';
|
|
6
|
+
import { randomBytes } from 'node:crypto';
|
|
7
|
+
import { readFileSync, writeFileSync, chmodSync } from 'node:fs';
|
|
8
|
+
import { getSessionTokenPath } from '../util/platform.js';
|
|
9
|
+
import { hasNoAuthOverride } from './connection-policy.js';
|
|
10
|
+
import { getLogger } from '../util/logger.js';
|
|
11
|
+
|
|
12
|
+
const log = getLogger('auth-manager');
|
|
13
|
+
|
|
14
|
+
export const AUTH_TIMEOUT_MS = 5_000;
|
|
15
|
+
|
|
16
|
+
export class AuthManager {
|
|
17
|
+
private sessionToken = '';
|
|
18
|
+
private authenticatedSockets = new Set<net.Socket>();
|
|
19
|
+
private authTimeouts = new Map<net.Socket, ReturnType<typeof setTimeout>>();
|
|
20
|
+
|
|
21
|
+
/** Initialize the session token — reuse from disk or generate a new one. */
|
|
22
|
+
initToken(): void {
|
|
23
|
+
const tokenPath = getSessionTokenPath();
|
|
24
|
+
let existingToken: string | null = null;
|
|
25
|
+
try {
|
|
26
|
+
const raw = readFileSync(tokenPath, 'utf-8').trim();
|
|
27
|
+
if (raw.length >= 32) existingToken = raw;
|
|
28
|
+
} catch { /* file doesn't exist yet */ }
|
|
29
|
+
|
|
30
|
+
if (existingToken) {
|
|
31
|
+
this.sessionToken = existingToken;
|
|
32
|
+
log.info({ tokenPath }, 'Reusing existing session token');
|
|
33
|
+
} else {
|
|
34
|
+
this.sessionToken = randomBytes(32).toString('hex');
|
|
35
|
+
writeFileSync(tokenPath, this.sessionToken, { mode: 0o600 });
|
|
36
|
+
chmodSync(tokenPath, 0o600);
|
|
37
|
+
log.info({ tokenPath }, 'New session token generated');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
isAuthenticated(socket: net.Socket): boolean {
|
|
42
|
+
return this.authenticatedSockets.has(socket);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Returns true if VELLUM_DAEMON_NOAUTH bypass is active. */
|
|
46
|
+
shouldAutoAuth(): boolean {
|
|
47
|
+
return hasNoAuthOverride();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
markAuthenticated(socket: net.Socket): void {
|
|
51
|
+
this.authenticatedSockets.add(socket);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Validate a token and authenticate the socket. Returns true on success. */
|
|
55
|
+
authenticate(socket: net.Socket, token: string): boolean {
|
|
56
|
+
if (token === this.sessionToken) {
|
|
57
|
+
this.authenticatedSockets.add(socket);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
log.warn('Client provided invalid auth token');
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Start the auth timeout for a newly connected socket. */
|
|
65
|
+
startTimeout(socket: net.Socket, onTimeout: () => void): void {
|
|
66
|
+
const timer = setTimeout(() => {
|
|
67
|
+
if (!this.authenticatedSockets.has(socket)) {
|
|
68
|
+
log.warn('Client failed to authenticate within timeout, disconnecting');
|
|
69
|
+
onTimeout();
|
|
70
|
+
}
|
|
71
|
+
}, AUTH_TIMEOUT_MS);
|
|
72
|
+
this.authTimeouts.set(socket, timer);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Clear the auth timeout (called when the first message arrives). */
|
|
76
|
+
clearTimeout(socket: net.Socket): void {
|
|
77
|
+
const timer = this.authTimeouts.get(socket);
|
|
78
|
+
if (timer) {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
this.authTimeouts.delete(socket);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Remove all auth state for a disconnected socket. */
|
|
85
|
+
cleanupSocket(socket: net.Socket): void {
|
|
86
|
+
this.clearTimeout(socket);
|
|
87
|
+
this.authenticatedSockets.delete(socket);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Tear down all auth state on server stop. */
|
|
91
|
+
cleanupAll(): void {
|
|
92
|
+
for (const timer of this.authTimeouts.values()) {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
}
|
|
95
|
+
this.authTimeouts.clear();
|
|
96
|
+
this.authenticatedSockets.clear();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Iterate over authenticated sockets (for broadcasting). */
|
|
100
|
+
getAuthenticatedSockets(): Set<net.Socket> {
|
|
101
|
+
return this.authenticatedSockets;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -897,7 +897,14 @@ export class ComputerUseSession {
|
|
|
897
897
|
decision: UserDecision,
|
|
898
898
|
selectedPattern?: string,
|
|
899
899
|
selectedScope?: string,
|
|
900
|
+
decisionContext?: string,
|
|
900
901
|
): void {
|
|
901
|
-
this.prompter?.resolveConfirmation(
|
|
902
|
+
this.prompter?.resolveConfirmation(
|
|
903
|
+
requestId,
|
|
904
|
+
decision,
|
|
905
|
+
selectedPattern,
|
|
906
|
+
selectedScope,
|
|
907
|
+
decisionContext,
|
|
908
|
+
);
|
|
902
909
|
}
|
|
903
910
|
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File watchers and config reload logic extracted from DaemonServer.
|
|
3
|
+
* Watches workspace files (config, prompts), protected directory
|
|
4
|
+
* (trust rules, secret allowlist), and skills directories for changes.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readdirSync, watch, type FSWatcher } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { getRootDir, getWorkspaceDir, getWorkspaceSkillsDir } from '../util/platform.js';
|
|
9
|
+
import { getConfig, invalidateConfigCache } from '../config/loader.js';
|
|
10
|
+
import { initializeProviders } from '../providers/registry.js';
|
|
11
|
+
import { clearCache as clearTrustCache } from '../permissions/trust-store.js';
|
|
12
|
+
import { resetAllowlist, validateAllowlistFile } from '../security/secret-allowlist.js';
|
|
13
|
+
import { clearEmbeddingBackendCache } from '../memory/embedding-backend.js';
|
|
14
|
+
import { DebouncerMap } from '../util/debounce.js';
|
|
15
|
+
import { getLogger } from '../util/logger.js';
|
|
16
|
+
|
|
17
|
+
const log = getLogger('config-watcher');
|
|
18
|
+
|
|
19
|
+
export class ConfigWatcher {
|
|
20
|
+
private watchers: FSWatcher[] = [];
|
|
21
|
+
private debounceTimers = new DebouncerMap({
|
|
22
|
+
defaultDelayMs: 200,
|
|
23
|
+
maxEntries: 1000,
|
|
24
|
+
protectedKeyPrefix: '__',
|
|
25
|
+
});
|
|
26
|
+
private suppressReload = false;
|
|
27
|
+
private lastFingerprint = '';
|
|
28
|
+
private lastRefreshTime = 0;
|
|
29
|
+
|
|
30
|
+
static readonly REFRESH_INTERVAL_MS = 30_000;
|
|
31
|
+
|
|
32
|
+
/** Expose the debounce timers so handlers can schedule debounced work. */
|
|
33
|
+
get timers(): DebouncerMap {
|
|
34
|
+
return this.debounceTimers;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get suppressConfigReload(): boolean {
|
|
38
|
+
return this.suppressReload;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
set suppressConfigReload(value: boolean) {
|
|
42
|
+
this.suppressReload = value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get lastConfigRefreshTime(): number {
|
|
46
|
+
return this.lastRefreshTime;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
set lastConfigRefreshTime(value: number) {
|
|
50
|
+
this.lastRefreshTime = value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Compute a fingerprint of the current config for change detection. */
|
|
54
|
+
configFingerprint(config: ReturnType<typeof getConfig>): string {
|
|
55
|
+
return JSON.stringify(config);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Initialize the config fingerprint (call after first config load). */
|
|
59
|
+
initFingerprint(config: ReturnType<typeof getConfig>): void {
|
|
60
|
+
this.lastFingerprint = this.configFingerprint(config);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Update the fingerprint to match the current config. */
|
|
64
|
+
updateFingerprint(): void {
|
|
65
|
+
this.lastFingerprint = this.configFingerprint(getConfig());
|
|
66
|
+
this.lastRefreshTime = Date.now();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Reload config from disk + secure storage, and refresh providers only
|
|
71
|
+
* when effective config values (including API keys) have changed.
|
|
72
|
+
* Returns true if config actually changed.
|
|
73
|
+
*/
|
|
74
|
+
refreshConfigFromSources(): boolean {
|
|
75
|
+
invalidateConfigCache();
|
|
76
|
+
const config = getConfig();
|
|
77
|
+
const fingerprint = this.configFingerprint(config);
|
|
78
|
+
if (fingerprint === this.lastFingerprint) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
clearTrustCache();
|
|
82
|
+
clearEmbeddingBackendCache();
|
|
83
|
+
const isFirstInit = this.lastFingerprint === '';
|
|
84
|
+
initializeProviders(config);
|
|
85
|
+
this.lastFingerprint = fingerprint;
|
|
86
|
+
return !isFirstInit;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Start all file watchers. `onSessionEvict` is called when watched
|
|
91
|
+
* files change and sessions need to be evicted for reload.
|
|
92
|
+
*/
|
|
93
|
+
start(onSessionEvict: () => void): void {
|
|
94
|
+
const workspaceDir = getWorkspaceDir();
|
|
95
|
+
const protectedDir = join(getRootDir(), 'protected');
|
|
96
|
+
|
|
97
|
+
const workspaceHandlers: Record<string, () => void> = {
|
|
98
|
+
'config.json': () => {
|
|
99
|
+
if (this.suppressReload) return;
|
|
100
|
+
try {
|
|
101
|
+
const changed = this.refreshConfigFromSources();
|
|
102
|
+
if (changed) onSessionEvict();
|
|
103
|
+
} catch (err) {
|
|
104
|
+
log.error({ err, configPath: join(workspaceDir, 'config.json') }, 'Failed to reload config after file change. Previous config remains active.');
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
'SOUL.md': () => onSessionEvict(),
|
|
108
|
+
'IDENTITY.md': () => onSessionEvict(),
|
|
109
|
+
'USER.md': () => onSessionEvict(),
|
|
110
|
+
'LOOKS.md': () => onSessionEvict(),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const protectedHandlers: Record<string, () => void> = {
|
|
114
|
+
'trust.json': () => {
|
|
115
|
+
clearTrustCache();
|
|
116
|
+
},
|
|
117
|
+
'secret-allowlist.json': () => {
|
|
118
|
+
resetAllowlist();
|
|
119
|
+
try {
|
|
120
|
+
const errors = validateAllowlistFile();
|
|
121
|
+
if (errors && errors.length > 0) {
|
|
122
|
+
for (const e of errors) {
|
|
123
|
+
log.warn({ index: e.index, pattern: e.pattern }, `Invalid regex in secret-allowlist.json: ${e.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
log.warn({ err }, 'Failed to validate secret-allowlist.json');
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const watchDir = (dir: string, handlers: Record<string, () => void>, label: string): void => {
|
|
133
|
+
try {
|
|
134
|
+
const watcher = watch(dir, (_eventType, filename) => {
|
|
135
|
+
if (!filename) return;
|
|
136
|
+
const file = String(filename);
|
|
137
|
+
if (!handlers[file]) return;
|
|
138
|
+
this.debounceTimers.schedule(`file:${file}`, () => {
|
|
139
|
+
log.info({ file }, 'File changed, reloading');
|
|
140
|
+
handlers[file]();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
this.watchers.push(watcher);
|
|
144
|
+
log.info({ dir }, `Watching ${label}`);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
log.warn({ err, dir }, `Failed to watch ${label}. Hot-reload will be unavailable.`);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
watchDir(workspaceDir, workspaceHandlers, 'workspace directory for config/prompt changes');
|
|
151
|
+
if (existsSync(protectedDir)) {
|
|
152
|
+
watchDir(protectedDir, protectedHandlers, 'protected directory for trust/allowlist changes');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.startSkillsWatchers(onSessionEvict);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
stop(): void {
|
|
159
|
+
this.debounceTimers.cancelAll();
|
|
160
|
+
for (const watcher of this.watchers) {
|
|
161
|
+
watcher.close();
|
|
162
|
+
}
|
|
163
|
+
this.watchers = [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private startSkillsWatchers(onSessionEvict: () => void): void {
|
|
167
|
+
const skillsDir = getWorkspaceSkillsDir();
|
|
168
|
+
if (!existsSync(skillsDir)) return;
|
|
169
|
+
|
|
170
|
+
const scheduleSkillsReload = (file: string): void => {
|
|
171
|
+
this.debounceTimers.schedule(`skills:${file}`, () => {
|
|
172
|
+
log.info({ file }, 'Skill file changed, reloading');
|
|
173
|
+
onSessionEvict();
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const recursiveWatcher = watch(skillsDir, { recursive: true }, (_eventType, filename) => {
|
|
179
|
+
scheduleSkillsReload(filename ? String(filename) : '(unknown)');
|
|
180
|
+
});
|
|
181
|
+
this.watchers.push(recursiveWatcher);
|
|
182
|
+
log.info({ dir: skillsDir }, 'Watching skills directory recursively');
|
|
183
|
+
return;
|
|
184
|
+
} catch (err) {
|
|
185
|
+
log.info({ err, dir: skillsDir }, 'Recursive skills watch unavailable; using per-directory watchers');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const childWatchers = new Map<string, FSWatcher>();
|
|
189
|
+
|
|
190
|
+
const watchDir = (dirPath: string, onChange: (filename: string) => void): FSWatcher | null => {
|
|
191
|
+
try {
|
|
192
|
+
const watcher = watch(dirPath, (_eventType, filename) => {
|
|
193
|
+
onChange(filename ? String(filename) : '(unknown)');
|
|
194
|
+
});
|
|
195
|
+
this.watchers.push(watcher);
|
|
196
|
+
return watcher;
|
|
197
|
+
} catch (err) {
|
|
198
|
+
log.warn({ err, dirPath }, 'Failed to watch skills directory');
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const removeWatcher = (watcher: FSWatcher): void => {
|
|
204
|
+
const idx = this.watchers.indexOf(watcher);
|
|
205
|
+
if (idx !== -1) {
|
|
206
|
+
this.watchers.splice(idx, 1);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const refreshChildWatchers = (): void => {
|
|
211
|
+
const nextChildDirs = new Set<string>();
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
215
|
+
for (const entry of entries) {
|
|
216
|
+
if (!entry.isDirectory()) continue;
|
|
217
|
+
const childDir = join(skillsDir, entry.name);
|
|
218
|
+
nextChildDirs.add(childDir);
|
|
219
|
+
|
|
220
|
+
if (childWatchers.has(childDir)) continue;
|
|
221
|
+
|
|
222
|
+
const watcher = watchDir(childDir, (filename) => {
|
|
223
|
+
const label = filename === '(unknown)' ? entry.name : `${entry.name}/${filename}`;
|
|
224
|
+
scheduleSkillsReload(label);
|
|
225
|
+
});
|
|
226
|
+
if (watcher) {
|
|
227
|
+
childWatchers.set(childDir, watcher);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch (err) {
|
|
231
|
+
log.warn({ err, skillsDir }, 'Failed to enumerate skill directories');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const [childDir, watcher] of childWatchers.entries()) {
|
|
236
|
+
if (nextChildDirs.has(childDir)) continue;
|
|
237
|
+
watcher.close();
|
|
238
|
+
childWatchers.delete(childDir);
|
|
239
|
+
removeWatcher(watcher);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const rootWatcher = watchDir(skillsDir, (filename) => {
|
|
244
|
+
scheduleSkillsReload(filename);
|
|
245
|
+
refreshChildWatchers();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
if (!rootWatcher) return;
|
|
249
|
+
|
|
250
|
+
refreshChildWatchers();
|
|
251
|
+
log.info({ dir: skillsDir }, 'Watching skills directory with non-recursive fallback');
|
|
252
|
+
}
|
|
253
|
+
}
|