@swarmclawai/swarmclaw 1.5.36 → 1.5.37

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 (45) hide show
  1. package/README.md +10 -1
  2. package/package.json +6 -1
  3. package/public/provider-logos/droid-cli.svg +7 -0
  4. package/src/app/api/setup/check-provider/route.ts +4 -2
  5. package/src/app/api/setup/doctor/route.ts +1 -0
  6. package/src/components/agents/agent-sheet.tsx +3 -1
  7. package/src/components/agents/inspector-panel.tsx +1 -0
  8. package/src/components/chat/activity-moment.tsx +1 -0
  9. package/src/components/chat/chat-header.tsx +2 -1
  10. package/src/components/chat/tool-call-bubble.tsx +5 -0
  11. package/src/lib/orchestrator-config.ts +1 -0
  12. package/src/lib/provider-sets.ts +3 -3
  13. package/src/lib/providers/cli-utils.test.ts +2 -0
  14. package/src/lib/providers/cli-utils.ts +28 -1
  15. package/src/lib/providers/droid-cli.ts +220 -0
  16. package/src/lib/providers/index.ts +11 -1
  17. package/src/lib/server/agents/agent-availability.test.ts +1 -1
  18. package/src/lib/server/agents/agent-thread-session.ts +1 -0
  19. package/src/lib/server/agents/task-session.ts +2 -0
  20. package/src/lib/server/capability-router.ts +3 -1
  21. package/src/lib/server/chat-execution/chat-execution-utils.ts +11 -0
  22. package/src/lib/server/chat-execution/chat-turn-finalization.ts +2 -0
  23. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +1 -0
  24. package/src/lib/server/chat-execution/prompt-sections.ts +2 -0
  25. package/src/lib/server/chatrooms/chatroom-helpers.ts +3 -0
  26. package/src/lib/server/chats/chat-session-service.ts +4 -0
  27. package/src/lib/server/connectors/session.ts +2 -0
  28. package/src/lib/server/context-manager.ts +1 -0
  29. package/src/lib/server/provider-health.ts +4 -2
  30. package/src/lib/server/provider-model-discovery.test.ts +1 -1
  31. package/src/lib/server/provider-model-discovery.ts +1 -1
  32. package/src/lib/server/runtime/daemon-state/core.ts +2 -2
  33. package/src/lib/server/session-reset-policy.ts +2 -0
  34. package/src/lib/server/session-tools/context.ts +2 -2
  35. package/src/lib/server/session-tools/delegate-droid.test.ts +24 -0
  36. package/src/lib/server/session-tools/delegate.ts +105 -12
  37. package/src/lib/server/session-tools/index.ts +3 -2
  38. package/src/lib/server/session-tools/session-info.ts +1 -0
  39. package/src/lib/server/storage-normalization.ts +3 -0
  40. package/src/lib/server/tool-aliases.ts +1 -1
  41. package/src/lib/server/tool-capability-policy.ts +2 -1
  42. package/src/lib/setup-defaults.ts +21 -0
  43. package/src/types/misc.ts +1 -1
  44. package/src/types/provider.ts +1 -1
  45. package/src/types/session.ts +3 -0
package/README.md CHANGED
@@ -40,6 +40,7 @@ Extension tutorial: https://swarmclaw.ai/docs/extension-tutorial
40
40
  <td align="center"><img src="doc/assets/logos/gemini-cli.svg" width="32" alt="Gemini CLI"><br><sub>Gemini CLI</sub></td>
41
41
  <td align="center"><img src="doc/assets/logos/opencode.svg" width="32" alt="OpenCode"><br><sub>OpenCode</sub></td>
42
42
  <td align="center"><img src="doc/assets/logos/copilot-cli.svg" width="32" alt="Copilot CLI"><br><sub>Copilot</sub></td>
43
+ <td align="center"><img src="public/provider-logos/droid-cli.svg" width="32" alt="Factory Droid CLI"><br><sub>Droid</sub></td>
43
44
  <td align="center"><img src="doc/assets/logos/cursor-cli.svg" width="32" alt="Cursor Agent CLI"><br><sub>Cursor</sub></td>
44
45
  <td align="center"><img src="doc/assets/logos/qwen-code-cli.svg" width="32" alt="Qwen Code CLI"><br><sub>Qwen Code</sub></td>
45
46
  <td align="center"><img src="doc/assets/logos/goose.svg" width="32" alt="Goose"><br><sub>Goose</sub></td>
@@ -65,7 +66,7 @@ Extension tutorial: https://swarmclaw.ai/docs/extension-tutorial
65
66
  - Node.js 22.6+ (`nvm use` will pick up the repo's `.nvmrc`, which matches CI)
66
67
  - npm 10+ or another supported package manager
67
68
  - Docker Desktop is recommended for sandbox browser execution
68
- - Optional provider CLIs if you want delegated CLI backends such as Claude Code, Codex, OpenCode, Gemini, Copilot, Cursor Agent, Qwen Code, or Goose
69
+ - Optional provider CLIs if you want delegated CLI backends such as Claude Code, Codex, OpenCode, Gemini, Copilot, Factory Droid, Cursor Agent, Qwen Code, or Goose
69
70
 
70
71
  ## Quick Start
71
72
 
@@ -388,6 +389,14 @@ Operational docs: https://swarmclaw.ai/docs/observability
388
389
 
389
390
  ## Releases
390
391
 
392
+ ### v1.5.37 Highlights
393
+
394
+ - **Factory Droid CLI as a provider and delegation backend**: adds [`droid`](https://docs.factory.ai/cli/droid-exec/overview) as a first-class chat provider and `delegate` backend with streaming JSON output, session resume, and a conservative `--auto low` autonomy pin on the delegate path. Install `droid` and sign in via browser (or set `FACTORY_API_KEY`), then pick **Factory Droid CLI** in the setup wizard. Resolves #38.
395
+ - **Desktop Release CI hardening**: v1.5.36's Electron build workflow failed on all three platforms. This release:
396
+ - Adds a proper `author` with email to `package.json` and a `linux.maintainer` entry in `electron-builder.yml` so the Linux `.deb` target stops rejecting the build.
397
+ - Pins `outputFileTracingRoot` in `next.config.ts` to the project root so the Next.js build no longer walks `C:\Users\<user>\Application Data` (a legacy NTFS junction that throws EPERM on Windows runners).
398
+ - Pins Python 3.11 in the desktop-release workflow so `node-gyp` rebuilds of native modules (`node-liblzma`, etc.) succeed on Python 3.12+ runners where `distutils` was removed from the stdlib.
399
+
391
400
  ### v1.5.36 Highlights
392
401
 
393
402
  - **Desktop app (Electron)**: SwarmClaw now ships as a native desktop app for macOS (Apple Silicon + Intel), Windows, and Linux (AppImage + .deb). Download from [swarmclaw.ai/downloads](https://swarmclaw.ai/downloads). The app wraps the existing standalone server inside an Electron shell, stores data in the OS app-data directory, and auto-updates via GitHub Releases (notify-only on unsigned macOS builds).
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.5.36",
3
+ "version": "1.5.37",
4
4
  "description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
5
5
  "main": "electron-dist/main.js",
6
6
  "license": "MIT",
7
+ "author": {
8
+ "name": "SwarmClaw",
9
+ "email": "noreply@swarmclaw.ai",
10
+ "url": "https://swarmclaw.ai"
11
+ },
7
12
  "publishConfig": {
8
13
  "access": "public",
9
14
  "registry": "https://registry.npmjs.org/"
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Factory Droid CLI">
2
+ <rect x="2" y="2" width="60" height="60" rx="14" fill="#0f172a"/>
3
+ <rect x="2" y="2" width="60" height="60" rx="14" fill="none" stroke="#38bdf8" stroke-width="2"/>
4
+ <path d="M20 22 h24 v6 h-9 v16 h-6 v-16 h-9 z" fill="#e2e8f0"/>
5
+ <circle cx="16" cy="48" r="2.5" fill="#38bdf8"/>
6
+ <circle cx="48" cy="48" r="2.5" fill="#38bdf8"/>
7
+ </svg>
@@ -12,6 +12,7 @@ type SetupProvider =
12
12
  | 'opencode-cli'
13
13
  | 'gemini-cli'
14
14
  | 'copilot-cli'
15
+ | 'droid-cli'
15
16
  | 'cursor-cli'
16
17
  | 'qwen-code-cli'
17
18
  | 'goose'
@@ -31,7 +32,7 @@ type SetupProvider =
31
32
  | 'openclaw'
32
33
  | 'hermes'
33
34
 
34
- type CliSetupProvider = 'claude-cli' | 'codex-cli' | 'opencode-cli' | 'gemini-cli' | 'copilot-cli' | 'cursor-cli' | 'qwen-code-cli' | 'goose'
35
+ type CliSetupProvider = 'claude-cli' | 'codex-cli' | 'opencode-cli' | 'gemini-cli' | 'copilot-cli' | 'droid-cli' | 'cursor-cli' | 'qwen-code-cli' | 'goose'
35
36
 
36
37
  interface SetupCheckBody {
37
38
  provider?: string
@@ -285,6 +286,7 @@ function checkCliProvider(provider: CliSetupProvider): { ok: boolean; message: s
285
286
  'opencode-cli': { binary: 'opencode', backend: 'opencode' as const, label: 'OpenCode CLI' },
286
287
  'gemini-cli': { binary: 'gemini', backend: 'gemini' as const, label: 'Gemini CLI' },
287
288
  'copilot-cli': { binary: 'copilot', backend: 'copilot' as const, label: 'GitHub Copilot CLI' },
289
+ 'droid-cli': { binary: 'droid', backend: 'droid' as const, label: 'Factory Droid CLI' },
288
290
  'cursor-cli': { binary: 'cursor-agent', backend: 'cursor' as const, label: 'Cursor Agent CLI' },
289
291
  'qwen-code-cli': { binary: 'qwen', backend: 'qwen' as const, label: 'Qwen Code CLI' },
290
292
  goose: { binary: 'goose', backend: 'goose' as const, label: 'Goose CLI' },
@@ -312,7 +314,7 @@ export async function POST(req: Request) {
312
314
  const credentialId = clean(body.credentialId)
313
315
  const endpoint = clean(body.endpoint)
314
316
  const model = clean(body.model)
315
- const CLI_PROVIDERS = new Set<CliSetupProvider>(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose'])
317
+ const CLI_PROVIDERS = new Set<CliSetupProvider>(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose'])
316
318
 
317
319
  // Resolve credentialId to an API key if no raw key was provided
318
320
  if (!apiKey && credentialId) {
@@ -203,6 +203,7 @@ export async function GET(req: Request) {
203
203
  { id: 'opencode-cli', label: 'OpenCode CLI', command: 'opencode' },
204
204
  { id: 'gemini-cli', label: 'Gemini CLI', command: 'gemini' },
205
205
  { id: 'copilot-cli', label: 'GitHub Copilot CLI', command: 'copilot' },
206
+ { id: 'droid-cli', label: 'Factory Droid CLI', command: 'droid' },
206
207
  { id: 'cursor-cli', label: 'Cursor Agent CLI', command: 'cursor-agent' },
207
208
  { id: 'qwen-code-cli', label: 'Qwen Code CLI', command: 'qwen' },
208
209
  { id: 'goose', label: 'Goose CLI', command: 'goose' },
@@ -2513,7 +2513,9 @@ export function AgentSheet() {
2513
2513
  ? 'Gemini CLI uses its own built-in tools and runtime — SwarmClaw does not inject local platform tools for it.'
2514
2514
  : provider === 'copilot-cli'
2515
2515
  ? 'GitHub Copilot CLI uses its own built-in tools and runtime — SwarmClaw does not inject local platform tools for it.'
2516
- : provider === 'cursor-cli'
2516
+ : provider === 'droid-cli'
2517
+ ? 'Factory Droid CLI uses its own built-in tools and autonomy controls — SwarmClaw does not inject local platform tools for it.'
2518
+ : provider === 'cursor-cli'
2517
2519
  ? 'Cursor Agent CLI runs with its own native tool/runtime layer — SwarmClaw sends prompts directly without injecting local platform tools.'
2518
2520
  : provider === 'qwen-code-cli'
2519
2521
  ? 'Qwen Code CLI uses its own native tools and runtime — SwarmClaw does not inject local platform tools for it.'
@@ -53,6 +53,7 @@ const PROVIDER_LABELS: Record<string, string> = {
53
53
  'opencode-cli': 'OpenCode CLI',
54
54
  'gemini-cli': 'Gemini CLI',
55
55
  'copilot-cli': 'Copilot CLI',
56
+ 'droid-cli': 'Droid CLI',
56
57
  'cursor-cli': 'Cursor CLI',
57
58
  'qwen-code-cli': 'Qwen Code CLI',
58
59
  goose: 'Goose',
@@ -14,6 +14,7 @@ const NOTABLE_TOOLS: Record<string, { label: string; color: string; icon: 'brain
14
14
  delegate_to_opencode_cli: { label: 'Delegated to OpenCode', color: '#38BDF8', icon: 'delegate' },
15
15
  delegate_to_gemini_cli: { label: 'Delegated to Gemini CLI', color: '#38BDF8', icon: 'delegate' },
16
16
  delegate_to_cursor_cli: { label: 'Delegated to Cursor CLI', color: '#38BDF8', icon: 'delegate' },
17
+ delegate_to_droid_cli: { label: 'Delegated to Factory Droid', color: '#38BDF8', icon: 'delegate' },
17
18
  delegate_to_qwen_code_cli: { label: 'Delegated to Qwen Code', color: '#38BDF8', icon: 'delegate' },
18
19
  delegate_to_agent: { label: 'Delegating task', color: '#6366F1', icon: 'delegate' },
19
20
  check_delegation_status: { label: 'Checking delegation', color: '#6366F1', icon: 'delegate' },
@@ -220,10 +220,11 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
220
220
  opencodeSessionId: null,
221
221
  geminiSessionId: null,
222
222
  copilotSessionId: null,
223
+ droidSessionId: null,
223
224
  cursorSessionId: null,
224
225
  qwenSessionId: null,
225
226
  acpSessionId: null,
226
- delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null, copilot: null, cursor: null, qwen: null },
227
+ delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null, copilot: null, droid: null, cursor: null, qwen: null },
227
228
  })
228
229
  await refreshSession(session.id)
229
230
  } catch { /* best-effort */ }
@@ -28,6 +28,7 @@ const TOOL_COLORS: Record<string, string> = {
28
28
  delegate_to_opencode_cli: '#14B8A6',
29
29
  delegate_to_gemini_cli: '#2563EB',
30
30
  delegate_to_copilot_cli: '#6366F1',
31
+ delegate_to_droid_cli: '#A855F7',
31
32
  delegate_to_cursor_cli: '#F97316',
32
33
  delegate_to_qwen_code_cli: '#22C55E',
33
34
  whoami_tool: '#8B5CF6',
@@ -81,6 +82,7 @@ export const TOOL_LABELS: Record<string, string> = {
81
82
  opencode_cli: 'OpenCode CLI',
82
83
  gemini_cli: 'Gemini CLI',
83
84
  copilot_cli: 'Copilot CLI',
85
+ droid_cli: 'Factory Droid',
84
86
  cursor_cli: 'Cursor CLI',
85
87
  qwen_code_cli: 'Qwen Code CLI',
86
88
  spawn_subagent: 'Subagent',
@@ -91,6 +93,7 @@ export const TOOL_LABELS: Record<string, string> = {
91
93
  delegate_to_opencode_cli: 'OpenCode CLI',
92
94
  delegate_to_gemini_cli: 'Gemini CLI',
93
95
  delegate_to_copilot_cli: 'Copilot CLI',
96
+ delegate_to_droid_cli: 'Factory Droid',
94
97
  delegate_to_cursor_cli: 'Cursor CLI',
95
98
  delegate_to_qwen_code_cli: 'Qwen Code CLI',
96
99
  whoami_tool: 'Who Am I',
@@ -129,6 +132,7 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
129
132
  opencode_cli: 'Enable delegation to OpenCode CLI',
130
133
  gemini_cli: 'Enable delegation to Gemini CLI',
131
134
  copilot_cli: 'Enable delegation to GitHub Copilot CLI',
135
+ droid_cli: 'Enable delegation to Factory Droid CLI',
132
136
  cursor_cli: 'Enable delegation to Cursor Agent CLI',
133
137
  qwen_code_cli: 'Enable delegation to Qwen Code CLI',
134
138
  spawn_subagent: 'Spawn native subagents with lineage tracking and batch support',
@@ -139,6 +143,7 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
139
143
  delegate_to_opencode_cli: 'Delegate complex coding tasks to OpenCode CLI',
140
144
  delegate_to_gemini_cli: 'Delegate complex coding tasks to Gemini CLI',
141
145
  delegate_to_copilot_cli: 'Delegate complex coding tasks to GitHub Copilot CLI',
146
+ delegate_to_droid_cli: 'Delegate complex coding tasks to Factory Droid CLI',
142
147
  delegate_to_cursor_cli: 'Delegate complex coding tasks to Cursor Agent CLI',
143
148
  delegate_to_qwen_code_cli: 'Delegate complex coding tasks to Qwen Code CLI',
144
149
  whoami_tool: 'Reveal the current agent and chat context',
@@ -6,6 +6,7 @@ export const NON_ORCHESTRATOR_PROVIDERS = new Set([
6
6
  'opencode-cli',
7
7
  'gemini-cli',
8
8
  'copilot-cli',
9
+ 'droid-cli',
9
10
  'cursor-cli',
10
11
  'qwen-code-cli',
11
12
  'goose',
@@ -1,11 +1,11 @@
1
1
  /** CLI providers that use their own tool execution outside the shared tool-runtime path. */
2
- export const NON_LANGGRAPH_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli'])
2
+ export const NON_LANGGRAPH_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli'])
3
3
 
4
4
  /** Providers that manage their own runtime/tool loop even when reached over an API endpoint. */
5
5
  export const RUNTIME_MANAGED_PROVIDER_IDS = new Set(['hermes', 'goose'])
6
6
 
7
7
  /** Providers with native tool/capability support (CLI providers + OpenClaw + Hermes). */
8
- export const NATIVE_CAPABILITY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose', 'openclaw', 'hermes'])
8
+ export const NATIVE_CAPABILITY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose', 'openclaw', 'hermes'])
9
9
 
10
10
  /** Providers that can only act as workers — no coordinator role, no heartbeat, no advanced settings. */
11
- export const WORKER_ONLY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose', 'openclaw', 'hermes'])
11
+ export const WORKER_ONLY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose', 'openclaw', 'hermes'])
@@ -94,6 +94,7 @@ describe('isCliProvider', () => {
94
94
  assert.equal(isCliProvider('opencode-cli'), true)
95
95
  assert.equal(isCliProvider('gemini-cli'), true)
96
96
  assert.equal(isCliProvider('copilot-cli'), true)
97
+ assert.equal(isCliProvider('droid-cli'), true)
97
98
  assert.equal(isCliProvider('cursor-cli'), true)
98
99
  assert.equal(isCliProvider('qwen-code-cli'), true)
99
100
  assert.equal(isCliProvider('goose'), true)
@@ -118,6 +119,7 @@ describe('CLI_PROVIDER_CAPABILITIES', () => {
118
119
  assert.ok('opencode-cli' in CLI_PROVIDER_CAPABILITIES)
119
120
  assert.ok('gemini-cli' in CLI_PROVIDER_CAPABILITIES)
120
121
  assert.ok('copilot-cli' in CLI_PROVIDER_CAPABILITIES)
122
+ assert.ok('droid-cli' in CLI_PROVIDER_CAPABILITIES)
121
123
  assert.ok('cursor-cli' in CLI_PROVIDER_CAPABILITIES)
122
124
  assert.ok('qwen-code-cli' in CLI_PROVIDER_CAPABILITIES)
123
125
  assert.ok('goose' in CLI_PROVIDER_CAPABILITIES)
@@ -45,6 +45,13 @@ const KNOWN_BINARY_PATHS: Record<string, string[]> = {
45
45
  '/opt/homebrew/bin/copilot',
46
46
  path.join(os.homedir(), '.npm-global/bin/copilot'),
47
47
  ],
48
+ droid: [
49
+ path.join(os.homedir(), '.local/bin/droid'),
50
+ '/usr/local/bin/droid',
51
+ '/opt/homebrew/bin/droid',
52
+ path.join(os.homedir(), '.npm-global/bin/droid'),
53
+ path.join(os.homedir(), '.factory/bin/droid'),
54
+ ],
48
55
  'cursor-agent': [
49
56
  path.join(os.homedir(), '.local/bin/cursor-agent'),
50
57
  '/usr/local/bin/cursor-agent',
@@ -166,7 +173,7 @@ export interface AuthProbeResult {
166
173
  */
167
174
  export function probeCliAuth(
168
175
  binary: string,
169
- backend: 'claude' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen' | 'goose',
176
+ backend: 'claude' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'droid' | 'cursor' | 'qwen' | 'goose',
170
177
  env: NodeJS.ProcessEnv,
171
178
  cwd?: string,
172
179
  ): AuthProbeResult {
@@ -277,6 +284,25 @@ export function probeCliAuth(
277
284
  return { authenticated: true }
278
285
  }
279
286
 
287
+ if (backend === 'droid') {
288
+ if (process.env.FACTORY_API_KEY || env.FACTORY_API_KEY) {
289
+ return { authenticated: true }
290
+ }
291
+ const configPaths = [
292
+ path.join(os.homedir(), '.factory', 'config.json'),
293
+ path.join(os.homedir(), '.config', 'factory', 'config.json'),
294
+ path.join(os.homedir(), '.factory', 'auth.json'),
295
+ ]
296
+ const hasConfig = configPaths.some((p) => fs.existsSync(p))
297
+ if (!hasConfig) {
298
+ return {
299
+ authenticated: false,
300
+ errorMessage: 'Factory Droid CLI is not authenticated. Run `droid` once to sign in via browser, or set FACTORY_API_KEY and try again.',
301
+ }
302
+ }
303
+ return { authenticated: true }
304
+ }
305
+
280
306
  if (backend === 'cursor') {
281
307
  try {
282
308
  const probe = spawnSync(binary, ['status'], {
@@ -427,6 +453,7 @@ export const CLI_PROVIDER_CAPABILITIES: Record<string, string> = {
427
453
  'opencode-cli': 'code analysis, generation across multiple LLM backends',
428
454
  'gemini-cli': 'code generation, analysis with Gemini models',
429
455
  'copilot-cli': 'code generation, analysis, multi-model support via GitHub Copilot',
456
+ 'droid-cli': 'code generation, refactoring, and automation via Factory Droid with configurable autonomy',
430
457
  'cursor-cli': 'full-agent coding workflows, multi-file edits, project-aware code changes',
431
458
  'qwen-code-cli': 'terminal-native coding workflows, code generation, review, and automation',
432
459
  goose: 'agentic coding workflows with extensions, tools, and runtime-managed execution',
@@ -0,0 +1,220 @@
1
+ import fs from 'fs'
2
+ import os from 'os'
3
+ import path from 'path'
4
+ import { spawn } from 'child_process'
5
+ import type { StreamChatOptions } from './index'
6
+ import { log } from '../server/logger'
7
+ import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
8
+ import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, symlinkConfigFiles, isStderrNoise } from './cli-utils'
9
+
10
+ /**
11
+ * Factory Droid CLI provider — spawns `droid exec <message> --output-format stream-json`.
12
+ * Tracks `session.droidSessionId` from streamed events to support multi-turn continuity.
13
+ */
14
+ export function streamDroidCliChat({ session, message, imagePath, systemPrompt, write, active, signal }: StreamChatOptions): Promise<string> {
15
+ const processTimeoutMs = loadRuntimeSettings().cliProcessTimeoutMs
16
+ const binary = resolveCliBinary('droid')
17
+ if (!binary) {
18
+ const msg = 'Factory Droid CLI not found. Install it (brew install --cask droid, npm i -g droid, or https://docs.factory.ai/cli/getting-started/quickstart) and ensure it is on your PATH.'
19
+ write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
20
+ return Promise.resolve('')
21
+ }
22
+
23
+ const env = buildCliEnv()
24
+
25
+ if (session.apiKey) {
26
+ env.FACTORY_API_KEY = session.apiKey
27
+ }
28
+
29
+ if (!session.apiKey) {
30
+ const auth = probeCliAuth(binary, 'droid', env, session.cwd)
31
+ if (!auth.authenticated) {
32
+ log.error('droid-cli', auth.errorMessage || 'Auth failed')
33
+ write(`data: ${JSON.stringify({ t: 'err', text: auth.errorMessage || 'Factory Droid CLI is not authenticated.' })}\n\n`)
34
+ return Promise.resolve('')
35
+ }
36
+ }
37
+
38
+ const promptParts: string[] = []
39
+ if (imagePath) {
40
+ promptParts.push(`[The user has shared an image at: ${imagePath}]`)
41
+ }
42
+ promptParts.push(message)
43
+ const prompt = promptParts.join('\n\n')
44
+
45
+ const args = ['exec', prompt, '--output-format', 'stream-json']
46
+ if (session.droidSessionId) args.push('-s', session.droidSessionId)
47
+ if (session.model) args.push('-m', session.model)
48
+
49
+ let tempFactoryHome: string | null = null
50
+ if (systemPrompt && !session.droidSessionId) {
51
+ const realFactoryHome = process.env.FACTORY_HOME || path.join(os.homedir(), '.factory')
52
+ tempFactoryHome = path.join(os.tmpdir(), `swarmclaw-droid-${session.id}`)
53
+ fs.mkdirSync(tempFactoryHome, { recursive: true })
54
+ symlinkConfigFiles(realFactoryHome, tempFactoryHome)
55
+ fs.writeFileSync(path.join(tempFactoryHome, 'AGENTS.override.md'), systemPrompt)
56
+ env.FACTORY_HOME = tempFactoryHome
57
+ }
58
+
59
+ log.info('droid-cli', `Spawning: ${binary}`, {
60
+ args: args.map((a) => a.length > 100 ? a.slice(0, 100) + '...' : a),
61
+ cwd: session.cwd,
62
+ promptLen: prompt.length,
63
+ hasSystemPrompt: !!systemPrompt,
64
+ resumeSessionId: session.droidSessionId || null,
65
+ })
66
+
67
+ const proc = spawn(binary, args, {
68
+ cwd: session.cwd,
69
+ env,
70
+ stdio: ['ignore', 'pipe', 'pipe'],
71
+ timeout: processTimeoutMs,
72
+ })
73
+
74
+ log.info('droid-cli', `Process spawned: pid=${proc.pid}`)
75
+ active.set(session.id, proc)
76
+ attachAbortHandler(proc, signal)
77
+
78
+ let fullResponse = ''
79
+ let buf = ''
80
+ let eventCount = 0
81
+ let stderrText = ''
82
+
83
+ proc.stdout!.on('data', (chunk: Buffer) => {
84
+ const raw = chunk.toString()
85
+ buf += raw
86
+
87
+ if (eventCount === 0) {
88
+ log.debug('droid-cli', `First stdout chunk (${raw.length} bytes)`, raw.slice(0, 500))
89
+ }
90
+
91
+ const lines = buf.split('\n')
92
+ buf = lines.pop()!
93
+
94
+ for (const line of lines) {
95
+ if (!line.trim()) continue
96
+ try {
97
+ const ev = JSON.parse(line) as Record<string, unknown>
98
+ eventCount++
99
+
100
+ const data = ev.data as Record<string, unknown> | undefined
101
+
102
+ if (typeof ev.session_id === 'string') {
103
+ session.droidSessionId = ev.session_id
104
+ } else if (typeof ev.sessionId === 'string') {
105
+ session.droidSessionId = ev.sessionId
106
+ }
107
+
108
+ if (ev.type === 'assistant.message_delta' && typeof data?.deltaContent === 'string') {
109
+ fullResponse += data.deltaContent
110
+ write(`data: ${JSON.stringify({ t: 'd', text: data.deltaContent })}\n\n`)
111
+ }
112
+
113
+ else if (ev.type === 'assistant.message' && typeof data?.content === 'string') {
114
+ if (!fullResponse) {
115
+ fullResponse = data.content
116
+ write(`data: ${JSON.stringify({ t: 'r', text: data.content })}\n\n`)
117
+ }
118
+ log.debug('droid-cli', `Assistant message (${data.content.length} chars)`)
119
+ }
120
+
121
+ else if (ev.type === 'content_block_delta') {
122
+ const delta = ev.delta as Record<string, unknown> | undefined
123
+ if (typeof delta?.text === 'string') {
124
+ fullResponse += delta.text
125
+ write(`data: ${JSON.stringify({ t: 'd', text: delta.text })}\n\n`)
126
+ }
127
+ }
128
+
129
+ else if (ev.type === 'agent_message_chunk' && typeof ev.text === 'string') {
130
+ fullResponse += ev.text
131
+ write(`data: ${JSON.stringify({ t: 'd', text: ev.text })}\n\n`)
132
+ }
133
+
134
+ else if (ev.type === 'message' && ev.role === 'assistant' && typeof ev.content === 'string') {
135
+ fullResponse += ev.content
136
+ write(`data: ${JSON.stringify({ t: 'd', text: ev.content })}\n\n`)
137
+ }
138
+
139
+ else if (ev.type === 'item.completed' && (ev.item as Record<string, unknown>)?.type === 'agent_message') {
140
+ const item = ev.item as Record<string, unknown>
141
+ if (typeof item.text === 'string') {
142
+ fullResponse = item.text
143
+ write(`data: ${JSON.stringify({ t: 'r', text: item.text })}\n\n`)
144
+ log.debug('droid-cli', `Agent message (${item.text.length} chars)`)
145
+ }
146
+ }
147
+
148
+ else if (ev.type === 'result' && typeof ev.result === 'string') {
149
+ fullResponse = ev.result
150
+ write(`data: ${JSON.stringify({ t: 'r', text: ev.result })}\n\n`)
151
+ log.debug('droid-cli', `Result event (${ev.result.length} chars)`)
152
+ }
153
+
154
+ else if (ev.type === 'result' && ev.status === 'error') {
155
+ const errMsg = typeof ev.error === 'string' ? ev.error : 'Droid error'
156
+ write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
157
+ log.warn('droid-cli', `Error result: ${errMsg}`)
158
+ }
159
+
160
+ else if (ev.type === 'error') {
161
+ const errMsg = typeof ev.message === 'string'
162
+ ? ev.message
163
+ : typeof ev.error === 'string'
164
+ ? ev.error
165
+ : 'Unknown Droid error'
166
+ write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
167
+ log.warn('droid-cli', `Event error: ${errMsg}`)
168
+ }
169
+
170
+ else if (eventCount <= 10) {
171
+ log.debug('droid-cli', `Event: ${String(ev.type)}`)
172
+ }
173
+ } catch {
174
+ if (line.trim()) {
175
+ log.debug('droid-cli', `Non-JSON stdout line`, line.slice(0, 300))
176
+ fullResponse += line + '\n'
177
+ write(`data: ${JSON.stringify({ t: 'd', text: line + '\n' })}\n\n`)
178
+ }
179
+ }
180
+ }
181
+ })
182
+
183
+ proc.stderr!.on('data', (chunk: Buffer) => {
184
+ const text = chunk.toString()
185
+ stderrText += text
186
+ if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
187
+ if (isStderrNoise(text)) {
188
+ log.debug('droid-cli', `stderr noise [${session.id}]`, text.slice(0, 500))
189
+ } else {
190
+ log.warn('droid-cli', `stderr [${session.id}]`, text.slice(0, 500))
191
+ }
192
+ })
193
+
194
+ return new Promise((resolve) => {
195
+ proc.on('close', (code, sig) => {
196
+ log.info('droid-cli', `Process closed: code=${code} signal=${sig} events=${eventCount} response=${fullResponse.length}chars`)
197
+ active.delete(session.id)
198
+ if (tempFactoryHome) {
199
+ try { fs.rmSync(tempFactoryHome, { recursive: true }) } catch { /* ignore */ }
200
+ }
201
+ if ((code ?? 0) !== 0 && !fullResponse.trim()) {
202
+ const msg = stderrText.trim()
203
+ ? `Factory Droid CLI exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''}: ${stderrText.trim().slice(0, 1200)}`
204
+ : `Factory Droid CLI exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''} and returned no output.`
205
+ write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
206
+ }
207
+ resolve(fullResponse)
208
+ })
209
+
210
+ proc.on('error', (e) => {
211
+ log.error('droid-cli', `Process error: ${e.message}`)
212
+ active.delete(session.id)
213
+ if (tempFactoryHome) {
214
+ try { fs.rmSync(tempFactoryHome, { recursive: true }) } catch { /* ignore */ }
215
+ }
216
+ write(`data: ${JSON.stringify({ t: 'err', text: e.message })}\n\n`)
217
+ resolve(fullResponse)
218
+ })
219
+ })
220
+ }
@@ -3,6 +3,7 @@ import { streamCodexCliChat } from './codex-cli'
3
3
  import { streamOpenCodeCliChat } from './opencode-cli'
4
4
  import { streamGeminiCliChat } from './gemini-cli'
5
5
  import { streamCopilotCliChat } from './copilot-cli'
6
+ import { streamDroidCliChat } from './droid-cli'
6
7
  import { streamCursorCliChat } from './cursor-cli'
7
8
  import { streamQwenCodeCliChat } from './qwen-code-cli'
8
9
  import { streamGooseChat } from './goose'
@@ -151,6 +152,15 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
151
152
  requiresEndpoint: false,
152
153
  handler: { streamChat: streamCopilotCliChat },
153
154
  },
155
+ 'droid-cli': {
156
+ id: 'droid-cli',
157
+ name: 'Factory Droid CLI',
158
+ models: ['default'],
159
+ requiresApiKey: false,
160
+ optionalApiKey: true,
161
+ requiresEndpoint: false,
162
+ handler: { streamChat: streamDroidCliChat },
163
+ },
154
164
  'cursor-cli': {
155
165
  id: 'cursor-cli',
156
166
  name: 'Cursor Agent CLI',
@@ -383,7 +393,7 @@ export function getProviderList(): ProviderInfo[] {
383
393
  ...info,
384
394
  models: overrides[info.id] || info.models,
385
395
  defaultModels: info.models,
386
- supportsModelDiscovery: !['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose', 'fireworks'].includes(info.id),
396
+ supportsModelDiscovery: !['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose', 'fireworks'].includes(info.id),
387
397
  }
388
398
  })
389
399
 
@@ -4,7 +4,7 @@ import type { Agent, ProviderType } from '@/types'
4
4
  import { isWorkerOnlyAgent, buildWorkerOnlyAgentMessage } from './agent-availability'
5
5
 
6
6
  describe('isWorkerOnlyAgent', () => {
7
- const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'openclaw'] satisfies ProviderType[]
7
+ const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'openclaw'] satisfies ProviderType[]
8
8
  const NON_CLI_PROVIDERS = ['openai', 'anthropic', 'google', 'deepseek', 'groq', 'together'] satisfies ProviderType[]
9
9
 
10
10
  function withProvider(provider: unknown): Pick<Agent, 'provider'> {
@@ -45,6 +45,7 @@ function buildThreadSession(agent: Agent, sessionId: string, user: string, creat
45
45
  opencodeSessionId: existing?.opencodeSessionId || null,
46
46
  geminiSessionId: existing?.geminiSessionId || null,
47
47
  copilotSessionId: existing?.copilotSessionId || null,
48
+ droidSessionId: existing?.droidSessionId || null,
48
49
  cursorSessionId: existing?.cursorSessionId || null,
49
50
  qwenSessionId: existing?.qwenSessionId || null,
50
51
  acpSessionId: existing?.acpSessionId || null,
@@ -42,6 +42,7 @@ export function createAgentTaskSession(
42
42
  opencodeSessionId: null,
43
43
  geminiSessionId: null,
44
44
  copilotSessionId: null,
45
+ droidSessionId: null,
45
46
  cursorSessionId: null,
46
47
  qwenSessionId: null,
47
48
  acpSessionId: null,
@@ -51,6 +52,7 @@ export function createAgentTaskSession(
51
52
  opencode: null,
52
53
  gemini: null,
53
54
  copilot: null,
55
+ droid: null,
54
56
  cursor: null,
55
57
  qwen: null,
56
58
  },
@@ -19,7 +19,7 @@ export interface CapabilityRoutingDecision {
19
19
  primaryUrl?: string
20
20
  }
21
21
 
22
- type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli' | 'delegate_to_copilot_cli' | 'delegate_to_cursor_cli' | 'delegate_to_qwen_code_cli'
22
+ type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli' | 'delegate_to_copilot_cli' | 'delegate_to_droid_cli' | 'delegate_to_cursor_cli' | 'delegate_to_qwen_code_cli'
23
23
 
24
24
  function findFirstUrl(text: string): string | undefined {
25
25
  const m = text.match(/https?:\/\/[^\s<>"')]+/i)
@@ -42,6 +42,7 @@ function normalizeDelegateOrder(value: unknown): DelegateTool[] {
42
42
  'delegate_to_opencode_cli',
43
43
  'delegate_to_gemini_cli',
44
44
  'delegate_to_copilot_cli',
45
+ 'delegate_to_droid_cli',
45
46
  'delegate_to_cursor_cli',
46
47
  'delegate_to_qwen_code_cli',
47
48
  ]
@@ -54,6 +55,7 @@ function normalizeDelegateOrder(value: unknown): DelegateTool[] {
54
55
  else if (raw === 'opencode') mapped.push('delegate_to_opencode_cli')
55
56
  else if (raw === 'gemini') mapped.push('delegate_to_gemini_cli')
56
57
  else if (raw === 'copilot') mapped.push('delegate_to_copilot_cli')
58
+ else if (raw === 'droid') mapped.push('delegate_to_droid_cli')
57
59
  else if (raw === 'cursor') mapped.push('delegate_to_cursor_cli')
58
60
  else if (raw === 'qwen') mapped.push('delegate_to_qwen_code_cli')
59
61
  }
@@ -31,6 +31,7 @@ export type DelegateTool =
31
31
  | 'delegate_to_opencode_cli'
32
32
  | 'delegate_to_gemini_cli'
33
33
  | 'delegate_to_copilot_cli'
34
+ | 'delegate_to_droid_cli'
34
35
  | 'delegate_to_cursor_cli'
35
36
  | 'delegate_to_qwen_code_cli'
36
37
 
@@ -138,6 +139,12 @@ export function translateRequestedToolInvocation(
138
139
  if (requestedName === 'delegate_to_gemini_cli') {
139
140
  return { toolName: 'delegate', args: { ...rawArgs, backend: 'gemini' } }
140
141
  }
142
+ if (requestedName === 'delegate_to_copilot_cli') {
143
+ return { toolName: 'delegate', args: { ...rawArgs, backend: 'copilot' } }
144
+ }
145
+ if (requestedName === 'delegate_to_droid_cli') {
146
+ return { toolName: 'delegate', args: { ...rawArgs, backend: 'droid' } }
147
+ }
141
148
  if (requestedName === 'delegate_to_cursor_cli') {
142
149
  return { toolName: 'delegate', args: { ...rawArgs, backend: 'cursor' } }
143
150
  }
@@ -306,6 +313,8 @@ export function requestedToolNamesFromMessage(message: string): string[] {
306
313
  'delegate_to_codex_cli',
307
314
  'delegate_to_opencode_cli',
308
315
  'delegate_to_gemini_cli',
316
+ 'delegate_to_copilot_cli',
317
+ 'delegate_to_droid_cli',
309
318
  'delegate_to_cursor_cli',
310
319
  'delegate_to_qwen_code_cli',
311
320
  'connector_message_tool',
@@ -372,6 +381,8 @@ export function enabledDelegationTools(session: SessionWithTools): DelegateTool[
372
381
  if (hasToolEnabled(session, 'codex_cli')) tools.push('delegate_to_codex_cli')
373
382
  if (hasToolEnabled(session, 'opencode_cli')) tools.push('delegate_to_opencode_cli')
374
383
  if (hasToolEnabled(session, 'gemini_cli')) tools.push('delegate_to_gemini_cli')
384
+ if (hasToolEnabled(session, 'copilot_cli')) tools.push('delegate_to_copilot_cli')
385
+ if (hasToolEnabled(session, 'droid_cli')) tools.push('delegate_to_droid_cli')
375
386
  if (hasToolEnabled(session, 'cursor_cli')) tools.push('delegate_to_cursor_cli')
376
387
  if (hasToolEnabled(session, 'qwen_code_cli')) tools.push('delegate_to_qwen_code_cli')
377
388
  return tools