@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.
Files changed (30) hide show
  1. package/README.md +20 -0
  2. package/package.json +3 -3
  3. package/src/app/.well-known/agent-card.json/route.ts +15 -0
  4. package/src/app/api/.well-known/agent-card/route.ts +6 -37
  5. package/src/app/home/page.tsx +10 -19
  6. package/src/components/auth/setup-wizard/index.tsx +2 -6
  7. package/src/components/auth/setup-wizard/step-next.tsx +39 -46
  8. package/src/components/auth/setup-wizard/step-providers.tsx +142 -76
  9. package/src/components/auth/setup-wizard/types.ts +2 -5
  10. package/src/components/auth/setup-wizard/utils.test.ts +19 -0
  11. package/src/components/auth/setup-wizard/utils.ts +69 -0
  12. package/src/components/home/home-launchpad.tsx +100 -80
  13. package/src/lib/a2a/agent-card.test.ts +94 -0
  14. package/src/lib/a2a/agent-card.ts +41 -1
  15. package/src/lib/home-launchpad.test.ts +31 -1
  16. package/src/lib/home-launchpad.ts +58 -0
  17. package/src/lib/providers/cli-utils.test.ts +10 -0
  18. package/src/lib/providers/cli-utils.ts +31 -0
  19. package/src/lib/providers/generic-cli.test.ts +71 -0
  20. package/src/lib/providers/generic-cli.ts +138 -0
  21. package/src/lib/providers/index.ts +56 -1
  22. package/src/lib/providers/opencode-cli.test.ts +9 -0
  23. package/src/lib/providers/opencode-cli.ts +5 -1
  24. package/src/lib/server/missions/mission-templates.test.ts +17 -0
  25. package/src/lib/server/missions/mission-templates.ts +69 -0
  26. package/src/lib/server/protocols/protocol-service.test.ts +25 -0
  27. package/src/lib/server/protocols/protocol-templates.ts +48 -0
  28. package/src/lib/strip-internal-metadata.test.ts +23 -0
  29. package/src/lib/strip-internal-metadata.ts +136 -7
  30. 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 { LaunchActionCard } from '@/components/shared/launch-action-card'
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
- onOpenFirstAgent: () => void
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
- onOpenFirstAgent,
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-[24px] border border-white/[0.06] bg-gradient-to-br from-white/[0.05] via-white/[0.02] to-transparent p-6">
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-[-0.03em] text-text">
63
- Start with the result you want, not the control plane.
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
- SwarmClaw already has the building blocks. Use this workspace to start a live agent chat, launch a bounded session, wire a connector, or move straight into reusable workflows.
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 md:grid-cols-2 xl:grid-cols-3">
98
- <LaunchActionCard
99
- title={firstAgent ? 'Open First Agent Chat' : 'Open Agents'}
100
- description={firstAgent
101
- ? `Jump into ${firstAgent.name} and start using the workspace immediately.`
102
- : 'Open the agents workspace to create or tune the first specialist agent.'}
103
- actionLabel={firstAgent ? 'Open Chat' : 'Open Agents'}
104
- onClick={onOpenFirstAgent}
105
- tone="primary"
106
- />
107
- <LaunchActionCard
108
- title="Start Structured Session"
109
- description="Open bounded collaboration runs for planning, review, decision-making, or focused multi-agent work."
110
- actionLabel="Open Protocols"
111
- onClick={onOpenProtocols}
112
- />
113
- <LaunchActionCard
114
- title="Open Workflow Builder"
115
- description="Move straight into reusable orchestration graphs if you want a durable workflow instead of a one-off run."
116
- actionLabel="Open Builder"
117
- onClick={onOpenBuilder}
118
- />
119
- <LaunchActionCard
120
- title="Connect a Platform"
121
- description="Bridge agents into chat surfaces like Discord, Slack, Telegram, and WhatsApp."
122
- actionLabel="Open Connectors"
123
- onClick={onOpenConnectors}
124
- />
125
- <LaunchActionCard
126
- title="Review Usage"
127
- description="Check cost, provider health, and activity so the workspace stays observable from the start."
128
- actionLabel="Open Usage"
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 { deriveHomeMode, isSparseWorkspace } from './home-launchpad'
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. */