@swarmclawai/swarmclaw 1.7.0 → 1.7.2

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 (42) hide show
  1. package/README.md +25 -9
  2. package/bin/swarmclaw.js +87 -0
  3. package/electron-dist/main.js +218 -0
  4. package/package.json +2 -2
  5. package/scripts/run-next-build.mjs +1 -1
  6. package/src/app/api/setup/check-provider/route.ts +5 -62
  7. package/src/app/api/setup/doctor/route.ts +19 -9
  8. package/src/app/home/page.tsx +19 -10
  9. package/src/cli/index.js +8 -2
  10. package/src/cli/index.ts +12 -3
  11. package/src/components/agents/inspector-panel.tsx +25 -3
  12. package/src/components/auth/setup-wizard/index.tsx +6 -2
  13. package/src/components/auth/setup-wizard/step-next.tsx +46 -39
  14. package/src/components/auth/setup-wizard/step-providers.tsx +113 -140
  15. package/src/components/auth/setup-wizard/types.ts +5 -2
  16. package/src/components/auth/setup-wizard/utils.test.ts +0 -19
  17. package/src/components/auth/setup-wizard/utils.ts +0 -69
  18. package/src/components/chat/chat-card.tsx +5 -0
  19. package/src/components/home/home-launchpad.tsx +123 -71
  20. package/src/components/layout/update-banner.tsx +43 -9
  21. package/src/lib/home-launchpad.test.ts +1 -31
  22. package/src/lib/home-launchpad.ts +0 -58
  23. package/src/lib/provider-sets.test.ts +19 -0
  24. package/src/lib/provider-sets.ts +8 -3
  25. package/src/lib/providers/cli-provider-metadata.test.ts +38 -0
  26. package/src/lib/providers/cli-provider-metadata.ts +208 -0
  27. package/src/lib/providers/cli-utils.test.ts +65 -1
  28. package/src/lib/providers/cli-utils.ts +26 -44
  29. package/src/lib/providers/codex-cli.ts +71 -75
  30. package/src/lib/providers/generic-cli.ts +2 -31
  31. package/src/lib/providers/index.ts +14 -44
  32. package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +189 -0
  33. package/src/lib/server/chat-execution/chat-turn-finalization.ts +26 -19
  34. package/src/lib/server/cli-provider-readiness.test.ts +45 -0
  35. package/src/lib/server/cli-provider-readiness.ts +84 -0
  36. package/src/lib/server/provider-health.test.ts +6 -0
  37. package/src/lib/server/provider-health.ts +2 -2
  38. package/src/lib/setup-defaults.test.ts +8 -0
  39. package/src/lib/setup-defaults.ts +38 -178
  40. package/src/stores/slices/session-slice.test.ts +40 -2
  41. package/src/stores/slices/session-slice.ts +41 -1
  42. package/tsconfig.json +1 -0
@@ -3,33 +3,10 @@
3
3
  * Isomorphic — no 'use client', no server imports.
4
4
  */
5
5
 
6
- export type SetupProvider =
7
- | 'claude-cli'
8
- | 'codex-cli'
9
- | 'opencode-cli'
10
- | 'opencode-web'
11
- | 'gemini-cli'
12
- | 'copilot-cli'
13
- | 'droid-cli'
14
- | 'cursor-cli'
15
- | 'qwen-code-cli'
16
- | 'goose'
17
- | 'anthropic'
18
- | 'openai'
19
- | 'openrouter'
20
- | 'google'
21
- | 'deepseek'
22
- | 'groq'
23
- | 'together'
24
- | 'mistral'
25
- | 'xai'
26
- | 'fireworks'
27
- | 'nebius'
28
- | 'deepinfra'
29
- | 'ollama'
30
- | 'openclaw'
31
- | 'hermes'
32
- | 'custom'
6
+ import { CLI_PROVIDER_METADATA, type CliProviderId, type CliProviderMetadata } from './providers/cli-provider-metadata.ts'
7
+ import type { ProviderType } from '../types/provider.ts'
8
+
9
+ export type SetupProvider = ProviderType | 'custom'
33
10
 
34
11
  export interface SetupProviderOption {
35
12
  id: SetupProvider
@@ -48,38 +25,27 @@ export interface SetupProviderOption {
48
25
  icon: string
49
26
  modelLibraryUrl?: string
50
27
  cloudEndpoint?: string
28
+ category?: 'cli' | 'api' | 'gateway' | 'local' | 'custom'
51
29
  }
52
30
 
31
+ const CLI_SETUP_PROVIDERS: SetupProviderOption[] = (CLI_PROVIDER_METADATA as readonly CliProviderMetadata[]).map((provider) => ({
32
+ id: provider.id,
33
+ name: provider.displayName,
34
+ description: provider.description,
35
+ requiresKey: false,
36
+ supportsEndpoint: false,
37
+ optionalKey: provider.optionalApiKey,
38
+ keyUrl: provider.keyUrl,
39
+ keyLabel: provider.keyLabel,
40
+ keyPlaceholder: provider.keyPlaceholder,
41
+ badge: provider.setupBadge,
42
+ icon: provider.icon,
43
+ modelLibraryUrl: provider.modelLibraryUrl,
44
+ category: 'cli',
45
+ }))
46
+
53
47
  export const SETUP_PROVIDERS: SetupProviderOption[] = [
54
- {
55
- id: 'claude-cli',
56
- name: 'Claude Code CLI',
57
- description: 'Anthropic’s coding agent with native tools, strong edits, and first-class CLI workflows.',
58
- requiresKey: false,
59
- supportsEndpoint: false,
60
- badge: 'CLI',
61
- icon: 'C',
62
- modelLibraryUrl: 'https://docs.anthropic.com/en/docs/about-claude/models',
63
- },
64
- {
65
- id: 'codex-cli',
66
- name: 'OpenAI Codex CLI',
67
- description: 'OpenAI’s terminal coding agent with resume support and structured headless output.',
68
- requiresKey: false,
69
- supportsEndpoint: false,
70
- badge: 'CLI',
71
- icon: 'O',
72
- modelLibraryUrl: 'https://platform.openai.com/docs/models',
73
- },
74
- {
75
- id: 'opencode-cli',
76
- name: 'OpenCode CLI',
77
- description: 'A flexible coding CLI that can route across multiple model backends.',
78
- requiresKey: false,
79
- supportsEndpoint: false,
80
- badge: 'CLI',
81
- icon: 'O',
82
- },
48
+ ...CLI_SETUP_PROVIDERS,
83
49
  {
84
50
  id: 'opencode-web',
85
51
  name: 'OpenCode Web',
@@ -92,66 +58,7 @@ export const SETUP_PROVIDERS: SetupProviderOption[] = [
92
58
  keyPlaceholder: 'opencode:••••••• (or just the password)',
93
59
  badge: 'HTTP',
94
60
  icon: 'O',
95
- },
96
- {
97
- id: 'gemini-cli',
98
- name: 'Gemini CLI',
99
- description: 'Google’s terminal coding agent with project-aware headless mode and resume support.',
100
- requiresKey: false,
101
- supportsEndpoint: false,
102
- badge: 'CLI',
103
- icon: 'G',
104
- modelLibraryUrl: 'https://ai.google.dev/gemini-api/docs/models',
105
- },
106
- {
107
- id: 'copilot-cli',
108
- name: 'GitHub Copilot CLI',
109
- description: 'GitHub’s multi-model terminal agent for coding and automation.',
110
- requiresKey: false,
111
- supportsEndpoint: false,
112
- badge: 'CLI',
113
- icon: 'P',
114
- },
115
- {
116
- id: 'droid-cli',
117
- name: 'Factory Droid CLI',
118
- description: 'Factory.ai’s terminal coding agent with headless exec mode, session resume, and autonomy controls.',
119
- requiresKey: false,
120
- supportsEndpoint: false,
121
- optionalKey: true,
122
- keyUrl: 'https://app.factory.ai/settings/api-keys',
123
- keyLabel: 'app.factory.ai',
124
- keyPlaceholder: 'FACTORY_API_KEY (optional if signed in via `droid`)',
125
- badge: 'CLI',
126
- icon: 'F',
127
- },
128
- {
129
- id: 'cursor-cli',
130
- name: 'Cursor Agent CLI',
131
- description: 'Cursor’s terminal agent with resume support, JSON output, and Cursor-native coding workflows.',
132
- requiresKey: false,
133
- supportsEndpoint: false,
134
- badge: 'CLI',
135
- icon: 'U',
136
- },
137
- {
138
- id: 'qwen-code-cli',
139
- name: 'Qwen Code CLI',
140
- description: 'Qwen’s terminal coding agent with structured headless mode and multi-provider model config.',
141
- requiresKey: false,
142
- supportsEndpoint: false,
143
- badge: 'CLI',
144
- icon: 'Q',
145
- },
146
- {
147
- id: 'goose',
148
- name: 'Goose',
149
- description: 'A runtime-managed terminal agent with extensions, session history, and ACP support.',
150
- requiresKey: false,
151
- supportsEndpoint: false,
152
- optionalKey: true,
153
- badge: 'Runtime',
154
- icon: 'G',
61
+ category: 'cli',
155
62
  },
156
63
  {
157
64
  id: 'openai',
@@ -190,6 +97,7 @@ export const SETUP_PROVIDERS: SetupProviderOption[] = [
190
97
  optionalKey: true,
191
98
  badge: 'First-Tier',
192
99
  icon: 'C',
100
+ category: 'gateway',
193
101
  },
194
102
  {
195
103
  id: 'hermes',
@@ -202,6 +110,7 @@ export const SETUP_PROVIDERS: SetupProviderOption[] = [
202
110
  optionalKey: true,
203
111
  badge: 'API Server',
204
112
  icon: 'H',
113
+ category: 'gateway',
205
114
  },
206
115
  {
207
116
  id: 'anthropic',
@@ -327,6 +236,7 @@ export const SETUP_PROVIDERS: SetupProviderOption[] = [
327
236
  icon: 'L',
328
237
  modelLibraryUrl: 'https://ollama.com/library',
329
238
  cloudEndpoint: 'https://api.ollama.com',
239
+ category: 'local',
330
240
  },
331
241
  {
332
242
  id: 'custom',
@@ -337,6 +247,7 @@ export const SETUP_PROVIDERS: SetupProviderOption[] = [
337
247
  allowMultiple: true,
338
248
  optionalKey: true,
339
249
  icon: '+',
250
+ category: 'custom',
340
251
  },
341
252
  ]
342
253
 
@@ -815,28 +726,19 @@ export interface DefaultAgentConfig {
815
726
  tools: string[]
816
727
  }
817
728
 
818
- export const DEFAULT_AGENTS: Record<SetupProvider, DefaultAgentConfig> = {
819
- 'claude-cli': {
820
- name: 'Claude CLI',
821
- description: 'A helpful assistant powered by Claude Code CLI.',
822
- systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
823
- model: 'claude-sonnet-4-6',
824
- tools: STARTER_AGENT_TOOLS,
825
- },
826
- 'codex-cli': {
827
- name: 'Codex CLI',
828
- description: 'A helpful assistant powered by OpenAI Codex CLI.',
829
- systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
830
- model: 'gpt-5.3-codex',
831
- tools: STARTER_AGENT_TOOLS,
832
- },
833
- 'opencode-cli': {
834
- name: 'OpenCode',
835
- description: 'A helpful assistant powered by OpenCode CLI.',
729
+ const CLI_DEFAULT_AGENTS = Object.fromEntries(CLI_PROVIDER_METADATA.map((provider) => [
730
+ provider.id,
731
+ {
732
+ name: provider.displayName.endsWith(' CLI') ? provider.displayName.slice(0, -4) : provider.displayName,
733
+ description: `A helpful assistant powered by ${provider.displayName}.`,
836
734
  systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
837
- model: 'claude-sonnet-4-6',
735
+ model: provider.defaultModel,
838
736
  tools: STARTER_AGENT_TOOLS,
839
737
  },
738
+ ])) as Record<CliProviderId, DefaultAgentConfig>
739
+
740
+ export const DEFAULT_AGENTS = {
741
+ ...CLI_DEFAULT_AGENTS,
840
742
  'opencode-web': {
841
743
  name: 'OpenCode Web',
842
744
  description: 'A helpful assistant powered by a remote OpenCode HTTP server.',
@@ -844,48 +746,6 @@ export const DEFAULT_AGENTS: Record<SetupProvider, DefaultAgentConfig> = {
844
746
  model: 'anthropic/claude-sonnet-4-6',
845
747
  tools: STARTER_AGENT_TOOLS,
846
748
  },
847
- 'gemini-cli': {
848
- name: 'Gemini CLI',
849
- description: 'A helpful assistant powered by Gemini CLI.',
850
- systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
851
- model: 'gemini-3.1-pro',
852
- tools: STARTER_AGENT_TOOLS,
853
- },
854
- 'copilot-cli': {
855
- name: 'Copilot CLI',
856
- description: 'A helpful assistant powered by GitHub Copilot CLI.',
857
- systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
858
- model: 'claude-sonnet-4-6',
859
- tools: STARTER_AGENT_TOOLS,
860
- },
861
- 'droid-cli': {
862
- name: 'Factory Droid',
863
- description: 'A helpful assistant powered by Factory Droid CLI.',
864
- systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
865
- model: 'default',
866
- tools: STARTER_AGENT_TOOLS,
867
- },
868
- 'cursor-cli': {
869
- name: 'Cursor CLI',
870
- description: 'A helpful assistant powered by Cursor Agent CLI.',
871
- systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
872
- model: 'auto',
873
- tools: STARTER_AGENT_TOOLS,
874
- },
875
- 'qwen-code-cli': {
876
- name: 'Qwen Code',
877
- description: 'A helpful assistant powered by Qwen Code CLI.',
878
- systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
879
- model: 'default',
880
- tools: STARTER_AGENT_TOOLS,
881
- },
882
- goose: {
883
- name: 'Goose',
884
- description: 'A helpful assistant powered by Goose.',
885
- systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
886
- model: 'default',
887
- tools: STARTER_AGENT_TOOLS,
888
- },
889
749
  anthropic: {
890
750
  name: 'Claude',
891
751
  description: 'A helpful Claude-powered assistant.',
@@ -998,7 +858,7 @@ export const DEFAULT_AGENTS: Record<SetupProvider, DefaultAgentConfig> = {
998
858
  model: '',
999
859
  tools: STARTER_AGENT_TOOLS,
1000
860
  },
1001
- }
861
+ } satisfies Record<SetupProvider, DefaultAgentConfig>
1002
862
 
1003
863
  export function getDefaultModelForProvider(provider: SetupProvider): string {
1004
864
  return DEFAULT_AGENTS[provider].model
@@ -32,11 +32,49 @@ test('selectActiveSessionId prefers override when present', () => {
32
32
  assert.equal(selectActiveSessionId(state), 'task-1')
33
33
  })
34
34
 
35
- test('selectActiveSessionId falls back to agent thread session', () => {
35
+ test('selectActiveSessionId chooses most recently active session for current agent', () => {
36
36
  const state = makeState({
37
37
  currentAgentId: 'agent-1',
38
38
  agents: { 'agent-1': makeAgent('agent-1', 'thread-1') },
39
- sessions: { 'thread-1': makeSession('thread-1') },
39
+ sessions: {
40
+ 'thread-1': { ...makeSession('thread-1'), agentId: 'agent-1', lastActiveAt: 100 } as unknown as Session,
41
+ 'old-1': { ...makeSession('old-1'), agentId: 'agent-1', lastActiveAt: 90 } as unknown as Session,
42
+ 'latest-1': { ...makeSession('latest-1'), agentId: 'agent-1', lastActiveAt: 200, messageCount: 1 } as unknown as Session,
43
+ 'other-agent': { ...makeSession('other-agent'), agentId: 'agent-2', lastActiveAt: 999 } as unknown as Session,
44
+ },
45
+ })
46
+ assert.equal(selectActiveSessionId(state), 'latest-1')
47
+ })
48
+
49
+ test('selectActiveSessionId prefers most recent session with content over newer empty thread session', () => {
50
+ const state = makeState({
51
+ currentAgentId: 'agent-1',
52
+ agents: { 'agent-1': makeAgent('agent-1', 'thread-1') },
53
+ sessions: {
54
+ 'thread-1': { ...makeSession('thread-1'), agentId: 'agent-1', lastActiveAt: 300 } as unknown as Session,
55
+ 'work-1': { ...makeSession('work-1'), agentId: 'agent-1', lastActiveAt: 200, messageCount: 2 } as unknown as Session,
56
+ },
57
+ })
58
+ assert.equal(selectActiveSessionId(state), 'work-1')
59
+ })
60
+
61
+ test('selectActiveSessionId falls back to thread session when agent has no loaded sessions', () => {
62
+ const state = makeState({
63
+ currentAgentId: 'agent-1',
64
+ agents: { 'agent-1': makeAgent('agent-1', 'thread-1') },
65
+ sessions: { 'unrelated': { ...makeSession('unrelated'), agentId: 'agent-2' } as unknown as Session },
66
+ })
67
+ assert.equal(selectActiveSessionId(state), 'thread-1')
68
+ })
69
+
70
+ test('selectActiveSessionId falls back to thread session when all loaded sessions are empty', () => {
71
+ const state = makeState({
72
+ currentAgentId: 'agent-1',
73
+ agents: { 'agent-1': makeAgent('agent-1', 'thread-1') },
74
+ sessions: {
75
+ 'thread-1': { ...makeSession('thread-1'), agentId: 'agent-1', lastActiveAt: 120 } as unknown as Session,
76
+ 'empty-newer': { ...makeSession('empty-newer'), agentId: 'agent-1', lastActiveAt: 220 } as unknown as Session,
77
+ },
40
78
  })
41
79
  assert.equal(selectActiveSessionId(state), 'thread-1')
42
80
  })
@@ -8,6 +8,46 @@ import { createLoader, createInflightDeduplicator } from '../store-utils'
8
8
 
9
9
  const sessionRefreshDedup = createInflightDeduplicator('sessionSlice_inflightRefreshes')
10
10
 
11
+ function getSessionSortScore(session: Session): number {
12
+ return session.lastAssistantAt
13
+ || session.lastActiveAt
14
+ || session.updatedAt
15
+ || session.createdAt
16
+ || 0
17
+ }
18
+
19
+ function hasSessionContent(session: Session): boolean {
20
+ if (typeof session.messageCount === 'number' && Number.isFinite(session.messageCount) && session.messageCount > 0) {
21
+ return true
22
+ }
23
+ if (session.lastMessageSummary) return true
24
+ return Array.isArray(session.messages) && session.messages.length > 0
25
+ }
26
+
27
+ function getLatestAgentSessionId(s: AppState, agentId: string, threadSessionId?: string | null): string | null {
28
+ let bestAnyId: string | null = null
29
+ let bestAnyScore = Number.NEGATIVE_INFINITY
30
+ let bestWithContentId: string | null = null
31
+ let bestWithContentScore = Number.NEGATIVE_INFINITY
32
+
33
+ for (const [sessionId, session] of Object.entries(s.sessions)) {
34
+ if (session.agentId !== agentId) continue
35
+ const score = getSessionSortScore(session)
36
+ if (score > bestAnyScore) {
37
+ bestAnyScore = score
38
+ bestAnyId = sessionId
39
+ }
40
+ if (hasSessionContent(session) && score > bestWithContentScore) {
41
+ bestWithContentScore = score
42
+ bestWithContentId = sessionId
43
+ }
44
+ }
45
+
46
+ if (bestWithContentId) return bestWithContentId
47
+ if (threadSessionId && s.sessions[threadSessionId]?.agentId === agentId) return threadSessionId
48
+ return bestAnyId
49
+ }
50
+
11
51
  /** Derive the active session ID from the current agent — no stored `currentSessionId`. */
12
52
  export function selectActiveSessionId(s: AppState): string | null {
13
53
  if (s.activeSessionIdOverride && s.sessions[s.activeSessionIdOverride]) {
@@ -15,7 +55,7 @@ export function selectActiveSessionId(s: AppState): string | null {
15
55
  }
16
56
  if (!s.currentAgentId) return null
17
57
  const agent = s.agents[s.currentAgentId]
18
- return agent?.threadSessionId ?? null
58
+ return getLatestAgentSessionId(s, s.currentAgentId, agent?.threadSessionId) || agent?.threadSessionId || null
19
59
  }
20
60
 
21
61
  export interface SessionSlice {
package/tsconfig.json CHANGED
@@ -13,6 +13,7 @@
13
13
  "esModuleInterop": true,
14
14
  "module": "esnext",
15
15
  "moduleResolution": "bundler",
16
+ "allowImportingTsExtensions": true,
16
17
  "resolveJsonModule": true,
17
18
  "isolatedModules": true,
18
19
  "jsx": "react-jsx",