@2en/clawly-plugins 1.28.1 → 1.29.0-beta.0
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/gateway/cron-telemetry.ts +1 -1
- package/gateway/index.ts +4 -2
- package/gateway/message-log.ts +156 -0
- package/gateway/node-dangerous-allowlist.ts +81 -0
- package/gateway/otel.test.ts +9 -1
- package/gateway/otel.ts +9 -4
- package/gateway/telemetry-config.test.ts +3 -0
- package/gateway/telemetry-config.ts +2 -0
- package/index.ts +3 -0
- package/package.json +1 -1
- package/tools/clawly-msg-break.ts +32 -0
- package/tools/index.ts +2 -0
- package/gateway/node-browser-allowlist.ts +0 -62
|
@@ -106,7 +106,7 @@ function extractJobId(sessionKey: string): string | null {
|
|
|
106
106
|
|
|
107
107
|
function emit(attributes: CronTelemetryAttributes, api: PluginApi) {
|
|
108
108
|
// Emit structured OTel LogRecord (if provider is initialized)
|
|
109
|
-
const otelLogger = getOtelLogger()
|
|
109
|
+
const otelLogger = getOtelLogger('cron-telemetry')
|
|
110
110
|
if (otelLogger) {
|
|
111
111
|
otelLogger.emit({
|
|
112
112
|
severityNumber: SeverityNumber.INFO,
|
package/gateway/index.ts
CHANGED
|
@@ -3,12 +3,13 @@ import {registerAgentSend} from './agent'
|
|
|
3
3
|
import {registerCalendarNative} from './calendar-native'
|
|
4
4
|
import {registerAnalytics} from './analytics'
|
|
5
5
|
import {registerAudit} from './audit'
|
|
6
|
-
import {
|
|
6
|
+
import {registerNodeDangerousAllowlist} from './node-dangerous-allowlist'
|
|
7
7
|
import {registerClawhub2gateway} from './clawhub2gateway'
|
|
8
8
|
import {registerConfigRepair} from './config-repair'
|
|
9
9
|
import {registerConfigTimezone} from './config-timezone'
|
|
10
10
|
import {registerCronDelivery} from './cron-delivery'
|
|
11
11
|
import {registerCronTelemetry} from './cron-telemetry'
|
|
12
|
+
import {registerMessageLog} from './message-log'
|
|
12
13
|
import {registerMemoryBrowser} from './memory'
|
|
13
14
|
import {registerNotification} from './notification'
|
|
14
15
|
import {registerOfflinePush} from './offline-push'
|
|
@@ -55,6 +56,7 @@ export function registerGateway(api: PluginApi) {
|
|
|
55
56
|
registerOfflinePush(api)
|
|
56
57
|
registerCronDelivery(api)
|
|
57
58
|
registerCronTelemetry(api)
|
|
59
|
+
registerMessageLog(api)
|
|
58
60
|
registerAnalytics(api)
|
|
59
61
|
registerConfigRepair(api)
|
|
60
62
|
registerConfigTimezone(api)
|
|
@@ -62,6 +64,6 @@ export function registerGateway(api: PluginApi) {
|
|
|
62
64
|
registerPairing(api)
|
|
63
65
|
registerVersion(api)
|
|
64
66
|
registerAudit(api)
|
|
65
|
-
|
|
67
|
+
registerNodeDangerousAllowlist(api)
|
|
66
68
|
registerCalendarNative(api)
|
|
67
69
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message lifecycle logging — captures LLM inputs, outputs, and channel
|
|
3
|
+
* deliveries via OTel structured logs and human-readable api.logger output.
|
|
4
|
+
*
|
|
5
|
+
* Gated by `messageLogEnabled` in pluginConfig (dev-only).
|
|
6
|
+
* Content is never logged — only metadata (model, provider, counts, success).
|
|
7
|
+
*
|
|
8
|
+
* Hooks:
|
|
9
|
+
* - llm_input — logs model, provider, history size, images count
|
|
10
|
+
* - llm_output — logs model, provider, content length
|
|
11
|
+
* - message_sent — logs channel delivery outcome
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {SeverityNumber} from '@opentelemetry/api-logs'
|
|
15
|
+
import type {PluginApi} from '../types'
|
|
16
|
+
import {getOtelLogger} from './otel'
|
|
17
|
+
import {readTelemetryPluginConfig} from './telemetry-config'
|
|
18
|
+
|
|
19
|
+
// ── Structured telemetry attributes (OTel LogRecord) ────────────
|
|
20
|
+
|
|
21
|
+
type MessageLogAttributes = {
|
|
22
|
+
'message.lifecycle': 'llm_input' | 'llm_output' | 'message_sent'
|
|
23
|
+
'message.model'?: string
|
|
24
|
+
'message.provider'?: string
|
|
25
|
+
'message.session_key'?: string
|
|
26
|
+
'message.session_id'?: string
|
|
27
|
+
'message.content_length'?: number
|
|
28
|
+
'message.history_count'?: number
|
|
29
|
+
'message.images_count'?: number
|
|
30
|
+
'message.to'?: string
|
|
31
|
+
'message.success'?: boolean
|
|
32
|
+
'message.error'?: string
|
|
33
|
+
'message.channel_id'?: string
|
|
34
|
+
'instance.id'?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function emit(attributes: MessageLogAttributes, api: PluginApi) {
|
|
40
|
+
const otelLogger = getOtelLogger('message-log')
|
|
41
|
+
if (otelLogger) {
|
|
42
|
+
otelLogger.emit({
|
|
43
|
+
severityNumber: SeverityNumber.INFO,
|
|
44
|
+
severityText: 'INFO',
|
|
45
|
+
body: `message.${attributes['message.lifecycle']}`,
|
|
46
|
+
attributes: attributes as Record<string, unknown>,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const a = attributes
|
|
51
|
+
switch (a['message.lifecycle']) {
|
|
52
|
+
case 'llm_input':
|
|
53
|
+
api.logger.info(
|
|
54
|
+
`message-log[llm-input]: model=${a['message.model'] ?? '?'} provider=${a['message.provider'] ?? '?'} history=${a['message.history_count'] ?? 0} images=${a['message.images_count'] ?? 0}`,
|
|
55
|
+
)
|
|
56
|
+
break
|
|
57
|
+
case 'llm_output':
|
|
58
|
+
api.logger.info(
|
|
59
|
+
`message-log[llm-output]: model=${a['message.model'] ?? '?'} provider=${a['message.provider'] ?? '?'} len=${a['message.content_length'] ?? 0}`,
|
|
60
|
+
)
|
|
61
|
+
break
|
|
62
|
+
case 'message_sent':
|
|
63
|
+
api.logger.info(
|
|
64
|
+
`message-log[sent]: to=${a['message.to'] ?? '?'} success=${a['message.success'] ?? '?'} channel=${a['message.channel_id'] ?? '?'}${a['message.error'] ? ` error="${a['message.error']}"` : ''}`,
|
|
65
|
+
)
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Registration ─────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
export function registerMessageLog(api: PluginApi) {
|
|
73
|
+
const cfg = readTelemetryPluginConfig(api.pluginConfig)
|
|
74
|
+
|
|
75
|
+
if (!cfg.messageLogEnabled) {
|
|
76
|
+
api.logger.info('message-log: disabled (messageLogEnabled not set in pluginConfig)')
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const instanceId = cfg.instanceId ?? process.env.INSTANCE_ID
|
|
81
|
+
|
|
82
|
+
// llm_input: capture what goes into the LLM (metadata only)
|
|
83
|
+
api.on('llm_input', (event: Record<string, unknown>, ctx?: Record<string, unknown>) => {
|
|
84
|
+
const model = typeof event.model === 'string' ? event.model : undefined
|
|
85
|
+
const provider = typeof event.provider === 'string' ? event.provider : undefined
|
|
86
|
+
const prompt = typeof event.prompt === 'string' ? event.prompt : ''
|
|
87
|
+
const historyMessages = Array.isArray(event.historyMessages) ? event.historyMessages : []
|
|
88
|
+
const imagesCount = typeof event.imagesCount === 'number' ? event.imagesCount : 0
|
|
89
|
+
const sessionKey = typeof ctx?.sessionKey === 'string' ? ctx.sessionKey : undefined
|
|
90
|
+
const sessionId = typeof ctx?.sessionId === 'string' ? ctx.sessionId : undefined
|
|
91
|
+
|
|
92
|
+
emit(
|
|
93
|
+
{
|
|
94
|
+
'message.lifecycle': 'llm_input',
|
|
95
|
+
...(model ? {'message.model': model} : {}),
|
|
96
|
+
...(provider ? {'message.provider': provider} : {}),
|
|
97
|
+
'message.content_length': prompt.length,
|
|
98
|
+
'message.history_count': historyMessages.length,
|
|
99
|
+
'message.images_count': imagesCount,
|
|
100
|
+
...(sessionKey ? {'message.session_key': sessionKey} : {}),
|
|
101
|
+
...(sessionId ? {'message.session_id': sessionId} : {}),
|
|
102
|
+
...(instanceId ? {'instance.id': instanceId} : {}),
|
|
103
|
+
},
|
|
104
|
+
api,
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// llm_output: capture LLM response metadata
|
|
109
|
+
api.on('llm_output', (event: Record<string, unknown>, ctx?: Record<string, unknown>) => {
|
|
110
|
+
const model = typeof event.model === 'string' ? event.model : undefined
|
|
111
|
+
const provider = typeof event.provider === 'string' ? event.provider : undefined
|
|
112
|
+
const assistantTexts = Array.isArray(event.assistantTexts) ? event.assistantTexts : []
|
|
113
|
+
const contentLength = assistantTexts
|
|
114
|
+
.filter((t): t is string => typeof t === 'string')
|
|
115
|
+
.reduce((sum, t) => sum + t.length, 0)
|
|
116
|
+
const sessionKey = typeof ctx?.sessionKey === 'string' ? ctx.sessionKey : undefined
|
|
117
|
+
const sessionId = typeof ctx?.sessionId === 'string' ? ctx.sessionId : undefined
|
|
118
|
+
|
|
119
|
+
emit(
|
|
120
|
+
{
|
|
121
|
+
'message.lifecycle': 'llm_output',
|
|
122
|
+
...(model ? {'message.model': model} : {}),
|
|
123
|
+
...(provider ? {'message.provider': provider} : {}),
|
|
124
|
+
'message.content_length': contentLength,
|
|
125
|
+
...(sessionKey ? {'message.session_key': sessionKey} : {}),
|
|
126
|
+
...(sessionId ? {'message.session_id': sessionId} : {}),
|
|
127
|
+
...(instanceId ? {'instance.id': instanceId} : {}),
|
|
128
|
+
},
|
|
129
|
+
api,
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// message_sent: capture channel delivery outcome
|
|
134
|
+
api.on('message_sent', (event: Record<string, unknown>, ctx?: Record<string, unknown>) => {
|
|
135
|
+
const to = typeof event.to === 'string' ? event.to : undefined
|
|
136
|
+
const content = typeof event.content === 'string' ? event.content : ''
|
|
137
|
+
const success = typeof event.success === 'boolean' ? event.success : undefined
|
|
138
|
+
const error = typeof event.error === 'string' ? event.error : undefined
|
|
139
|
+
const channelId = typeof ctx?.channelId === 'string' ? ctx.channelId : undefined
|
|
140
|
+
|
|
141
|
+
emit(
|
|
142
|
+
{
|
|
143
|
+
'message.lifecycle': 'message_sent',
|
|
144
|
+
...(to ? {'message.to': to} : {}),
|
|
145
|
+
'message.content_length': content.length,
|
|
146
|
+
...(success != null ? {'message.success': success} : {}),
|
|
147
|
+
...(error ? {'message.error': error} : {}),
|
|
148
|
+
...(channelId ? {'message.channel_id': channelId} : {}),
|
|
149
|
+
...(instanceId ? {'instance.id': instanceId} : {}),
|
|
150
|
+
},
|
|
151
|
+
api,
|
|
152
|
+
)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
api.logger.info('message-log: registered llm_input + llm_output + message_sent hooks')
|
|
156
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensures all dangerous node commands are in gateway.nodes.allowCommands
|
|
3
|
+
* so the AI agent can invoke them on connected Clawly nodes.
|
|
4
|
+
*
|
|
5
|
+
* Dangerous commands are defined by OpenClaw's node-command-policy and
|
|
6
|
+
* must be explicitly allowlisted. Safe commands work without allowlisting.
|
|
7
|
+
*
|
|
8
|
+
* Runs on gateway_start. Writes directly to openclaw.json via
|
|
9
|
+
* writeOpenclawConfig (no restart triggered — gateway.* changes are
|
|
10
|
+
* classified as "none" by the config watcher).
|
|
11
|
+
*/
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
|
|
14
|
+
import type {PluginApi} from '../types'
|
|
15
|
+
import {readOpenclawConfig, writeOpenclawConfig} from '../model-gateway-setup'
|
|
16
|
+
|
|
17
|
+
const DANGEROUS_COMMANDS = [
|
|
18
|
+
// Browser commands (Mac nodes)
|
|
19
|
+
'browser.proxy',
|
|
20
|
+
'browser.navigate',
|
|
21
|
+
'browser.click',
|
|
22
|
+
'browser.type',
|
|
23
|
+
'browser.screenshot',
|
|
24
|
+
'browser.read',
|
|
25
|
+
'browser.tabs',
|
|
26
|
+
'browser.back',
|
|
27
|
+
'browser.scroll',
|
|
28
|
+
'browser.evaluate',
|
|
29
|
+
// Reminders + calendar (iOS nodes)
|
|
30
|
+
'reminders.add',
|
|
31
|
+
'calendar.add',
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
export function registerNodeDangerousAllowlist(api: PluginApi) {
|
|
35
|
+
api.on('gateway_start', async () => {
|
|
36
|
+
let stateDir: string
|
|
37
|
+
try {
|
|
38
|
+
stateDir = api.runtime.state.resolveStateDir()
|
|
39
|
+
} catch {
|
|
40
|
+
api.logger.warn('node-dangerous-allowlist: cannot resolve state dir, skipping')
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const configPath = path.join(stateDir, 'openclaw.json')
|
|
45
|
+
let config: Record<string, unknown>
|
|
46
|
+
try {
|
|
47
|
+
config = readOpenclawConfig(configPath)
|
|
48
|
+
} catch {
|
|
49
|
+
api.logger.warn('node-dangerous-allowlist: failed to read config, skipping')
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const gateway = (config.gateway as Record<string, unknown>) ?? {}
|
|
54
|
+
const nodes = (gateway.nodes as Record<string, unknown>) ?? {}
|
|
55
|
+
const existing: string[] = Array.isArray(nodes.allowCommands)
|
|
56
|
+
? (nodes.allowCommands as string[])
|
|
57
|
+
: []
|
|
58
|
+
|
|
59
|
+
const existingSet = new Set(existing)
|
|
60
|
+
const toAdd = DANGEROUS_COMMANDS.filter((cmd) => !existingSet.has(cmd))
|
|
61
|
+
|
|
62
|
+
if (toAdd.length === 0) {
|
|
63
|
+
api.logger.info('node-dangerous-allowlist: all commands already allowlisted')
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
nodes.allowCommands = [...existing, ...toAdd]
|
|
68
|
+
gateway.nodes = nodes
|
|
69
|
+
config.gateway = gateway
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
writeOpenclawConfig(configPath, config)
|
|
73
|
+
} catch {
|
|
74
|
+
api.logger.warn('node-dangerous-allowlist: failed to write config')
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
api.logger.info(
|
|
78
|
+
`node-dangerous-allowlist: added ${toAdd.length} commands to allowlist: ${toAdd.join(', ')}`,
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
}
|
package/gateway/otel.test.ts
CHANGED
|
@@ -69,7 +69,15 @@ describe('initOtel', () => {
|
|
|
69
69
|
},
|
|
70
70
|
})
|
|
71
71
|
expect(getOtelLogger()).toBeTruthy()
|
|
72
|
-
expect(loggerName).toBe('
|
|
72
|
+
expect(loggerName).toBe('clawly-plugins')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('returns scoped loggers by name', () => {
|
|
76
|
+
initOtel({otelToken: 'tok', otelDataset: 'ds'})
|
|
77
|
+
const a = getOtelLogger('cron-telemetry')
|
|
78
|
+
const b = getOtelLogger('message-log')
|
|
79
|
+
expect(a).toBeTruthy()
|
|
80
|
+
expect(b).toBeTruthy()
|
|
73
81
|
})
|
|
74
82
|
|
|
75
83
|
test('falls back to legacy env vars', () => {
|
package/gateway/otel.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {BatchLogRecordProcessor, LoggerProvider} from '@opentelemetry/sdk-logs'
|
|
|
17
17
|
import {readTelemetryPluginConfig} from './telemetry-config'
|
|
18
18
|
|
|
19
19
|
let provider: LoggerProvider | null = null
|
|
20
|
-
|
|
20
|
+
const loggers = new Map<string, Logger>()
|
|
21
21
|
|
|
22
22
|
export function initOtel(
|
|
23
23
|
pluginConfig?: Record<string, unknown>,
|
|
@@ -40,11 +40,16 @@ export function initOtel(
|
|
|
40
40
|
|
|
41
41
|
provider = new LoggerProvider({resource})
|
|
42
42
|
provider.addLogRecordProcessor(new BatchLogRecordProcessor(exporter))
|
|
43
|
-
logger = provider.getLogger('cron-telemetry')
|
|
44
43
|
return true
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
export function getOtelLogger(): Logger | null {
|
|
46
|
+
export function getOtelLogger(scope = 'clawly-plugins'): Logger | null {
|
|
47
|
+
if (!provider) return null
|
|
48
|
+
let logger = loggers.get(scope)
|
|
49
|
+
if (!logger) {
|
|
50
|
+
logger = provider.getLogger(scope)
|
|
51
|
+
loggers.set(scope, logger)
|
|
52
|
+
}
|
|
48
53
|
return logger
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -52,6 +57,6 @@ export async function shutdownOtel(): Promise<void> {
|
|
|
52
57
|
if (provider) {
|
|
53
58
|
await provider.shutdown()
|
|
54
59
|
provider = null
|
|
55
|
-
|
|
60
|
+
loggers.clear()
|
|
56
61
|
}
|
|
57
62
|
}
|
|
@@ -17,6 +17,7 @@ describe('readTelemetryPluginConfig', () => {
|
|
|
17
17
|
otelDataset: 'clawly-otel-logs-dev',
|
|
18
18
|
posthogApiKey: 'ph-key',
|
|
19
19
|
posthogHost: 'https://us.i.posthog.com',
|
|
20
|
+
messageLogEnabled: false,
|
|
20
21
|
})
|
|
21
22
|
})
|
|
22
23
|
|
|
@@ -35,6 +36,7 @@ describe('readTelemetryPluginConfig', () => {
|
|
|
35
36
|
otelDataset: 'clawly-otel-logs-dev',
|
|
36
37
|
posthogApiKey: 'ph-key',
|
|
37
38
|
posthogHost: 'https://us.i.posthog.com',
|
|
39
|
+
messageLogEnabled: false,
|
|
38
40
|
})
|
|
39
41
|
})
|
|
40
42
|
|
|
@@ -53,6 +55,7 @@ describe('readTelemetryPluginConfig', () => {
|
|
|
53
55
|
otelDataset: undefined,
|
|
54
56
|
posthogApiKey: undefined,
|
|
55
57
|
posthogHost: undefined,
|
|
58
|
+
messageLogEnabled: false,
|
|
56
59
|
})
|
|
57
60
|
})
|
|
58
61
|
})
|
|
@@ -4,6 +4,7 @@ export interface TelemetryPluginConfig {
|
|
|
4
4
|
otelDataset?: string
|
|
5
5
|
posthogApiKey?: string
|
|
6
6
|
posthogHost?: string
|
|
7
|
+
messageLogEnabled?: boolean
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
function readString(value: unknown): string | undefined {
|
|
@@ -23,5 +24,6 @@ export function readTelemetryPluginConfig(
|
|
|
23
24
|
otelDataset: readString(cfg.otelDataset),
|
|
24
25
|
posthogApiKey: readString(cfg.posthogApiKey),
|
|
25
26
|
posthogHost: readString(cfg.posthogHost),
|
|
27
|
+
messageLogEnabled: cfg.messageLogEnabled === true || cfg.messageLogEnabled === 'true',
|
|
26
28
|
}
|
|
27
29
|
}
|
package/index.ts
CHANGED
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
* - agent_end — sends push notification when client is offline; injects cron results into main session
|
|
35
35
|
* - after_tool_call — cron telemetry: captures cron job creation/deletion
|
|
36
36
|
* - agent_end (pri 100) — cron telemetry: captures cron execution outcomes with delivery/push flags
|
|
37
|
+
* - llm_input — message-log (dev-only): logs model, provider, history size, images count (no content)
|
|
38
|
+
* - llm_output — message-log (dev-only): logs model, provider, content length (no content)
|
|
39
|
+
* - message_sent — message-log (dev-only): logs channel delivery outcome (no content)
|
|
37
40
|
* - gateway_start — auto-approves device pairing for Clawly mobile clients (clientId: openclaw-ios)
|
|
38
41
|
* - gateway_start — registers auto-update cron job (0 3 * * *) for clawly-plugins
|
|
39
42
|
*/
|
package/package.json
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tool: clawly_msg_break — split the current response into multiple messages.
|
|
3
|
+
*
|
|
4
|
+
* When called, this tool acts as a structural boundary: the text before the
|
|
5
|
+
* tool call becomes one assistant message, and the text after becomes a new one.
|
|
6
|
+
* The client relies on the existing "text reset" mechanism (post-tool-call text
|
|
7
|
+
* detection) to create the split — the tool itself is a no-op.
|
|
8
|
+
*
|
|
9
|
+
* This is preferred over in-band separator tokens (e.g. [MSG_BREAK]) because
|
|
10
|
+
* tool calls are structured and never leak to other channels (Telegram, etc.).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {PluginApi} from '../types'
|
|
14
|
+
|
|
15
|
+
const TOOL_NAME = 'clawly_msg_break'
|
|
16
|
+
|
|
17
|
+
export function registerMsgBreakTool(api: PluginApi) {
|
|
18
|
+
api.registerTool({
|
|
19
|
+
name: TOOL_NAME,
|
|
20
|
+
description:
|
|
21
|
+
'Split the current response into multiple messages. Call this between distinct thoughts or topics to send them as separate message bubbles. The text you wrote before this call becomes one message; continue writing after the call to start a new message.',
|
|
22
|
+
parameters: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {},
|
|
25
|
+
},
|
|
26
|
+
async execute() {
|
|
27
|
+
return {content: [{type: 'text', text: JSON.stringify({ok: true})}]}
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
api.logger.info(`tool: registered ${TOOL_NAME} agent tool`)
|
|
32
|
+
}
|
package/tools/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {PluginApi} from '../types'
|
|
2
2
|
import {registerCalendarTools} from './clawly-calendar'
|
|
3
3
|
import {registerIsUserOnlineTool} from './clawly-is-user-online'
|
|
4
|
+
import {registerMsgBreakTool} from './clawly-msg-break'
|
|
4
5
|
import {registerDeepSearchTool, registerGrokSearchTool, registerSearchTool} from './clawly-search'
|
|
5
6
|
import {registerSendAppPushTool} from './clawly-send-app-push'
|
|
6
7
|
import {registerSendImageTool} from './clawly-send-image'
|
|
@@ -9,6 +10,7 @@ import {registerSendMessageTool} from './clawly-send-message'
|
|
|
9
10
|
export function registerTools(api: PluginApi) {
|
|
10
11
|
registerCalendarTools(api)
|
|
11
12
|
registerIsUserOnlineTool(api)
|
|
13
|
+
registerMsgBreakTool(api)
|
|
12
14
|
registerSearchTool(api)
|
|
13
15
|
registerDeepSearchTool(api)
|
|
14
16
|
registerGrokSearchTool(api)
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ensures browser.* node commands are in gateway.nodes.allowCommands
|
|
3
|
-
* so the AI agent can invoke them on connected Clawly Mac nodes.
|
|
4
|
-
*
|
|
5
|
-
* Runs on gateway_start. Writes directly to openclaw.json via
|
|
6
|
-
* writeOpenclawConfig (no restart triggered — gateway.* changes are
|
|
7
|
-
* classified as "none" by the config watcher).
|
|
8
|
-
*/
|
|
9
|
-
import path from 'node:path'
|
|
10
|
-
|
|
11
|
-
import type {PluginApi} from '../types'
|
|
12
|
-
import {readOpenclawConfig, writeOpenclawConfig} from '../model-gateway-setup'
|
|
13
|
-
|
|
14
|
-
const BROWSER_COMMANDS = [
|
|
15
|
-
'browser.proxy',
|
|
16
|
-
'browser.navigate',
|
|
17
|
-
'browser.click',
|
|
18
|
-
'browser.type',
|
|
19
|
-
'browser.screenshot',
|
|
20
|
-
'browser.read',
|
|
21
|
-
'browser.tabs',
|
|
22
|
-
'browser.back',
|
|
23
|
-
'browser.scroll',
|
|
24
|
-
'browser.evaluate',
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
export function registerNodeBrowserAllowlist(api: PluginApi) {
|
|
28
|
-
api.on('gateway_start', async () => {
|
|
29
|
-
let stateDir: string
|
|
30
|
-
try {
|
|
31
|
-
stateDir = api.runtime.state.resolveStateDir()
|
|
32
|
-
} catch {
|
|
33
|
-
api.logger.warn('node-browser-allowlist: cannot resolve state dir, skipping')
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const configPath = path.join(stateDir, 'openclaw.json')
|
|
38
|
-
const config = readOpenclawConfig(configPath)
|
|
39
|
-
|
|
40
|
-
// Ensure gateway.nodes.allowCommands includes browser.* commands
|
|
41
|
-
const gateway = (config.gateway as Record<string, unknown>) ?? {}
|
|
42
|
-
const nodes = (gateway.nodes as Record<string, unknown>) ?? {}
|
|
43
|
-
const existing: string[] = Array.isArray(nodes.allowCommands)
|
|
44
|
-
? (nodes.allowCommands as string[])
|
|
45
|
-
: []
|
|
46
|
-
|
|
47
|
-
const existingSet = new Set(existing)
|
|
48
|
-
const toAdd = BROWSER_COMMANDS.filter((cmd) => !existingSet.has(cmd))
|
|
49
|
-
|
|
50
|
-
if (toAdd.length === 0) {
|
|
51
|
-
api.logger.info('node-browser-allowlist: browser commands already allowlisted')
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
nodes.allowCommands = [...existing, ...toAdd]
|
|
56
|
-
gateway.nodes = nodes
|
|
57
|
-
config.gateway = gateway
|
|
58
|
-
|
|
59
|
-
writeOpenclawConfig(configPath, config)
|
|
60
|
-
api.logger.info(`node-browser-allowlist: added ${toAdd.length} browser commands to allowlist`)
|
|
61
|
-
})
|
|
62
|
-
}
|