@swarmclawai/swarmclaw 1.9.2 → 1.9.3
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 +11 -1
- package/electron-dist/main.js +218 -0
- package/package.json +2 -2
- package/src/app/api/extensions/managed-resources/route.test.ts +117 -0
- package/src/app/api/extensions/managed-resources/route.ts +116 -0
- package/src/cli/index.js +2 -0
- package/src/cli/spec.js +2 -0
- package/src/lib/server/extension-managed-resources.test.ts +159 -0
- package/src/lib/server/extension-managed-resources.ts +905 -0
- package/src/lib/server/extensions.ts +113 -2
- package/src/lib/server/session-tools/extension-creator.ts +50 -0
- package/src/types/agent.ts +2 -0
- package/src/types/app-settings.ts +8 -0
- package/src/types/extension.ts +132 -0
- package/src/types/schedule.ts +3 -0
- package/src/views/settings/extension-manager.tsx +157 -1
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
ExtensionUIDefinition,
|
|
12
12
|
ExtensionProviderDefinition,
|
|
13
13
|
ExtensionConnectorDefinition,
|
|
14
|
+
ExtensionManagedResources,
|
|
14
15
|
Session,
|
|
15
16
|
ExtensionPackageManager,
|
|
16
17
|
ExtensionDependencyInstallStatus,
|
|
@@ -467,11 +468,60 @@ function coerceTools(rawTools: unknown): ExtensionToolDef[] {
|
|
|
467
468
|
return []
|
|
468
469
|
}
|
|
469
470
|
|
|
471
|
+
function coerceManagedResources(raw: Record<string, unknown>): ExtensionManagedResources | undefined {
|
|
472
|
+
const explicit = isRecord(raw.managedResources)
|
|
473
|
+
? raw.managedResources as Record<string, unknown>
|
|
474
|
+
: {}
|
|
475
|
+
const agents = Array.isArray(explicit.agents)
|
|
476
|
+
? explicit.agents
|
|
477
|
+
: Array.isArray(raw.agents)
|
|
478
|
+
? raw.agents
|
|
479
|
+
: undefined
|
|
480
|
+
const schedules = Array.isArray(explicit.schedules)
|
|
481
|
+
? explicit.schedules
|
|
482
|
+
: Array.isArray(raw.schedules)
|
|
483
|
+
? raw.schedules
|
|
484
|
+
: undefined
|
|
485
|
+
const routines = Array.isArray(explicit.routines)
|
|
486
|
+
? explicit.routines
|
|
487
|
+
: Array.isArray(raw.routines)
|
|
488
|
+
? raw.routines
|
|
489
|
+
: undefined
|
|
490
|
+
const localFolders = Array.isArray(explicit.localFolders)
|
|
491
|
+
? explicit.localFolders
|
|
492
|
+
: Array.isArray(raw.localFolders)
|
|
493
|
+
? raw.localFolders
|
|
494
|
+
: undefined
|
|
495
|
+
const gatewayPlatforms = Array.isArray(explicit.gatewayPlatforms)
|
|
496
|
+
? explicit.gatewayPlatforms
|
|
497
|
+
: Array.isArray(raw.gatewayPlatforms)
|
|
498
|
+
? raw.gatewayPlatforms
|
|
499
|
+
: undefined
|
|
500
|
+
const setupChecks = Array.isArray(explicit.setupChecks)
|
|
501
|
+
? explicit.setupChecks
|
|
502
|
+
: Array.isArray(raw.setupChecks)
|
|
503
|
+
? raw.setupChecks
|
|
504
|
+
: undefined
|
|
505
|
+
|
|
506
|
+
const managedResources: ExtensionManagedResources = {
|
|
507
|
+
agents: agents as ExtensionManagedResources['agents'],
|
|
508
|
+
schedules: schedules as ExtensionManagedResources['schedules'],
|
|
509
|
+
routines: routines as ExtensionManagedResources['routines'],
|
|
510
|
+
localFolders: localFolders as ExtensionManagedResources['localFolders'],
|
|
511
|
+
gatewayPlatforms: gatewayPlatforms as ExtensionManagedResources['gatewayPlatforms'],
|
|
512
|
+
setupChecks: setupChecks as ExtensionManagedResources['setupChecks'],
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return Object.values(managedResources).some((value) => Array.isArray(value) && value.length > 0)
|
|
516
|
+
? managedResources
|
|
517
|
+
: undefined
|
|
518
|
+
}
|
|
519
|
+
|
|
470
520
|
function normalizeExtension(mod: unknown): Extension | null {
|
|
471
521
|
const modObj = mod as Record<string, unknown>
|
|
472
522
|
const raw: Record<string, unknown> = (modObj?.default as Record<string, unknown>) || modObj
|
|
473
523
|
|
|
474
|
-
if (raw.name && (raw.hooks || raw.tools || raw.ui || raw.providers || raw.connectors)) {
|
|
524
|
+
if (raw.name && (raw.hooks || raw.tools || raw.ui || raw.providers || raw.connectors || raw.managedResources || raw.agents || raw.schedules || raw.routines || raw.localFolders || raw.gatewayPlatforms || raw.setupChecks)) {
|
|
475
525
|
const hooks = isRecord(raw.hooks) ? (raw.hooks as ExtensionHooks) : {}
|
|
476
526
|
return {
|
|
477
527
|
name: raw.name as string,
|
|
@@ -484,6 +534,7 @@ function normalizeExtension(mod: unknown): Extension | null {
|
|
|
484
534
|
ui: isRecord(raw.ui) ? (raw.ui as ExtensionUIDefinition) : undefined,
|
|
485
535
|
providers: Array.isArray(raw.providers) ? (raw.providers as ExtensionProviderDefinition[]) : undefined,
|
|
486
536
|
connectors: Array.isArray(raw.connectors) ? (raw.connectors as ExtensionConnectorDefinition[]) : undefined,
|
|
537
|
+
managedResources: coerceManagedResources(raw),
|
|
487
538
|
} as Extension
|
|
488
539
|
}
|
|
489
540
|
|
|
@@ -639,6 +690,7 @@ interface LoadedExtension {
|
|
|
639
690
|
ui?: ExtensionUIDefinition
|
|
640
691
|
providers?: ExtensionProviderDefinition[]
|
|
641
692
|
connectors?: ExtensionConnectorDefinition[]
|
|
693
|
+
managedResources?: ExtensionManagedResources
|
|
642
694
|
isBuiltin?: boolean
|
|
643
695
|
}
|
|
644
696
|
|
|
@@ -1017,6 +1069,7 @@ class ExtensionManager {
|
|
|
1017
1069
|
ui: p.ui,
|
|
1018
1070
|
providers: p.providers,
|
|
1019
1071
|
connectors: p.connectors,
|
|
1072
|
+
managedResources: p.managedResources || coerceManagedResources(p as unknown as Record<string, unknown>),
|
|
1020
1073
|
isBuiltin: true
|
|
1021
1074
|
})
|
|
1022
1075
|
this.markExtensionSuccess(id)
|
|
@@ -1064,6 +1117,7 @@ class ExtensionManager {
|
|
|
1064
1117
|
ui: ext.ui,
|
|
1065
1118
|
providers: ext.providers,
|
|
1066
1119
|
connectors: ext.connectors,
|
|
1120
|
+
managedResources: ext.managedResources,
|
|
1067
1121
|
})
|
|
1068
1122
|
this.markExtensionSuccess(file)
|
|
1069
1123
|
} catch (err: unknown) {
|
|
@@ -1145,6 +1199,55 @@ class ExtensionManager {
|
|
|
1145
1199
|
return allUI
|
|
1146
1200
|
}
|
|
1147
1201
|
|
|
1202
|
+
getManagedResourceExtensions(): Array<{
|
|
1203
|
+
extensionId: string
|
|
1204
|
+
extensionName: string
|
|
1205
|
+
enabled: boolean
|
|
1206
|
+
isBuiltin: boolean
|
|
1207
|
+
source?: ExtensionMeta['source']
|
|
1208
|
+
managedResources: ExtensionManagedResources
|
|
1209
|
+
}> {
|
|
1210
|
+
this.load()
|
|
1211
|
+
const result: Array<{
|
|
1212
|
+
extensionId: string
|
|
1213
|
+
extensionName: string
|
|
1214
|
+
enabled: boolean
|
|
1215
|
+
isBuiltin: boolean
|
|
1216
|
+
source?: ExtensionMeta['source']
|
|
1217
|
+
managedResources: ExtensionManagedResources
|
|
1218
|
+
}> = []
|
|
1219
|
+
|
|
1220
|
+
for (const [id, entry] of this.extensions.entries()) {
|
|
1221
|
+
const managedResources = entry.managedResources
|
|
1222
|
+
if (!managedResources) continue
|
|
1223
|
+
if (!Object.values(managedResources).some((value) => Array.isArray(value) && value.length > 0)) continue
|
|
1224
|
+
result.push({
|
|
1225
|
+
extensionId: id,
|
|
1226
|
+
extensionName: entry.meta.name,
|
|
1227
|
+
enabled: entry.meta.enabled,
|
|
1228
|
+
isBuiltin: entry.isBuiltin === true,
|
|
1229
|
+
source: entry.meta.source,
|
|
1230
|
+
managedResources,
|
|
1231
|
+
})
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
return result
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
getManagedResources(extensionId: string): ExtensionManagedResources | null {
|
|
1238
|
+
this.load()
|
|
1239
|
+
const candidateIds = expandExtensionIds([extensionId])
|
|
1240
|
+
for (const id of candidateIds) {
|
|
1241
|
+
const loaded = this.extensions.get(id)
|
|
1242
|
+
if (loaded?.managedResources) return loaded.managedResources
|
|
1243
|
+
const builtin = this.builtins.get(id)
|
|
1244
|
+
if (builtin) {
|
|
1245
|
+
return builtin.managedResources || coerceManagedResources(builtin as unknown as Record<string, unknown>) || null
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return null
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1148
1251
|
listExtensionIds(): string[] {
|
|
1149
1252
|
this.load()
|
|
1150
1253
|
return Array.from(this.extensions.keys())
|
|
@@ -1842,11 +1945,14 @@ class ExtensionManager {
|
|
|
1842
1945
|
const failures = this.readFailureState()
|
|
1843
1946
|
const metas: ExtensionMeta[] = []
|
|
1844
1947
|
|
|
1845
|
-
const describeCapabilities = (loaded?: LoadedExtension, fallback?: Extension): Pick<ExtensionMeta, 'toolCount' | 'hookCount' | 'hasUI' | 'providerCount' | 'connectorCount' | 'settingsFields'> => {
|
|
1948
|
+
const describeCapabilities = (loaded?: LoadedExtension, fallback?: Extension): Pick<ExtensionMeta, 'toolCount' | 'hookCount' | 'hasUI' | 'providerCount' | 'connectorCount' | 'settingsFields' | 'managedAgentCount' | 'managedScheduleCount' | 'localFolderCount' | 'gatewayPlatformCount' | 'setupCheckCount'> => {
|
|
1846
1949
|
const tools = loaded?.tools || fallback?.tools || []
|
|
1847
1950
|
const hooks = loaded?.hooks || fallback?.hooks || {}
|
|
1848
1951
|
const providers = loaded?.providers || fallback?.providers || []
|
|
1849
1952
|
const connectors = loaded?.connectors || fallback?.connectors || []
|
|
1953
|
+
const managedResources = loaded?.managedResources
|
|
1954
|
+
|| fallback?.managedResources
|
|
1955
|
+
|| (fallback ? coerceManagedResources(fallback as unknown as Record<string, unknown>) : undefined)
|
|
1850
1956
|
const hasUi = !!(loaded?.ui || fallback?.ui)
|
|
1851
1957
|
const settingsFields = loaded?.ui?.settingsFields || fallback?.ui?.settingsFields
|
|
1852
1958
|
return {
|
|
@@ -1855,6 +1961,11 @@ class ExtensionManager {
|
|
|
1855
1961
|
hasUI: hasUi,
|
|
1856
1962
|
providerCount: Array.isArray(providers) ? providers.length : 0,
|
|
1857
1963
|
connectorCount: Array.isArray(connectors) ? connectors.length : 0,
|
|
1964
|
+
managedAgentCount: managedResources?.agents?.length || 0,
|
|
1965
|
+
managedScheduleCount: (managedResources?.schedules?.length || 0) + (managedResources?.routines?.length || 0),
|
|
1966
|
+
localFolderCount: managedResources?.localFolders?.length || 0,
|
|
1967
|
+
gatewayPlatformCount: managedResources?.gatewayPlatforms?.length || 0,
|
|
1968
|
+
setupCheckCount: managedResources?.setupChecks?.length || 0,
|
|
1858
1969
|
settingsFields: settingsFields?.length ? settingsFields : undefined,
|
|
1859
1970
|
}
|
|
1860
1971
|
}
|
|
@@ -138,6 +138,54 @@ module.exports = {
|
|
|
138
138
|
}
|
|
139
139
|
],
|
|
140
140
|
|
|
141
|
+
// --- Managed Resources (Paperclip-compatible) ---
|
|
142
|
+
managedResources: {
|
|
143
|
+
agents: [
|
|
144
|
+
{
|
|
145
|
+
agentKey: 'researcher',
|
|
146
|
+
displayName: 'Managed Researcher',
|
|
147
|
+
description: 'Reusable agent this extension can provision.',
|
|
148
|
+
systemPrompt: 'Research carefully and cite durable evidence.',
|
|
149
|
+
provider: 'openai',
|
|
150
|
+
model: 'gpt-4o-mini',
|
|
151
|
+
capabilities: ['research', 'analysis'],
|
|
152
|
+
extensions: ['web', 'memory']
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
schedules: [
|
|
156
|
+
{
|
|
157
|
+
scheduleKey: 'daily-digest',
|
|
158
|
+
displayName: 'Daily Digest',
|
|
159
|
+
agentRef: { resourceKind: 'agent', resourceKey: 'researcher' },
|
|
160
|
+
taskPrompt: 'Prepare the daily digest.',
|
|
161
|
+
scheduleType: 'cron',
|
|
162
|
+
cron: '0 9 * * *',
|
|
163
|
+
timezone: 'UTC',
|
|
164
|
+
status: 'paused'
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
localFolders: [
|
|
168
|
+
{
|
|
169
|
+
folderKey: 'workspace',
|
|
170
|
+
displayName: 'Workspace Folder',
|
|
171
|
+
access: 'readWrite',
|
|
172
|
+
requiredDirectories: ['inputs', 'outputs']
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
gatewayPlatforms: [
|
|
176
|
+
{
|
|
177
|
+
platformKey: 'openai-compatible-api',
|
|
178
|
+
displayName: 'OpenAI-compatible API',
|
|
179
|
+
transport: 'http',
|
|
180
|
+
endpoint: 'http://127.0.0.1:8642/v1',
|
|
181
|
+
authMode: 'bearer'
|
|
182
|
+
}
|
|
183
|
+
],
|
|
184
|
+
setupChecks: [
|
|
185
|
+
{ checkKey: 'api-key', displayName: 'API key configured', kind: 'env', target: 'OPENAI_API_KEY', required: true }
|
|
186
|
+
]
|
|
187
|
+
},
|
|
188
|
+
|
|
141
189
|
// --- Real OpenClaw Format (register API) ---
|
|
142
190
|
register(api) {
|
|
143
191
|
api.registerHook('agent:start', (ctx) => {
|
|
@@ -162,6 +210,8 @@ Key rules:
|
|
|
162
210
|
- If your extension needs npm/pnpm/yarn/bun packages, include a packageJson object during scaffold or call install_dependencies later.
|
|
163
211
|
- Dependency installs are run by the extension manager inside a per-extension workspace using the selected package manager with scripts disabled.
|
|
164
212
|
- Extension settings are declared through ui.settingsFields and stored per extension ID
|
|
213
|
+
- Managed resources let an extension declare provisionable agents, schedules/routines, trusted local folders, gateway platforms, and setup checks. Operators reconcile them through Extensions > Managed Resources or /api/extensions/managed-resources.
|
|
214
|
+
- Paperclip-compatible top-level agents, routines, and localFolders are also accepted; SwarmClaw reconciles routines as schedules when they include schedule timing.
|
|
165
215
|
- Keep extensions focused: one clear purpose per extension
|
|
166
216
|
`
|
|
167
217
|
}
|
package/src/types/agent.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ProviderId, ProviderType, OllamaMode } from './provider'
|
|
|
2
2
|
import type { SessionResetMode, IdentityContinuityState } from './session'
|
|
3
3
|
import type { SkillAllowlistMode } from './skill'
|
|
4
4
|
import type { DreamConfig } from './dream'
|
|
5
|
+
import type { ExtensionManagedResourceMarker } from './extension'
|
|
5
6
|
|
|
6
7
|
// --- SwarmFeed Heartbeat ---
|
|
7
8
|
|
|
@@ -237,6 +238,7 @@ export interface Agent {
|
|
|
237
238
|
swarmfeedLastAutoPostAt?: number | null
|
|
238
239
|
origin?: 'swarmdock' | 'swarmfeed' | 'swarmclaw' | 'external'
|
|
239
240
|
swarmfeedHeartbeat?: SwarmFeedHeartbeatConfig | null
|
|
241
|
+
managedByExtension?: ExtensionManagedResourceMarker | null
|
|
240
242
|
|
|
241
243
|
// SwarmDock (marketplace integration)
|
|
242
244
|
swarmdockEnabled?: boolean
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SessionResetMode } from './session'
|
|
2
|
+
import type { ExtensionManagedLocalFolderDeclaration } from './extension'
|
|
2
3
|
|
|
3
4
|
// --- App Settings ---
|
|
4
5
|
export type LoopMode = 'bounded' | 'ongoing'
|
|
@@ -150,6 +151,13 @@ export interface AppSettings {
|
|
|
150
151
|
toolLoopCircuitBreaker?: number
|
|
151
152
|
// Per-extension settings (keyed by extensionId)
|
|
152
153
|
extensionSettings?: Record<string, Record<string, unknown>>
|
|
154
|
+
// Extension-managed resource settings such as trusted local folder roots.
|
|
155
|
+
extensionManagedResources?: Record<string, {
|
|
156
|
+
localFolders?: Record<string, ExtensionManagedLocalFolderDeclaration & {
|
|
157
|
+
path?: string | null
|
|
158
|
+
updatedAt?: number
|
|
159
|
+
}>
|
|
160
|
+
}>
|
|
153
161
|
// Approval policies — opt-in governance gates for sensitive operations
|
|
154
162
|
approvalPolicies?: {
|
|
155
163
|
requireApprovalForAgentCreate?: boolean
|
package/src/types/extension.ts
CHANGED
|
@@ -251,6 +251,125 @@ export interface ExtensionUIDefinition {
|
|
|
251
251
|
}>
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
export type ExtensionManagedResourceKind = 'agent' | 'schedule' | 'local_folder'
|
|
255
|
+
|
|
256
|
+
export interface ExtensionManagedResourceRef {
|
|
257
|
+
extensionId?: string
|
|
258
|
+
resourceKind: 'agent' | 'schedule'
|
|
259
|
+
resourceKey: string
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface ExtensionManagedResourceMarker {
|
|
263
|
+
extensionId: string
|
|
264
|
+
extensionName?: string | null
|
|
265
|
+
resourceKind: ExtensionManagedResourceKind
|
|
266
|
+
resourceKey: string
|
|
267
|
+
declarationHash?: string | null
|
|
268
|
+
reconciledAt: number
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface ExtensionManagedAgentDeclaration {
|
|
272
|
+
agentKey: string
|
|
273
|
+
displayName: string
|
|
274
|
+
description?: string | null
|
|
275
|
+
systemPrompt?: string | null
|
|
276
|
+
instructions?: {
|
|
277
|
+
content?: string | null
|
|
278
|
+
entryFile?: string | null
|
|
279
|
+
assetPath?: string | null
|
|
280
|
+
} | null
|
|
281
|
+
provider?: ProviderId | string | null
|
|
282
|
+
model?: string | null
|
|
283
|
+
apiEndpoint?: string | null
|
|
284
|
+
credentialId?: string | null
|
|
285
|
+
fallbackCredentialIds?: string[]
|
|
286
|
+
gatewayProfileId?: string | null
|
|
287
|
+
preferredGatewayTags?: string[]
|
|
288
|
+
preferredGatewayUseCase?: string | null
|
|
289
|
+
capabilities?: string[] | string | null
|
|
290
|
+
tools?: string[]
|
|
291
|
+
extensions?: string[]
|
|
292
|
+
skills?: string[]
|
|
293
|
+
skillIds?: string[]
|
|
294
|
+
mcpServerIds?: string[]
|
|
295
|
+
monthlyBudget?: number | null
|
|
296
|
+
dailyBudget?: number | null
|
|
297
|
+
hourlyBudget?: number | null
|
|
298
|
+
disabled?: boolean
|
|
299
|
+
heartbeatEnabled?: boolean
|
|
300
|
+
planningMode?: 'off' | 'strict' | null
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface ExtensionManagedScheduleTrigger {
|
|
304
|
+
kind?: 'schedule' | 'api' | 'webhook'
|
|
305
|
+
label?: string | null
|
|
306
|
+
enabled?: boolean
|
|
307
|
+
cronExpression?: string | null
|
|
308
|
+
timezone?: string | null
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export interface ExtensionManagedScheduleDeclaration {
|
|
312
|
+
scheduleKey?: string
|
|
313
|
+
routineKey?: string
|
|
314
|
+
displayName?: string
|
|
315
|
+
title?: string
|
|
316
|
+
description?: string | null
|
|
317
|
+
taskPrompt?: string | null
|
|
318
|
+
message?: string | null
|
|
319
|
+
taskMode?: 'task' | 'wake_only' | 'protocol'
|
|
320
|
+
agentId?: string | null
|
|
321
|
+
agentRef?: ExtensionManagedResourceRef | null
|
|
322
|
+
assigneeRef?: ExtensionManagedResourceRef | null
|
|
323
|
+
scheduleType?: 'cron' | 'interval' | 'once'
|
|
324
|
+
cron?: string | null
|
|
325
|
+
intervalMs?: number | null
|
|
326
|
+
runAt?: number | null
|
|
327
|
+
timezone?: string | null
|
|
328
|
+
status?: 'active' | 'paused' | 'completed' | 'failed' | 'archived'
|
|
329
|
+
priority?: string | null
|
|
330
|
+
triggers?: ExtensionManagedScheduleTrigger[]
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export interface ExtensionManagedLocalFolderDeclaration {
|
|
334
|
+
folderKey: string
|
|
335
|
+
displayName: string
|
|
336
|
+
description?: string | null
|
|
337
|
+
access?: 'read' | 'readWrite'
|
|
338
|
+
requiredDirectories?: string[]
|
|
339
|
+
requiredFiles?: string[]
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export interface ExtensionGatewayPlatformDeclaration {
|
|
343
|
+
platformKey: string
|
|
344
|
+
displayName: string
|
|
345
|
+
description?: string | null
|
|
346
|
+
transport?: 'http' | 'ws' | 'stdio' | 'cli' | 'gateway' | 'custom'
|
|
347
|
+
endpoint?: string | null
|
|
348
|
+
authMode?: 'none' | 'bearer' | 'api_key' | 'oauth' | 'custom'
|
|
349
|
+
setupCheckKey?: string | null
|
|
350
|
+
capabilities?: string[]
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export interface ExtensionSetupCheckDeclaration {
|
|
354
|
+
checkKey: string
|
|
355
|
+
displayName: string
|
|
356
|
+
description?: string | null
|
|
357
|
+
kind: 'env' | 'command' | 'url' | 'manual'
|
|
358
|
+
target?: string | null
|
|
359
|
+
required?: boolean
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export interface ExtensionManagedResources {
|
|
363
|
+
agents?: ExtensionManagedAgentDeclaration[]
|
|
364
|
+
schedules?: ExtensionManagedScheduleDeclaration[]
|
|
365
|
+
/** Paperclip-compatible alias. SwarmClaw reconciles routines as managed schedules. */
|
|
366
|
+
routines?: ExtensionManagedScheduleDeclaration[]
|
|
367
|
+
localFolders?: ExtensionManagedLocalFolderDeclaration[]
|
|
368
|
+
/** Hermes-style gateway/platform declaration metadata for setup and diagnostics surfaces. */
|
|
369
|
+
gatewayPlatforms?: ExtensionGatewayPlatformDeclaration[]
|
|
370
|
+
setupChecks?: ExtensionSetupCheckDeclaration[]
|
|
371
|
+
}
|
|
372
|
+
|
|
254
373
|
export interface ExtensionProviderDefinition {
|
|
255
374
|
id: string
|
|
256
375
|
name: string
|
|
@@ -300,6 +419,14 @@ export interface Extension {
|
|
|
300
419
|
ui?: ExtensionUIDefinition
|
|
301
420
|
providers?: ExtensionProviderDefinition[]
|
|
302
421
|
connectors?: ExtensionConnectorDefinition[]
|
|
422
|
+
managedResources?: ExtensionManagedResources
|
|
423
|
+
/** Paperclip-compatible top-level aliases. Prefer managedResources for new SwarmClaw extensions. */
|
|
424
|
+
agents?: ExtensionManagedAgentDeclaration[]
|
|
425
|
+
schedules?: ExtensionManagedScheduleDeclaration[]
|
|
426
|
+
routines?: ExtensionManagedScheduleDeclaration[]
|
|
427
|
+
localFolders?: ExtensionManagedLocalFolderDeclaration[]
|
|
428
|
+
gatewayPlatforms?: ExtensionGatewayPlatformDeclaration[]
|
|
429
|
+
setupChecks?: ExtensionSetupCheckDeclaration[]
|
|
303
430
|
}
|
|
304
431
|
|
|
305
432
|
export interface ExtensionMeta {
|
|
@@ -325,6 +452,11 @@ export interface ExtensionMeta {
|
|
|
325
452
|
hasUI?: boolean
|
|
326
453
|
providerCount?: number
|
|
327
454
|
connectorCount?: number
|
|
455
|
+
managedAgentCount?: number
|
|
456
|
+
managedScheduleCount?: number
|
|
457
|
+
localFolderCount?: number
|
|
458
|
+
gatewayPlatformCount?: number
|
|
459
|
+
setupCheckCount?: number
|
|
328
460
|
createdByAgentId?: string | null
|
|
329
461
|
settingsFields?: ExtensionSettingsField[]
|
|
330
462
|
hasDependencyManifest?: boolean
|
package/src/types/schedule.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ExtensionManagedResourceMarker } from './extension'
|
|
2
|
+
|
|
1
3
|
export type ScheduleType = 'cron' | 'interval' | 'once'
|
|
2
4
|
export type ScheduleStatus = 'active' | 'paused' | 'completed' | 'failed' | 'archived'
|
|
3
5
|
export type ScheduleTaskMode = 'task' | 'wake_only' | 'protocol'
|
|
@@ -54,6 +56,7 @@ export interface Schedule {
|
|
|
54
56
|
followupThreadId?: string | null
|
|
55
57
|
followupSenderId?: string | null
|
|
56
58
|
followupSenderName?: string | null
|
|
59
|
+
managedByExtension?: ExtensionManagedResourceMarker | null
|
|
57
60
|
createdAt: number
|
|
58
61
|
updatedAt?: number
|
|
59
62
|
}
|
|
@@ -8,14 +8,43 @@ import { toast } from 'sonner'
|
|
|
8
8
|
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
9
9
|
import { errorMessage } from '@/lib/shared-utils'
|
|
10
10
|
|
|
11
|
+
type ManagedResourceStatus = 'declared' | 'resolved' | 'missing' | 'missing_ref' | 'unsupported_trigger'
|
|
12
|
+
type ManagedResourceSummary = {
|
|
13
|
+
extensions: Array<{
|
|
14
|
+
extensionId: string
|
|
15
|
+
extensionName: string
|
|
16
|
+
enabled: boolean
|
|
17
|
+
isBuiltin: boolean
|
|
18
|
+
agents: Array<{ resourceKey: string; displayName: string; status: ManagedResourceStatus; resourceId: string | null }>
|
|
19
|
+
schedules: Array<{ resourceKey: string; displayName: string; status: ManagedResourceStatus; resourceId: string | null }>
|
|
20
|
+
localFolders: Array<{ resourceKey: string; displayName: string; status: ManagedResourceStatus; configured?: boolean; healthy?: boolean }>
|
|
21
|
+
gatewayPlatforms: Array<{ platformKey: string; displayName: string; transport?: string; endpoint?: string | null }>
|
|
22
|
+
setupChecks: Array<{ checkKey: string; displayName: string; kind: string; required: boolean }>
|
|
23
|
+
}>
|
|
24
|
+
totals: {
|
|
25
|
+
extensions: number
|
|
26
|
+
agents: number
|
|
27
|
+
schedules: number
|
|
28
|
+
localFolders: number
|
|
29
|
+
gatewayPlatforms: number
|
|
30
|
+
setupChecks: number
|
|
31
|
+
resolvedAgents: number
|
|
32
|
+
resolvedSchedules: number
|
|
33
|
+
healthyLocalFolders: number
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
11
37
|
export function ExtensionManager() {
|
|
12
|
-
const [tab, setTab] = useState<'installed' | 'marketplace' | 'url'>('installed')
|
|
38
|
+
const [tab, setTab] = useState<'installed' | 'managed' | 'marketplace' | 'url'>('installed')
|
|
13
39
|
const [extensions, setExtensions] = useState<ExtensionMeta[]>([])
|
|
40
|
+
const [managedResources, setManagedResources] = useState<ManagedResourceSummary | null>(null)
|
|
14
41
|
const [marketplace, setMarketplace] = useState<MarketplaceExtension[]>([])
|
|
15
42
|
const [loading, setLoading] = useState(false)
|
|
43
|
+
const [managedLoading, setManagedLoading] = useState(false)
|
|
16
44
|
const [installing, setInstalling] = useState<string | null>(null)
|
|
17
45
|
const [updating, setUpdating] = useState<string | null>(null)
|
|
18
46
|
const [updatingAll, setUpdatingAll] = useState(false)
|
|
47
|
+
const [reconciling, setReconciling] = useState<string | null>(null)
|
|
19
48
|
const [urlInput, setUrlInput] = useState('')
|
|
20
49
|
const [urlFilename, setUrlFilename] = useState('')
|
|
21
50
|
const [urlStatus, setUrlStatus] = useState<{ ok: boolean; message: string } | null>(null)
|
|
@@ -29,6 +58,15 @@ export function ExtensionManager() {
|
|
|
29
58
|
} catch { /* ignore */ }
|
|
30
59
|
}, [])
|
|
31
60
|
|
|
61
|
+
const loadManagedResources = useCallback(async () => {
|
|
62
|
+
setManagedLoading(true)
|
|
63
|
+
try {
|
|
64
|
+
const data = await api<ManagedResourceSummary>('GET', '/extensions/managed-resources')
|
|
65
|
+
setManagedResources(data)
|
|
66
|
+
} catch { /* ignore */ }
|
|
67
|
+
setManagedLoading(false)
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
32
70
|
const loadMarketplace = useCallback(async (q = '') => {
|
|
33
71
|
setLoading(true)
|
|
34
72
|
try {
|
|
@@ -39,6 +77,7 @@ export function ExtensionManager() {
|
|
|
39
77
|
}, [])
|
|
40
78
|
|
|
41
79
|
useEffect(() => { loadExtensions() }, [loadExtensions])
|
|
80
|
+
useEffect(() => { if (tab === 'managed') loadManagedResources() }, [tab, loadManagedResources])
|
|
42
81
|
useEffect(() => { if (tab === 'marketplace') loadMarketplace(marketplaceQuery) }, [tab, loadMarketplace, marketplaceQuery])
|
|
43
82
|
|
|
44
83
|
const toggleExtension = async (filename: string, enabled: boolean) => {
|
|
@@ -87,6 +126,25 @@ export function ExtensionManager() {
|
|
|
87
126
|
}
|
|
88
127
|
}
|
|
89
128
|
|
|
129
|
+
const handleReconcileManaged = async (extensionId?: string) => {
|
|
130
|
+
const id = extensionId || 'all'
|
|
131
|
+
setReconciling(id)
|
|
132
|
+
try {
|
|
133
|
+
const result = await api<{ createdAgents: string[]; updatedAgents: string[]; createdSchedules: string[]; updatedSchedules: string[] }>(
|
|
134
|
+
'POST',
|
|
135
|
+
'/extensions/managed-resources',
|
|
136
|
+
{ action: 'reconcile', ...(extensionId ? { extensionId } : {}) },
|
|
137
|
+
{ timeoutMs: 30_000 },
|
|
138
|
+
)
|
|
139
|
+
await Promise.all([loadExtensions(), loadManagedResources()])
|
|
140
|
+
toast.success(`Reconciled ${result.createdAgents.length + result.updatedAgents.length} agents and ${result.createdSchedules.length + result.updatedSchedules.length} schedules`)
|
|
141
|
+
} catch (err: unknown) {
|
|
142
|
+
toast.error(errorMessage(err))
|
|
143
|
+
} finally {
|
|
144
|
+
setReconciling(null)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
90
148
|
const installFromMarketplace = async (p: MarketplaceExtension) => {
|
|
91
149
|
setInstalling(p.id)
|
|
92
150
|
const toastId = toast.loading(`Installing ${p.name}...`)
|
|
@@ -188,6 +246,14 @@ export function ExtensionManager() {
|
|
|
188
246
|
</span>
|
|
189
247
|
</div>
|
|
190
248
|
)}
|
|
249
|
+
{((p.managedAgentCount || 0) + (p.managedScheduleCount || 0) + (p.localFolderCount || 0) + (p.gatewayPlatformCount || 0) + (p.setupCheckCount || 0)) > 0 && (
|
|
250
|
+
<div className="mt-1.5 flex items-center gap-1.5 text-[10px] font-700 uppercase tracking-[0.08em] flex-wrap">
|
|
251
|
+
{(p.managedAgentCount || 0) > 0 && <span className="px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-300">{p.managedAgentCount} agents</span>}
|
|
252
|
+
{(p.managedScheduleCount || 0) > 0 && <span className="px-1.5 py-0.5 rounded bg-violet-500/10 text-violet-300">{p.managedScheduleCount} routines</span>}
|
|
253
|
+
{(p.localFolderCount || 0) > 0 && <span className="px-1.5 py-0.5 rounded bg-sky-500/10 text-sky-300">{p.localFolderCount} folders</span>}
|
|
254
|
+
{(p.gatewayPlatformCount || 0) > 0 && <span className="px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-300">{p.gatewayPlatformCount} gateways</span>}
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
191
257
|
{p.autoDisabled && (
|
|
192
258
|
<div className="text-[11px] text-amber-400/90 mt-1.5 p-2 rounded-[8px] bg-amber-500/[0.03] border border-amber-500/10">
|
|
193
259
|
{p.lastFailureStage ? `Error at ${p.lastFailureStage}:` : 'Last error:'} {p.lastFailureError}
|
|
@@ -242,6 +308,7 @@ export function ExtensionManager() {
|
|
|
242
308
|
</div>
|
|
243
309
|
<div className="flex bg-surface p-1.5 rounded-[14px] border border-white/[0.04]">
|
|
244
310
|
<button onClick={() => setTab('installed')} className={tabClass('installed')}>Installed</button>
|
|
311
|
+
<button onClick={() => setTab('managed')} className={tabClass('managed')}>Managed</button>
|
|
245
312
|
<button onClick={() => setTab('marketplace')} className={tabClass('marketplace')}>Marketplace</button>
|
|
246
313
|
<button onClick={() => setTab('url')} className={tabClass('url')}>Manual</button>
|
|
247
314
|
</div>
|
|
@@ -302,6 +369,95 @@ export function ExtensionManager() {
|
|
|
302
369
|
</div>
|
|
303
370
|
)}
|
|
304
371
|
|
|
372
|
+
{tab === 'managed' && (
|
|
373
|
+
<div className="space-y-6">
|
|
374
|
+
<div className="flex items-center justify-between gap-4 px-1">
|
|
375
|
+
<div>
|
|
376
|
+
<h3 className="text-[13px] font-700 text-text-2">Managed Resources</h3>
|
|
377
|
+
<p className="text-[12px] text-text-3/50 mt-0.5">Extension-declared agents, routines, folders, gateways, and setup checks.</p>
|
|
378
|
+
</div>
|
|
379
|
+
<button
|
|
380
|
+
onClick={() => handleReconcileManaged()}
|
|
381
|
+
disabled={!!reconciling || managedLoading}
|
|
382
|
+
className="h-9 px-4 rounded-[10px] bg-accent-bright text-white text-[11px] font-800 uppercase tracking-[0.08em] border-none cursor-pointer disabled:opacity-50"
|
|
383
|
+
>
|
|
384
|
+
{reconciling === 'all' ? 'Reconciling...' : 'Reconcile All'}
|
|
385
|
+
</button>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
{managedLoading ? (
|
|
389
|
+
<div className="py-20 flex justify-center">
|
|
390
|
+
<div className="w-8 h-8 border-2 border-accent-bright/20 border-t-accent-bright rounded-full animate-spin" />
|
|
391
|
+
</div>
|
|
392
|
+
) : !managedResources || managedResources.extensions.length === 0 ? (
|
|
393
|
+
<div className="py-20 text-center rounded-[24px] border border-dashed border-white/[0.06]">
|
|
394
|
+
<p className="text-[14px] text-text-3/50">No managed resource declarations found</p>
|
|
395
|
+
</div>
|
|
396
|
+
) : (
|
|
397
|
+
<div className="space-y-4">
|
|
398
|
+
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
|
399
|
+
{[
|
|
400
|
+
['Agents', `${managedResources.totals.resolvedAgents}/${managedResources.totals.agents}`],
|
|
401
|
+
['Routines', `${managedResources.totals.resolvedSchedules}/${managedResources.totals.schedules}`],
|
|
402
|
+
['Folders', `${managedResources.totals.healthyLocalFolders}/${managedResources.totals.localFolders}`],
|
|
403
|
+
['Gateways', String(managedResources.totals.gatewayPlatforms)],
|
|
404
|
+
['Setup', String(managedResources.totals.setupChecks)],
|
|
405
|
+
].map(([label, value]) => (
|
|
406
|
+
<div key={label} className="rounded-[12px] bg-surface border border-white/[0.06] p-4">
|
|
407
|
+
<div className="text-[10px] font-800 uppercase tracking-[0.12em] text-text-3/50">{label}</div>
|
|
408
|
+
<div className="text-[22px] font-800 text-text mt-1">{value}</div>
|
|
409
|
+
</div>
|
|
410
|
+
))}
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
{managedResources.extensions.map((entry) => (
|
|
414
|
+
<div key={entry.extensionId} className="rounded-[18px] bg-surface border border-white/[0.06] p-5">
|
|
415
|
+
<div className="flex items-center justify-between gap-4 mb-4">
|
|
416
|
+
<div className="min-w-0">
|
|
417
|
+
<div className="flex items-center gap-2">
|
|
418
|
+
<h4 className="text-[14px] font-800 text-text truncate">{entry.extensionName}</h4>
|
|
419
|
+
<span className="text-[10px] font-mono text-text-3/50 truncate">{entry.extensionId}</span>
|
|
420
|
+
</div>
|
|
421
|
+
<div className="text-[11px] text-text-3/50 mt-0.5">
|
|
422
|
+
{entry.agents.length} agents, {entry.schedules.length} routines, {entry.localFolders.length} folders, {entry.gatewayPlatforms.length} gateways
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
<button
|
|
426
|
+
onClick={() => handleReconcileManaged(entry.extensionId)}
|
|
427
|
+
disabled={!!reconciling}
|
|
428
|
+
className="h-8 px-3 rounded-[9px] bg-white/[0.05] hover:bg-white/[0.08] text-text-2 text-[10px] font-800 uppercase tracking-[0.08em] border border-white/[0.06] cursor-pointer disabled:opacity-50"
|
|
429
|
+
>
|
|
430
|
+
{reconciling === entry.extensionId ? 'Reconciling...' : 'Reconcile'}
|
|
431
|
+
</button>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
435
|
+
{[
|
|
436
|
+
['Agents', entry.agents.map((item) => `${item.displayName} - ${item.status}`)],
|
|
437
|
+
['Routines', entry.schedules.map((item) => `${item.displayName} - ${item.status}`)],
|
|
438
|
+
['Folders', entry.localFolders.map((item) => `${item.displayName} - ${item.configured ? 'configured' : 'missing'}`)],
|
|
439
|
+
].map(([label, rows]) => (
|
|
440
|
+
<div key={label as string} className="rounded-[12px] bg-bg/60 border border-white/[0.04] p-3 min-h-[96px]">
|
|
441
|
+
<div className="text-[10px] font-800 uppercase tracking-[0.12em] text-text-3/50 mb-2">{label as string}</div>
|
|
442
|
+
{(rows as string[]).length > 0 ? (
|
|
443
|
+
<div className="space-y-1">
|
|
444
|
+
{(rows as string[]).slice(0, 4).map((row) => (
|
|
445
|
+
<div key={row} className="text-[11px] text-text-3/80 truncate">{row}</div>
|
|
446
|
+
))}
|
|
447
|
+
</div>
|
|
448
|
+
) : (
|
|
449
|
+
<div className="text-[11px] text-text-3/35">None</div>
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
))}
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
))}
|
|
456
|
+
</div>
|
|
457
|
+
)}
|
|
458
|
+
</div>
|
|
459
|
+
)}
|
|
460
|
+
|
|
305
461
|
{tab === 'marketplace' && (
|
|
306
462
|
<div className="space-y-6">
|
|
307
463
|
<div className="relative group">
|