@swarmclawai/swarmclaw 0.9.3 → 0.9.4
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/README.md +12 -10
- package/bundled-skills/google-workspace/SKILL.md +2 -0
- package/package.json +1 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +1 -1
- package/src/app/api/clawhub/install/route.ts +2 -0
- package/src/app/api/skills/[id]/route.ts +4 -0
- package/src/app/api/skills/route.ts +4 -0
- package/src/components/agents/agent-sheet.tsx +5 -5
- package/src/lib/server/agents/agent-thread-session.test.ts +64 -0
- package/src/lib/server/agents/agent-thread-session.ts +1 -1
- package/src/lib/server/agents/main-agent-loop-advanced.test.ts +77 -0
- package/src/lib/server/agents/main-agent-loop.ts +259 -0
- package/src/lib/server/agents/orchestrator-lg.ts +12 -8
- package/src/lib/server/agents/orchestrator.ts +11 -7
- package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +11 -10
- package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +116 -3
- package/src/lib/server/chat-execution/chat-execution.ts +74 -26
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +65 -30
- package/src/lib/server/chat-execution/stream-agent-chat.ts +69 -25
- package/src/lib/server/chatrooms/chatroom-helpers.test.ts +26 -0
- package/src/lib/server/chatrooms/chatroom-helpers.ts +11 -8
- package/src/lib/server/connectors/contact-boundaries.ts +101 -0
- package/src/lib/server/connectors/manager.test.ts +504 -73
- package/src/lib/server/connectors/manager.ts +40 -9
- package/src/lib/server/connectors/session-consolidation.ts +2 -0
- package/src/lib/server/connectors/session-kind.ts +7 -0
- package/src/lib/server/connectors/session.test.ts +104 -0
- package/src/lib/server/connectors/session.ts +5 -2
- package/src/lib/server/identity-continuity.test.ts +4 -3
- package/src/lib/server/identity-continuity.ts +8 -4
- package/src/lib/server/memory/session-archive-memory.ts +2 -1
- package/src/lib/server/session-reset-policy.test.ts +17 -3
- package/src/lib/server/session-reset-policy.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +11 -10
- package/src/lib/server/session-tools/crud.ts +41 -7
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/manage-skills.test.ts +194 -0
- package/src/lib/server/session-tools/memory.ts +12 -23
- package/src/lib/server/session-tools/skill-runtime.test.ts +175 -0
- package/src/lib/server/session-tools/skill-runtime.ts +382 -0
- package/src/lib/server/session-tools/skills.ts +575 -0
- package/src/lib/server/skills/runtime-skill-resolver.test.ts +162 -0
- package/src/lib/server/skills/runtime-skill-resolver.ts +750 -0
- package/src/lib/server/skills/skill-discovery.ts +4 -0
- package/src/lib/server/skills/skills-normalize.test.ts +28 -0
- package/src/lib/server/skills/skills-normalize.ts +93 -1
- package/src/lib/server/storage.ts +1 -1
- package/src/lib/server/tasks/task-followups.test.ts +124 -0
- package/src/lib/server/tasks/task-followups.ts +88 -13
- package/src/types/index.ts +26 -2
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
|
+
import type { ApprovalRequest, Agent, Skill } from '@/types'
|
|
3
|
+
import { dedup, errorMessage } from '@/lib/shared-utils'
|
|
4
|
+
import { requestApproval } from '@/lib/server/approvals'
|
|
5
|
+
import {
|
|
6
|
+
loadAgent,
|
|
7
|
+
loadApprovals,
|
|
8
|
+
loadSkills,
|
|
9
|
+
patchAgent,
|
|
10
|
+
saveSkills,
|
|
11
|
+
} from '@/lib/server/storage'
|
|
12
|
+
import { fetchSkillContent, searchClawHub } from '@/lib/server/skills/clawhub-client'
|
|
13
|
+
import { clearDiscoveredSkillsCache } from '@/lib/server/skills/skill-discovery'
|
|
14
|
+
import {
|
|
15
|
+
buildRuntimeSkillPromptBlocks,
|
|
16
|
+
findResolvedSkill,
|
|
17
|
+
recommendRuntimeSkillsForTask,
|
|
18
|
+
resolveRuntimeSkills,
|
|
19
|
+
type ResolvedRuntimeSkill,
|
|
20
|
+
} from '@/lib/server/skills/runtime-skill-resolver'
|
|
21
|
+
import { normalizeSkillPayload } from '@/lib/server/skills/skills-normalize'
|
|
22
|
+
import type { ToolBuildContext } from './context'
|
|
23
|
+
|
|
24
|
+
type SkillSelectorInput = {
|
|
25
|
+
id?: string
|
|
26
|
+
skillId?: string
|
|
27
|
+
name?: string
|
|
28
|
+
url?: string
|
|
29
|
+
content?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeKey(value: unknown): string {
|
|
33
|
+
return typeof value === 'string'
|
|
34
|
+
? value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '')
|
|
35
|
+
: ''
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildCrudPayload(
|
|
39
|
+
normalized: Record<string, unknown>,
|
|
40
|
+
action: string | undefined,
|
|
41
|
+
data: string | undefined,
|
|
42
|
+
): Record<string, unknown> {
|
|
43
|
+
if (data) return JSON.parse(data)
|
|
44
|
+
if (action !== 'create' && action !== 'update') return {}
|
|
45
|
+
const entries = Object.entries(normalized).filter(([key]) =>
|
|
46
|
+
![
|
|
47
|
+
'action',
|
|
48
|
+
'id',
|
|
49
|
+
'skillId',
|
|
50
|
+
'data',
|
|
51
|
+
'query',
|
|
52
|
+
'task',
|
|
53
|
+
'url',
|
|
54
|
+
'approvalId',
|
|
55
|
+
'attach',
|
|
56
|
+
'agentId',
|
|
57
|
+
'targetAgentId',
|
|
58
|
+
'input',
|
|
59
|
+
'args',
|
|
60
|
+
'arguments',
|
|
61
|
+
'payload',
|
|
62
|
+
'parameters',
|
|
63
|
+
].includes(key),
|
|
64
|
+
)
|
|
65
|
+
return entries.length > 0 ? Object.fromEntries(entries) : {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function summarizeSkill(skill: ResolvedRuntimeSkill): Record<string, unknown> {
|
|
69
|
+
return {
|
|
70
|
+
id: skill.id,
|
|
71
|
+
storageId: skill.storageId || null,
|
|
72
|
+
key: skill.key,
|
|
73
|
+
name: skill.name,
|
|
74
|
+
description: skill.description || '',
|
|
75
|
+
source: skill.source,
|
|
76
|
+
managed: skill.managed,
|
|
77
|
+
attached: skill.attached,
|
|
78
|
+
eligible: skill.eligible,
|
|
79
|
+
status: skill.status,
|
|
80
|
+
missing: skill.missing,
|
|
81
|
+
toolNames: skill.toolNames,
|
|
82
|
+
capabilities: skill.capabilities,
|
|
83
|
+
installOptions: skill.installOptions || [],
|
|
84
|
+
autoMatch: skill.autoMatch,
|
|
85
|
+
matchReasons: skill.matchReasons,
|
|
86
|
+
invocation: skill.invocation || null,
|
|
87
|
+
commandDispatch: skill.commandDispatch || null,
|
|
88
|
+
executionMode: skill.executionMode,
|
|
89
|
+
runnable: skill.runnable,
|
|
90
|
+
selected: skill.selected,
|
|
91
|
+
dispatchBlocker: skill.dispatchBlocker || null,
|
|
92
|
+
sourcePath: skill.sourcePath || null,
|
|
93
|
+
sourceUrl: skill.sourceUrl || null,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveActiveAgent(bctx: ToolBuildContext): Agent | null {
|
|
98
|
+
const agentId = bctx.ctx?.agentId
|
|
99
|
+
if (!agentId) return null
|
|
100
|
+
return loadAgent(agentId) as Agent | null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveTargetAgentId(
|
|
104
|
+
payload: Record<string, unknown>,
|
|
105
|
+
bctx: ToolBuildContext,
|
|
106
|
+
): string | null {
|
|
107
|
+
const requested = typeof payload.agentId === 'string' && payload.agentId.trim()
|
|
108
|
+
? payload.agentId.trim()
|
|
109
|
+
: typeof payload.targetAgentId === 'string' && payload.targetAgentId.trim()
|
|
110
|
+
? payload.targetAgentId.trim()
|
|
111
|
+
: bctx.ctx?.agentId || null
|
|
112
|
+
|
|
113
|
+
if (!requested) return null
|
|
114
|
+
if (bctx.ctx?.platformAssignScope !== 'all' && requested !== bctx.ctx?.agentId) {
|
|
115
|
+
throw new Error(`You may only attach skills to your own agent (${bctx.ctx?.agentId || 'current agent'}) in this session.`)
|
|
116
|
+
}
|
|
117
|
+
const target = loadAgent(requested)
|
|
118
|
+
if (!target) throw new Error(`Agent "${requested}" not found.`)
|
|
119
|
+
return requested
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function upsertStoredSkill(input: {
|
|
123
|
+
existingId?: string
|
|
124
|
+
body: Record<string, unknown>
|
|
125
|
+
}): Skill {
|
|
126
|
+
const skills = loadSkills()
|
|
127
|
+
const normalized = normalizeSkillPayload(input.body)
|
|
128
|
+
const now = Date.now()
|
|
129
|
+
const id = input.existingId || genId()
|
|
130
|
+
const previous = input.existingId ? skills[input.existingId] : null
|
|
131
|
+
|
|
132
|
+
const next: Skill = {
|
|
133
|
+
id,
|
|
134
|
+
name: normalized.name,
|
|
135
|
+
filename: normalized.filename || previous?.filename || `skill-${id}.md`,
|
|
136
|
+
content: normalized.content || '',
|
|
137
|
+
description: normalized.description || '',
|
|
138
|
+
sourceUrl: normalized.sourceUrl,
|
|
139
|
+
sourceFormat: normalized.sourceFormat,
|
|
140
|
+
author: normalized.author,
|
|
141
|
+
tags: normalized.tags,
|
|
142
|
+
version: normalized.version,
|
|
143
|
+
homepage: normalized.homepage,
|
|
144
|
+
primaryEnv: normalized.primaryEnv,
|
|
145
|
+
skillKey: normalized.skillKey,
|
|
146
|
+
toolNames: normalized.toolNames,
|
|
147
|
+
capabilities: normalized.capabilities,
|
|
148
|
+
always: normalized.always,
|
|
149
|
+
installOptions: normalized.installOptions,
|
|
150
|
+
skillRequirements: normalized.skillRequirements,
|
|
151
|
+
detectedEnvVars: normalized.detectedEnvVars,
|
|
152
|
+
security: normalized.security,
|
|
153
|
+
invocation: normalized.invocation,
|
|
154
|
+
commandDispatch: normalized.commandDispatch,
|
|
155
|
+
frontmatter: normalized.frontmatter,
|
|
156
|
+
scope: input.body.scope === 'agent' ? 'agent' : previous?.scope || 'global',
|
|
157
|
+
agentIds: Array.isArray(input.body.agentIds)
|
|
158
|
+
? (input.body.agentIds as unknown[]).filter((value): value is string => typeof value === 'string')
|
|
159
|
+
: previous?.agentIds,
|
|
160
|
+
createdAt: previous?.createdAt || now,
|
|
161
|
+
updatedAt: now,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
skills[id] = next
|
|
165
|
+
saveSkills(skills)
|
|
166
|
+
clearDiscoveredSkillsCache()
|
|
167
|
+
return next
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function attachSkillToAgent(skillId: string, agentId: string): Agent {
|
|
171
|
+
const updated = patchAgent(agentId, (current) => {
|
|
172
|
+
if (!current) return current
|
|
173
|
+
const nextSkillIds = dedup([...(Array.isArray(current.skillIds) ? current.skillIds : []), skillId])
|
|
174
|
+
current.skillIds = nextSkillIds
|
|
175
|
+
current.updatedAt = Date.now()
|
|
176
|
+
return current
|
|
177
|
+
})
|
|
178
|
+
if (!updated) throw new Error(`Agent "${agentId}" not found.`)
|
|
179
|
+
return updated as Agent
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function findStoredSkillBySelector(skills: Record<string, Skill>, selector: SkillSelectorInput): Skill | null {
|
|
183
|
+
const directId = selector.id || selector.skillId
|
|
184
|
+
if (directId && skills[directId]) return skills[directId]
|
|
185
|
+
|
|
186
|
+
const normalizedName = normalizeKey(selector.name)
|
|
187
|
+
if (!normalizedName) return null
|
|
188
|
+
return Object.values(skills).find((skill) =>
|
|
189
|
+
normalizeKey(skill.id) === normalizedName
|
|
190
|
+
|| normalizeKey(skill.name) === normalizedName
|
|
191
|
+
|| normalizeKey(skill.skillKey || '') === normalizedName,
|
|
192
|
+
) || null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildSkillSnapshot(bctx: ToolBuildContext) {
|
|
196
|
+
clearDiscoveredSkillsCache()
|
|
197
|
+
const activeAgent = resolveActiveAgent(bctx)
|
|
198
|
+
const session = bctx.resolveCurrentSession?.()
|
|
199
|
+
return resolveRuntimeSkills({
|
|
200
|
+
cwd: bctx.cwd,
|
|
201
|
+
enabledPlugins: bctx.activePlugins,
|
|
202
|
+
agentSkillIds: activeAgent?.skillIds || [],
|
|
203
|
+
storedSkills: loadSkills(),
|
|
204
|
+
selectedSkillId: typeof session?.skillRuntimeState?.selectedSkillId === 'string'
|
|
205
|
+
? session.skillRuntimeState.selectedSkillId
|
|
206
|
+
: null,
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function findPendingInstallApproval(params: {
|
|
211
|
+
sessionId?: string | null
|
|
212
|
+
agentId?: string | null
|
|
213
|
+
question: string
|
|
214
|
+
prompt: string
|
|
215
|
+
}): ApprovalRequest | null {
|
|
216
|
+
return Object.values(loadApprovals()).find((approval) =>
|
|
217
|
+
approval.status === 'pending'
|
|
218
|
+
&& approval.category === 'human_loop'
|
|
219
|
+
&& (approval.sessionId || null) === (params.sessionId || null)
|
|
220
|
+
&& (approval.agentId || null) === (params.agentId || null)
|
|
221
|
+
&& approval.data?.question === params.question
|
|
222
|
+
&& approval.data?.prompt === params.prompt,
|
|
223
|
+
) || null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function ensureApprovedInstall(approvalId: string | null | undefined): ApprovalRequest {
|
|
227
|
+
const normalized = typeof approvalId === 'string' ? approvalId.trim() : ''
|
|
228
|
+
if (!normalized) {
|
|
229
|
+
throw new Error('This install requires approval. Call manage_skills install first to create the approval request, then retry with approvalId after approval.')
|
|
230
|
+
}
|
|
231
|
+
const approval = loadApprovals()[normalized]
|
|
232
|
+
if (!approval) throw new Error(`Approval "${normalized}" not found.`)
|
|
233
|
+
if (approval.status !== 'approved') {
|
|
234
|
+
throw new Error(`Approval "${normalized}" is not approved yet.`)
|
|
235
|
+
}
|
|
236
|
+
return approval
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function materializeResolvedSkill(skill: ResolvedRuntimeSkill): Promise<Skill> {
|
|
240
|
+
const skills = loadSkills()
|
|
241
|
+
const existing = skill.storageId ? skills[skill.storageId] : null
|
|
242
|
+
if (existing) return existing
|
|
243
|
+
const duplicate = Object.values(skills).find((entry) =>
|
|
244
|
+
normalizeKey(entry.skillKey || entry.name) === normalizeKey(skill.skillKey || skill.name),
|
|
245
|
+
)
|
|
246
|
+
if (duplicate) return duplicate
|
|
247
|
+
return upsertStoredSkill({
|
|
248
|
+
body: {
|
|
249
|
+
name: skill.name,
|
|
250
|
+
filename: skill.filename,
|
|
251
|
+
description: skill.description,
|
|
252
|
+
content: skill.content,
|
|
253
|
+
sourceUrl: skill.sourceUrl,
|
|
254
|
+
sourceFormat: skill.sourceFormat,
|
|
255
|
+
author: skill.author,
|
|
256
|
+
tags: skill.tags,
|
|
257
|
+
version: skill.version,
|
|
258
|
+
homepage: skill.homepage,
|
|
259
|
+
primaryEnv: skill.primaryEnv,
|
|
260
|
+
skillKey: skill.skillKey,
|
|
261
|
+
toolNames: skill.toolNames,
|
|
262
|
+
capabilities: skill.capabilities,
|
|
263
|
+
always: skill.always,
|
|
264
|
+
installOptions: skill.installOptions,
|
|
265
|
+
skillRequirements: skill.skillRequirements,
|
|
266
|
+
detectedEnvVars: skill.detectedEnvVars,
|
|
267
|
+
security: skill.security,
|
|
268
|
+
invocation: skill.invocation,
|
|
269
|
+
commandDispatch: skill.commandDispatch,
|
|
270
|
+
frontmatter: skill.frontmatter,
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function installRemoteSkill(params: {
|
|
276
|
+
name?: string
|
|
277
|
+
description?: string
|
|
278
|
+
url: string
|
|
279
|
+
author?: string
|
|
280
|
+
tags?: string[]
|
|
281
|
+
content?: string
|
|
282
|
+
}): Promise<Skill> {
|
|
283
|
+
const content = params.content || await fetchSkillContent(params.url)
|
|
284
|
+
const skills = loadSkills()
|
|
285
|
+
const normalizedBody = normalizeSkillPayload({
|
|
286
|
+
name: params.name,
|
|
287
|
+
description: params.description,
|
|
288
|
+
sourceUrl: params.url,
|
|
289
|
+
author: params.author,
|
|
290
|
+
tags: params.tags,
|
|
291
|
+
content,
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
const duplicate = Object.values(skills).find((skill) =>
|
|
295
|
+
normalizeKey(skill.skillKey || skill.name) === normalizeKey(normalizedBody.skillKey || normalizedBody.name),
|
|
296
|
+
)
|
|
297
|
+
if (duplicate) return duplicate
|
|
298
|
+
|
|
299
|
+
return upsertStoredSkill({
|
|
300
|
+
body: {
|
|
301
|
+
...normalizedBody,
|
|
302
|
+
content,
|
|
303
|
+
},
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function parseSearchLimit(raw: unknown, fallback = 8): number {
|
|
308
|
+
const parsed = typeof raw === 'number' ? raw : Number.parseInt(String(raw || ''), 10)
|
|
309
|
+
if (!Number.isFinite(parsed)) return fallback
|
|
310
|
+
return Math.max(1, Math.min(20, Math.trunc(parsed)))
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function buildManageSkillsDescription(): string {
|
|
314
|
+
return [
|
|
315
|
+
'Manage reusable skills and runtime skill discovery.',
|
|
316
|
+
'Supported actions: `list`, `get`, `create`, `update`, `delete`, `status`, `search_available`, `recommend_for_task`, `attach`, `install`.',
|
|
317
|
+
'Use `status` to inspect local/runtime skills with eligibility and missing requirements.',
|
|
318
|
+
'Use `recommend_for_task` when a task may benefit from a reusable workflow.',
|
|
319
|
+
'Use `use_skill` for runtime selection, loading, and execution of an already-discovered skill.',
|
|
320
|
+
'Use `install` only when you intentionally want to add a skill. Installation is explicit and approval-gated.',
|
|
321
|
+
'Use this direct tool name exactly as shown (`manage_skills`).',
|
|
322
|
+
].join('\n\n')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export async function executeManageSkillsAction(
|
|
326
|
+
rawArgs: Record<string, unknown>,
|
|
327
|
+
bctx: ToolBuildContext,
|
|
328
|
+
): Promise<string> {
|
|
329
|
+
const normalized = rawArgs
|
|
330
|
+
const action = typeof normalized.action === 'string' ? normalized.action.trim().toLowerCase() : ''
|
|
331
|
+
const data = typeof normalized.data === 'string' ? normalized.data : undefined
|
|
332
|
+
const payload = buildCrudPayload(normalized, action, data)
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
switch (action) {
|
|
336
|
+
case 'list': {
|
|
337
|
+
return JSON.stringify(Object.values(loadSkills()))
|
|
338
|
+
}
|
|
339
|
+
case 'get': {
|
|
340
|
+
const skills = loadSkills()
|
|
341
|
+
const selected = findStoredSkillBySelector(skills, {
|
|
342
|
+
id: typeof normalized.id === 'string' ? normalized.id : undefined,
|
|
343
|
+
skillId: typeof normalized.skillId === 'string' ? normalized.skillId : undefined,
|
|
344
|
+
name: typeof normalized.name === 'string' ? normalized.name : undefined,
|
|
345
|
+
})
|
|
346
|
+
if (!selected) return 'Not found.'
|
|
347
|
+
return JSON.stringify(selected)
|
|
348
|
+
}
|
|
349
|
+
case 'create': {
|
|
350
|
+
const created = upsertStoredSkill({ body: payload })
|
|
351
|
+
return JSON.stringify(created)
|
|
352
|
+
}
|
|
353
|
+
case 'update': {
|
|
354
|
+
const skillId = typeof normalized.id === 'string' ? normalized.id.trim() : ''
|
|
355
|
+
if (!skillId) return 'Error: "id" is required for update action.'
|
|
356
|
+
const existing = loadSkills()[skillId]
|
|
357
|
+
if (!existing) return `Not found: skills "${skillId}"`
|
|
358
|
+
const updated = upsertStoredSkill({
|
|
359
|
+
existingId: skillId,
|
|
360
|
+
body: { ...existing, ...payload },
|
|
361
|
+
})
|
|
362
|
+
return JSON.stringify(updated)
|
|
363
|
+
}
|
|
364
|
+
case 'delete': {
|
|
365
|
+
const skillId = typeof normalized.id === 'string' ? normalized.id.trim() : ''
|
|
366
|
+
if (!skillId) return 'Error: "id" is required for delete action.'
|
|
367
|
+
const skills = loadSkills()
|
|
368
|
+
if (!skills[skillId]) return `Not found: skills "${skillId}"`
|
|
369
|
+
delete skills[skillId]
|
|
370
|
+
saveSkills(skills)
|
|
371
|
+
return JSON.stringify({ deleted: skillId })
|
|
372
|
+
}
|
|
373
|
+
case 'status': {
|
|
374
|
+
const snapshot = buildSkillSnapshot(bctx)
|
|
375
|
+
const query = typeof normalized.query === 'string' ? normalized.query.trim() : ''
|
|
376
|
+
const ranked = query
|
|
377
|
+
? recommendRuntimeSkillsForTask(snapshot.skills, query, bctx.activePlugins).map((entry) => entry.skill)
|
|
378
|
+
: snapshot.skills
|
|
379
|
+
const limit = parseSearchLimit(normalized.limit, 12)
|
|
380
|
+
return JSON.stringify(ranked.slice(0, limit).map(summarizeSkill))
|
|
381
|
+
}
|
|
382
|
+
case 'search_available': {
|
|
383
|
+
const snapshot = buildSkillSnapshot(bctx)
|
|
384
|
+
const query = typeof normalized.query === 'string' ? normalized.query.trim() : ''
|
|
385
|
+
const limit = parseSearchLimit(normalized.limit, 8)
|
|
386
|
+
const local = query
|
|
387
|
+
? recommendRuntimeSkillsForTask(snapshot.skills, query, bctx.activePlugins).slice(0, limit)
|
|
388
|
+
: snapshot.skills.slice(0, limit).map((skill) => ({ skill, score: skill.score, reasons: skill.matchReasons }))
|
|
389
|
+
const marketplace = query ? await searchClawHub(query, 1, limit) : { skills: [], total: 0, page: 1 }
|
|
390
|
+
return JSON.stringify({
|
|
391
|
+
local: local.map((entry) => ({
|
|
392
|
+
...summarizeSkill(entry.skill),
|
|
393
|
+
score: entry.score,
|
|
394
|
+
reasons: entry.reasons,
|
|
395
|
+
})),
|
|
396
|
+
marketplace: marketplace.skills,
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
case 'recommend_for_task': {
|
|
400
|
+
const task = typeof normalized.task === 'string' && normalized.task.trim()
|
|
401
|
+
? normalized.task.trim()
|
|
402
|
+
: typeof normalized.query === 'string' ? normalized.query.trim() : ''
|
|
403
|
+
if (!task) return 'Error: "task" or "query" is required for recommend_for_task.'
|
|
404
|
+
const snapshot = buildSkillSnapshot(bctx)
|
|
405
|
+
const local = recommendRuntimeSkillsForTask(snapshot.skills, task, bctx.activePlugins).slice(0, 8)
|
|
406
|
+
const remote = local.length >= 3 ? { skills: [] } : await searchClawHub(task, 1, 5)
|
|
407
|
+
return JSON.stringify({
|
|
408
|
+
local: local.map((entry) => ({
|
|
409
|
+
...summarizeSkill(entry.skill),
|
|
410
|
+
score: entry.score,
|
|
411
|
+
reasons: entry.reasons,
|
|
412
|
+
promptEligible: snapshot.promptSkills.some((skill) => skill.id === entry.skill.id),
|
|
413
|
+
})),
|
|
414
|
+
marketplace: remote.skills,
|
|
415
|
+
promptBlocks: buildRuntimeSkillPromptBlocks({
|
|
416
|
+
...snapshot,
|
|
417
|
+
promptSkills: local.map((entry) => entry.skill).filter((skill) => skill.eligible),
|
|
418
|
+
availableSkills: snapshot.skills.filter((skill) => !local.some((entry) => entry.skill.id === skill.id)),
|
|
419
|
+
}),
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
case 'attach': {
|
|
423
|
+
const snapshot = buildSkillSnapshot(bctx)
|
|
424
|
+
const target = findResolvedSkill(snapshot.skills, String(normalized.id || normalized.skillId || normalized.name || ''))
|
|
425
|
+
if (!target) return 'Error: skill not found in local/runtime skills.'
|
|
426
|
+
const stored = await materializeResolvedSkill(target)
|
|
427
|
+
const agentId = resolveTargetAgentId(normalized, bctx)
|
|
428
|
+
if (!agentId) return 'Error: no target agent available for attach.'
|
|
429
|
+
attachSkillToAgent(stored.id, agentId)
|
|
430
|
+
return JSON.stringify({
|
|
431
|
+
ok: true,
|
|
432
|
+
agentId,
|
|
433
|
+
skillId: stored.id,
|
|
434
|
+
skillName: stored.name,
|
|
435
|
+
attached: true,
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
case 'install': {
|
|
439
|
+
const snapshot = buildSkillSnapshot(bctx)
|
|
440
|
+
const selector = String(normalized.id || normalized.skillId || normalized.name || '').trim()
|
|
441
|
+
const localTarget = selector ? findResolvedSkill(snapshot.skills, selector) : null
|
|
442
|
+
const attach = normalized.attach === true
|
|
443
|
+
const attachAgentId = attach ? resolveTargetAgentId(normalized, bctx) : null
|
|
444
|
+
|
|
445
|
+
if (localTarget) {
|
|
446
|
+
if (!localTarget.storageId) {
|
|
447
|
+
const question = `Install local skill "${localTarget.name}" into managed skills?`
|
|
448
|
+
const prompt = localTarget.sourcePath || localTarget.id
|
|
449
|
+
const pending = findPendingInstallApproval({
|
|
450
|
+
sessionId: bctx.ctx?.sessionId || null,
|
|
451
|
+
agentId: bctx.ctx?.agentId || null,
|
|
452
|
+
question,
|
|
453
|
+
prompt,
|
|
454
|
+
})
|
|
455
|
+
if (!normalized.approvalId) {
|
|
456
|
+
const approval = pending || requestApproval({
|
|
457
|
+
category: 'human_loop',
|
|
458
|
+
title: `Install skill "${localTarget.name}"`,
|
|
459
|
+
description: 'Approve copying this local skill into managed skills so it can be attached and reused durably.',
|
|
460
|
+
agentId: bctx.ctx?.agentId || null,
|
|
461
|
+
sessionId: bctx.ctx?.sessionId || null,
|
|
462
|
+
data: {
|
|
463
|
+
question,
|
|
464
|
+
prompt,
|
|
465
|
+
action: 'manage_skills.install',
|
|
466
|
+
skillName: localTarget.name,
|
|
467
|
+
runtimeSkillId: localTarget.id,
|
|
468
|
+
attachAgentId,
|
|
469
|
+
},
|
|
470
|
+
})
|
|
471
|
+
return JSON.stringify({
|
|
472
|
+
ok: false,
|
|
473
|
+
requiresApproval: true,
|
|
474
|
+
approval,
|
|
475
|
+
skill: summarizeSkill(localTarget),
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
ensureApprovedInstall(typeof normalized.approvalId === 'string' ? normalized.approvalId : null)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const stored = await materializeResolvedSkill(localTarget)
|
|
482
|
+
if (attach && attachAgentId) attachSkillToAgent(stored.id, attachAgentId)
|
|
483
|
+
return JSON.stringify({
|
|
484
|
+
ok: true,
|
|
485
|
+
installed: true,
|
|
486
|
+
source: localTarget.source,
|
|
487
|
+
deduplicated: Boolean(localTarget.storageId),
|
|
488
|
+
skill: stored,
|
|
489
|
+
attachedToAgentId: attachAgentId,
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const url = typeof normalized.url === 'string' ? normalized.url.trim() : ''
|
|
494
|
+
let remoteTarget = {
|
|
495
|
+
name: typeof normalized.name === 'string' ? normalized.name.trim() : '',
|
|
496
|
+
description: typeof normalized.description === 'string' ? normalized.description.trim() : '',
|
|
497
|
+
url,
|
|
498
|
+
author: typeof normalized.author === 'string' ? normalized.author.trim() : '',
|
|
499
|
+
tags: Array.isArray(normalized.tags)
|
|
500
|
+
? normalized.tags.filter((value): value is string => typeof value === 'string')
|
|
501
|
+
: [],
|
|
502
|
+
content: typeof normalized.content === 'string' ? normalized.content : undefined,
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (!remoteTarget.url) {
|
|
506
|
+
const query = remoteTarget.name || (typeof normalized.query === 'string' ? normalized.query.trim() : '')
|
|
507
|
+
if (!query) return 'Error: install requires a local skill selector, `url`, or a `name`/`query` to search.'
|
|
508
|
+
const marketplace = await searchClawHub(query, 1, 5)
|
|
509
|
+
const exact = marketplace.skills.find((skill) =>
|
|
510
|
+
normalizeKey(skill.name) === normalizeKey(query)
|
|
511
|
+
|| normalizeKey(skill.id) === normalizeKey(query),
|
|
512
|
+
) || marketplace.skills[0]
|
|
513
|
+
if (!exact) return `Error: no marketplace skill found for "${query}".`
|
|
514
|
+
remoteTarget = {
|
|
515
|
+
...remoteTarget,
|
|
516
|
+
name: exact.name,
|
|
517
|
+
description: exact.description,
|
|
518
|
+
url: exact.url,
|
|
519
|
+
author: exact.author,
|
|
520
|
+
tags: exact.tags,
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const question = `Install skill "${remoteTarget.name || remoteTarget.url}" from ${remoteTarget.url}?`
|
|
525
|
+
const prompt = remoteTarget.url
|
|
526
|
+
const pending = findPendingInstallApproval({
|
|
527
|
+
sessionId: bctx.ctx?.sessionId || null,
|
|
528
|
+
agentId: bctx.ctx?.agentId || null,
|
|
529
|
+
question,
|
|
530
|
+
prompt,
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
if (!normalized.approvalId) {
|
|
534
|
+
const approval = pending || requestApproval({
|
|
535
|
+
category: 'human_loop',
|
|
536
|
+
title: `Install skill "${remoteTarget.name || 'remote skill'}"`,
|
|
537
|
+
description: 'Approve adding this external skill to managed skills.',
|
|
538
|
+
agentId: bctx.ctx?.agentId || null,
|
|
539
|
+
sessionId: bctx.ctx?.sessionId || null,
|
|
540
|
+
data: {
|
|
541
|
+
question,
|
|
542
|
+
prompt,
|
|
543
|
+
action: 'manage_skills.install',
|
|
544
|
+
skillName: remoteTarget.name,
|
|
545
|
+
url: remoteTarget.url,
|
|
546
|
+
attachAgentId,
|
|
547
|
+
},
|
|
548
|
+
})
|
|
549
|
+
return JSON.stringify({
|
|
550
|
+
ok: false,
|
|
551
|
+
requiresApproval: true,
|
|
552
|
+
approval,
|
|
553
|
+
source: 'clawhub',
|
|
554
|
+
skill: remoteTarget,
|
|
555
|
+
})
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
ensureApprovedInstall(typeof normalized.approvalId === 'string' ? normalized.approvalId : null)
|
|
559
|
+
const installed = await installRemoteSkill(remoteTarget)
|
|
560
|
+
if (attach && attachAgentId) attachSkillToAgent(installed.id, attachAgentId)
|
|
561
|
+
return JSON.stringify({
|
|
562
|
+
ok: true,
|
|
563
|
+
installed: true,
|
|
564
|
+
source: 'clawhub',
|
|
565
|
+
skill: installed,
|
|
566
|
+
attachedToAgentId: attachAgentId,
|
|
567
|
+
})
|
|
568
|
+
}
|
|
569
|
+
default:
|
|
570
|
+
return `Error: Unknown action "${action}".`
|
|
571
|
+
}
|
|
572
|
+
} catch (err: unknown) {
|
|
573
|
+
return `Error: ${errorMessage(err)}`
|
|
574
|
+
}
|
|
575
|
+
}
|