vellum 0.2.12 → 0.2.14
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/README.md +32 -0
- package/bun.lock +2 -2
- package/docs/skills.md +4 -4
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
- package/src/__tests__/app-git-history.test.ts +176 -0
- package/src/__tests__/app-git-service.test.ts +169 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
- package/src/__tests__/browser-skill-endstate.test.ts +6 -6
- package/src/__tests__/call-bridge.test.ts +105 -13
- package/src/__tests__/call-domain.test.ts +163 -0
- package/src/__tests__/call-orchestrator.test.ts +171 -0
- package/src/__tests__/call-routes-http.test.ts +246 -6
- package/src/__tests__/channel-approval-routes.test.ts +438 -0
- package/src/__tests__/channel-approval.test.ts +266 -0
- package/src/__tests__/channel-approvals.test.ts +393 -0
- package/src/__tests__/channel-delivery-store.test.ts +447 -0
- package/src/__tests__/checker.test.ts +607 -1048
- package/src/__tests__/cli.test.ts +1 -56
- package/src/__tests__/config-schema.test.ts +402 -5
- package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
- package/src/__tests__/conflict-policy.test.ts +121 -0
- package/src/__tests__/conflict-store.test.ts +2 -0
- package/src/__tests__/contacts-tools.test.ts +3 -3
- package/src/__tests__/contradiction-checker.test.ts +99 -1
- package/src/__tests__/credential-security-invariants.test.ts +22 -6
- package/src/__tests__/credential-vault-unit.test.ts +780 -0
- package/src/__tests__/elevenlabs-client.test.ts +271 -0
- package/src/__tests__/ephemeral-permissions.test.ts +73 -23
- package/src/__tests__/filesystem-tools.test.ts +579 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
- package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
- package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
- package/src/__tests__/handlers-slack-config.test.ts +2 -1
- package/src/__tests__/handlers-telegram-config.test.ts +855 -0
- package/src/__tests__/handlers-twitter-config.test.ts +141 -1
- package/src/__tests__/hooks-runner.test.ts +6 -2
- package/src/__tests__/host-file-edit-tool.test.ts +124 -0
- package/src/__tests__/host-file-read-tool.test.ts +62 -0
- package/src/__tests__/host-file-write-tool.test.ts +59 -0
- package/src/__tests__/host-shell-tool.test.ts +251 -0
- package/src/__tests__/ingress-reconcile.test.ts +581 -0
- package/src/__tests__/ipc-snapshot.test.ts +100 -41
- package/src/__tests__/ipc-validate.test.ts +50 -0
- package/src/__tests__/key-migration.test.ts +23 -0
- package/src/__tests__/memory-regressions.test.ts +99 -0
- package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
- package/src/__tests__/oauth-callback-registry.test.ts +11 -4
- package/src/__tests__/playbook-execution.test.ts +502 -0
- package/src/__tests__/playbook-tools.test.ts +4 -6
- package/src/__tests__/public-ingress-urls.test.ts +34 -0
- package/src/__tests__/qdrant-manager.test.ts +267 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
- package/src/__tests__/recurrence-engine.test.ts +9 -0
- package/src/__tests__/recurrence-types.test.ts +8 -0
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/runtime-runs.test.ts +1 -25
- package/src/__tests__/schedule-store.test.ts +16 -14
- package/src/__tests__/schedule-tools.test.ts +83 -0
- package/src/__tests__/scheduler-recurrence.test.ts +111 -10
- package/src/__tests__/secret-allowlist.test.ts +18 -17
- package/src/__tests__/secret-ingress-handler.test.ts +11 -0
- package/src/__tests__/secret-scanner.test.ts +43 -0
- package/src/__tests__/session-conflict-gate.test.ts +442 -6
- package/src/__tests__/session-init.benchmark.test.ts +3 -0
- package/src/__tests__/session-process-bridge.test.ts +242 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -1
- package/src/__tests__/shell-identity.test.ts +256 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
- package/src/__tests__/subagent-tools.test.ts +637 -54
- package/src/__tests__/task-management-tools.test.ts +936 -0
- package/src/__tests__/task-runner.test.ts +2 -2
- package/src/__tests__/terminal-tools.test.ts +840 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
- package/src/__tests__/tool-executor.test.ts +85 -151
- package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
- package/src/__tests__/trust-store.test.ts +28 -453
- package/src/__tests__/twilio-provider.test.ts +153 -3
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +127 -0
- package/src/__tests__/twilio-routes.test.ts +17 -262
- package/src/__tests__/twitter-auth-handler.test.ts +2 -1
- package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
- package/src/__tests__/twitter-cli-routing.test.ts +252 -0
- package/src/__tests__/twitter-oauth-client.test.ts +209 -0
- package/src/__tests__/workspace-policy.test.ts +213 -0
- package/src/calls/call-bridge.ts +92 -19
- package/src/calls/call-domain.ts +157 -5
- package/src/calls/call-orchestrator.ts +96 -8
- package/src/calls/call-store.ts +6 -0
- package/src/calls/elevenlabs-client.ts +97 -0
- package/src/calls/elevenlabs-config.ts +31 -0
- package/src/calls/twilio-provider.ts +91 -0
- package/src/calls/twilio-routes.ts +50 -6
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-quality.ts +114 -0
- package/src/cli/twitter.ts +200 -21
- package/src/cli.ts +1 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
- package/src/config/bundled-skills/messaging/SKILL.md +17 -2
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +207 -19
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
- package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
- package/src/config/bundled-skills/twitter/SKILL.md +103 -17
- package/src/config/defaults.ts +26 -2
- package/src/config/schema.ts +178 -9
- package/src/config/types.ts +3 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
- package/src/daemon/assistant-attachments.ts +4 -2
- package/src/daemon/handlers/apps.ts +69 -0
- package/src/daemon/handlers/config.ts +543 -24
- package/src/daemon/handlers/index.ts +1 -0
- package/src/daemon/handlers/sessions.ts +22 -6
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +5 -20
- package/src/daemon/ipc-contract-inventory.json +28 -0
- package/src/daemon/ipc-contract.ts +168 -10
- package/src/daemon/ipc-validate.ts +17 -0
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/server.ts +78 -72
- package/src/daemon/session-attachments.ts +1 -1
- package/src/daemon/session-conflict-gate.ts +62 -6
- package/src/daemon/session-notifiers.ts +1 -1
- package/src/daemon/session-process.ts +62 -3
- package/src/daemon/session-tool-setup.ts +1 -2
- package/src/daemon/tls-certs.ts +189 -0
- package/src/daemon/video-thumbnail.ts +5 -3
- package/src/hooks/manager.ts +5 -9
- package/src/memory/app-git-service.ts +295 -0
- package/src/memory/app-store.ts +21 -0
- package/src/memory/conflict-intent.ts +47 -4
- package/src/memory/conflict-policy.ts +73 -0
- package/src/memory/conflict-store.ts +9 -1
- package/src/memory/contradiction-checker.ts +28 -0
- package/src/memory/conversation-key-store.ts +15 -0
- package/src/memory/db.ts +81 -0
- package/src/memory/embedding-local.ts +3 -13
- package/src/memory/external-conversation-store.ts +234 -0
- package/src/memory/job-handlers/conflict.ts +22 -2
- package/src/memory/jobs-worker.ts +67 -28
- package/src/memory/runs-store.ts +54 -7
- package/src/memory/schema.ts +20 -0
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
- package/src/messaging/providers/telegram-bot/client.ts +104 -0
- package/src/messaging/providers/telegram-bot/types.ts +15 -0
- package/src/messaging/registry.ts +1 -0
- package/src/permissions/checker.ts +48 -44
- package/src/permissions/defaults.ts +11 -0
- package/src/permissions/prompter.ts +0 -4
- package/src/permissions/shell-identity.ts +227 -0
- package/src/permissions/trust-store.ts +76 -53
- package/src/permissions/types.ts +0 -19
- package/src/permissions/workspace-policy.ts +114 -0
- package/src/providers/retry.ts +12 -37
- package/src/runtime/assistant-event-hub.ts +41 -4
- package/src/runtime/channel-approval-parser.ts +60 -0
- package/src/runtime/channel-approval-types.ts +71 -0
- package/src/runtime/channel-approvals.ts +145 -0
- package/src/runtime/gateway-client.ts +16 -0
- package/src/runtime/http-server.ts +29 -9
- package/src/runtime/routes/call-routes.ts +52 -2
- package/src/runtime/routes/channel-routes.ts +296 -16
- package/src/runtime/routes/conversation-routes.ts +12 -5
- package/src/runtime/routes/events-routes.ts +97 -28
- package/src/runtime/routes/run-routes.ts +2 -7
- package/src/runtime/run-orchestrator.ts +0 -3
- package/src/schedule/recurrence-engine.ts +26 -2
- package/src/schedule/recurrence-types.ts +1 -1
- package/src/schedule/schedule-store.ts +12 -3
- package/src/security/secret-scanner.ts +7 -0
- package/src/tasks/ephemeral-permissions.ts +0 -2
- package/src/tasks/task-scheduler.ts +2 -1
- package/src/tools/calls/call-start.ts +8 -0
- package/src/tools/execution-target.ts +21 -0
- package/src/tools/execution-timeout.ts +49 -0
- package/src/tools/executor.ts +6 -135
- package/src/tools/network/web-search.ts +9 -32
- package/src/tools/policy-context.ts +29 -0
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/terminal/parser.ts +16 -18
- package/src/tools/types.ts +4 -11
- package/src/twitter/oauth-client.ts +102 -0
- package/src/twitter/router.ts +101 -0
- package/src/util/debounce.ts +88 -0
- package/src/util/network-info.ts +47 -0
- package/src/util/platform.ts +29 -4
- package/src/util/promise-guard.ts +37 -0
- package/src/util/retry.ts +98 -0
- package/src/util/truncate.ts +1 -1
- package/src/workspace/git-service.ts +129 -112
- package/src/tools/contacts/contact-merge.ts +0 -55
- package/src/tools/contacts/contact-search.ts +0 -58
- package/src/tools/contacts/contact-upsert.ts +0 -64
- package/src/tools/playbooks/index.ts +0 -4
- package/src/tools/playbooks/playbook-create.ts +0 -96
- package/src/tools/playbooks/playbook-delete.ts +0 -52
- package/src/tools/playbooks/playbook-list.ts +0 -74
- package/src/tools/playbooks/playbook-update.ts +0 -111
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getConfig } from '../config/loader.js';
|
|
2
|
+
import { getSecureKey } from '../security/secure-keys.js';
|
|
3
|
+
|
|
4
|
+
export interface ElevenLabsConfig {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
apiBaseUrl: string;
|
|
7
|
+
agentId: string;
|
|
8
|
+
registerCallTimeoutMs: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getElevenLabsConfig(): ElevenLabsConfig {
|
|
12
|
+
const config = getConfig();
|
|
13
|
+
const voice = config.calls.voice;
|
|
14
|
+
|
|
15
|
+
const apiKey = getSecureKey('credential:elevenlabs:api_key') ?? '';
|
|
16
|
+
if (!apiKey) {
|
|
17
|
+
throw new Error('ElevenLabs API key is not configured. Set credential:elevenlabs:api_key in the secure key store.');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const agentId = voice.elevenlabs.agentId;
|
|
21
|
+
if (!agentId) {
|
|
22
|
+
throw new Error('ElevenLabs agent ID is not configured. Set calls.voice.elevenlabs.agentId in config.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
apiKey,
|
|
27
|
+
apiBaseUrl: voice.elevenlabs.apiBaseUrl,
|
|
28
|
+
agentId,
|
|
29
|
+
registerCallTimeoutMs: voice.elevenlabs.registerCallTimeoutMs,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -131,6 +131,97 @@ export class TwilioConversationRelayProvider implements VoiceProvider {
|
|
|
131
131
|
return data.status;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// ── Caller ID eligibility ───────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check whether a phone number can be used as an outbound caller ID
|
|
138
|
+
* by the current Twilio account. A number is eligible if it appears as
|
|
139
|
+
* either an Incoming Phone Number (owned) or an Outgoing Caller ID
|
|
140
|
+
* (verified) on the account.
|
|
141
|
+
*/
|
|
142
|
+
async checkCallerIdEligibility(
|
|
143
|
+
phoneNumber: string,
|
|
144
|
+
): Promise<{ eligible: boolean; reason?: string }> {
|
|
145
|
+
const { accountSid, authToken } = this.getCredentials();
|
|
146
|
+
const encodedNumber = encodeURIComponent(phoneNumber);
|
|
147
|
+
|
|
148
|
+
let incomingOk = false;
|
|
149
|
+
let outgoingOk = false;
|
|
150
|
+
|
|
151
|
+
// Check incoming phone numbers (owned by this account)
|
|
152
|
+
const incomingRes = await fetch(
|
|
153
|
+
`${this.baseUrl(accountSid)}/IncomingPhoneNumbers.json?PhoneNumber=${encodedNumber}`,
|
|
154
|
+
{
|
|
155
|
+
method: 'GET',
|
|
156
|
+
headers: {
|
|
157
|
+
Authorization: this.authHeader(accountSid, authToken),
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (incomingRes.ok) {
|
|
163
|
+
incomingOk = true;
|
|
164
|
+
const incomingData = (await incomingRes.json()) as {
|
|
165
|
+
incoming_phone_numbers: unknown[];
|
|
166
|
+
};
|
|
167
|
+
if (incomingData.incoming_phone_numbers.length > 0) {
|
|
168
|
+
log.info({ phoneNumber }, 'Number found in IncomingPhoneNumbers — eligible as caller ID');
|
|
169
|
+
return { eligible: true };
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
log.warn(
|
|
173
|
+
{ status: incomingRes.status, phoneNumber },
|
|
174
|
+
'Failed to query IncomingPhoneNumbers — falling through to OutgoingCallerIds',
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check outgoing caller IDs (verified with this account)
|
|
179
|
+
const outgoingRes = await fetch(
|
|
180
|
+
`${this.baseUrl(accountSid)}/OutgoingCallerIds.json?PhoneNumber=${encodedNumber}`,
|
|
181
|
+
{
|
|
182
|
+
method: 'GET',
|
|
183
|
+
headers: {
|
|
184
|
+
Authorization: this.authHeader(accountSid, authToken),
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (outgoingRes.ok) {
|
|
190
|
+
outgoingOk = true;
|
|
191
|
+
const outgoingData = (await outgoingRes.json()) as {
|
|
192
|
+
outgoing_caller_ids: unknown[];
|
|
193
|
+
};
|
|
194
|
+
if (outgoingData.outgoing_caller_ids.length > 0) {
|
|
195
|
+
log.info({ phoneNumber }, 'Number found in OutgoingCallerIds — eligible as caller ID');
|
|
196
|
+
return { eligible: true };
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
log.warn(
|
|
200
|
+
{ status: outgoingRes.status, phoneNumber },
|
|
201
|
+
'Failed to query OutgoingCallerIds',
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// If any API call failed, the eligibility check is inconclusive —
|
|
206
|
+
// propagate as an error rather than returning a false negative.
|
|
207
|
+
if (!incomingOk || !outgoingOk) {
|
|
208
|
+
const failedEndpoints = [
|
|
209
|
+
...(!incomingOk ? [`IncomingPhoneNumbers: ${incomingRes.status}`] : []),
|
|
210
|
+
...(!outgoingOk ? [`OutgoingCallerIds: ${outgoingRes.status}`] : []),
|
|
211
|
+
].join(', ');
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Unable to verify caller ID eligibility for ${phoneNumber}: Twilio API error (${failedEndpoints}). The number may be eligible but could not be confirmed. Please check your Twilio credentials and try again.`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
log.info({ phoneNumber }, 'Number not found in either IncomingPhoneNumbers or OutgoingCallerIds');
|
|
218
|
+
return {
|
|
219
|
+
eligible: false,
|
|
220
|
+
reason:
|
|
221
|
+
'Number is not owned by or verified with your Twilio account. To use this number as caller ID, either: (1) add it as an Incoming Phone Number, or (2) verify it as an Outgoing Caller ID in the Twilio Console.',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
134
225
|
// ── Webhook signature verification ──────────────────────────────────
|
|
135
226
|
|
|
136
227
|
/**
|
|
@@ -25,6 +25,7 @@ import { getTwilioConfig } from './twilio-config.js';
|
|
|
25
25
|
import { loadConfig } from '../config/loader.js';
|
|
26
26
|
import { getTwilioRelayUrl } from '../inbound/public-ingress-urls.js';
|
|
27
27
|
import { fireCallCompletionNotifier } from './call-state.js';
|
|
28
|
+
import { resolveVoiceQualityProfile, isVoiceProfileValid } from './voice-quality.js';
|
|
28
29
|
|
|
29
30
|
const log = getLogger('twilio-routes');
|
|
30
31
|
|
|
@@ -39,17 +40,22 @@ function escapeXml(str: string): string {
|
|
|
39
40
|
.replace(/'/g, ''');
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
function generateTwiML(
|
|
43
|
+
export function generateTwiML(
|
|
44
|
+
callSessionId: string,
|
|
45
|
+
relayUrl: string,
|
|
46
|
+
welcomeGreeting: string,
|
|
47
|
+
profile: { language: string; transcriptionProvider: string; ttsProvider: string; voice: string },
|
|
48
|
+
): string {
|
|
43
49
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
44
50
|
<Response>
|
|
45
51
|
<Connect>
|
|
46
52
|
<ConversationRelay
|
|
47
53
|
url="${escapeXml(relayUrl)}?callSessionId=${escapeXml(callSessionId)}"
|
|
48
54
|
welcomeGreeting="${escapeXml(welcomeGreeting)}"
|
|
49
|
-
voice="
|
|
50
|
-
language="
|
|
51
|
-
transcriptionProvider="
|
|
52
|
-
ttsProvider="
|
|
55
|
+
voice="${escapeXml(profile.voice)}"
|
|
56
|
+
language="${escapeXml(profile.language)}"
|
|
57
|
+
transcriptionProvider="${escapeXml(profile.transcriptionProvider)}"
|
|
58
|
+
ttsProvider="${escapeXml(profile.ttsProvider)}"
|
|
53
59
|
interruptible="true"
|
|
54
60
|
dtmfDetection="true"
|
|
55
61
|
/>
|
|
@@ -131,6 +137,44 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
|
|
|
131
137
|
log.info({ callSessionId, callSid }, 'Stored CallSid from voice webhook');
|
|
132
138
|
}
|
|
133
139
|
|
|
140
|
+
let profile = resolveVoiceQualityProfile(loadConfig());
|
|
141
|
+
|
|
142
|
+
log.info({ callSessionId, mode: profile.mode, ttsProvider: profile.ttsProvider, voice: profile.voice }, 'Voice quality profile resolved');
|
|
143
|
+
|
|
144
|
+
if (profile.validationErrors.length > 0) {
|
|
145
|
+
log.warn({ callSessionId, errors: profile.validationErrors }, 'Voice quality profile has validation warnings');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// WS-A: Enforce strict fallback semantics — reject invalid profiles when fallback is disabled
|
|
149
|
+
if (!isVoiceProfileValid(profile)) {
|
|
150
|
+
if (!profile.fallbackToStandardOnError) {
|
|
151
|
+
const errorMsg = `Voice quality configuration error: ${profile.validationErrors.join('; ')}`;
|
|
152
|
+
log.error({ callSessionId, errors: profile.validationErrors }, errorMsg);
|
|
153
|
+
return new Response(errorMsg, { status: 500 });
|
|
154
|
+
}
|
|
155
|
+
// Fallback is enabled — profile already resolved to standard; log explicitly
|
|
156
|
+
log.info({ callSessionId }, 'Profile invalid with fallback enabled; proceeding with standard mode');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// WS-B: Guard elevenlabs_agent until consultation bridge exists.
|
|
160
|
+
// This fires BEFORE any ElevenLabs API calls, blocking the entire mode.
|
|
161
|
+
if (profile.mode === 'elevenlabs_agent') {
|
|
162
|
+
if (!profile.fallbackToStandardOnError) {
|
|
163
|
+
const msg = 'elevenlabs_agent mode is restricted: consultation bridging (waiting_on_user) is not yet supported. Set calls.voice.fallbackToStandardOnError=true to fall back to standard mode.';
|
|
164
|
+
log.error({ callSessionId }, msg);
|
|
165
|
+
return new Response(msg, { status: 501 });
|
|
166
|
+
}
|
|
167
|
+
log.warn({ callSessionId }, 'elevenlabs_agent mode is restricted/experimental — consultation bridging is not yet supported; falling back to standard ConversationRelay TwiML');
|
|
168
|
+
const standardConfig = loadConfig();
|
|
169
|
+
profile = resolveVoiceQualityProfile({
|
|
170
|
+
...standardConfig,
|
|
171
|
+
calls: {
|
|
172
|
+
...standardConfig.calls,
|
|
173
|
+
voice: { ...standardConfig.calls.voice, mode: 'twilio_standard' },
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
134
178
|
const twilioConfig = getTwilioConfig();
|
|
135
179
|
let relayUrl: string;
|
|
136
180
|
try {
|
|
@@ -141,7 +185,7 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
|
|
|
141
185
|
}
|
|
142
186
|
const welcomeGreeting = process.env.CALL_WELCOME_GREETING ?? 'Hello, how can I help you today?';
|
|
143
187
|
|
|
144
|
-
const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting);
|
|
188
|
+
const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, profile);
|
|
145
189
|
|
|
146
190
|
log.info({ callSessionId }, 'Returning ConversationRelay TwiML');
|
|
147
191
|
|
package/src/calls/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type CallStatus = 'initiated' | 'ringing' | 'in_progress' | 'waiting_on_user' | 'completed' | 'failed' | 'cancelled';
|
|
2
|
-
export type CallEventType = 'call_started' | 'call_connected' | 'caller_spoke' | 'assistant_spoke' | 'user_question_asked' | 'user_answered' | 'call_ended' | 'call_failed';
|
|
2
|
+
export type CallEventType = 'call_started' | 'call_connected' | 'caller_spoke' | 'assistant_spoke' | 'user_question_asked' | 'user_answered' | 'user_instruction_relayed' | 'call_ended' | 'call_failed';
|
|
3
3
|
export type PendingQuestionStatus = 'pending' | 'answered' | 'expired' | 'cancelled';
|
|
4
4
|
|
|
5
5
|
export interface CallSession {
|
|
@@ -11,6 +11,8 @@ export interface CallSession {
|
|
|
11
11
|
toNumber: string;
|
|
12
12
|
task: string | null;
|
|
13
13
|
status: CallStatus;
|
|
14
|
+
callerIdentityMode: string | null;
|
|
15
|
+
callerIdentitySource: string | null;
|
|
14
16
|
startedAt: number | null;
|
|
15
17
|
endedAt: number | null;
|
|
16
18
|
lastError: string | null;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { loadConfig } from '../config/loader.js';
|
|
2
|
+
|
|
3
|
+
export interface VoiceQualityProfile {
|
|
4
|
+
mode: 'twilio_standard' | 'twilio_elevenlabs_tts' | 'elevenlabs_agent';
|
|
5
|
+
language: string;
|
|
6
|
+
transcriptionProvider: string;
|
|
7
|
+
ttsProvider: string;
|
|
8
|
+
voice: string;
|
|
9
|
+
agentId?: string;
|
|
10
|
+
fallbackToStandardOnError: boolean;
|
|
11
|
+
validationErrors: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build a Twilio-compatible ElevenLabs voice string.
|
|
16
|
+
*
|
|
17
|
+
* Twilio ConversationRelay accepts:
|
|
18
|
+
* - bare voiceId
|
|
19
|
+
* - voiceId-model-speed_stability_similarity
|
|
20
|
+
*
|
|
21
|
+
* We default to bare voiceId unless a model is explicitly configured.
|
|
22
|
+
* This avoids forcing model/tuning suffixes that may be rejected for some
|
|
23
|
+
* voice + model combinations.
|
|
24
|
+
*
|
|
25
|
+
* See: https://www.twilio.com/docs/voice/conversationrelay/voice-configuration
|
|
26
|
+
*/
|
|
27
|
+
export function buildElevenLabsVoiceSpec(config: {
|
|
28
|
+
voiceId: string;
|
|
29
|
+
voiceModelId?: string;
|
|
30
|
+
speed?: number;
|
|
31
|
+
stability?: number;
|
|
32
|
+
similarityBoost?: number;
|
|
33
|
+
}): string {
|
|
34
|
+
const voiceId = config.voiceId?.trim();
|
|
35
|
+
if (!voiceId) return '';
|
|
36
|
+
|
|
37
|
+
const voiceModelId = config.voiceModelId?.trim();
|
|
38
|
+
if (!voiceModelId) return voiceId;
|
|
39
|
+
|
|
40
|
+
const speed = config.speed ?? 1.0;
|
|
41
|
+
const stability = config.stability ?? 0.5;
|
|
42
|
+
const similarityBoost = config.similarityBoost ?? 0.75;
|
|
43
|
+
return `${voiceId}-${voiceModelId}-${speed}_${stability}_${similarityBoost}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the effective voice quality profile from config.
|
|
48
|
+
* Returns a profile with all resolved values ready for use by TwiML generation
|
|
49
|
+
* and call orchestration.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveVoiceQualityProfile(config?: ReturnType<typeof loadConfig>): VoiceQualityProfile {
|
|
52
|
+
const cfg = config ?? loadConfig();
|
|
53
|
+
const voice = cfg.calls.voice;
|
|
54
|
+
const errors: string[] = [];
|
|
55
|
+
|
|
56
|
+
// Default/standard profile
|
|
57
|
+
const standardProfile: VoiceQualityProfile = {
|
|
58
|
+
mode: 'twilio_standard',
|
|
59
|
+
language: voice.language,
|
|
60
|
+
transcriptionProvider: voice.transcriptionProvider,
|
|
61
|
+
ttsProvider: 'Google',
|
|
62
|
+
voice: 'Google.en-US-Journey-O',
|
|
63
|
+
fallbackToStandardOnError: voice.fallbackToStandardOnError,
|
|
64
|
+
validationErrors: [],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (voice.mode === 'twilio_standard') {
|
|
68
|
+
return standardProfile;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (voice.mode === 'twilio_elevenlabs_tts') {
|
|
72
|
+
if (!voice.elevenlabs.voiceId && !voice.fallbackToStandardOnError) {
|
|
73
|
+
errors.push('calls.voice.elevenlabs.voiceId is required for twilio_elevenlabs_tts mode when fallback is disabled');
|
|
74
|
+
}
|
|
75
|
+
if (!voice.elevenlabs.voiceId && voice.fallbackToStandardOnError) {
|
|
76
|
+
return { ...standardProfile, validationErrors: ['calls.voice.elevenlabs.voiceId is empty; falling back to twilio_standard'] };
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
mode: 'twilio_elevenlabs_tts',
|
|
80
|
+
language: voice.language,
|
|
81
|
+
transcriptionProvider: voice.transcriptionProvider,
|
|
82
|
+
ttsProvider: 'ElevenLabs',
|
|
83
|
+
voice: buildElevenLabsVoiceSpec(voice.elevenlabs),
|
|
84
|
+
fallbackToStandardOnError: voice.fallbackToStandardOnError,
|
|
85
|
+
validationErrors: errors,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (voice.mode === 'elevenlabs_agent') {
|
|
90
|
+
if (!voice.elevenlabs.agentId && !voice.fallbackToStandardOnError) {
|
|
91
|
+
errors.push('calls.voice.elevenlabs.agentId is required for elevenlabs_agent mode when fallback is disabled');
|
|
92
|
+
}
|
|
93
|
+
if (!voice.elevenlabs.agentId && voice.fallbackToStandardOnError) {
|
|
94
|
+
return { ...standardProfile, validationErrors: ['calls.voice.elevenlabs.agentId is empty; falling back to twilio_standard'] };
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
mode: 'elevenlabs_agent',
|
|
98
|
+
language: voice.language,
|
|
99
|
+
transcriptionProvider: voice.transcriptionProvider,
|
|
100
|
+
ttsProvider: 'ElevenLabs',
|
|
101
|
+
voice: buildElevenLabsVoiceSpec(voice.elevenlabs),
|
|
102
|
+
agentId: voice.elevenlabs.agentId,
|
|
103
|
+
fallbackToStandardOnError: voice.fallbackToStandardOnError,
|
|
104
|
+
validationErrors: errors,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return standardProfile;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Returns false when the profile has any validation errors. */
|
|
112
|
+
export function isVoiceProfileValid(profile: VoiceQualityProfile): boolean {
|
|
113
|
+
return profile.validationErrors.length === 0;
|
|
114
|
+
}
|
package/src/cli/twitter.ts
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
clearSession,
|
|
14
14
|
} from '../twitter/session.js';
|
|
15
15
|
import {
|
|
16
|
-
postTweet,
|
|
17
16
|
getUserByScreenName,
|
|
18
17
|
getUserTweets,
|
|
19
18
|
getTweetDetail,
|
|
@@ -27,6 +26,7 @@ import {
|
|
|
27
26
|
getUserMedia,
|
|
28
27
|
SessionExpiredError,
|
|
29
28
|
} from '../twitter/client.js';
|
|
29
|
+
import { routedPostTweet } from '../twitter/router.js';
|
|
30
30
|
import { getSocketPath, readSessionToken } from '../util/platform.js';
|
|
31
31
|
import {
|
|
32
32
|
serialize,
|
|
@@ -69,11 +69,32 @@ async function run(cmd: Command, fn: () => Promise<unknown>): Promise<void> {
|
|
|
69
69
|
getJson(cmd),
|
|
70
70
|
);
|
|
71
71
|
} catch (err) {
|
|
72
|
+
const meta = err as Record<string, unknown>;
|
|
72
73
|
if (err instanceof SessionExpiredError) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
// Preserve backward-compatible error code while surfacing router metadata
|
|
75
|
+
const payload: Record<string, unknown> = {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: 'session_expired',
|
|
78
|
+
message: SESSION_EXPIRED_MSG,
|
|
79
|
+
};
|
|
80
|
+
if (meta.pathUsed !== undefined) payload.pathUsed = meta.pathUsed;
|
|
81
|
+
if (meta.suggestAlternative !== undefined) payload.suggestAlternative = meta.suggestAlternative;
|
|
82
|
+
if (meta.oauthError !== undefined) payload.oauthError = meta.oauthError;
|
|
83
|
+
output(payload, getJson(cmd));
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// For routed errors with any router metadata, emit structured JSON
|
|
88
|
+
// so callers can see dual-path diagnostics (pathUsed, oauthError, etc.)
|
|
89
|
+
if (err instanceof Error && (meta.pathUsed !== undefined || meta.suggestAlternative !== undefined || meta.oauthError !== undefined)) {
|
|
90
|
+
const payload: Record<string, unknown> = {
|
|
91
|
+
ok: false,
|
|
92
|
+
error: err.message,
|
|
93
|
+
};
|
|
94
|
+
if (meta.pathUsed !== undefined) payload.pathUsed = meta.pathUsed;
|
|
95
|
+
if (meta.suggestAlternative !== undefined) payload.suggestAlternative = meta.suggestAlternative;
|
|
96
|
+
if (meta.oauthError !== undefined) payload.oauthError = meta.oauthError;
|
|
97
|
+
output(payload, getJson(cmd));
|
|
77
98
|
process.exitCode = 1;
|
|
78
99
|
return;
|
|
79
100
|
}
|
|
@@ -90,7 +111,7 @@ export function registerTwitterCommand(program: Command): void {
|
|
|
90
111
|
.command('x')
|
|
91
112
|
.alias('twitter')
|
|
92
113
|
.description(
|
|
93
|
-
'Post on X and manage
|
|
114
|
+
'Post on X and manage connections. Supports OAuth (official API) and browser session paths.',
|
|
94
115
|
)
|
|
95
116
|
.option('--json', 'Machine-readable JSON output');
|
|
96
117
|
|
|
@@ -172,25 +193,95 @@ export function registerTwitterCommand(program: Command): void {
|
|
|
172
193
|
});
|
|
173
194
|
|
|
174
195
|
// =========================================================================
|
|
175
|
-
// status — check session status
|
|
196
|
+
// status — check session status + OAuth and strategy info
|
|
176
197
|
// =========================================================================
|
|
177
198
|
tw.command('status')
|
|
178
|
-
.description('Check
|
|
179
|
-
.action((_opts: unknown, cmd: Command) => {
|
|
199
|
+
.description('Check Twitter session, OAuth, and strategy status')
|
|
200
|
+
.action(async (_opts: unknown, cmd: Command) => {
|
|
180
201
|
const session = loadSession();
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
ok: true,
|
|
185
|
-
loggedIn: true,
|
|
202
|
+
const browserInfo: Record<string, unknown> = session
|
|
203
|
+
? {
|
|
204
|
+
browserSessionActive: true,
|
|
186
205
|
cookieCount: session.cookies.length,
|
|
187
206
|
importedAt: session.importedAt,
|
|
188
207
|
recordingId: session.recordingId,
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
208
|
+
}
|
|
209
|
+
: { browserSessionActive: false };
|
|
210
|
+
|
|
211
|
+
// Query daemon for OAuth / strategy config
|
|
212
|
+
let oauthInfo: Record<string, unknown> = {};
|
|
213
|
+
try {
|
|
214
|
+
const daemonResponse = await sendDaemonMessage({
|
|
215
|
+
type: 'twitter_integration_config',
|
|
216
|
+
action: 'get',
|
|
217
|
+
} as import('../daemon/ipc-protocol.js').ClientMessage, 'twitter_integration_config_response');
|
|
218
|
+
const r = daemonResponse as Record<string, unknown>;
|
|
219
|
+
oauthInfo = {
|
|
220
|
+
oauthConnected: r.connected ?? false,
|
|
221
|
+
oauthAccount: r.accountInfo ?? undefined,
|
|
222
|
+
preferredStrategy: r.strategy ?? 'auto',
|
|
223
|
+
strategyConfigured: r.strategyConfigured ?? false,
|
|
224
|
+
};
|
|
225
|
+
} catch {
|
|
226
|
+
// Daemon may not be running; report what we can from the local session
|
|
227
|
+
oauthInfo = {
|
|
228
|
+
oauthConnected: undefined,
|
|
229
|
+
oauthAccount: undefined,
|
|
230
|
+
preferredStrategy: undefined,
|
|
231
|
+
strategyConfigured: undefined,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
output(
|
|
236
|
+
{
|
|
237
|
+
ok: true,
|
|
238
|
+
loggedIn: !!session,
|
|
239
|
+
...browserInfo,
|
|
240
|
+
...oauthInfo,
|
|
241
|
+
},
|
|
242
|
+
getJson(cmd),
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// =========================================================================
|
|
247
|
+
// strategy — get or set the Twitter operation strategy
|
|
248
|
+
// =========================================================================
|
|
249
|
+
const strategyCli = tw.command('strategy')
|
|
250
|
+
.description('Get or set the Twitter operation strategy (oauth, browser, auto)')
|
|
251
|
+
.action(async (_opts: unknown, cmd: Command) => {
|
|
252
|
+
const json = getJson(cmd);
|
|
253
|
+
try {
|
|
254
|
+
const daemonResponse = await sendDaemonMessage({
|
|
255
|
+
type: 'twitter_integration_config',
|
|
256
|
+
action: 'get_strategy',
|
|
257
|
+
} as import('../daemon/ipc-protocol.js').ClientMessage, 'twitter_integration_config_response');
|
|
258
|
+
const r = daemonResponse as Record<string, unknown>;
|
|
259
|
+
output({ ok: true, strategy: r.strategy ?? 'auto' }, json);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
outputError(err instanceof Error ? err.message : String(err));
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
strategyCli.command('set')
|
|
266
|
+
.description('Set the Twitter operation strategy')
|
|
267
|
+
.argument('<value>', 'Strategy value: oauth, browser, or auto')
|
|
268
|
+
.action(async (value: string, _opts: unknown, cmd: Command) => {
|
|
269
|
+
const json = getJson(cmd);
|
|
270
|
+
try {
|
|
271
|
+
const daemonResponse = await sendDaemonMessage({
|
|
272
|
+
type: 'twitter_integration_config',
|
|
273
|
+
action: 'set_strategy',
|
|
274
|
+
strategy: value,
|
|
275
|
+
} as import('../daemon/ipc-protocol.js').ClientMessage, 'twitter_integration_config_response');
|
|
276
|
+
const r = daemonResponse as Record<string, unknown>;
|
|
277
|
+
if (r.success) {
|
|
278
|
+
output({ ok: true, strategy: r.strategy }, json);
|
|
279
|
+
} else {
|
|
280
|
+
output({ ok: false, error: r.error ?? 'Failed to set strategy' }, json);
|
|
281
|
+
process.exitCode = 1;
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
outputError(err instanceof Error ? err.message : String(err));
|
|
194
285
|
}
|
|
195
286
|
});
|
|
196
287
|
|
|
@@ -202,11 +293,12 @@ export function registerTwitterCommand(program: Command): void {
|
|
|
202
293
|
.argument('<text>', 'Tweet text')
|
|
203
294
|
.action(async (text: string, _opts: unknown, cmd: Command) => {
|
|
204
295
|
await run(cmd, async () => {
|
|
205
|
-
const result = await
|
|
296
|
+
const { result, pathUsed } = await routedPostTweet(text);
|
|
206
297
|
return {
|
|
207
298
|
tweetId: result.tweetId,
|
|
208
299
|
text: result.text,
|
|
209
300
|
url: result.url,
|
|
301
|
+
pathUsed,
|
|
210
302
|
};
|
|
211
303
|
});
|
|
212
304
|
});
|
|
@@ -226,12 +318,13 @@ export function registerTwitterCommand(program: Command): void {
|
|
|
226
318
|
throw new Error(`Could not extract tweet ID from: ${tweetUrl}`);
|
|
227
319
|
}
|
|
228
320
|
const inReplyToTweetId = idMatch[1];
|
|
229
|
-
const result = await
|
|
321
|
+
const { result, pathUsed } = await routedPostTweet(text, { inReplyToTweetId });
|
|
230
322
|
return {
|
|
231
323
|
tweetId: result.tweetId,
|
|
232
324
|
text: result.text,
|
|
233
325
|
url: result.url,
|
|
234
326
|
inReplyToTweetId,
|
|
327
|
+
pathUsed,
|
|
235
328
|
};
|
|
236
329
|
});
|
|
237
330
|
});
|
|
@@ -382,6 +475,92 @@ export function registerTwitterCommand(program: Command): void {
|
|
|
382
475
|
});
|
|
383
476
|
}
|
|
384
477
|
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
// Daemon IPC helper — send a message and wait for the first response
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
|
|
482
|
+
function sendDaemonMessage(
|
|
483
|
+
message: import('../daemon/ipc-protocol.js').ClientMessage,
|
|
484
|
+
expectedResponseType: string,
|
|
485
|
+
): Promise<Record<string, unknown>> {
|
|
486
|
+
return new Promise((resolve, reject) => {
|
|
487
|
+
const socketPath = getSocketPath();
|
|
488
|
+
const sessionToken = readSessionToken();
|
|
489
|
+
const socket = net.createConnection(socketPath);
|
|
490
|
+
const parser = createMessageParser();
|
|
491
|
+
|
|
492
|
+
const timeoutHandle = setTimeout(() => {
|
|
493
|
+
socket.destroy();
|
|
494
|
+
reject(new Error('Daemon request timed out after 10s'));
|
|
495
|
+
}, 10_000);
|
|
496
|
+
timeoutHandle.unref();
|
|
497
|
+
|
|
498
|
+
let authenticated = !sessionToken;
|
|
499
|
+
let messageSent = false;
|
|
500
|
+
|
|
501
|
+
const sendPayload = () => {
|
|
502
|
+
if (messageSent) return;
|
|
503
|
+
messageSent = true;
|
|
504
|
+
socket.write(serialize(message));
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
socket.on('error', (err) => {
|
|
508
|
+
clearTimeout(timeoutHandle);
|
|
509
|
+
reject(new Error(`Cannot connect to daemon: ${err.message}. Is the daemon running?`));
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
socket.on('data', (chunk) => {
|
|
513
|
+
const messages = parser.feed(chunk.toString('utf-8'));
|
|
514
|
+
for (const msg of messages) {
|
|
515
|
+
const m = msg as unknown as Record<string, unknown>;
|
|
516
|
+
|
|
517
|
+
if (!authenticated && m.type === 'auth_result') {
|
|
518
|
+
if ((m as { success: boolean }).success) {
|
|
519
|
+
authenticated = true;
|
|
520
|
+
sendPayload();
|
|
521
|
+
} else {
|
|
522
|
+
clearTimeout(timeoutHandle);
|
|
523
|
+
socket.destroy();
|
|
524
|
+
reject(new Error('Daemon authentication failed'));
|
|
525
|
+
}
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Reject immediately on daemon error frames so the CLI surfaces the
|
|
530
|
+
// real failure reason instead of hanging until the timeout fires.
|
|
531
|
+
if (m.type === 'error') {
|
|
532
|
+
clearTimeout(timeoutHandle);
|
|
533
|
+
socket.destroy();
|
|
534
|
+
reject(new Error((m as { message?: string }).message ?? 'Daemon returned an error'));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Only resolve on the expected response type; skip everything else
|
|
539
|
+
if (m.type === expectedResponseType) {
|
|
540
|
+
clearTimeout(timeoutHandle);
|
|
541
|
+
socket.destroy();
|
|
542
|
+
resolve(m);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
// Skip all other message types (auth_result, daemon_status, pong, session_info, tasks_changed, etc.)
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
socket.on('connect', () => {
|
|
550
|
+
if (sessionToken) {
|
|
551
|
+
socket.write(
|
|
552
|
+
serialize({
|
|
553
|
+
type: 'auth',
|
|
554
|
+
token: sessionToken,
|
|
555
|
+
} as unknown as import('../daemon/ipc-protocol.js').ClientMessage),
|
|
556
|
+
);
|
|
557
|
+
} else {
|
|
558
|
+
sendPayload();
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
385
564
|
// ---------------------------------------------------------------------------
|
|
386
565
|
// Chrome CDP restart helper
|
|
387
566
|
// ---------------------------------------------------------------------------
|
package/src/cli.ts
CHANGED
|
@@ -20,7 +20,6 @@ import { ensureDaemonRunning } from './daemon/lifecycle.js';
|
|
|
20
20
|
import { shouldAutoStartDaemon } from './daemon/connection-policy.js';
|
|
21
21
|
import { renderMainScreen, updateStatusText, updateDaemonText, type MainScreenLayout } from './cli/main-screen.jsx';
|
|
22
22
|
|
|
23
|
-
const SHORT_HASH_LENGTH = 8;
|
|
24
23
|
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
25
24
|
const HEARTBEAT_TIMEOUT_MS = 10_000;
|
|
26
25
|
const RECONNECT_BASE_DELAY_MS = 1_000;
|
|
@@ -43,21 +42,6 @@ export function sanitizeUrlForDisplay(rawUrl: unknown): string {
|
|
|
43
42
|
}
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
/**
|
|
47
|
-
* Format a human-readable principal tag for display in permission prompts.
|
|
48
|
-
* Core principals are omitted (empty string) since they're the default.
|
|
49
|
-
* Skill principals show the skill name, shortened version hash, and target.
|
|
50
|
-
*/
|
|
51
|
-
export function formatPrincipalTag(req: Pick<ConfirmationRequest, 'principalKind' | 'principalId' | 'principalVersion' | 'executionTarget'>): string {
|
|
52
|
-
if (!req.principalKind || req.principalKind === 'core') return '';
|
|
53
|
-
const name = req.principalId ?? req.principalKind;
|
|
54
|
-
// Show a shortened version hash when available (first 8 hex chars after any scheme prefix)
|
|
55
|
-
const versionSuffix = req.principalVersion
|
|
56
|
-
? `@${req.principalVersion.replace(/^[^:]+:/, '').slice(0, SHORT_HASH_LENGTH)}`
|
|
57
|
-
: '';
|
|
58
|
-
const target = req.executionTarget ? ` \u2192 ${req.executionTarget}` : '';
|
|
59
|
-
return `[${req.principalKind}: ${name}${versionSuffix}${target}]`;
|
|
60
|
-
}
|
|
61
45
|
|
|
62
46
|
export async function startCli(): Promise<void> {
|
|
63
47
|
const socketPath = getSocketPath();
|
|
@@ -186,13 +170,10 @@ export async function startCli(): Promise<void> {
|
|
|
186
170
|
|
|
187
171
|
function renderConfirmationPrompt(req: ConfirmationRequest): void {
|
|
188
172
|
const preview = formatCommandPreview(req);
|
|
189
|
-
const principalTag = formatPrincipalTag(req);
|
|
190
173
|
process.stdout.write('\n');
|
|
191
174
|
process.stdout.write(`\u250C ${req.toolName}: ${preview}\n`);
|
|
192
175
|
process.stdout.write(`\u2502 Risk: ${req.riskLevel}${req.sandboxed ? ' [sandboxed]' : ''}\n`);
|
|
193
|
-
if (
|
|
194
|
-
process.stdout.write(`\u2502 Principal: ${principalTag}\n`);
|
|
195
|
-
} else if (req.executionTarget) {
|
|
176
|
+
if (req.executionTarget) {
|
|
196
177
|
process.stdout.write(`\u2502 Target: ${req.executionTarget}\n`);
|
|
197
178
|
}
|
|
198
179
|
if (req.diff) {
|