@swarmclawai/swarmclaw 1.6.0 → 1.7.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/README.md +20 -0
- package/package.json +3 -3
- package/src/app/.well-known/agent-card.json/route.ts +15 -0
- package/src/app/api/.well-known/agent-card/route.ts +6 -37
- package/src/app/home/page.tsx +10 -19
- package/src/components/auth/setup-wizard/index.tsx +2 -6
- package/src/components/auth/setup-wizard/step-next.tsx +39 -46
- package/src/components/auth/setup-wizard/step-providers.tsx +142 -76
- package/src/components/auth/setup-wizard/types.ts +2 -5
- package/src/components/auth/setup-wizard/utils.test.ts +19 -0
- package/src/components/auth/setup-wizard/utils.ts +69 -0
- package/src/components/home/home-launchpad.tsx +100 -80
- package/src/lib/a2a/agent-card.test.ts +94 -0
- package/src/lib/a2a/agent-card.ts +41 -1
- package/src/lib/home-launchpad.test.ts +31 -1
- package/src/lib/home-launchpad.ts +58 -0
- package/src/lib/providers/cli-utils.test.ts +10 -0
- package/src/lib/providers/cli-utils.ts +31 -0
- package/src/lib/providers/generic-cli.test.ts +71 -0
- package/src/lib/providers/generic-cli.ts +138 -0
- package/src/lib/providers/index.ts +56 -1
- package/src/lib/providers/opencode-cli.test.ts +9 -0
- package/src/lib/providers/opencode-cli.ts +5 -1
- package/src/lib/server/missions/mission-templates.test.ts +17 -0
- package/src/lib/server/missions/mission-templates.ts +69 -0
- package/src/lib/server/protocols/protocol-service.test.ts +25 -0
- package/src/lib/server/protocols/protocol-templates.ts +48 -0
- package/src/lib/strip-internal-metadata.test.ts +23 -0
- package/src/lib/strip-internal-metadata.ts +136 -7
- package/src/types/provider.ts +1 -1
|
@@ -1,14 +1,47 @@
|
|
|
1
1
|
import {
|
|
2
|
+
SETUP_PROVIDERS,
|
|
2
3
|
STARTER_KITS,
|
|
3
4
|
getDefaultModelForProvider,
|
|
4
5
|
type OnboardingPath,
|
|
5
6
|
type StarterKit,
|
|
7
|
+
type SetupProviderOption,
|
|
6
8
|
type SetupProvider,
|
|
7
9
|
type StarterKitAgentTemplate,
|
|
8
10
|
} from '@/lib/setup-defaults'
|
|
9
11
|
import type { ConfiguredProvider, SetupStep, StarterDraftAgent } from './types'
|
|
10
12
|
import { STEP_ORDER } from './types'
|
|
11
13
|
|
|
14
|
+
export type SetupProviderGroupId = 'fast-local' | 'recommended-api' | 'advanced-catalog'
|
|
15
|
+
|
|
16
|
+
export interface SetupProviderGroup {
|
|
17
|
+
id: SetupProviderGroupId
|
|
18
|
+
title: string
|
|
19
|
+
description: string
|
|
20
|
+
providers: SetupProviderOption[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const FAST_LOCAL_PROVIDER_IDS = new Set<SetupProvider>([
|
|
24
|
+
'claude-cli',
|
|
25
|
+
'codex-cli',
|
|
26
|
+
'opencode-cli',
|
|
27
|
+
'gemini-cli',
|
|
28
|
+
'copilot-cli',
|
|
29
|
+
'droid-cli',
|
|
30
|
+
'cursor-cli',
|
|
31
|
+
'qwen-code-cli',
|
|
32
|
+
'goose',
|
|
33
|
+
'ollama',
|
|
34
|
+
'openclaw',
|
|
35
|
+
'hermes',
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
const RECOMMENDED_API_PROVIDER_IDS = new Set<SetupProvider>([
|
|
39
|
+
'openai',
|
|
40
|
+
'openrouter',
|
|
41
|
+
'anthropic',
|
|
42
|
+
'google',
|
|
43
|
+
])
|
|
44
|
+
|
|
12
45
|
export function stepIndex(step: SetupStep): number {
|
|
13
46
|
if (step === 'connect') return STEP_ORDER.indexOf('providers')
|
|
14
47
|
return STEP_ORDER.indexOf(step)
|
|
@@ -38,6 +71,42 @@ export function getStarterKitsForPath(path: OnboardingPath): StarterKit[] {
|
|
|
38
71
|
return STARTER_KITS
|
|
39
72
|
}
|
|
40
73
|
|
|
74
|
+
export function getSetupProviderGroups(providers: SetupProviderOption[] = SETUP_PROVIDERS): SetupProviderGroup[] {
|
|
75
|
+
const groups: SetupProviderGroup[] = [
|
|
76
|
+
{
|
|
77
|
+
id: 'fast-local',
|
|
78
|
+
title: 'Fast local and no-key starts',
|
|
79
|
+
description: 'Use an installed CLI, a local runtime, or an existing OpenClaw/Hermes gateway.',
|
|
80
|
+
providers: [],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'recommended-api',
|
|
84
|
+
title: 'Recommended API providers',
|
|
85
|
+
description: 'Good first choices for cloud-backed chats, agents, and workflow runs.',
|
|
86
|
+
providers: [],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'advanced-catalog',
|
|
90
|
+
title: 'Advanced catalog and custom endpoints',
|
|
91
|
+
description: 'Specialized model catalogs, OpenAI-compatible servers, and provider-specific setups.',
|
|
92
|
+
providers: [],
|
|
93
|
+
},
|
|
94
|
+
]
|
|
95
|
+
const byId = new Map(groups.map((group) => [group.id, group]))
|
|
96
|
+
|
|
97
|
+
for (const provider of providers) {
|
|
98
|
+
if (FAST_LOCAL_PROVIDER_IDS.has(provider.id)) {
|
|
99
|
+
byId.get('fast-local')!.providers.push(provider)
|
|
100
|
+
} else if (RECOMMENDED_API_PROVIDER_IDS.has(provider.id)) {
|
|
101
|
+
byId.get('recommended-api')!.providers.push(provider)
|
|
102
|
+
} else {
|
|
103
|
+
byId.get('advanced-catalog')!.providers.push(provider)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return groups.filter((group) => group.providers.length > 0)
|
|
108
|
+
}
|
|
109
|
+
|
|
41
110
|
export function applyIntentContext(prompt: string, intentText: string): string {
|
|
42
111
|
const trimmed = intentText.trim()
|
|
43
112
|
if (!trimmed) return prompt
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getLaunchPathCards,
|
|
6
|
+
type LaunchPathAction,
|
|
7
|
+
type LaunchPathCardCopy,
|
|
8
|
+
type LaunchPathId,
|
|
9
|
+
} from '@/lib/home-launchpad'
|
|
5
10
|
import type { Agent } from '@/types'
|
|
6
11
|
|
|
7
12
|
function SnapshotItem({ label, value, hint }: { label: string; value: string; hint: string }) {
|
|
@@ -14,6 +19,59 @@ function SnapshotItem({ label, value, hint }: { label: string; value: string; hi
|
|
|
14
19
|
)
|
|
15
20
|
}
|
|
16
21
|
|
|
22
|
+
function PathCard({
|
|
23
|
+
card,
|
|
24
|
+
onAction,
|
|
25
|
+
}: {
|
|
26
|
+
card: LaunchPathCardCopy
|
|
27
|
+
onAction: (id: LaunchPathId, action: LaunchPathAction) => void
|
|
28
|
+
}) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="flex min-h-[220px] flex-col rounded-[18px] border border-white/[0.07] bg-white/[0.03] p-5">
|
|
31
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/55">{card.kicker}</div>
|
|
32
|
+
<div className="mt-3 text-[18px] font-display font-700 tracking-normal text-text">{card.title}</div>
|
|
33
|
+
<p className="mt-2 flex-1 text-[13px] leading-relaxed text-text-3/72">{card.description}</p>
|
|
34
|
+
<div className="mt-5 flex flex-wrap gap-2">
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
onClick={() => onAction(card.id, 'primary')}
|
|
38
|
+
className="rounded-[10px] bg-accent-bright px-3.5 py-2 text-[12px] font-display font-700 text-black transition-opacity hover:opacity-90"
|
|
39
|
+
>
|
|
40
|
+
{card.primaryLabel}
|
|
41
|
+
</button>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={() => onAction(card.id, 'secondary')}
|
|
45
|
+
className="rounded-[10px] border border-white/[0.08] bg-white/[0.04] px-3.5 py-2 text-[12px] font-display font-700 text-text-2 transition-colors hover:bg-white/[0.08]"
|
|
46
|
+
>
|
|
47
|
+
{card.secondaryLabel}
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function SecondaryAction({
|
|
55
|
+
label,
|
|
56
|
+
description,
|
|
57
|
+
onClick,
|
|
58
|
+
}: {
|
|
59
|
+
label: string
|
|
60
|
+
description: string
|
|
61
|
+
onClick: () => void
|
|
62
|
+
}) {
|
|
63
|
+
return (
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
onClick={onClick}
|
|
67
|
+
className="rounded-[12px] border border-white/[0.07] bg-white/[0.025] px-3 py-2 text-left transition-colors hover:bg-white/[0.05]"
|
|
68
|
+
>
|
|
69
|
+
<div className="text-[12px] font-display font-700 text-text-2">{label}</div>
|
|
70
|
+
<div className="mt-0.5 text-[11px] leading-relaxed text-text-3/65">{description}</div>
|
|
71
|
+
</button>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
17
75
|
type Props = {
|
|
18
76
|
firstAgent: Agent | null
|
|
19
77
|
agentCount: number
|
|
@@ -22,15 +80,8 @@ type Props = {
|
|
|
22
80
|
scheduleCount: number
|
|
23
81
|
connectorCount: number
|
|
24
82
|
todayCost: number
|
|
25
|
-
|
|
26
|
-
onOpenProtocols: () => void
|
|
27
|
-
onOpenBuilder: () => void
|
|
28
|
-
onOpenConnectors: () => void
|
|
83
|
+
onLaunchPathAction: (id: LaunchPathId, action: LaunchPathAction) => void
|
|
29
84
|
onOpenUsage: () => void
|
|
30
|
-
onRunEvalSuite: () => void
|
|
31
|
-
onReviewApprovals: () => void
|
|
32
|
-
onInspectFailedRuns: () => void
|
|
33
|
-
onStartReleaseQaMission: () => void
|
|
34
85
|
}
|
|
35
86
|
|
|
36
87
|
export function HomeLaunchpad({
|
|
@@ -41,29 +92,24 @@ export function HomeLaunchpad({
|
|
|
41
92
|
scheduleCount,
|
|
42
93
|
connectorCount,
|
|
43
94
|
todayCost,
|
|
44
|
-
|
|
45
|
-
onOpenProtocols,
|
|
46
|
-
onOpenBuilder,
|
|
47
|
-
onOpenConnectors,
|
|
95
|
+
onLaunchPathAction,
|
|
48
96
|
onOpenUsage,
|
|
49
|
-
onRunEvalSuite,
|
|
50
|
-
onReviewApprovals,
|
|
51
|
-
onInspectFailedRuns,
|
|
52
|
-
onStartReleaseQaMission,
|
|
53
97
|
}: Props) {
|
|
98
|
+
const launchPathCards = getLaunchPathCards({ firstAgentName: firstAgent?.name })
|
|
99
|
+
|
|
54
100
|
return (
|
|
55
101
|
<div className="max-w-[980px] mx-auto px-6 py-10">
|
|
56
|
-
<div className="rounded-[
|
|
102
|
+
<div className="rounded-[20px] border border-white/[0.06] bg-white/[0.025] p-6">
|
|
57
103
|
<div className="inline-flex rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1 text-[11px] font-700 uppercase tracking-[0.16em] text-text-3/70">
|
|
58
|
-
Launchpad
|
|
104
|
+
v1.6 Launchpad
|
|
59
105
|
</div>
|
|
60
106
|
<div className="mt-4 flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
61
107
|
<div className="max-w-[620px]">
|
|
62
|
-
<h1 className="font-display text-[34px] font-700 tracking-
|
|
63
|
-
|
|
108
|
+
<h1 className="font-display text-[34px] font-700 tracking-normal text-text">
|
|
109
|
+
Pick a path and watch the workspace move.
|
|
64
110
|
</h1>
|
|
65
111
|
<p className="mt-3 text-[15px] leading-relaxed text-text-3/72">
|
|
66
|
-
|
|
112
|
+
Start with a local assistant, a reusable workflow, or a budgeted autonomous mission. The rest of the control plane stays one click away.
|
|
67
113
|
</p>
|
|
68
114
|
</div>
|
|
69
115
|
<div className="rounded-[18px] border border-white/[0.06] bg-white/[0.03] p-4 min-w-[240px]">
|
|
@@ -94,64 +140,38 @@ export function HomeLaunchpad({
|
|
|
94
140
|
</div>
|
|
95
141
|
</div>
|
|
96
142
|
|
|
97
|
-
<div className="mt-6 grid gap-3
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
onClick={onOpenUsage}
|
|
130
|
-
/>
|
|
131
|
-
<LaunchActionCard
|
|
132
|
-
title="Run Eval Suite"
|
|
133
|
-
description="Open the Quality Center and run scenario or suite checks against an agent before shipping."
|
|
134
|
-
actionLabel="Open Eval Lab"
|
|
135
|
-
onClick={onRunEvalSuite}
|
|
136
|
-
/>
|
|
137
|
-
<LaunchActionCard
|
|
138
|
-
title="Review Approvals"
|
|
139
|
-
description="Clear pending human-loop, tool, connector, skill, agent, and budget requests from one desk."
|
|
140
|
-
actionLabel="Open Approvals"
|
|
141
|
-
onClick={onReviewApprovals}
|
|
142
|
-
/>
|
|
143
|
-
<LaunchActionCard
|
|
144
|
-
title="Inspect Failed Runs"
|
|
145
|
-
description="Filter recent run failures and open replay evidence without leaving the operator workflow."
|
|
146
|
-
actionLabel="Open Run Review"
|
|
147
|
-
onClick={onInspectFailedRuns}
|
|
148
|
-
/>
|
|
149
|
-
<LaunchActionCard
|
|
150
|
-
title="Start Release QA Mission"
|
|
151
|
-
description="Use a budgeted mission template to collect release readiness evidence and quality notes."
|
|
152
|
-
actionLabel="Open Missions"
|
|
153
|
-
onClick={onStartReleaseQaMission}
|
|
154
|
-
/>
|
|
143
|
+
<div className="mt-6 grid gap-3 lg:grid-cols-3">
|
|
144
|
+
{launchPathCards.map((card) => (
|
|
145
|
+
<PathCard key={card.id} card={card} onAction={onLaunchPathAction} />
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div className="mt-6 rounded-[16px] border border-white/[0.06] bg-white/[0.02] p-4">
|
|
150
|
+
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
|
151
|
+
<div>
|
|
152
|
+
<div className="text-[12px] font-display font-700 text-text">Keep the workspace observable</div>
|
|
153
|
+
<p className="mt-1 text-[12px] leading-relaxed text-text-3/65">
|
|
154
|
+
Provider spend, connector status, and quality evidence stay nearby after you pick a path.
|
|
155
|
+
</p>
|
|
156
|
+
</div>
|
|
157
|
+
<div className="grid gap-2 sm:grid-cols-3 md:min-w-[520px]">
|
|
158
|
+
<SecondaryAction
|
|
159
|
+
label="Usage"
|
|
160
|
+
description="Cost and provider health"
|
|
161
|
+
onClick={onOpenUsage}
|
|
162
|
+
/>
|
|
163
|
+
<SecondaryAction
|
|
164
|
+
label="Connectors"
|
|
165
|
+
description="Platform bridges"
|
|
166
|
+
onClick={() => onLaunchPathAction('assistant', 'secondary')}
|
|
167
|
+
/>
|
|
168
|
+
<SecondaryAction
|
|
169
|
+
label="Quality"
|
|
170
|
+
description="Evals and run review"
|
|
171
|
+
onClick={() => onLaunchPathAction('mission', 'secondary')}
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
155
175
|
</div>
|
|
156
176
|
|
|
157
177
|
<div className="mt-8 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { after, before, describe, it } from 'node:test'
|
|
6
|
+
|
|
7
|
+
const originalEnv = {
|
|
8
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
9
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
10
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let tempDir = ''
|
|
14
|
+
let storage: typeof import('@/lib/server/storage')
|
|
15
|
+
let canonicalRoute: typeof import('@/app/.well-known/agent-card.json/route')
|
|
16
|
+
let legacyRoute: typeof import('@/app/api/.well-known/agent-card/route')
|
|
17
|
+
|
|
18
|
+
function testAgent(id: string, overrides: Record<string, unknown> = {}) {
|
|
19
|
+
const now = Date.now()
|
|
20
|
+
return {
|
|
21
|
+
id,
|
|
22
|
+
name: id === 'agent-active' ? 'Active Agent' : 'Hidden Agent',
|
|
23
|
+
description: 'A2A route test agent',
|
|
24
|
+
systemPrompt: '',
|
|
25
|
+
provider: 'ollama',
|
|
26
|
+
model: 'qwen3.5',
|
|
27
|
+
credentialId: null,
|
|
28
|
+
fallbackCredentialIds: [],
|
|
29
|
+
apiEndpoint: null,
|
|
30
|
+
gatewayProfileId: null,
|
|
31
|
+
extensions: [],
|
|
32
|
+
capabilities: ['research'],
|
|
33
|
+
createdAt: now,
|
|
34
|
+
updatedAt: now,
|
|
35
|
+
...overrides,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
before(async () => {
|
|
40
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-a2a-card-'))
|
|
41
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
42
|
+
process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
|
|
43
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
44
|
+
storage = await import('@/lib/server/storage')
|
|
45
|
+
canonicalRoute = await import('@/app/.well-known/agent-card.json/route')
|
|
46
|
+
legacyRoute = await import('@/app/api/.well-known/agent-card/route')
|
|
47
|
+
storage.saveAgents({
|
|
48
|
+
'agent-active': testAgent('agent-active'),
|
|
49
|
+
'agent-disabled': testAgent('agent-disabled', { disabled: true }),
|
|
50
|
+
'agent-trashed': testAgent('agent-trashed', { trashedAt: Date.now() }),
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
after(() => {
|
|
55
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
56
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
57
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
58
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
59
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
60
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
61
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('A2A agent card discovery', () => {
|
|
65
|
+
it('serves the canonical well-known directory and hides disabled agents', async () => {
|
|
66
|
+
const response = await canonicalRoute.GET(new Request('http://local.test/.well-known/agent-card.json'))
|
|
67
|
+
assert.equal(response.status, 200)
|
|
68
|
+
const body = await response.json()
|
|
69
|
+
|
|
70
|
+
assert.equal(body.protocolVersion, '0.3.0')
|
|
71
|
+
assert.equal(body.kind, 'directory')
|
|
72
|
+
assert.deepEqual(body.agents.map((agent: { agentId: string }) => agent.agentId), ['agent-active'])
|
|
73
|
+
assert.equal(body.agents[0].apiEndpoint, 'http://local.test/api/a2a')
|
|
74
|
+
assert.equal(body.agents[0].cardUrl, 'http://local.test/.well-known/agent-card.json?agentId=agent-active')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('returns a full card from both canonical and legacy routes', async () => {
|
|
78
|
+
const canonical = await canonicalRoute.GET(new Request('http://local.test/.well-known/agent-card.json?agentId=agent-active'))
|
|
79
|
+
const legacy = await legacyRoute.GET(new Request('http://local.test/api/.well-known/agent-card?agentId=agent-active'))
|
|
80
|
+
|
|
81
|
+
assert.equal(canonical.status, 200)
|
|
82
|
+
assert.equal(legacy.status, 200)
|
|
83
|
+
assert.equal((await canonical.json()).name, 'Active Agent')
|
|
84
|
+
assert.equal((await legacy.json()).apiEndpoint, 'http://local.test/api/a2a')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('returns 404 for disabled or missing agent cards', async () => {
|
|
88
|
+
const disabled = await canonicalRoute.GET(new Request('http://local.test/.well-known/agent-card.json?agentId=agent-disabled'))
|
|
89
|
+
const missing = await canonicalRoute.GET(new Request('http://local.test/.well-known/agent-card.json?agentId=nope'))
|
|
90
|
+
|
|
91
|
+
assert.equal(disabled.status, 404)
|
|
92
|
+
assert.equal(missing.status, 404)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Agent } from '@/types/agent'
|
|
2
2
|
import type { AgentCard } from './types'
|
|
3
|
+
import { getAgent, listAgents } from '@/lib/server/agents/agent-repository'
|
|
3
4
|
|
|
4
|
-
const A2A_PROTOCOL_VERSION = '0.3.0'
|
|
5
|
+
export const A2A_PROTOCOL_VERSION = '0.3.0'
|
|
5
6
|
const SWARMCLAW_VERSION = '1.0.0'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -59,3 +60,42 @@ export function generateAgentCard(agent: Agent, baseUrl: string): AgentCard {
|
|
|
59
60
|
],
|
|
60
61
|
}
|
|
61
62
|
}
|
|
63
|
+
|
|
64
|
+
function isPubliclyDiscoverableAgent(agent: Agent): boolean {
|
|
65
|
+
return agent.disabled !== true && !agent.trashedAt
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildAgentCardDiscoveryPayload(req: Request): {
|
|
69
|
+
body: unknown
|
|
70
|
+
status?: number
|
|
71
|
+
} {
|
|
72
|
+
const url = new URL(req.url)
|
|
73
|
+
const agentId = url.searchParams.get('agentId')
|
|
74
|
+
const baseUrl = url.origin
|
|
75
|
+
|
|
76
|
+
if (agentId) {
|
|
77
|
+
const agent = getAgent(agentId)
|
|
78
|
+
if (!agent || !isPubliclyDiscoverableAgent(agent)) {
|
|
79
|
+
return { body: { error: 'Agent not found' }, status: 404 }
|
|
80
|
+
}
|
|
81
|
+
return { body: generateAgentCard(agent, baseUrl) }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const directory = Object.values(listAgents())
|
|
85
|
+
.filter(isPubliclyDiscoverableAgent)
|
|
86
|
+
.map((agent) => ({
|
|
87
|
+
name: agent.name,
|
|
88
|
+
description: agent.description || `SwarmClaw agent: ${agent.name}`,
|
|
89
|
+
agentId: agent.id,
|
|
90
|
+
apiEndpoint: `${baseUrl}/api/a2a`,
|
|
91
|
+
cardUrl: `${baseUrl}/.well-known/agent-card.json?agentId=${encodeURIComponent(agent.id)}`,
|
|
92
|
+
}))
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
body: {
|
|
96
|
+
agents: directory,
|
|
97
|
+
kind: 'directory',
|
|
98
|
+
protocolVersion: A2A_PROTOCOL_VERSION,
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { test } from 'node:test'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_BUILDER_ROUTE,
|
|
5
|
+
deriveHomeMode,
|
|
6
|
+
getLaunchPathCards,
|
|
7
|
+
isSparseWorkspace,
|
|
8
|
+
resolveLaunchPathHref,
|
|
9
|
+
} from './home-launchpad'
|
|
4
10
|
|
|
5
11
|
test('isSparseWorkspace detects a fresh workspace', () => {
|
|
6
12
|
assert.equal(isSparseWorkspace({
|
|
@@ -47,3 +53,27 @@ test('deriveHomeMode falls back to ops for active workspaces', () => {
|
|
|
47
53
|
todayCost: 0,
|
|
48
54
|
}), 'ops')
|
|
49
55
|
})
|
|
56
|
+
|
|
57
|
+
test('getLaunchPathCards returns the three first-run paths in order', () => {
|
|
58
|
+
const cards = getLaunchPathCards({ firstAgentName: 'Ada' })
|
|
59
|
+
assert.deepEqual(cards.map((card) => card.id), ['assistant', 'workflow', 'mission'])
|
|
60
|
+
assert.equal(cards[0]?.title, 'Work with Ada')
|
|
61
|
+
assert.equal(cards[0]?.primaryLabel, 'Open Chat')
|
|
62
|
+
assert.equal(cards[1]?.primaryLabel, 'Open Builder')
|
|
63
|
+
assert.equal(cards[2]?.secondaryLabel, 'Quality Center')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('getLaunchPathCards falls back to agent creation copy when no agent exists', () => {
|
|
67
|
+
const [assistant] = getLaunchPathCards()
|
|
68
|
+
assert.equal(assistant?.title, 'Create the first agent')
|
|
69
|
+
assert.equal(assistant?.primaryLabel, 'Open Agents')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('resolveLaunchPathHref builds primary and secondary destinations', () => {
|
|
73
|
+
assert.equal(resolveLaunchPathHref('assistant', 'primary', 'agent one'), '/agents/agent%20one')
|
|
74
|
+
assert.equal(resolveLaunchPathHref('assistant', 'secondary', 'agent one'), '/connectors')
|
|
75
|
+
assert.equal(resolveLaunchPathHref('workflow', 'primary'), DEFAULT_BUILDER_ROUTE)
|
|
76
|
+
assert.equal(resolveLaunchPathHref('workflow', 'secondary'), '/protocols')
|
|
77
|
+
assert.equal(resolveLaunchPathHref('mission', 'primary'), '/missions')
|
|
78
|
+
assert.equal(resolveLaunchPathHref('mission', 'secondary'), '/quality')
|
|
79
|
+
})
|
|
@@ -2,6 +2,17 @@ export const HOME_LAUNCHPAD_AFTER_SETUP_KEY = 'sc_launchpad_after_setup_v1'
|
|
|
2
2
|
export const DEFAULT_BUILDER_ROUTE = '/protocols/builder/facilitated_discussion'
|
|
3
3
|
|
|
4
4
|
export type HomeMode = 'launchpad' | 'ops'
|
|
5
|
+
export type LaunchPathId = 'assistant' | 'workflow' | 'mission'
|
|
6
|
+
export type LaunchPathAction = 'primary' | 'secondary'
|
|
7
|
+
|
|
8
|
+
export interface LaunchPathCardCopy {
|
|
9
|
+
id: LaunchPathId
|
|
10
|
+
kicker: string
|
|
11
|
+
title: string
|
|
12
|
+
description: string
|
|
13
|
+
primaryLabel: string
|
|
14
|
+
secondaryLabel: string
|
|
15
|
+
}
|
|
5
16
|
|
|
6
17
|
export interface HomeModeInput {
|
|
7
18
|
hasLaunchpadFlag: boolean
|
|
@@ -28,3 +39,50 @@ export function deriveHomeMode(input: HomeModeInput): HomeMode {
|
|
|
28
39
|
if (input.hasLaunchpadFlag) return 'launchpad'
|
|
29
40
|
return isSparseWorkspace(input) ? 'launchpad' : 'ops'
|
|
30
41
|
}
|
|
42
|
+
|
|
43
|
+
export function getLaunchPathCards(input: { firstAgentName?: string | null } = {}): LaunchPathCardCopy[] {
|
|
44
|
+
const firstAgentName = input.firstAgentName?.trim() || null
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
id: 'assistant',
|
|
48
|
+
kicker: 'Assistant',
|
|
49
|
+
title: firstAgentName ? `Work with ${firstAgentName}` : 'Create the first agent',
|
|
50
|
+
description: 'Open a live agent chat, then add memory, local tools, provider routing, or connector access as the work demands.',
|
|
51
|
+
primaryLabel: firstAgentName ? 'Open Chat' : 'Open Agents',
|
|
52
|
+
secondaryLabel: 'Connect Platform',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'workflow',
|
|
56
|
+
kicker: 'Workflow',
|
|
57
|
+
title: 'Shape a reusable run',
|
|
58
|
+
description: 'Use protocol templates and the builder to turn review, research, planning, or release checks into durable workflows.',
|
|
59
|
+
primaryLabel: 'Open Builder',
|
|
60
|
+
secondaryLabel: 'Use Templates',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'mission',
|
|
64
|
+
kicker: 'Mission',
|
|
65
|
+
title: 'Run with budgets',
|
|
66
|
+
description: 'Start a mission template for release QA, research, support triage, cost audit, or failed-run review with reports and caps.',
|
|
67
|
+
primaryLabel: 'Open Missions',
|
|
68
|
+
secondaryLabel: 'Quality Center',
|
|
69
|
+
},
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function resolveLaunchPathHref(
|
|
74
|
+
id: LaunchPathId,
|
|
75
|
+
action: LaunchPathAction,
|
|
76
|
+
firstAgentId?: string | null,
|
|
77
|
+
): string {
|
|
78
|
+
if (id === 'assistant') {
|
|
79
|
+
if (action === 'primary') {
|
|
80
|
+
return firstAgentId ? `/agents/${encodeURIComponent(firstAgentId)}` : '/agents'
|
|
81
|
+
}
|
|
82
|
+
return '/connectors'
|
|
83
|
+
}
|
|
84
|
+
if (id === 'workflow') {
|
|
85
|
+
return action === 'primary' ? DEFAULT_BUILDER_ROUTE : '/protocols'
|
|
86
|
+
}
|
|
87
|
+
return action === 'primary' ? '/missions' : '/quality'
|
|
88
|
+
}
|
|
@@ -100,6 +100,16 @@ describe('isCliProvider', () => {
|
|
|
100
100
|
assert.equal(isCliProvider('goose'), true)
|
|
101
101
|
})
|
|
102
102
|
|
|
103
|
+
it('returns true for the extended generic-cli roster', () => {
|
|
104
|
+
const sample = [
|
|
105
|
+
'aider-cli', 'cline-cli', 'continue-cli', 'windsurf-cli', 'warp-cli',
|
|
106
|
+
'roo-code-cli', 'kilo-code-cli', 'qoder-cli', 'openhands-cli', 'kimi-cli',
|
|
107
|
+
]
|
|
108
|
+
for (const id of sample) {
|
|
109
|
+
assert.equal(isCliProvider(id), true, `${id} should be recognized as a CLI provider`)
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
103
113
|
it('returns false for non-CLI providers', () => {
|
|
104
114
|
assert.equal(isCliProvider('openai'), false)
|
|
105
115
|
assert.equal(isCliProvider('anthropic'), false)
|
|
@@ -457,6 +457,37 @@ export const CLI_PROVIDER_CAPABILITIES: Record<string, string> = {
|
|
|
457
457
|
'cursor-cli': 'full-agent coding workflows, multi-file edits, project-aware code changes',
|
|
458
458
|
'qwen-code-cli': 'terminal-native coding workflows, code generation, review, and automation',
|
|
459
459
|
goose: 'agentic coding workflows with extensions, tools, and runtime-managed execution',
|
|
460
|
+
'aider-cli': 'paired-programming-style multi-file edits and git-aware code changes',
|
|
461
|
+
'amp-cli': 'agentic coding via Sourcegraph Amp',
|
|
462
|
+
'augment-cli': 'codebase-aware agentic edits via Augment',
|
|
463
|
+
'adal-cli': 'AdaL coding agent for terminal-driven workflows',
|
|
464
|
+
'bob-cli': 'IBM watsonx Code Assistant (Bob) terminal coding workflows',
|
|
465
|
+
'cline-cli': 'autonomous file-level edits and terminal automation via Cline',
|
|
466
|
+
'codebuddy-cli': 'CodeBuddy agentic coding workflows',
|
|
467
|
+
'command-code-cli': 'Command Code terminal-native coding agent',
|
|
468
|
+
'continue-cli': 'agentic coding via the Continue CLI',
|
|
469
|
+
'cortex-cli': 'Snowflake Cortex Code agentic workflows',
|
|
470
|
+
'crush-cli': 'Crush terminal coding agent',
|
|
471
|
+
'deepagents-cli': 'long-horizon planning and multi-step coding via Deep Agents',
|
|
472
|
+
'firebender-cli': 'Firebender JetBrains-aligned coding agent',
|
|
473
|
+
'iflow-cli': 'iFlow CLI agentic coding workflows',
|
|
474
|
+
'junie-cli': 'JetBrains Junie coding agent for terminal use',
|
|
475
|
+
'kilo-code-cli': 'Kilo Code agentic coding workflows',
|
|
476
|
+
'kimi-cli': 'Kimi Code CLI coding agent',
|
|
477
|
+
'kode-cli': 'Kode terminal coding agent',
|
|
478
|
+
'mcpjam-cli': 'MCPJam-tooled agentic coding workflows',
|
|
479
|
+
'mistral-vibe-cli': 'Mistral Vibe coding agent',
|
|
480
|
+
'mux-cli': 'Mux multi-tool coding agent',
|
|
481
|
+
'neovate-cli': 'Neovate coding agent for terminal workflows',
|
|
482
|
+
'openhands-cli': 'OpenHands agentic coding via terminal',
|
|
483
|
+
'pochi-cli': 'Pochi coding agent',
|
|
484
|
+
'qoder-cli': 'Qoder agentic coding workflows',
|
|
485
|
+
'replit-cli': 'Replit Agent terminal coding workflows',
|
|
486
|
+
'roo-code-cli': 'Roo Code agentic coding workflows',
|
|
487
|
+
'trae-cn-cli': 'TRAE CN coding agent',
|
|
488
|
+
'warp-cli': 'Warp Agent terminal-native coding workflows',
|
|
489
|
+
'windsurf-cli': 'Windsurf agentic coding workflows',
|
|
490
|
+
'zencoder-cli': 'Zencoder agentic coding workflows',
|
|
460
491
|
}
|
|
461
492
|
|
|
462
493
|
/** Check if a provider ID is a CLI-based provider. */
|