@2en/clawly-plugins 1.11.0 → 1.13.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/agent.ts CHANGED
@@ -3,53 +3,111 @@
3
3
  * external callers (cron, other agents) and optionally push-notify
4
4
  * when the mobile client is offline.
5
5
  *
6
+ * Uses `openclaw gateway call agent` (gateway RPC) instead of
7
+ * `openclaw agent --message` for richer parameter support
8
+ * (extraSystemPrompt, sessionKey, lane, spawnedBy, etc.).
9
+ *
6
10
  * Methods:
7
- * - clawly.agent.send — run `openclaw agent --message <msg>`
8
- * - clawly.agent.echo — same, but wraps message as `/clawly.echo <msg>`
11
+ * - clawly.agent.send — trigger agent turn via gateway RPC
12
+ * - clawly.agent.echo — echo-wrapped agent message (bypasses LLM)
9
13
  */
10
14
 
11
15
  import {$} from 'zx'
12
16
  import type {PluginApi} from '../index'
17
+ import {stripCliLogs} from '../lib/stripCliLogs'
13
18
  import {sendPushNotification} from './notification'
14
19
  import {isClientOnline} from './presence'
15
20
 
16
21
  // Suppress zx default stdout logging
17
22
  $.verbose = false
18
23
 
19
- interface AgentSendParams {
24
+ // ── Shared agent gateway call ────────────────────────────────────
25
+
26
+ export interface AgentCallParams {
20
27
  message: string
21
- agent?: string
22
- notificationMessage?: string
28
+ agentId?: string
29
+ sessionKey?: string
30
+ extraSystemPrompt?: string
31
+ label?: string
32
+ spawnedBy?: string
33
+ lane?: string
23
34
  }
24
35
 
25
- interface AgentSendResult {
36
+ export interface AgentCallResult {
26
37
  ok: boolean
27
- online: boolean
28
- pushSent: boolean
38
+ runId?: string
39
+ error?: string
29
40
  }
30
41
 
31
- async function runAgentMessage(
32
- message: string,
33
- agent: string,
42
+ /**
43
+ * Trigger an agent turn via `openclaw gateway call agent`.
44
+ * Exported so other modules (e.g. clawly_send_message tool) can reuse.
45
+ */
46
+ export async function callAgentGateway(
47
+ params: AgentCallParams,
34
48
  api: PluginApi,
35
- ): Promise<{ok: boolean; error?: string}> {
49
+ ): Promise<AgentCallResult> {
50
+ const rpcParams = JSON.stringify({
51
+ message: params.message,
52
+ ...(params.agentId ? {agentId: params.agentId} : {}),
53
+ ...(params.sessionKey ? {sessionKey: params.sessionKey} : {}),
54
+ ...(params.extraSystemPrompt ? {extraSystemPrompt: params.extraSystemPrompt} : {}),
55
+ ...(params.label ? {label: params.label} : {}),
56
+ ...(params.spawnedBy ? {spawnedBy: params.spawnedBy} : {}),
57
+ ...(params.lane ? {lane: params.lane} : {}),
58
+ idempotencyKey: crypto.randomUUID(),
59
+ })
60
+
36
61
  try {
37
- await $`openclaw agent --agent ${agent} --message ${message}`
38
- return {ok: true}
62
+ const result = await $`openclaw gateway call agent --json --params ${rpcParams}`
63
+ const jsonStr = stripCliLogs(result.stdout)
64
+ const parsed = JSON.parse(jsonStr)
65
+ return {ok: true, runId: parsed.runId}
39
66
  } catch (err) {
40
67
  const msg = err instanceof Error ? err.message : String(err)
41
- api.logger.error(`agent-send: openclaw agent failed — ${msg}`)
68
+ api.logger.error(`agent-send: gateway call agent failed — ${msg}`)
42
69
  return {ok: false, error: msg}
43
70
  }
44
71
  }
45
72
 
73
+ // ── clawly.agent.send / clawly.agent.echo ────────────────────────
74
+
75
+ interface AgentSendParams {
76
+ message: string
77
+ agent?: string
78
+ sessionKey?: string
79
+ extraSystemPrompt?: string
80
+ label?: string
81
+ spawnedBy?: string
82
+ lane?: string
83
+ notificationMessage?: string
84
+ }
85
+
86
+ interface AgentSendResult {
87
+ ok: boolean
88
+ runId?: string
89
+ online: boolean
90
+ pushSent: boolean
91
+ }
92
+
46
93
  async function handleAgentSend(
47
94
  rawMessage: string,
48
95
  params: AgentSendParams,
49
96
  api: PluginApi,
50
97
  ): Promise<AgentSendResult> {
51
- const agent = params.agent || 'clawly'
52
- const {ok} = await runAgentMessage(rawMessage, agent, api)
98
+ const {ok, runId} = await callAgentGateway(
99
+ {
100
+ message: rawMessage,
101
+ agentId: params.agent || 'clawly',
102
+ sessionKey: params.sessionKey,
103
+ extraSystemPrompt: params.extraSystemPrompt,
104
+ label: params.label,
105
+ spawnedBy: params.spawnedBy,
106
+ lane: params.lane,
107
+ },
108
+ api,
109
+ )
110
+
53
111
  const online = await isClientOnline()
54
112
 
55
113
  let pushSent = false
@@ -57,11 +115,15 @@ async function handleAgentSend(
57
115
  pushSent = await sendPushNotification({body: params.notificationMessage}, api)
58
116
  }
59
117
 
60
- return {ok, online, pushSent}
118
+ return {ok, ...(runId ? {runId} : {}), online, pushSent}
119
+ }
120
+
121
+ function parseString(val: unknown): string | undefined {
122
+ return typeof val === 'string' ? val.trim() || undefined : undefined
61
123
  }
62
124
 
63
125
  export function registerAgentSend(api: PluginApi) {
64
- // clawly.agent.send — send a raw message to the agent
126
+ // clawly.agent.send — trigger an agent turn via gateway RPC
65
127
  api.registerGatewayMethod('clawly.agent.send', async ({params, respond}) => {
66
128
  const message = typeof params.message === 'string' ? params.message.trim() : ''
67
129
  if (!message) {
@@ -69,11 +131,22 @@ export function registerAgentSend(api: PluginApi) {
69
131
  return
70
132
  }
71
133
 
72
- const agent = typeof params.agent === 'string' ? params.agent.trim() : undefined
73
- const notificationMessage =
74
- typeof params.notificationMessage === 'string' ? params.notificationMessage : undefined
75
-
76
- const result = await handleAgentSend(message, {message, agent, notificationMessage}, api)
134
+ const result = await handleAgentSend(
135
+ message,
136
+ {
137
+ message,
138
+ agent: parseString(params.agent),
139
+ sessionKey: parseString(params.sessionKey),
140
+ extraSystemPrompt:
141
+ typeof params.extraSystemPrompt === 'string' ? params.extraSystemPrompt : undefined,
142
+ label: parseString(params.label),
143
+ spawnedBy: parseString(params.spawnedBy),
144
+ lane: parseString(params.lane),
145
+ notificationMessage:
146
+ typeof params.notificationMessage === 'string' ? params.notificationMessage : undefined,
147
+ },
148
+ api,
149
+ )
77
150
  respond(result.ok, result)
78
151
  })
79
152
 
@@ -85,12 +158,17 @@ export function registerAgentSend(api: PluginApi) {
85
158
  return
86
159
  }
87
160
 
88
- const agent = typeof params.agent === 'string' ? params.agent.trim() : undefined
89
- const notificationMessage =
90
- typeof params.notificationMessage === 'string' ? params.notificationMessage : undefined
91
-
92
161
  const echoMessage = `/clawly_echo ${message}`
93
- const result = await handleAgentSend(echoMessage, {message, agent, notificationMessage}, api)
162
+ const result = await handleAgentSend(
163
+ echoMessage,
164
+ {
165
+ message: echoMessage,
166
+ agent: parseString(params.agent),
167
+ notificationMessage:
168
+ typeof params.notificationMessage === 'string' ? params.notificationMessage : undefined,
169
+ },
170
+ api,
171
+ )
94
172
  respond(result.ok, result)
95
173
  })
96
174
 
package/gateway/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type {PluginApi} from '../index'
2
2
  import {registerAgentSend} from './agent'
3
3
  import {registerClawhub2gateway} from './clawhub2gateway'
4
+ import {registerInject} from './inject'
4
5
  import {registerMemoryBrowser} from './memory'
5
6
  import {registerNotification} from './notification'
6
7
  import {registerPlugins} from './plugins'
@@ -10,6 +11,7 @@ export function registerGateway(api: PluginApi) {
10
11
  registerPresence(api)
11
12
  registerNotification(api)
12
13
  registerAgentSend(api)
14
+ registerInject(api)
13
15
  registerMemoryBrowser(api)
14
16
  registerClawhub2gateway(api)
15
17
  registerPlugins(api)
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Assistant-role message injection — inject messages into the chat transcript
3
+ * as role: "assistant" without triggering an agent run.
4
+ *
5
+ * Method:
6
+ * - clawly.inject({ sessionKey, message, label? }) → { ok, messageId }
7
+ *
8
+ * Wraps the core `chat.inject` OpenClaw gateway method.
9
+ */
10
+
11
+ import {$} from 'zx'
12
+ import type {PluginApi} from '../index'
13
+ import {stripCliLogs} from '../lib/stripCliLogs'
14
+
15
+ $.verbose = false
16
+
17
+ interface InjectParams {
18
+ sessionKey: string
19
+ message: string
20
+ label?: string
21
+ }
22
+
23
+ interface InjectResult {
24
+ ok: boolean
25
+ messageId: string
26
+ }
27
+
28
+ /**
29
+ * Inject a message into the chat transcript as role: "assistant".
30
+ * Calls the core `chat.inject` gateway method via CLI.
31
+ */
32
+ export async function injectAssistantMessage(
33
+ params: InjectParams,
34
+ api: PluginApi,
35
+ ): Promise<InjectResult> {
36
+ const rpcParams = JSON.stringify({
37
+ sessionKey: params.sessionKey,
38
+ message: params.message,
39
+ ...(params.label ? {label: params.label} : {}),
40
+ })
41
+
42
+ try {
43
+ const result = await $`openclaw gateway call chat.inject --json --params ${rpcParams}`
44
+ const jsonStr = stripCliLogs(result.stdout)
45
+ const parsed = JSON.parse(jsonStr)
46
+ return {ok: parsed.ok ?? true, messageId: parsed.messageId ?? ''}
47
+ } catch (err) {
48
+ const msg = err instanceof Error ? err.message : String(err)
49
+ api.logger.error(`inject: chat.inject failed — ${msg}`)
50
+ throw new Error(`chat.inject failed: ${msg}`)
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Resolve the session key for an agent via `sessions.resolve`.
56
+ */
57
+ export async function resolveSessionKey(agentId: string, api: PluginApi): Promise<string> {
58
+ try {
59
+ const rpcParams = JSON.stringify({agentId})
60
+ const result = await $`openclaw gateway call sessions.resolve --json --params ${rpcParams}`
61
+ const jsonStr = stripCliLogs(result.stdout)
62
+ const parsed = JSON.parse(jsonStr)
63
+ if (parsed.key) return parsed.key
64
+ throw new Error(`sessions.resolve returned no key for agentId=${agentId}`)
65
+ } catch (err) {
66
+ const msg = err instanceof Error ? err.message : String(err)
67
+ api.logger.error(`inject: sessions.resolve failed — ${msg}`)
68
+ throw new Error(`sessions.resolve failed: ${msg}`)
69
+ }
70
+ }
71
+
72
+ export function registerInject(api: PluginApi) {
73
+ api.registerGatewayMethod('clawly.inject', async ({params, respond}) => {
74
+ const sessionKey = typeof params.sessionKey === 'string' ? params.sessionKey.trim() : ''
75
+ const message = typeof params.message === 'string' ? params.message.trim() : ''
76
+ const label = typeof params.label === 'string' ? params.label.trim() : undefined
77
+
78
+ if (!sessionKey) {
79
+ respond(false, undefined, {code: 'invalid_params', message: 'sessionKey is required'})
80
+ return
81
+ }
82
+ if (!message) {
83
+ respond(false, undefined, {code: 'invalid_params', message: 'message is required'})
84
+ return
85
+ }
86
+
87
+ try {
88
+ const result = await injectAssistantMessage({sessionKey, message, label}, api)
89
+ respond(true, result)
90
+ } catch (err) {
91
+ respond(false, undefined, {
92
+ code: 'inject_failed',
93
+ message: err instanceof Error ? err.message : String(err),
94
+ })
95
+ }
96
+ })
97
+
98
+ api.logger.info('inject: registered clawly.inject method')
99
+ }
package/gateway/memory.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
- * Memory directory browser — expose memory directory .md files as Gateway RPC methods.
2
+ * Memory & workspace browser — expose workspace .md files as Gateway RPC methods.
3
3
  *
4
4
  * Gateway methods:
5
- * - memory-browser.list — list all .md files in the OpenClaw memory directory
6
- * - memory-browser.get — return the content of a single .md file by path
5
+ * - memory-browser.list — list .md files in memory directory (may be shadowed by built-in)
6
+ * - memory-browser.get — read a .md file from memory directory (may be shadowed by built-in)
7
+ * - clawly.workspace.list — list root-level .md files in workspace directory
8
+ * - clawly.workspace.get — read a .md file from workspace root
7
9
  */
8
10
 
9
11
  import fs from 'node:fs/promises'
@@ -108,30 +110,84 @@ async function listRootMdFiles(dir: string): Promise<string[]> {
108
110
  .sort()
109
111
  }
110
112
 
113
+ /** Validate path and read a .md file, responding via the RPC respond callback. */
114
+ async function readMdFile(
115
+ dir: string,
116
+ relativePath: string | undefined,
117
+ api: PluginApi,
118
+ respond: (ok: boolean, payload?: unknown, error?: {code: string; message: string}) => void,
119
+ ) {
120
+ if (!relativePath) {
121
+ respond(false, undefined, {
122
+ code: 'invalid_params',
123
+ message: 'path (relative path to .md file) is required',
124
+ })
125
+ return
126
+ }
127
+ if (!isSafeRelativePath(relativePath)) {
128
+ respond(false, undefined, {
129
+ code: 'invalid_params',
130
+ message: 'path must be a safe relative path (no directory traversal)',
131
+ })
132
+ return
133
+ }
134
+ if (!relativePath.toLowerCase().endsWith('.md')) {
135
+ respond(false, undefined, {
136
+ code: 'invalid_params',
137
+ message: 'path must point to a .md file',
138
+ })
139
+ return
140
+ }
141
+
142
+ const fullPath = path.join(dir, path.normalize(relativePath))
143
+ const realBase = await fs.realpath(dir).catch(() => dir)
144
+ const realResolved = await fs.realpath(fullPath).catch(() => null)
145
+
146
+ if (!realResolved) {
147
+ respond(false, undefined, {code: 'not_found', message: 'file not found'})
148
+ return
149
+ }
150
+ const rel = path.relative(realBase, realResolved)
151
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
152
+ respond(false, undefined, {
153
+ code: 'not_found',
154
+ message: 'file not found or outside allowed directory',
155
+ })
156
+ return
157
+ }
158
+
159
+ try {
160
+ const content = await fs.readFile(fullPath, 'utf-8')
161
+ respond(true, {path: relativePath, content})
162
+ } catch (err) {
163
+ const code = (err as NodeJS.ErrnoException).code
164
+ if (code === 'ENOENT') {
165
+ respond(false, undefined, {code: 'not_found', message: 'file not found'})
166
+ return
167
+ }
168
+ api.logger.error(`readMdFile failed: ${err instanceof Error ? err.message : String(err)}`)
169
+ respond(false, undefined, {
170
+ code: 'error',
171
+ message: err instanceof Error ? err.message : String(err),
172
+ })
173
+ }
174
+ }
175
+
111
176
  export function registerMemoryBrowser(api: PluginApi) {
112
177
  const memoryDir = resolveMemoryDir(api)
113
- api.logger.info(`memory-browser: memory directory: ${memoryDir}`)
178
+ const wsRoot = resolveWorkspaceRoot(api)
179
+ api.logger.info(`memory-browser: memory=${memoryDir} workspace=${wsRoot}`)
114
180
 
115
181
  // -----------------------------
116
- // memory-browser.list
182
+ // memory-browser.list (may be shadowed by built-in)
117
183
  // -----------------------------
118
184
  api.registerGatewayMethod('memory-browser.list', async ({params, respond}) => {
119
185
  try {
120
- api.logger.info(`memory-browser.list params: ${JSON.stringify(params)}`)
121
186
  const profile = readString(params, 'profile')
122
- const scope = readString(params, 'scope') ?? 'memory'
123
-
124
- let files: string[]
125
- if (scope === 'root') {
126
- const wsRoot = resolveWorkspaceRoot(api, profile)
127
- files = await listRootMdFiles(wsRoot)
128
- api.logger.info(`memory-browser.list (root): ${files.length} files found in ${wsRoot}`)
129
- } else {
130
- const dir = profile ? resolveMemoryDir(api, profile) : memoryDir
131
- files = await collectMdFiles(dir, dir)
132
- files.sort()
133
- api.logger.info(`memory-browser.list: ${files.length} files found`)
134
- }
187
+ const dir = profile ? resolveMemoryDir(api, profile) : memoryDir
188
+ const files = await collectMdFiles(dir, dir)
189
+ files.sort()
190
+ api.logger.info(`memory-browser.list: ${files.length} files found`)
135
191
  respond(true, {files})
136
192
  } catch (err) {
137
193
  api.logger.error(
@@ -145,71 +201,27 @@ export function registerMemoryBrowser(api: PluginApi) {
145
201
  })
146
202
 
147
203
  // -----------------------------
148
- // memory-browser.get
204
+ // memory-browser.get (may be shadowed by built-in)
149
205
  // -----------------------------
150
206
  api.registerGatewayMethod('memory-browser.get', async ({params, respond}) => {
151
- const relativePath = readString(params, 'path')
152
207
  const profile = readString(params, 'profile')
153
- const scope = readString(params, 'scope') ?? 'memory'
154
- const dir =
155
- scope === 'root'
156
- ? resolveWorkspaceRoot(api, profile)
157
- : profile
158
- ? resolveMemoryDir(api, profile)
159
- : memoryDir
160
- if (!relativePath) {
161
- respond(false, undefined, {
162
- code: 'invalid_params',
163
- message: 'path (relative path to .md file) is required',
164
- })
165
- return
166
- }
167
- if (!isSafeRelativePath(relativePath)) {
168
- respond(false, undefined, {
169
- code: 'invalid_params',
170
- message: 'path must be a safe relative path (no directory traversal)',
171
- })
172
- return
173
- }
174
- if (!relativePath.toLowerCase().endsWith('.md')) {
175
- respond(false, undefined, {
176
- code: 'invalid_params',
177
- message: 'path must point to a .md file',
178
- })
179
- return
180
- }
181
-
182
- const fullPath = path.join(dir, path.normalize(relativePath))
183
- const realMemory = await fs.realpath(dir).catch(() => dir)
184
- const realResolved = await fs.realpath(fullPath).catch(() => null)
185
-
186
- if (!realResolved) {
187
- respond(false, undefined, {
188
- code: 'not_found',
189
- message: 'file not found',
190
- })
191
- return
192
- }
193
- const rel = path.relative(realMemory, realResolved)
194
- if (rel.startsWith('..') || path.isAbsolute(rel)) {
195
- respond(false, undefined, {
196
- code: 'not_found',
197
- message: 'file not found or outside memory directory',
198
- })
199
- return
200
- }
208
+ const dir = profile ? resolveMemoryDir(api, profile) : memoryDir
209
+ await readMdFile(dir, readString(params, 'path'), api, respond)
210
+ })
201
211
 
212
+ // -----------------------------
213
+ // clawly.workspace.list — workspace root .md files (not shadowed)
214
+ // -----------------------------
215
+ api.registerGatewayMethod('clawly.workspace.list', async ({params, respond}) => {
202
216
  try {
203
- const content = await fs.readFile(fullPath, 'utf-8')
204
- respond(true, {path: relativePath, content})
217
+ const profile = readString(params, 'profile')
218
+ const root = resolveWorkspaceRoot(api, profile)
219
+ const files = await listRootMdFiles(root)
220
+ api.logger.info(`clawly.workspace.list: ${files.length} files in ${root}`)
221
+ respond(true, {files})
205
222
  } catch (err) {
206
- const code = (err as NodeJS.ErrnoException).code
207
- if (code === 'ENOENT') {
208
- respond(false, undefined, {code: 'not_found', message: 'file not found'})
209
- return
210
- }
211
223
  api.logger.error(
212
- `memory-browser.get failed: ${err instanceof Error ? err.message : String(err)}`,
224
+ `clawly.workspace.list failed: ${err instanceof Error ? err.message : String(err)}`,
213
225
  )
214
226
  respond(false, undefined, {
215
227
  code: 'error',
@@ -218,5 +230,14 @@ export function registerMemoryBrowser(api: PluginApi) {
218
230
  }
219
231
  })
220
232
 
221
- api.logger.info(`memory-browser: registered v1.11.0 (dir: ${memoryDir})`)
233
+ // -----------------------------
234
+ // clawly.workspace.get — read a single .md file from workspace root
235
+ // -----------------------------
236
+ api.registerGatewayMethod('clawly.workspace.get', async ({params, respond}) => {
237
+ const profile = readString(params, 'profile')
238
+ const root = resolveWorkspaceRoot(api, profile)
239
+ await readMdFile(root, readString(params, 'path'), api, respond)
240
+ })
241
+
242
+ api.logger.info('memory-browser: registered')
222
243
  }
package/index.ts CHANGED
@@ -6,8 +6,9 @@
6
6
  * - clawly.isOnline — check if a mobile client is connected
7
7
  * - clawly.notification.setToken — register Expo push token for offline notifications
8
8
  * - clawly.notification.send — send a push notification directly
9
- * - clawly.agent.send — send a message to the agent (+ optional push)
9
+ * - clawly.agent.send — trigger agent turn with a message (+ optional push)
10
10
  * - clawly.agent.echo — echo-wrapped agent message (bypasses LLM)
11
+ * - clawly.inject — inject assistant-role message into transcript (wraps chat.inject)
11
12
  * - memory-browser.list — list all .md files in the memory directory
12
13
  * - memory-browser.get — return content of a single .md file
13
14
  * - clawhub2gateway.* — ClawHub CLI RPC bridge (search/install/update/list/explore/inspect/star/unstar)
@@ -15,7 +16,7 @@
15
16
  * Agent tools:
16
17
  * - clawly_is_user_online — check if user's device is connected
17
18
  * - clawly_send_app_push — send a push notification to user's device
18
- * - clawly_send_message — send a message to user via main session agent
19
+ * - clawly_send_message — send a message to user via main session agent (supports role: user|assistant)
19
20
  *
20
21
  * Commands:
21
22
  * - /clawly_echo — echo text back without LLM
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2en/clawly-plugins",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "repository": {
@@ -2,14 +2,14 @@
2
2
  * Agent tool: clawly_send_message — send a message to the main session
3
3
  * agent from an isolated context (e.g. cron job).
4
4
  *
5
- * Runs `openclaw agent --agent <agent> --message <message>` which delivers
6
- * the text as a natural assistant response in the user's chat.
5
+ * - role: "user" (default) — triggers agent turn via gateway RPC
6
+ * - role: "assistant" injects into transcript via chat.inject (no agent trigger,
7
+ * since the calling agent has already decided to send)
7
8
  */
8
9
 
9
- import {$} from 'zx'
10
10
  import type {PluginApi} from '../index'
11
-
12
- $.verbose = false
11
+ import {callAgentGateway} from '../gateway/agent'
12
+ import {injectAssistantMessage, resolveSessionKey} from '../gateway/inject'
13
13
 
14
14
  const TOOL_NAME = 'clawly_send_message'
15
15
 
@@ -19,6 +19,16 @@ const parameters: Record<string, unknown> = {
19
19
  properties: {
20
20
  message: {type: 'string', description: 'The message to send to the user'},
21
21
  agent: {type: 'string', description: 'Target agent ID (default: "clawly")'},
22
+ role: {
23
+ type: 'string',
24
+ enum: ['user', 'assistant'],
25
+ description:
26
+ 'Message role. "user" (default) sends as user and triggers agent run. "assistant" injects into transcript as assistant without triggering a run.',
27
+ },
28
+ sessionKey: {
29
+ type: 'string',
30
+ description: 'Session key for assistant injection. Auto-resolved from agent if omitted.',
31
+ },
22
32
  },
23
33
  }
24
34
 
@@ -26,7 +36,7 @@ export function registerSendMessageTool(api: PluginApi) {
26
36
  api.registerTool({
27
37
  name: TOOL_NAME,
28
38
  description:
29
- 'Send a message to the user via the main session agent. Use this from cron jobs or isolated sessions to deliver results to the user.',
39
+ 'Send a message to the user via the main session agent. Use this from cron jobs or isolated sessions to deliver results to the user. Set role to "assistant" to inject the message directly into the transcript without triggering an agent run.',
30
40
  parameters,
31
41
  async execute(_toolCallId, params) {
32
42
  const message = typeof params.message === 'string' ? params.message.trim() : ''
@@ -35,13 +45,34 @@ export function registerSendMessageTool(api: PluginApi) {
35
45
  }
36
46
 
37
47
  const agent = typeof params.agent === 'string' ? params.agent.trim() : 'clawly'
48
+ const role = params.role === 'assistant' ? 'assistant' : 'user'
38
49
 
39
50
  try {
40
- await $`openclaw agent --agent ${agent} --message ${message}`
51
+ if (role === 'assistant') {
52
+ const sessionKey =
53
+ typeof params.sessionKey === 'string' && params.sessionKey.trim()
54
+ ? params.sessionKey.trim()
55
+ : await resolveSessionKey(agent, api)
56
+ const result = await injectAssistantMessage({sessionKey, message}, api)
57
+ api.logger.info(
58
+ `${TOOL_NAME}: injected assistant message (${message.length} chars) into session ${sessionKey}`,
59
+ )
60
+ return {
61
+ content: [
62
+ {type: 'text', text: JSON.stringify({sent: true, messageId: result.messageId})},
63
+ ],
64
+ }
65
+ }
66
+
67
+ const {ok, runId, error} = await callAgentGateway({message, agentId: agent}, api)
68
+ if (!ok) {
69
+ api.logger.error(`${TOOL_NAME}: failed — ${error}`)
70
+ return {content: [{type: 'text', text: JSON.stringify({sent: false, error})}]}
71
+ }
41
72
  api.logger.info(
42
- `${TOOL_NAME}: delivered message (${message.length} chars) to agent ${agent}`,
73
+ `${TOOL_NAME}: delivered message (${message.length} chars) to agent ${agent} (runId: ${runId})`,
43
74
  )
44
- return {content: [{type: 'text', text: JSON.stringify({sent: true})}]}
75
+ return {content: [{type: 'text', text: JSON.stringify({sent: true, runId})}]}
45
76
  } catch (err) {
46
77
  const msg = err instanceof Error ? err.message : String(err)
47
78
  api.logger.error(`${TOOL_NAME}: failed — ${msg}`)