@2en/clawly-plugins 1.26.0-beta.0 → 1.26.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/auto-pair.ts +5 -0
- package/config-setup.ts +113 -17
- package/gateway/analytics.ts +83 -0
- package/gateway/clawhub2gateway.ts +15 -0
- package/gateway/cron-delivery.ts +13 -2
- package/gateway/cron-telemetry.test.ts +407 -0
- package/gateway/cron-telemetry.ts +253 -0
- package/gateway/index.ts +33 -0
- package/gateway/offline-push.test.ts +209 -0
- package/gateway/offline-push.ts +107 -12
- package/gateway/otel.test.ts +88 -0
- package/gateway/otel.ts +57 -0
- package/gateway/plugins.ts +3 -0
- package/gateway/posthog.test.ts +73 -0
- package/gateway/posthog.ts +61 -0
- package/gateway/telemetry-config.test.ts +58 -0
- package/gateway/telemetry-config.ts +27 -0
- package/index.ts +7 -1
- package/model-gateway-setup.ts +5 -7
- package/openclaw.plugin.json +6 -1
- package/outbound.ts +67 -32
- package/package.json +7 -1
- package/tools/clawly-send-image.ts +228 -0
- package/tools/index.ts +2 -0
package/auto-pair.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {captureEvent} from './gateway/posthog'
|
|
1
2
|
import type {PluginApi} from './index'
|
|
2
3
|
|
|
3
4
|
const AUTO_APPROVE_CLIENT_IDS = new Set(['openclaw-ios'])
|
|
@@ -49,6 +50,10 @@ export function registerAutoPair(api: PluginApi) {
|
|
|
49
50
|
if (req.clientId && AUTO_APPROVE_CLIENT_IDS.has(req.clientId)) {
|
|
50
51
|
const result = await sdk.approveDevicePairing(req.requestId)
|
|
51
52
|
if (result) {
|
|
53
|
+
captureEvent('device.paired', {
|
|
54
|
+
client_id: req.clientId,
|
|
55
|
+
platform: result.device.platform ?? 'unknown',
|
|
56
|
+
})
|
|
52
57
|
api.logger.info(
|
|
53
58
|
`auto-pair: approved device=${result.device.deviceId} ` +
|
|
54
59
|
`name=${result.device.displayName ?? 'unknown'} ` +
|
package/config-setup.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Reconciles runtime config owned by clawly-plugins after provision has already
|
|
3
|
+
* established the bootstrap baseline (plugin entry, pluginConfig inputs,
|
|
4
|
+
* workspace files, install metadata).
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* provider setup.
|
|
6
|
+
* This file intentionally owns two narrower responsibilities:
|
|
7
|
+
* - repair legacy state damaged by older provision flows
|
|
8
|
+
* - reconcile runtime-facing config derived from pluginConfig
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* It does not try to redefine provision's startup contract. Anything that must
|
|
11
|
+
* exist before the first gateway boot should remain provision-owned.
|
|
12
|
+
*
|
|
13
|
+
* Backward compatibility: old sprites may have only the original gateway
|
|
14
|
+
* credentials in pluginConfig. Helpers check for newer fields and skip
|
|
15
|
+
* gracefully when absent.
|
|
13
16
|
*/
|
|
14
17
|
|
|
18
|
+
import fs from 'node:fs'
|
|
15
19
|
import path from 'node:path'
|
|
16
20
|
|
|
17
21
|
import type {PluginApi} from './index'
|
|
@@ -23,6 +27,7 @@ import {
|
|
|
23
27
|
} from './model-gateway-setup'
|
|
24
28
|
|
|
25
29
|
export interface ConfigPluginConfig {
|
|
30
|
+
instanceId?: string
|
|
26
31
|
agentId?: string
|
|
27
32
|
agentName?: string
|
|
28
33
|
workspaceDir?: string
|
|
@@ -32,6 +37,10 @@ export interface ConfigPluginConfig {
|
|
|
32
37
|
elevenlabsVoiceId?: string
|
|
33
38
|
modelGatewayBaseUrl?: string
|
|
34
39
|
modelGatewayToken?: string
|
|
40
|
+
otelToken?: string
|
|
41
|
+
otelDataset?: string
|
|
42
|
+
posthogApiKey?: string
|
|
43
|
+
posthogHost?: string
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
function toPC(api: PluginApi): ConfigPluginConfig {
|
|
@@ -372,8 +381,100 @@ export function patchSession(config: Record<string, unknown>): boolean {
|
|
|
372
381
|
return dirty
|
|
373
382
|
}
|
|
374
383
|
|
|
384
|
+
const PLUGIN_ID = 'clawly-plugins'
|
|
385
|
+
const NPM_PKG_NAME = '@2en/clawly-plugins'
|
|
386
|
+
|
|
387
|
+
function asObj(value: unknown): Record<string, unknown> {
|
|
388
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
389
|
+
? (value as Record<string, unknown>)
|
|
390
|
+
: {}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Self-healing: reconstruct missing `plugins.installs.clawly-plugins` record.
|
|
395
|
+
* Older provisioned sprites had this record destroyed by a full-overwrite bug
|
|
396
|
+
* in the provision write-plugins-config step. Without this record,
|
|
397
|
+
* `openclaw plugins update` cannot function.
|
|
398
|
+
*/
|
|
399
|
+
export function patchInstallRecord(config: Record<string, unknown>, stateDir: string): boolean {
|
|
400
|
+
const plugins = asObj(config.plugins)
|
|
401
|
+
const installs = asObj(plugins.installs)
|
|
402
|
+
|
|
403
|
+
// Already has a valid install record — nothing to do
|
|
404
|
+
if (
|
|
405
|
+
installs[PLUGIN_ID] &&
|
|
406
|
+
typeof installs[PLUGIN_ID] === 'object' &&
|
|
407
|
+
(installs[PLUGIN_ID] as Record<string, unknown>).source &&
|
|
408
|
+
(installs[PLUGIN_ID] as Record<string, unknown>).spec
|
|
409
|
+
)
|
|
410
|
+
return false
|
|
411
|
+
|
|
412
|
+
// Read version from installed plugin's package.json
|
|
413
|
+
const installPath = path.join(stateDir, 'extensions', PLUGIN_ID)
|
|
414
|
+
const pkgPath = path.join(installPath, 'package.json')
|
|
415
|
+
let rawPkg: string
|
|
416
|
+
try {
|
|
417
|
+
rawPkg = fs.readFileSync(pkgPath, 'utf-8')
|
|
418
|
+
} catch {
|
|
419
|
+
// Plugin not installed on disk — skip
|
|
420
|
+
return false
|
|
421
|
+
}
|
|
422
|
+
let version: string | undefined
|
|
423
|
+
try {
|
|
424
|
+
const pkg = JSON.parse(rawPkg)
|
|
425
|
+
if (typeof pkg.version === 'string') version = pkg.version
|
|
426
|
+
} catch {
|
|
427
|
+
// package.json is malformed — proceed without version
|
|
428
|
+
}
|
|
429
|
+
let installedAt = new Date().toISOString()
|
|
430
|
+
try {
|
|
431
|
+
installedAt = fs.statSync(pkgPath).mtime.toISOString()
|
|
432
|
+
} catch {
|
|
433
|
+
// Fall back to repair time if file metadata is unavailable.
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
installs[PLUGIN_ID] = {
|
|
437
|
+
source: 'npm',
|
|
438
|
+
spec: version ? `${NPM_PKG_NAME}@${version}` : NPM_PKG_NAME,
|
|
439
|
+
installPath,
|
|
440
|
+
version,
|
|
441
|
+
installedAt,
|
|
442
|
+
}
|
|
443
|
+
plugins.installs = installs
|
|
444
|
+
config.plugins = plugins
|
|
445
|
+
return true
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function repairLegacyProvisionState(
|
|
449
|
+
api: PluginApi,
|
|
450
|
+
config: Record<string, unknown>,
|
|
451
|
+
stateDir: string,
|
|
452
|
+
) {
|
|
453
|
+
const installRecordPatched = patchInstallRecord(config, stateDir)
|
|
454
|
+
if (installRecordPatched) {
|
|
455
|
+
api.logger.warn('plugins.installs.clawly-plugins was missing — self-healed from disk.')
|
|
456
|
+
}
|
|
457
|
+
return installRecordPatched
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function reconcileRuntimeConfig(
|
|
461
|
+
api: PluginApi,
|
|
462
|
+
config: Record<string, unknown>,
|
|
463
|
+
pc: ConfigPluginConfig,
|
|
464
|
+
): boolean {
|
|
465
|
+
let dirty = false
|
|
466
|
+
dirty = patchAgent(config, pc) || dirty
|
|
467
|
+
dirty = patchGateway(config) || dirty
|
|
468
|
+
dirty = patchBrowser(config) || dirty
|
|
469
|
+
dirty = patchSession(config) || dirty
|
|
470
|
+
dirty = patchTts(config, pc) || dirty
|
|
471
|
+
dirty = patchWebSearch(config, pc) || dirty
|
|
472
|
+
dirty = patchModelGateway(config, api) || dirty
|
|
473
|
+
return dirty
|
|
474
|
+
}
|
|
475
|
+
|
|
375
476
|
// ---------------------------------------------------------------------------
|
|
376
|
-
// Entry point — single read,
|
|
477
|
+
// Entry point — single read, targeted reconcile, single write
|
|
377
478
|
// ---------------------------------------------------------------------------
|
|
378
479
|
|
|
379
480
|
export function setupConfig(api: PluginApi): void {
|
|
@@ -388,13 +489,8 @@ export function setupConfig(api: PluginApi): void {
|
|
|
388
489
|
const pc = toPC(api)
|
|
389
490
|
|
|
390
491
|
let dirty = false
|
|
391
|
-
dirty =
|
|
392
|
-
dirty =
|
|
393
|
-
dirty = patchBrowser(config) || dirty
|
|
394
|
-
dirty = patchSession(config) || dirty
|
|
395
|
-
dirty = patchTts(config, pc) || dirty
|
|
396
|
-
dirty = patchWebSearch(config, pc) || dirty
|
|
397
|
-
dirty = patchModelGateway(config, api) || dirty
|
|
492
|
+
dirty = repairLegacyProvisionState(api, config, stateDir) || dirty
|
|
493
|
+
dirty = reconcileRuntimeConfig(api, config, pc) || dirty
|
|
398
494
|
|
|
399
495
|
if (!dirty) {
|
|
400
496
|
api.logger.info('Config setup: no changes needed.')
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broad PostHog analytics — captures agent turns, LLM usage, and tool calls.
|
|
3
|
+
*
|
|
4
|
+
* Hooks:
|
|
5
|
+
* - agent_end — tracks every agent turn completion (trigger, duration, success)
|
|
6
|
+
* - llm_output — tracks LLM token usage per response (provider, model, tokens)
|
|
7
|
+
* - after_tool_call — tracks tool usage (tool name, duration, success)
|
|
8
|
+
*
|
|
9
|
+
* Cron-specific lifecycle events (created/executed/deleted) are handled by
|
|
10
|
+
* cron-telemetry.ts and are NOT duplicated here.
|
|
11
|
+
*
|
|
12
|
+
* Other modules (offline-push, auto-pair, plugins, clawhub2gateway) call
|
|
13
|
+
* captureEvent() directly for their domain-specific events.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {PluginApi} from '../types'
|
|
17
|
+
import {captureEvent} from './posthog'
|
|
18
|
+
|
|
19
|
+
export function registerAnalytics(api: PluginApi) {
|
|
20
|
+
// ── agent_end: track every agent turn ───────────────────────────
|
|
21
|
+
api.on('agent_end', (event: Record<string, unknown>, ctx?: Record<string, unknown>) => {
|
|
22
|
+
const trigger = typeof ctx?.trigger === 'string' ? ctx.trigger : 'unknown'
|
|
23
|
+
const agentId = typeof ctx?.agentId === 'string' ? ctx.agentId : undefined
|
|
24
|
+
const sessionKey = typeof ctx?.sessionKey === 'string' ? ctx.sessionKey : undefined
|
|
25
|
+
const sessionId = typeof ctx?.sessionId === 'string' ? ctx.sessionId : undefined
|
|
26
|
+
const success = event.error == null
|
|
27
|
+
const durationMs = typeof event.durationMs === 'number' ? event.durationMs : undefined
|
|
28
|
+
|
|
29
|
+
captureEvent('agent.turn_completed', {
|
|
30
|
+
trigger,
|
|
31
|
+
...(agentId ? {agent_id: agentId} : {}),
|
|
32
|
+
...(sessionKey ? {session_key: sessionKey} : {}),
|
|
33
|
+
...(sessionId ? {session_id: sessionId} : {}),
|
|
34
|
+
success,
|
|
35
|
+
...(durationMs != null ? {duration_ms: durationMs} : {}),
|
|
36
|
+
...(typeof event.error === 'string' ? {error: event.error} : {}),
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// ── llm_output: track token usage ──────────────────────────────
|
|
41
|
+
api.on('llm_output', (event: Record<string, unknown>, ctx?: Record<string, unknown>) => {
|
|
42
|
+
const provider = typeof event.provider === 'string' ? event.provider : undefined
|
|
43
|
+
const model = typeof event.model === 'string' ? event.model : undefined
|
|
44
|
+
const usage = event.usage as Record<string, unknown> | undefined
|
|
45
|
+
|
|
46
|
+
if (!usage) return
|
|
47
|
+
|
|
48
|
+
const trigger = typeof ctx?.trigger === 'string' ? ctx.trigger : undefined
|
|
49
|
+
const sessionId = typeof ctx?.sessionId === 'string' ? ctx.sessionId : undefined
|
|
50
|
+
|
|
51
|
+
captureEvent('llm.response', {
|
|
52
|
+
...(provider ? {provider} : {}),
|
|
53
|
+
...(model ? {model} : {}),
|
|
54
|
+
...(trigger ? {trigger} : {}),
|
|
55
|
+
...(sessionId ? {session_id: sessionId} : {}),
|
|
56
|
+
...(typeof usage.input === 'number' ? {tokens_input: usage.input} : {}),
|
|
57
|
+
...(typeof usage.output === 'number' ? {tokens_output: usage.output} : {}),
|
|
58
|
+
...(typeof usage.cacheRead === 'number' ? {tokens_cache_read: usage.cacheRead} : {}),
|
|
59
|
+
...(typeof usage.cacheWrite === 'number' ? {tokens_cache_write: usage.cacheWrite} : {}),
|
|
60
|
+
...(typeof usage.total === 'number' ? {tokens_total: usage.total} : {}),
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// ── after_tool_call: track tool usage ──────────────────────────
|
|
65
|
+
api.on('after_tool_call', (event: Record<string, unknown>, ctx?: Record<string, unknown>) => {
|
|
66
|
+
const toolName = typeof event.toolName === 'string' ? event.toolName : undefined
|
|
67
|
+
if (!toolName) return
|
|
68
|
+
|
|
69
|
+
const sessionId = typeof ctx?.sessionId === 'string' ? ctx.sessionId : undefined
|
|
70
|
+
const success = event.error == null
|
|
71
|
+
const durationMs = typeof event.durationMs === 'number' ? event.durationMs : undefined
|
|
72
|
+
|
|
73
|
+
captureEvent('tool.called', {
|
|
74
|
+
tool_name: toolName,
|
|
75
|
+
success,
|
|
76
|
+
...(sessionId ? {session_id: sessionId} : {}),
|
|
77
|
+
...(durationMs != null ? {duration_ms: durationMs} : {}),
|
|
78
|
+
...(typeof event.error === 'string' ? {error: event.error} : {}),
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
api.logger.info('analytics: registered agent_end + llm_output + after_tool_call hooks')
|
|
83
|
+
}
|
|
@@ -20,6 +20,7 @@ import fs from 'node:fs/promises'
|
|
|
20
20
|
import path from 'node:path'
|
|
21
21
|
|
|
22
22
|
import type {PluginApi} from '../types'
|
|
23
|
+
import {captureEvent} from './posthog'
|
|
23
24
|
|
|
24
25
|
type JsonSchema = Record<string, unknown>
|
|
25
26
|
|
|
@@ -182,13 +183,21 @@ export function registerClawhub2gateway(api: PluginApi) {
|
|
|
182
183
|
env,
|
|
183
184
|
})
|
|
184
185
|
const output = joinOutput(res.stdout, res.stderr)
|
|
186
|
+
const action = rpc.method.split('.').pop() ?? rpc.method
|
|
185
187
|
if (res.code && res.code !== 0) {
|
|
188
|
+
captureEvent('clawhub.action', {action, success: false})
|
|
186
189
|
respond(false, undefined, {
|
|
187
190
|
code: 'command_failed',
|
|
188
191
|
message: output || `clawhub exited with code ${res.code}`,
|
|
189
192
|
})
|
|
190
193
|
return
|
|
191
194
|
}
|
|
195
|
+
captureEvent('clawhub.action', {
|
|
196
|
+
action,
|
|
197
|
+
success: true,
|
|
198
|
+
...(params.slug ? {slug: params.slug} : {}),
|
|
199
|
+
...(params.query ? {query: params.query} : {}),
|
|
200
|
+
})
|
|
192
201
|
respond(true, {
|
|
193
202
|
ok: true,
|
|
194
203
|
output,
|
|
@@ -201,6 +210,12 @@ export function registerClawhub2gateway(api: PluginApi) {
|
|
|
201
210
|
cwd: built.cwd,
|
|
202
211
|
})
|
|
203
212
|
} catch (err) {
|
|
213
|
+
const action = rpc.method.split('.').pop() ?? rpc.method
|
|
214
|
+
captureEvent('clawhub.action', {
|
|
215
|
+
action,
|
|
216
|
+
success: false,
|
|
217
|
+
error: err instanceof Error ? err.message : String(err),
|
|
218
|
+
})
|
|
204
219
|
respond(false, undefined, {
|
|
205
220
|
code: 'error',
|
|
206
221
|
message: err instanceof Error ? err.message : String(err),
|
package/gateway/cron-delivery.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type {PluginApi} from '../types'
|
|
18
|
+
import {markCronDelivered, markCronDeliverySkipped} from './cron-telemetry'
|
|
18
19
|
import {injectAssistantMessage, resolveSessionKey} from './inject'
|
|
19
20
|
import {shouldSkipPushForMessage} from './offline-push'
|
|
20
21
|
|
|
@@ -51,6 +52,10 @@ export function registerCronDelivery(api: PluginApi) {
|
|
|
51
52
|
const sessionKey = typeof ctx?.sessionKey === 'string' ? ctx.sessionKey : undefined
|
|
52
53
|
const agentId = typeof ctx?.agentId === 'string' ? ctx.agentId : undefined
|
|
53
54
|
|
|
55
|
+
api.logger.info(
|
|
56
|
+
`cron-delivery[debug]: agent_end fired sessionKey=${sessionKey ?? 'undefined'} agentId=${agentId ?? 'undefined'} trigger=${String(ctx?.trigger ?? 'undefined')}`,
|
|
57
|
+
)
|
|
58
|
+
|
|
54
59
|
// Only fire for cron sessions
|
|
55
60
|
if (!sessionKey?.startsWith('agent:clawly:cron:')) return
|
|
56
61
|
|
|
@@ -59,6 +64,7 @@ export function registerCronDelivery(api: PluginApi) {
|
|
|
59
64
|
const text = getRawLastAssistantText(event.messages)
|
|
60
65
|
if (text == null) {
|
|
61
66
|
api.logger.info('cron-delivery: skipped (no assistant message)')
|
|
67
|
+
if (sessionKey) markCronDeliverySkipped(sessionKey, 'no assistant message')
|
|
62
68
|
return
|
|
63
69
|
}
|
|
64
70
|
|
|
@@ -66,12 +72,14 @@ export function registerCronDelivery(api: PluginApi) {
|
|
|
66
72
|
const reason = shouldSkipPushForMessage(text)
|
|
67
73
|
if (reason) {
|
|
68
74
|
api.logger.info(`cron-delivery: skipped (filtered: ${reason})`)
|
|
75
|
+
if (sessionKey) markCronDeliverySkipped(sessionKey, `filtered: ${reason}`)
|
|
69
76
|
return
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
// Resolve main session key for this agent
|
|
73
80
|
if (!agentId) {
|
|
74
81
|
api.logger.error('cron-delivery: skipped (no agentId on ctx)')
|
|
82
|
+
if (sessionKey) markCronDeliverySkipped(sessionKey, 'no agentId')
|
|
75
83
|
return
|
|
76
84
|
}
|
|
77
85
|
|
|
@@ -86,13 +94,16 @@ export function registerCronDelivery(api: PluginApi) {
|
|
|
86
94
|
api,
|
|
87
95
|
)
|
|
88
96
|
|
|
97
|
+
if (sessionKey) markCronDelivered(sessionKey)
|
|
89
98
|
api.logger.info(
|
|
90
99
|
`cron-delivery: injected into ${mainSessionKey} (messageId=${result.messageId})`,
|
|
91
100
|
)
|
|
92
101
|
} catch (err) {
|
|
93
|
-
|
|
102
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
103
|
+
if (sessionKey) markCronDeliverySkipped(sessionKey, msg)
|
|
104
|
+
api.logger.error(`cron-delivery: ${msg}`)
|
|
94
105
|
}
|
|
95
106
|
})
|
|
96
107
|
|
|
97
|
-
api.logger.info('cron-delivery: registered agent_end hook')
|
|
108
|
+
api.logger.info('cron-delivery: registered agent_end hook (debug-instrumented)')
|
|
98
109
|
}
|