vellum 0.2.11 → 0.2.13
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/bun.lock +6 -2
- package/package.json +2 -2
- package/src/__tests__/call-orchestrator.test.ts +58 -0
- package/src/__tests__/config-schema.test.ts +278 -0
- package/src/__tests__/elevenlabs-client.test.ts +209 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +9 -35
- package/src/__tests__/oauth2-gateway-transport.test.ts +14 -33
- package/src/__tests__/skills.test.ts +2 -2
- package/src/__tests__/trust-store.test.ts +1 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +127 -0
- package/src/__tests__/twilio-routes.test.ts +78 -153
- package/src/__tests__/twitter-auth-handler.test.ts +1 -1
- package/src/calls/call-orchestrator.ts +3 -1
- package/src/calls/elevenlabs-client.ts +89 -0
- package/src/calls/elevenlabs-config.ts +29 -0
- package/src/calls/twilio-routes.ts +55 -6
- package/src/calls/voice-quality.ts +92 -0
- package/src/cli/main-screen.tsx +15 -117
- package/src/config/bundled-skills/macos-automation/SKILL.md +66 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +414 -0
- package/src/config/defaults.ts +18 -0
- package/src/config/schema.ts +110 -0
- package/src/config/system-prompt.ts +9 -59
- package/src/config/types.ts +2 -0
- package/src/daemon/lifecycle.ts +20 -7
- package/src/memory/db.ts +36 -0
- package/src/permissions/defaults.ts +11 -0
- package/src/runtime/routes/conversation-routes.ts +12 -5
- package/src/security/oauth2.ts +8 -8
- package/src/util/logger.ts +4 -4
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getConfig } from '../config/loader.js';
|
|
2
|
+
import { getSecureKey } from '../security/secure-keys.js';
|
|
3
|
+
import { getLogger } from '../util/logger.js';
|
|
4
|
+
|
|
5
|
+
const log = getLogger('elevenlabs-config');
|
|
6
|
+
|
|
7
|
+
export interface ElevenLabsConfig {
|
|
8
|
+
apiKey: string;
|
|
9
|
+
apiBaseUrl: string;
|
|
10
|
+
agentId: string;
|
|
11
|
+
registerCallTimeoutMs: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getElevenLabsConfig(): ElevenLabsConfig {
|
|
15
|
+
const config = getConfig();
|
|
16
|
+
const voice = config.calls.voice;
|
|
17
|
+
|
|
18
|
+
const apiKey = getSecureKey('credential:elevenlabs:api_key') ?? '';
|
|
19
|
+
if (!apiKey) {
|
|
20
|
+
log.warn('No ElevenLabs API key found in secure key store (credential:elevenlabs:api_key)');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
apiKey,
|
|
25
|
+
apiBaseUrl: voice.elevenlabs.apiBaseUrl,
|
|
26
|
+
agentId: voice.elevenlabs.agentId,
|
|
27
|
+
registerCallTimeoutMs: voice.elevenlabs.registerCallTimeoutMs,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -25,6 +25,9 @@ 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 } from './voice-quality.js';
|
|
29
|
+
import { getElevenLabsConfig } from './elevenlabs-config.js';
|
|
30
|
+
import { ElevenLabsClient } from './elevenlabs-client.js';
|
|
28
31
|
|
|
29
32
|
const log = getLogger('twilio-routes');
|
|
30
33
|
|
|
@@ -39,17 +42,22 @@ function escapeXml(str: string): string {
|
|
|
39
42
|
.replace(/'/g, ''');
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
function generateTwiML(
|
|
45
|
+
export function generateTwiML(
|
|
46
|
+
callSessionId: string,
|
|
47
|
+
relayUrl: string,
|
|
48
|
+
welcomeGreeting: string,
|
|
49
|
+
profile: { language: string; transcriptionProvider: string; ttsProvider: string; voice: string },
|
|
50
|
+
): string {
|
|
43
51
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
44
52
|
<Response>
|
|
45
53
|
<Connect>
|
|
46
54
|
<ConversationRelay
|
|
47
55
|
url="${escapeXml(relayUrl)}?callSessionId=${escapeXml(callSessionId)}"
|
|
48
56
|
welcomeGreeting="${escapeXml(welcomeGreeting)}"
|
|
49
|
-
voice="
|
|
50
|
-
language="
|
|
51
|
-
transcriptionProvider="
|
|
52
|
-
ttsProvider="
|
|
57
|
+
voice="${escapeXml(profile.voice)}"
|
|
58
|
+
language="${escapeXml(profile.language)}"
|
|
59
|
+
transcriptionProvider="${escapeXml(profile.transcriptionProvider)}"
|
|
60
|
+
ttsProvider="${escapeXml(profile.ttsProvider)}"
|
|
53
61
|
interruptible="true"
|
|
54
62
|
dtmfDetection="true"
|
|
55
63
|
/>
|
|
@@ -131,6 +139,14 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
|
|
|
131
139
|
log.info({ callSessionId, callSid }, 'Stored CallSid from voice webhook');
|
|
132
140
|
}
|
|
133
141
|
|
|
142
|
+
const profile = resolveVoiceQualityProfile(loadConfig());
|
|
143
|
+
|
|
144
|
+
log.info({ callSessionId, mode: profile.mode, ttsProvider: profile.ttsProvider, voice: profile.voice }, 'Voice quality profile resolved');
|
|
145
|
+
|
|
146
|
+
if (profile.validationErrors.length > 0) {
|
|
147
|
+
log.warn({ callSessionId, errors: profile.validationErrors }, 'Voice quality profile has validation warnings');
|
|
148
|
+
}
|
|
149
|
+
|
|
134
150
|
const twilioConfig = getTwilioConfig();
|
|
135
151
|
let relayUrl: string;
|
|
136
152
|
try {
|
|
@@ -141,7 +157,40 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
|
|
|
141
157
|
}
|
|
142
158
|
const welcomeGreeting = process.env.CALL_WELCOME_GREETING ?? 'Hello, how can I help you today?';
|
|
143
159
|
|
|
144
|
-
|
|
160
|
+
if (profile.mode === 'elevenlabs_agent') {
|
|
161
|
+
try {
|
|
162
|
+
const elevenLabsConfig = getElevenLabsConfig();
|
|
163
|
+
const client = new ElevenLabsClient({
|
|
164
|
+
apiBaseUrl: elevenLabsConfig.apiBaseUrl,
|
|
165
|
+
apiKey: elevenLabsConfig.apiKey,
|
|
166
|
+
timeoutMs: elevenLabsConfig.registerCallTimeoutMs,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const result = await client.registerCall({
|
|
170
|
+
agent_id: elevenLabsConfig.agentId,
|
|
171
|
+
from_number: formBody.get('From') || session.fromNumber,
|
|
172
|
+
to_number: formBody.get('To') || session.toNumber,
|
|
173
|
+
direction: 'outbound',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
log.info({ callSessionId }, 'ElevenLabs register-call succeeded');
|
|
177
|
+
return new Response(result.twiml, {
|
|
178
|
+
status: 200,
|
|
179
|
+
headers: { 'Content-Type': 'text/xml' },
|
|
180
|
+
});
|
|
181
|
+
} catch (err) {
|
|
182
|
+
log.error({ err, callSessionId }, 'ElevenLabs register-call failed');
|
|
183
|
+
if (profile.fallbackToStandardOnError) {
|
|
184
|
+
log.warn({ callSessionId }, 'Falling back to twilio_standard mode');
|
|
185
|
+
const standardProfile = resolveVoiceQualityProfile({ ...loadConfig(), calls: { ...loadConfig().calls, voice: { ...loadConfig().calls.voice, mode: 'twilio_standard' } } });
|
|
186
|
+
const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, standardProfile);
|
|
187
|
+
return new Response(twiml, { status: 200, headers: { 'Content-Type': 'text/xml' } });
|
|
188
|
+
}
|
|
189
|
+
return new Response('ElevenLabs service unavailable', { status: 502 });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, profile);
|
|
145
194
|
|
|
146
195
|
log.info({ callSessionId }, 'Returning ConversationRelay TwiML');
|
|
147
196
|
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
* Format: voiceId or voiceId-modelId-stability_similarity_style
|
|
17
|
+
*/
|
|
18
|
+
export function buildElevenLabsVoiceSpec(config: {
|
|
19
|
+
voiceId: string;
|
|
20
|
+
voiceModelId: string;
|
|
21
|
+
stability: number;
|
|
22
|
+
similarityBoost: number;
|
|
23
|
+
style: number;
|
|
24
|
+
}): string {
|
|
25
|
+
if (!config.voiceId) return '';
|
|
26
|
+
return `${config.voiceId}-${config.voiceModelId}-${config.stability}_${config.similarityBoost}_${config.style}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the effective voice quality profile from config.
|
|
31
|
+
* Returns a profile with all resolved values ready for use by TwiML generation
|
|
32
|
+
* and call orchestration.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveVoiceQualityProfile(config?: ReturnType<typeof loadConfig>): VoiceQualityProfile {
|
|
35
|
+
const cfg = config ?? loadConfig();
|
|
36
|
+
const voice = cfg.calls.voice;
|
|
37
|
+
const errors: string[] = [];
|
|
38
|
+
|
|
39
|
+
// Default/standard profile
|
|
40
|
+
const standardProfile: VoiceQualityProfile = {
|
|
41
|
+
mode: 'twilio_standard',
|
|
42
|
+
language: voice.language,
|
|
43
|
+
transcriptionProvider: voice.transcriptionProvider,
|
|
44
|
+
ttsProvider: 'Google',
|
|
45
|
+
voice: 'Google.en-US-Journey-O',
|
|
46
|
+
fallbackToStandardOnError: voice.fallbackToStandardOnError,
|
|
47
|
+
validationErrors: [],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (voice.mode === 'twilio_standard') {
|
|
51
|
+
return standardProfile;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (voice.mode === 'twilio_elevenlabs_tts') {
|
|
55
|
+
if (!voice.elevenlabs.voiceId && !voice.fallbackToStandardOnError) {
|
|
56
|
+
errors.push('calls.voice.elevenlabs.voiceId is required for twilio_elevenlabs_tts mode when fallback is disabled');
|
|
57
|
+
}
|
|
58
|
+
if (!voice.elevenlabs.voiceId && voice.fallbackToStandardOnError) {
|
|
59
|
+
return { ...standardProfile, validationErrors: ['calls.voice.elevenlabs.voiceId is empty; falling back to twilio_standard'] };
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
mode: 'twilio_elevenlabs_tts',
|
|
63
|
+
language: voice.language,
|
|
64
|
+
transcriptionProvider: voice.transcriptionProvider,
|
|
65
|
+
ttsProvider: 'ElevenLabs',
|
|
66
|
+
voice: buildElevenLabsVoiceSpec(voice.elevenlabs),
|
|
67
|
+
fallbackToStandardOnError: voice.fallbackToStandardOnError,
|
|
68
|
+
validationErrors: errors,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (voice.mode === 'elevenlabs_agent') {
|
|
73
|
+
if (!voice.elevenlabs.agentId && !voice.fallbackToStandardOnError) {
|
|
74
|
+
errors.push('calls.voice.elevenlabs.agentId is required for elevenlabs_agent mode when fallback is disabled');
|
|
75
|
+
}
|
|
76
|
+
if (!voice.elevenlabs.agentId && voice.fallbackToStandardOnError) {
|
|
77
|
+
return { ...standardProfile, validationErrors: ['calls.voice.elevenlabs.agentId is empty; falling back to twilio_standard'] };
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
mode: 'elevenlabs_agent',
|
|
81
|
+
language: voice.language,
|
|
82
|
+
transcriptionProvider: voice.transcriptionProvider,
|
|
83
|
+
ttsProvider: 'ElevenLabs',
|
|
84
|
+
voice: buildElevenLabsVoiceSpec(voice.elevenlabs),
|
|
85
|
+
agentId: voice.elevenlabs.agentId,
|
|
86
|
+
fallbackToStandardOnError: voice.fallbackToStandardOnError,
|
|
87
|
+
validationErrors: errors,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return standardProfile;
|
|
92
|
+
}
|
package/src/cli/main-screen.tsx
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { Box, render as inkRender, Text } from "ink";
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
4
3
|
import { getSocketPath, getWorkspaceDir } from "../util/platform.js";
|
|
5
|
-
import { APP_VERSION } from "../version.js";
|
|
6
4
|
|
|
7
5
|
const LEFT_PANEL_WIDTH = 36;
|
|
8
|
-
|
|
9
|
-
const VELLY_ART = [
|
|
10
|
-
" ,___,",
|
|
11
|
-
" ( O O )",
|
|
12
|
-
" /)V(\\",
|
|
13
|
-
" // \\\\",
|
|
14
|
-
' /" "\\',
|
|
15
|
-
" ^ ^",
|
|
16
|
-
];
|
|
6
|
+
const RIGHT_LINE_COUNT = 11;
|
|
17
7
|
|
|
18
8
|
export interface MainScreenLayout {
|
|
19
9
|
height: number;
|
|
@@ -21,116 +11,24 @@ export interface MainScreenLayout {
|
|
|
21
11
|
statusCol: number;
|
|
22
12
|
}
|
|
23
13
|
|
|
24
|
-
function
|
|
14
|
+
export function renderMainScreen(): MainScreenLayout {
|
|
25
15
|
const socketPath = getSocketPath();
|
|
26
16
|
const workspace = getWorkspaceDir();
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const tips = [
|
|
30
|
-
"Send a message to start chatting",
|
|
31
|
-
"Use /help to see available commands",
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
const leftLines = [
|
|
35
|
-
" ",
|
|
36
|
-
" Meet your Assistant!",
|
|
37
|
-
" ",
|
|
38
|
-
...VELLY_ART.map((l) => ` ${l}`),
|
|
39
|
-
" ",
|
|
40
|
-
` ${socketPath}`,
|
|
41
|
-
` ~/${dirName}`,
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
const rightLines = [
|
|
45
|
-
" ",
|
|
46
|
-
"Tips for getting started",
|
|
47
|
-
...tips,
|
|
48
|
-
" ",
|
|
49
|
-
"Daemon",
|
|
50
|
-
"connecting...",
|
|
51
|
-
"Version",
|
|
52
|
-
APP_VERSION,
|
|
53
|
-
"Status",
|
|
54
|
-
"checking...",
|
|
55
|
-
];
|
|
17
|
+
const assistantId = workspace.split("/").pop() ?? "vellum";
|
|
56
18
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const line = leftLines[i] ?? " ";
|
|
66
|
-
if (i === 1) {
|
|
67
|
-
return (
|
|
68
|
-
<Text key={i} bold>
|
|
69
|
-
{line}
|
|
70
|
-
</Text>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
if (i > 2 && i <= 2 + VELLY_ART.length) {
|
|
74
|
-
return (
|
|
75
|
-
<Text key={i} color="magenta">
|
|
76
|
-
{line}
|
|
77
|
-
</Text>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
if (i > 2 + VELLY_ART.length) {
|
|
81
|
-
return (
|
|
82
|
-
<Text key={i} dimColor>
|
|
83
|
-
{line}
|
|
84
|
-
</Text>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
return <Text key={i}>{line}</Text>;
|
|
88
|
-
})}
|
|
89
|
-
</Box>
|
|
90
|
-
<Box flexDirection="column">
|
|
91
|
-
{Array.from({ length: maxLines }, (_, i) => {
|
|
92
|
-
const line = rightLines[i] ?? " ";
|
|
93
|
-
const isHeading = i === 1 || i === 6;
|
|
94
|
-
const isDim = i === 5 || i === 7 || i === 9;
|
|
95
|
-
if (isHeading) {
|
|
96
|
-
return (
|
|
97
|
-
<Text key={i} color="magenta">
|
|
98
|
-
{line}
|
|
99
|
-
</Text>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
if (isDim) {
|
|
103
|
-
return (
|
|
104
|
-
<Text key={i} dimColor>
|
|
105
|
-
{line}
|
|
106
|
-
</Text>
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
return <Text key={i}>{line}</Text>;
|
|
110
|
-
})}
|
|
111
|
-
</Box>
|
|
112
|
-
</Box>
|
|
113
|
-
<Text dimColor>{"─".repeat(72)}</Text>
|
|
114
|
-
<Text> </Text>
|
|
115
|
-
<Text dimColor> ? for shortcuts</Text>
|
|
116
|
-
<Text> </Text>
|
|
117
|
-
</Box>
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function renderMainScreen(): MainScreenLayout {
|
|
122
|
-
const leftLineCount = 3 + VELLY_ART.length + 3;
|
|
123
|
-
const rightLineCount = 11;
|
|
124
|
-
const maxLines = Math.max(leftLineCount, rightLineCount);
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const cliPkgPath = require.resolve("@vellumai/cli/package.json");
|
|
21
|
+
const cliRoot = dirname(cliPkgPath);
|
|
22
|
+
// Dynamic require to bypass NodeNext strict module resolution for the
|
|
23
|
+
// CLI package which ships raw TypeScript with bundler-style imports.
|
|
24
|
+
const { render } = require(join(cliRoot, "src", "components", "DefaultMainScreen.tsx")) as {
|
|
25
|
+
render: (runtimeUrl: string, assistantId: string, species: string) => number;
|
|
26
|
+
};
|
|
125
27
|
|
|
126
|
-
const
|
|
127
|
-
exitOnCtrlC: false,
|
|
128
|
-
});
|
|
129
|
-
unmount();
|
|
28
|
+
const height = render(socketPath, assistantId, "vellum");
|
|
130
29
|
|
|
131
|
-
const statusCanvasLine =
|
|
30
|
+
const statusCanvasLine = RIGHT_LINE_COUNT + 1;
|
|
132
31
|
const statusCol = LEFT_PANEL_WIDTH + 1;
|
|
133
|
-
const height = 1 + maxLines + 4;
|
|
134
32
|
|
|
135
33
|
return { height, statusLine: statusCanvasLine, statusCol };
|
|
136
34
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "macOS Automation"
|
|
3
|
+
description: "Automate native macOS apps and system interactions via osascript (AppleScript)"
|
|
4
|
+
user-invocable: false
|
|
5
|
+
disable-model-invocation: false
|
|
6
|
+
metadata: {"vellum": {"emoji": "🍎", "os": ["darwin"]}}
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Use this skill to interact with native macOS apps and system-level features via `osascript` (AppleScript) through `host_bash`. Always prefer osascript over browser automation or computer-use for anything involving a native macOS app.
|
|
10
|
+
|
|
11
|
+
## Supported Apps
|
|
12
|
+
|
|
13
|
+
**Communication:** Messages, Mail, Microsoft Outlook, FaceTime
|
|
14
|
+
**Contacts & Calendar:** Contacts, Calendar, Reminders
|
|
15
|
+
**Notes & Writing:** Notes, TextEdit, Pages, BBEdit, CotEditor
|
|
16
|
+
**Files:** Finder, Path Finder
|
|
17
|
+
**Browsers:** Safari, Google Chrome
|
|
18
|
+
**Music & Media:** Music (iTunes), Spotify, VLC, Podcasts, TV
|
|
19
|
+
**Productivity:** OmniFocus, Things 3, OmniOutliner, OmniPlan, OmniGraffle
|
|
20
|
+
**Office:** Microsoft Word, Microsoft Excel, Numbers, Keynote
|
|
21
|
+
**Developer tools:** Xcode, Terminal, iTerm2, Script Editor
|
|
22
|
+
**System:** System Events (UI scripting for any app), System Settings
|
|
23
|
+
**Automation:** Keyboard Maestro, Alfred, Automator
|
|
24
|
+
**Creative:** Adobe Photoshop, Final Cut Pro
|
|
25
|
+
|
|
26
|
+
For any unlisted app, check scriptability first:
|
|
27
|
+
```bash
|
|
28
|
+
osascript -e 'tell application "AppName" to get name'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Examples
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Send an iMessage
|
|
35
|
+
osascript -e 'tell application "Messages" to send "Hello!" to buddy "user@example.com"'
|
|
36
|
+
|
|
37
|
+
# Look up a contact
|
|
38
|
+
osascript -e 'tell application "Contacts" to get {name, phones} of every person whose name contains "Marina"'
|
|
39
|
+
|
|
40
|
+
# Read upcoming calendar events
|
|
41
|
+
osascript -e 'tell application "Calendar" to get summary of every event of calendar "Home" whose start date > (current date)'
|
|
42
|
+
|
|
43
|
+
# Create a reminder
|
|
44
|
+
osascript -e 'tell application "Reminders" to make new reminder with properties {name:"Buy milk", due date:((current date) + 1 * hours)}'
|
|
45
|
+
|
|
46
|
+
# Send an email
|
|
47
|
+
osascript -e 'tell application "Mail" to send (make new outgoing message with properties {subject:"Hi", content:"Hello", visible:true})'
|
|
48
|
+
|
|
49
|
+
# Create a note
|
|
50
|
+
osascript -e 'tell application "Notes" to make new note at folder "Notes" with properties {body:"My note"}'
|
|
51
|
+
|
|
52
|
+
# Open a URL in Safari
|
|
53
|
+
osascript -e 'tell application "Safari" to open location "https://example.com"'
|
|
54
|
+
|
|
55
|
+
# Play/pause Music
|
|
56
|
+
osascript -e 'tell application "Music" to playpause'
|
|
57
|
+
|
|
58
|
+
# Display a system notification
|
|
59
|
+
osascript -e 'display notification "Done!" with title "Vellum"'
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Tips
|
|
63
|
+
|
|
64
|
+
- For multi-line scripts, write them to a `.applescript` file and run with `osascript path/to/script.applescript`
|
|
65
|
+
- Use `System Events` for UI scripting apps that don't have their own AppleScript dictionary
|
|
66
|
+
- AppleScript permissions are gated by macOS TCC — if a command fails with a permission error, use `request_system_permission` to prompt the user
|