@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 +107 -29
- package/gateway/index.ts +2 -0
- package/gateway/inject.ts +99 -0
- package/gateway/memory.ts +99 -78
- package/index.ts +3 -2
- package/package.json +1 -1
- package/tools/clawly-send-message.ts +40 -9
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 —
|
|
8
|
-
* - clawly.agent.echo —
|
|
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
|
-
|
|
24
|
+
// ── Shared agent gateway call ────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export interface AgentCallParams {
|
|
20
27
|
message: string
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
agentId?: string
|
|
29
|
+
sessionKey?: string
|
|
30
|
+
extraSystemPrompt?: string
|
|
31
|
+
label?: string
|
|
32
|
+
spawnedBy?: string
|
|
33
|
+
lane?: string
|
|
23
34
|
}
|
|
24
35
|
|
|
25
|
-
interface
|
|
36
|
+
export interface AgentCallResult {
|
|
26
37
|
ok: boolean
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
runId?: string
|
|
39
|
+
error?: string
|
|
29
40
|
}
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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<
|
|
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
|
|
38
|
-
|
|
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:
|
|
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
|
|
52
|
-
|
|
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 —
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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(
|
|
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
|
|
2
|
+
* Memory & workspace browser — expose workspace .md files as Gateway RPC methods.
|
|
3
3
|
*
|
|
4
4
|
* Gateway methods:
|
|
5
|
-
* - memory-browser.list — list
|
|
6
|
-
* - memory-browser.get —
|
|
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
|
-
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
204
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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 —
|
|
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
|
@@ -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
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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}`)
|