@2en/clawly-plugins 1.30.0-beta.4 → 1.30.0-beta.6
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/clawly-config-defaults.json5 +14 -0
- package/gateway/config-model.ts +68 -0
- package/gateway/config-timezone.ts +19 -14
- package/gateway/cron-delivery.test.ts +16 -0
- package/gateway/index.ts +2 -0
- package/gateway/offline-push.test.ts +43 -0
- package/gateway/offline-push.ts +8 -8
- package/gateway/presence.ts +7 -4
- package/gateway-fetch.ts +12 -5
- package/index.ts +1 -1
- package/lib/calendar-cache.ts +4 -0
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- package/tools/clawly-calendar.ts +44 -0
|
@@ -179,9 +179,23 @@
|
|
|
179
179
|
},
|
|
180
180
|
},
|
|
181
181
|
|
|
182
|
+
// gateway.reload — use "hot" mode to prevent spurious SIGUSR1 restarts.
|
|
183
|
+
// config-setup writes restart-requiring keys (commands.*, gateway.*, env.*)
|
|
184
|
+
// after the gateway has already captured its startup config snapshot.
|
|
185
|
+
// In "hybrid" (default) mode, any subsequent config file write triggers the
|
|
186
|
+
// file watcher to diff against the stale snapshot, discover the config-setup
|
|
187
|
+
// changes as "new", and issue SIGUSR1. "hot" mode makes the watcher ignore
|
|
188
|
+
// restart-requiring diffs — only hot-reloadable changes (heartbeat, cron,
|
|
189
|
+
// hooks, browser) are applied. Direct SIGUSR1 from explicit restart commands
|
|
190
|
+
// and plugin auto-update is unaffected. If you manually edit a restart-
|
|
191
|
+
// requiring field in openclaw.json, run `openclaw restart` to apply it.
|
|
192
|
+
//
|
|
182
193
|
// gateway.nodes — allowlist dangerous node commands so the AI agent can
|
|
183
194
|
// invoke them on connected Clawly nodes (browser, reminders, calendar, device).
|
|
184
195
|
gateway: {
|
|
196
|
+
reload: {
|
|
197
|
+
mode: "hot",
|
|
198
|
+
},
|
|
185
199
|
nodes: {
|
|
186
200
|
allowCommands: [
|
|
187
201
|
// Browser commands (Mac nodes)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model switch RPC: writes agents.defaults.model.primary to openclaw.json
|
|
3
|
+
* without triggering a gateway restart.
|
|
4
|
+
*
|
|
5
|
+
* OpenClaw reads agents.defaults.model.primary dynamically on each chat
|
|
6
|
+
* request (loadConfig() has a ~200ms cache). A disk write takes effect
|
|
7
|
+
* within that window — no SIGUSR1 needed.
|
|
8
|
+
*
|
|
9
|
+
* Uses the runtime config APIs (loadConfig + writeConfigFile) for atomic
|
|
10
|
+
* writes, env var preservation, and config validation.
|
|
11
|
+
*
|
|
12
|
+
* Methods:
|
|
13
|
+
* - clawly.config.setModel({ model }) → { changed, model }
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {PluginApi} from '../types'
|
|
17
|
+
import type {OpenClawConfig} from '../types/openclaw'
|
|
18
|
+
|
|
19
|
+
export function registerConfigModel(api: PluginApi) {
|
|
20
|
+
api.registerGatewayMethod('clawly.config.setModel', async ({params, respond}) => {
|
|
21
|
+
const model = typeof params.model === 'string' ? params.model : ''
|
|
22
|
+
if (!model) {
|
|
23
|
+
respond(true, {changed: false, model: '', error: 'Missing model param'})
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let config: OpenClawConfig
|
|
28
|
+
try {
|
|
29
|
+
config = {...(api.runtime.config.loadConfig() as OpenClawConfig)}
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
32
|
+
respond(true, {changed: false, model, error: `Load failed: ${msg}`})
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const current = (config.agents as Record<string, unknown> | undefined)?.defaults as
|
|
37
|
+
| Record<string, unknown>
|
|
38
|
+
| undefined
|
|
39
|
+
const currentModel = (current?.model as Record<string, unknown> | undefined)?.primary
|
|
40
|
+
|
|
41
|
+
if (currentModel === model) {
|
|
42
|
+
respond(true, {changed: false, model})
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Shallow-copy nested objects to avoid polluting the loadConfig() cache
|
|
47
|
+
// if writeConfigFile fails below.
|
|
48
|
+
const agents = {...((config.agents ?? {}) as Record<string, unknown>)}
|
|
49
|
+
const defaults = {...((agents.defaults ?? {}) as Record<string, unknown>)}
|
|
50
|
+
const modelObj = {...((defaults.model ?? {}) as Record<string, unknown>)}
|
|
51
|
+
modelObj.primary = model
|
|
52
|
+
defaults.model = modelObj
|
|
53
|
+
agents.defaults = defaults
|
|
54
|
+
config.agents = agents
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await api.runtime.config.writeConfigFile(config)
|
|
58
|
+
api.logger.info(`config-model: set model.primary to ${model}`)
|
|
59
|
+
respond(true, {changed: true, model})
|
|
60
|
+
} catch (err) {
|
|
61
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
62
|
+
api.logger.error(`config-model: write failed — ${msg}`)
|
|
63
|
+
respond(true, {changed: false, model, error: `Write failed: ${msg}`})
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
api.logger.info('config-model: registered clawly.config.setModel')
|
|
68
|
+
}
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
* Timezone sync RPC: writes agents.defaults.userTimezone to openclaw.json
|
|
3
3
|
* without triggering a gateway restart.
|
|
4
4
|
*
|
|
5
|
+
* Uses the runtime config APIs (loadConfig + writeConfigFile) for atomic
|
|
6
|
+
* writes, env var preservation, and config validation.
|
|
7
|
+
*
|
|
5
8
|
* Methods:
|
|
6
9
|
* - clawly.config.setTimezone({ timezone }) → { changed, timezone }
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
|
-
import path from 'node:path'
|
|
10
|
-
|
|
11
12
|
import type {PluginApi} from '../types'
|
|
12
|
-
import {
|
|
13
|
+
import type {OpenClawConfig} from '../types/openclaw'
|
|
13
14
|
|
|
14
15
|
export function registerConfigTimezone(api: PluginApi) {
|
|
15
16
|
api.registerGatewayMethod('clawly.config.setTimezone', async ({params, respond}) => {
|
|
@@ -19,30 +20,34 @@ export function registerConfigTimezone(api: PluginApi) {
|
|
|
19
20
|
return
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
let config: OpenClawConfig
|
|
24
|
+
try {
|
|
25
|
+
config = {...(api.runtime.config.loadConfig() as OpenClawConfig)}
|
|
26
|
+
} catch (err) {
|
|
27
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
28
|
+
respond(true, {changed: false, timezone, error: `Load failed: ${msg}`})
|
|
25
29
|
return
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const agents = (config.agents ?? {}) as Record<string, unknown>
|
|
32
|
-
const defaults = (agents.defaults ?? {}) as Record<string, unknown>
|
|
33
|
-
const current = defaults.userTimezone
|
|
32
|
+
const currentDefaults = (config.agents as Record<string, unknown> | undefined)?.defaults as
|
|
33
|
+
| Record<string, unknown>
|
|
34
|
+
| undefined
|
|
34
35
|
|
|
35
|
-
if (
|
|
36
|
+
if (currentDefaults?.userTimezone === timezone) {
|
|
36
37
|
respond(true, {changed: false, timezone})
|
|
37
38
|
return
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
// Shallow-copy nested objects to avoid polluting the loadConfig() cache
|
|
42
|
+
// if writeConfigFile fails below.
|
|
43
|
+
const agents = {...((config.agents ?? {}) as Record<string, unknown>)}
|
|
44
|
+
const defaults = {...((agents.defaults ?? {}) as Record<string, unknown>)}
|
|
40
45
|
defaults.userTimezone = timezone
|
|
41
46
|
agents.defaults = defaults
|
|
42
47
|
config.agents = agents
|
|
43
48
|
|
|
44
49
|
try {
|
|
45
|
-
|
|
50
|
+
await api.runtime.config.writeConfigFile(config)
|
|
46
51
|
api.logger.info(`config-timezone: set userTimezone to ${timezone}`)
|
|
47
52
|
respond(true, {changed: true, timezone})
|
|
48
53
|
} catch (err) {
|
|
@@ -263,6 +263,22 @@ describe('cron-delivery', () => {
|
|
|
263
263
|
})
|
|
264
264
|
})
|
|
265
265
|
|
|
266
|
+
test('skips HEARTBEAT OK with space (bare)', async () => {
|
|
267
|
+
const {api, logs, handlers} = createMockApi()
|
|
268
|
+
registerCronDelivery(api)
|
|
269
|
+
|
|
270
|
+
await handlers.get('agent_end')!(
|
|
271
|
+
{messages: makeMessages('HEARTBEAT OK')},
|
|
272
|
+
{sessionKey: 'agent:clawly:cron:weather-check', agentId: 'clawly'},
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
expect(injectCalls).toHaveLength(0)
|
|
276
|
+
expect(logs).toContainEqual({
|
|
277
|
+
level: 'info',
|
|
278
|
+
msg: expect.stringContaining('skipped (filtered: heartbeat ack)'),
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
266
282
|
test('skips empty text', async () => {
|
|
267
283
|
const {api, logs, handlers} = createMockApi()
|
|
268
284
|
registerCronDelivery(api)
|
package/gateway/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {registerCalendarNative} from './calendar-native'
|
|
|
4
4
|
import {registerAnalytics} from './analytics'
|
|
5
5
|
import {registerAudit} from './audit'
|
|
6
6
|
import {registerClawhub2gateway} from './clawhub2gateway'
|
|
7
|
+
import {registerConfigModel} from './config-model'
|
|
7
8
|
import {registerConfigRepair} from './config-repair'
|
|
8
9
|
import {registerConfigTimezone} from './config-timezone'
|
|
9
10
|
import {registerCronDelivery} from './cron-delivery'
|
|
@@ -57,6 +58,7 @@ export function registerGateway(api: PluginApi) {
|
|
|
57
58
|
registerCronTelemetry(api)
|
|
58
59
|
registerMessageLog(api)
|
|
59
60
|
registerAnalytics(api)
|
|
61
|
+
registerConfigModel(api)
|
|
60
62
|
registerConfigRepair(api)
|
|
61
63
|
registerConfigTimezone(api)
|
|
62
64
|
registerSessionSanitize(api)
|
|
@@ -446,6 +446,22 @@ describe('shouldSkipPushForMessage', () => {
|
|
|
446
446
|
expect(shouldSkipPushForMessage('The token HEARTBEAT_OK is used for health checks.')).toBeNull()
|
|
447
447
|
})
|
|
448
448
|
|
|
449
|
+
test('skips heartbeat ack with space variant (HEARTBEAT OK)', () => {
|
|
450
|
+
expect(shouldSkipPushForMessage('HEARTBEAT OK')).toBe('heartbeat ack')
|
|
451
|
+
expect(shouldSkipPushForMessage('HEARTBEAT OK.')).toBe('heartbeat ack')
|
|
452
|
+
expect(shouldSkipPushForMessage('HEARTBEAT OK\n')).toBe('heartbeat ack')
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
test('skips short content ending with HEARTBEAT OK (space variant)', () => {
|
|
456
|
+
expect(shouldSkipPushForMessage('All good. HEARTBEAT OK')).toBe('heartbeat ack')
|
|
457
|
+
expect(shouldSkipPushForMessage('Nothing to report. HEARTBEAT OK。')).toBe('heartbeat ack')
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
test('skips HEARTBEAT OK at start with short status note (space variant)', () => {
|
|
461
|
+
expect(shouldSkipPushForMessage('HEARTBEAT OK — all systems nominal.')).toBe('heartbeat ack')
|
|
462
|
+
expect(shouldSkipPushForMessage('HEARTBEAT OK. Nothing to report.')).toBe('heartbeat ack')
|
|
463
|
+
})
|
|
464
|
+
|
|
449
465
|
test('skips system prompt leak', () => {
|
|
450
466
|
expect(
|
|
451
467
|
shouldSkipPushForMessage('Here is some Conversation info (untrusted metadata) text'),
|
|
@@ -584,6 +600,33 @@ describe('offline-push with filtered messages', () => {
|
|
|
584
600
|
expect(lastPushOpts?.body?.endsWith('…')).toBe(true)
|
|
585
601
|
})
|
|
586
602
|
|
|
603
|
+
test('sends push for long content and strips HEARTBEAT OK (space variant) from body', async () => {
|
|
604
|
+
const {api, logs, handlers} = createMockApi()
|
|
605
|
+
registerOfflinePush(api)
|
|
606
|
+
|
|
607
|
+
const longContent = 'A'.repeat(301)
|
|
608
|
+
await handlers.get('agent_end')!(
|
|
609
|
+
{
|
|
610
|
+
messages: [
|
|
611
|
+
{
|
|
612
|
+
role: 'assistant',
|
|
613
|
+
content: `${longContent}\n\nHEARTBEAT OK`,
|
|
614
|
+
},
|
|
615
|
+
],
|
|
616
|
+
},
|
|
617
|
+
{sessionKey: 'agent:clawly:main'},
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
expect(logs).toContainEqual({
|
|
621
|
+
level: 'info',
|
|
622
|
+
msg: expect.stringContaining('notified (session=agent:clawly:main)'),
|
|
623
|
+
})
|
|
624
|
+
// HEARTBEAT OK should be stripped from body, content truncated to 140
|
|
625
|
+
expect(lastPushOpts?.body?.length).toBe(141)
|
|
626
|
+
expect(lastPushOpts?.body?.endsWith('…')).toBe(true)
|
|
627
|
+
expect(lastPushOpts?.body).not.toContain('HEARTBEAT')
|
|
628
|
+
})
|
|
629
|
+
|
|
587
630
|
test('sends push for normal message text', async () => {
|
|
588
631
|
const {api, logs, handlers} = createMockApi()
|
|
589
632
|
registerOfflinePush(api)
|
package/gateway/offline-push.ts
CHANGED
|
@@ -160,12 +160,12 @@ export function shouldSkipPushForMessage(text: string): string | null {
|
|
|
160
160
|
// Heartbeat acknowledgment — strip HEARTBEAT_OK from both edges (mirrors
|
|
161
161
|
// OpenClaw's stripTokenAtEdges). Skip if remaining text ≤ ackMaxChars (300).
|
|
162
162
|
const HEARTBEAT_ACK_MAX_CHARS = 300
|
|
163
|
-
const hasAtEnd = /
|
|
164
|
-
const hasAtStart = /^[\p{P}\s]*
|
|
163
|
+
const hasAtEnd = /HEARTBEAT[_ ]OK[\p{P}\s]*$/u.test(text)
|
|
164
|
+
const hasAtStart = /^[\p{P}\s]*HEARTBEAT[_ ]OK/u.test(text)
|
|
165
165
|
if (hasAtEnd || hasAtStart) {
|
|
166
166
|
let stripped = text
|
|
167
|
-
if (hasAtEnd) stripped = stripped.replace(/
|
|
168
|
-
if (hasAtStart) stripped = stripped.replace(/^[\p{P}\s]*
|
|
167
|
+
if (hasAtEnd) stripped = stripped.replace(/HEARTBEAT[_ ]OK[\p{P}\s]*$/u, '')
|
|
168
|
+
if (hasAtStart) stripped = stripped.replace(/^[\p{P}\s]*HEARTBEAT[_ ]OK[\p{P}\s]*/u, '')
|
|
169
169
|
stripped = stripped.trim()
|
|
170
170
|
if (stripped.length <= HEARTBEAT_ACK_MAX_CHARS) return 'heartbeat ack'
|
|
171
171
|
}
|
|
@@ -308,12 +308,12 @@ export function registerOfflinePush(api: PluginApi) {
|
|
|
308
308
|
const noHeartbeat =
|
|
309
309
|
(() => {
|
|
310
310
|
if (!fullText) return null
|
|
311
|
-
const atEnd = /
|
|
312
|
-
const atStart = /^[\p{P}\s]*
|
|
311
|
+
const atEnd = /HEARTBEAT[_ ]OK[\p{P}\s]*$/u.test(fullText)
|
|
312
|
+
const atStart = /^[\p{P}\s]*HEARTBEAT[_ ]OK/u.test(fullText)
|
|
313
313
|
if (!atEnd && !atStart) return fullText.trim()
|
|
314
314
|
let s = fullText
|
|
315
|
-
if (atEnd) s = s.replace(/
|
|
316
|
-
if (atStart) s = s.replace(/^[\p{P}\s]*
|
|
315
|
+
if (atEnd) s = s.replace(/HEARTBEAT[_ ]OK[\p{P}\s]*$/u, '')
|
|
316
|
+
if (atStart) s = s.replace(/^[\p{P}\s]*HEARTBEAT[_ ]OK[\p{P}\s]*/u, '')
|
|
317
317
|
return s.trim()
|
|
318
318
|
})() ?? null
|
|
319
319
|
const cleaned = noHeartbeat ? stripPlaceholders(noHeartbeat) : null
|
package/gateway/presence.ts
CHANGED
|
@@ -27,14 +27,17 @@ export function isOnlineEntry(entry: PresenceEntry | undefined): boolean {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Shells out to `openclaw gateway call system-presence` and
|
|
31
|
-
*
|
|
30
|
+
* Shells out to `openclaw gateway call system-presence` and returns true
|
|
31
|
+
* if any device has a `foreground` presence entry.
|
|
32
|
+
*
|
|
33
|
+
* False-negatives (beacon lapsed) are handled client-side — the mobile
|
|
34
|
+
* app suppresses `agent_end` notifications while in the foreground.
|
|
32
35
|
*/
|
|
33
36
|
export async function isClientOnline(): Promise<boolean> {
|
|
34
37
|
try {
|
|
35
38
|
const result = await $`openclaw gateway call system-presence --json`
|
|
36
|
-
const
|
|
37
|
-
const entries: PresenceEntry[] =
|
|
39
|
+
const parsed: unknown = JSON.parse(stripCliLogs(result.stdout))
|
|
40
|
+
const entries: PresenceEntry[] = Array.isArray(parsed) ? parsed : []
|
|
38
41
|
return entries.some(isOnlineEntry)
|
|
39
42
|
} catch {
|
|
40
43
|
return false
|
package/gateway-fetch.ts
CHANGED
|
@@ -11,11 +11,18 @@ export type HandlerResult = {ok: boolean; data?: unknown; error?: {code: string;
|
|
|
11
11
|
export function getGatewayConfig(api: PluginApi): GatewayCfg {
|
|
12
12
|
const cfg = api.pluginConfig && typeof api.pluginConfig === 'object' ? api.pluginConfig : {}
|
|
13
13
|
const c = cfg as Record<string, unknown>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
|
|
15
|
+
// URL: prefer clawlyApiBaseUrl, fall back to deprecated skillGatewayBaseUrl
|
|
16
|
+
const rawUrl =
|
|
17
|
+
(typeof c.clawlyApiBaseUrl === 'string' ? (c.clawlyApiBaseUrl as string) : '') ||
|
|
18
|
+
(typeof c.skillGatewayBaseUrl === 'string' ? (c.skillGatewayBaseUrl as string) : '')
|
|
19
|
+
const baseUrl = rawUrl.replace(/\/$/, '')
|
|
20
|
+
|
|
21
|
+
// Token: prefer modelGatewayToken, fall back to deprecated skillGatewayToken
|
|
22
|
+
const token =
|
|
23
|
+
(typeof c.modelGatewayToken === 'string' ? (c.modelGatewayToken as string) : '') ||
|
|
24
|
+
(typeof c.skillGatewayToken === 'string' ? (c.skillGatewayToken as string) : '')
|
|
25
|
+
|
|
19
26
|
return {baseUrl, token}
|
|
20
27
|
}
|
|
21
28
|
|
package/index.ts
CHANGED
|
@@ -80,7 +80,7 @@ export default {
|
|
|
80
80
|
registerAutoPair(api)
|
|
81
81
|
registerAutoUpdate(api)
|
|
82
82
|
|
|
83
|
-
// Email & calendar (optional — requires
|
|
83
|
+
// Email & calendar (optional — requires API base URL + token)
|
|
84
84
|
const gw = getGatewayConfig(api)
|
|
85
85
|
if (gw.baseUrl && gw.token) {
|
|
86
86
|
registerEmail(api, gw)
|
package/lib/calendar-cache.ts
CHANGED
|
@@ -23,6 +23,9 @@ export interface NativeCalendar {
|
|
|
23
23
|
type: string
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export type AlarmMethod = 'alarm' | 'alert' | 'email' | 'default' | 'sms'
|
|
27
|
+
export type AlarmInput = {relativeOffset?: number; method?: AlarmMethod}
|
|
28
|
+
|
|
26
29
|
export interface NativeCalendarEvent {
|
|
27
30
|
id: string
|
|
28
31
|
calendarId: string
|
|
@@ -35,6 +38,7 @@ export interface NativeCalendarEvent {
|
|
|
35
38
|
url: string
|
|
36
39
|
timeZone: string
|
|
37
40
|
organizer: string
|
|
41
|
+
alarms?: AlarmInput[]
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
export interface CalendarCache {
|
package/openclaw.plugin.json
CHANGED
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"defaultNoInput": { "type": "boolean" },
|
|
48
48
|
"defaultTimeoutMs": { "type": "number", "minimum": 1000 },
|
|
49
49
|
"configPath": { "type": "string" },
|
|
50
|
+
"clawlyApiBaseUrl": { "type": "string" },
|
|
50
51
|
"skillGatewayBaseUrl": { "type": "string" },
|
|
51
52
|
"skillGatewayToken": { "type": "string" },
|
|
52
53
|
"modelGatewayBaseUrl": { "type": "string" },
|
package/package.json
CHANGED
package/tools/clawly-calendar.ts
CHANGED
|
@@ -158,6 +158,26 @@ export function registerCalendarTools(api: PluginApi) {
|
|
|
158
158
|
location: {type: 'string', description: 'Event location'},
|
|
159
159
|
notes: {type: 'string', description: 'Event description/notes'},
|
|
160
160
|
timeZone: {type: 'string', description: 'IANA time zone (e.g. America/New_York)'},
|
|
161
|
+
alarms: {
|
|
162
|
+
type: 'array',
|
|
163
|
+
description:
|
|
164
|
+
'Array of alarm objects to set reminders. Each alarm has a relativeOffset in minutes (negative = before event). Example: [{relativeOffset: -15}] for 15 minutes before. Only alarms with valid relativeOffset (number) or method (string) will be applied. To verify reminders were set, use clawly_calendar_list_events after creation.',
|
|
165
|
+
items: {
|
|
166
|
+
type: 'object',
|
|
167
|
+
properties: {
|
|
168
|
+
relativeOffset: {
|
|
169
|
+
type: 'number',
|
|
170
|
+
description:
|
|
171
|
+
'Minutes relative to the event start time. Use negative values for reminders before the event (e.g. -15 = 15 min before, -60 = 1 hour before).',
|
|
172
|
+
},
|
|
173
|
+
method: {
|
|
174
|
+
type: 'string',
|
|
175
|
+
enum: ['alarm', 'alert', 'email', 'default', 'sms'],
|
|
176
|
+
description: 'Alarm method (Android only). iOS always uses notification.',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
161
181
|
},
|
|
162
182
|
},
|
|
163
183
|
async execute(_toolCallId, params) {
|
|
@@ -211,6 +231,9 @@ export function registerCalendarTools(api: PluginApi) {
|
|
|
211
231
|
...(params.location ? {location: params.location} : {}),
|
|
212
232
|
...(params.notes ? {notes: params.notes} : {}),
|
|
213
233
|
...(params.timeZone ? {timeZone: params.timeZone} : {}),
|
|
234
|
+
...(Array.isArray(params.alarms) && params.alarms.length > 0
|
|
235
|
+
? {alarms: params.alarms}
|
|
236
|
+
: {}),
|
|
214
237
|
}
|
|
215
238
|
|
|
216
239
|
const resultPromise = waitForActionResult(actionId)
|
|
@@ -290,6 +313,26 @@ export function registerCalendarTools(api: PluginApi) {
|
|
|
290
313
|
location: {type: 'string', description: 'New event location'},
|
|
291
314
|
notes: {type: 'string', description: 'New event description/notes'},
|
|
292
315
|
timeZone: {type: 'string', description: 'IANA time zone (e.g. America/New_York)'},
|
|
316
|
+
alarms: {
|
|
317
|
+
type: 'array',
|
|
318
|
+
description:
|
|
319
|
+
'Array of alarm objects to set reminders. Each alarm has a relativeOffset in minutes (negative = before event). Example: [{relativeOffset: -15}] for 15 minutes before. Replaces all existing alarms. Pass an empty array [] to clear all existing reminders.',
|
|
320
|
+
items: {
|
|
321
|
+
type: 'object',
|
|
322
|
+
properties: {
|
|
323
|
+
relativeOffset: {
|
|
324
|
+
type: 'number',
|
|
325
|
+
description:
|
|
326
|
+
'Minutes relative to the event start time. Use negative values for reminders before the event (e.g. -15 = 15 min before, -60 = 1 hour before).',
|
|
327
|
+
},
|
|
328
|
+
method: {
|
|
329
|
+
type: 'string',
|
|
330
|
+
enum: ['alarm', 'alert', 'email', 'default', 'sms'],
|
|
331
|
+
description: 'Alarm method (Android only). iOS always uses notification.',
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
293
336
|
},
|
|
294
337
|
},
|
|
295
338
|
async execute(_toolCallId, params) {
|
|
@@ -314,6 +357,7 @@ export function registerCalendarTools(api: PluginApi) {
|
|
|
314
357
|
...(params.location !== undefined ? {location: params.location} : {}),
|
|
315
358
|
...(params.notes !== undefined ? {notes: params.notes} : {}),
|
|
316
359
|
...(params.timeZone !== undefined ? {timeZone: params.timeZone} : {}),
|
|
360
|
+
...(Array.isArray(params.alarms) ? {alarms: params.alarms} : {}),
|
|
317
361
|
}
|
|
318
362
|
|
|
319
363
|
const resultPromise = waitForActionResult(actionId)
|