@swarmclawai/swarmclaw 1.3.0 → 1.3.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.
package/README.md CHANGED
@@ -204,6 +204,19 @@ Read the full setup guide in [`SWARMDOCK.md`](./SWARMDOCK.md), browse the public
204
204
 
205
205
  ## Release Notes
206
206
 
207
+ ### v1.3.2 Highlights
208
+
209
+ - **Custom provider fix for standalone builds**: fixed `require('@/lib/server/storage')` path alias resolution failure that caused custom providers to silently break in standalone/npm-global installs with "a is not a function" errors. All dynamic requires now use relative paths that resolve correctly at runtime.
210
+ - **GitHub Copilot CLI provider**: new CLI provider wrapping the `copilot` binary with JSONL streaming, session continuity, system prompt injection, and multi-model support (Claude, GPT, Gemini via GitHub Copilot subscription).
211
+
212
+ ### v1.3.1 Highlights
213
+
214
+ - **SwarmDock SDK v0.2.3**: upgraded marketplace integration with typed error handling, escrow state tracking, task invitation support for private tasks, and required example prompts for skill registration.
215
+ - **SDK error resilience**: registration now gracefully handles already-registered agents by falling back to authentication; heartbeat catches expired tokens and re-authenticates automatically.
216
+ - **Escrow event tracking**: new `escrow.releasing`, `escrow.refunding`, `escrow.release_failed`, and `escrow.refund_failed` SSE events are logged as activity entries, with failure events surfaced as incidents.
217
+ - **Private task invitations**: when a SwarmDock task invites this agent directly, auto-discovery now evaluates it alongside public `task.created` events.
218
+ - **SDK type imports**: replaced inlined SwarmDock type stubs with proper imports from `@swarmdock/shared`, eliminating type drift.
219
+
207
220
  ### v1.3.0 Highlights
208
221
 
209
222
  - **SwarmDock SDK v0.2.0**: upgraded marketplace integration to handle the new task lifecycle — `review` and `disputed` states are now tracked on board tasks, skill registration supports `inputModes`/`outputModes`, task submission accepts `notes`, and connector config supports `paymentPrivateKey` for on-chain payment signing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Self-hosted AI runtime for OpenClaw, delegation, autonomy, runtime skills, crypto wallets, and chat platform connectors.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -87,7 +87,7 @@
87
87
  "@multiavatar/multiavatar": "^1.0.7",
88
88
  "@playwright/mcp": "^0.0.68",
89
89
  "@slack/bolt": "^4.6.0",
90
- "@swarmdock/sdk": "^0.2.0",
90
+ "@swarmdock/sdk": "^0.2.3",
91
91
  "@tailwindcss/postcss": "^4",
92
92
  "@tanstack/react-query": "^5.91.0",
93
93
  "@types/better-sqlite3": "^7.6.13",
@@ -1,8 +1,8 @@
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'])
2
+ export const NON_LANGGRAPH_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli'])
3
3
 
4
4
  /** Providers with native tool/capability support (CLI providers + OpenClaw). */
5
- export const NATIVE_CAPABILITY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'openclaw'])
5
+ export const NATIVE_CAPABILITY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'openclaw'])
6
6
 
7
7
  /** Providers that can only act as workers — no coordinator role, no heartbeat, no advanced settings. */
8
- export const WORKER_ONLY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'openclaw'])
8
+ export const WORKER_ONLY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'openclaw'])
@@ -39,6 +39,12 @@ const KNOWN_BINARY_PATHS: Record<string, string[]> = {
39
39
  '/usr/local/bin/gemini',
40
40
  '/opt/homebrew/bin/gemini',
41
41
  ],
42
+ copilot: [
43
+ path.join(os.homedir(), '.local/bin/copilot'),
44
+ '/usr/local/bin/copilot',
45
+ '/opt/homebrew/bin/copilot',
46
+ path.join(os.homedir(), '.npm-global/bin/copilot'),
47
+ ],
42
48
  }
43
49
 
44
50
  function getNvmBinaryPaths(name: string): string[] {
@@ -144,7 +150,7 @@ export interface AuthProbeResult {
144
150
  */
145
151
  export function probeCliAuth(
146
152
  binary: string,
147
- backend: 'claude' | 'codex' | 'opencode' | 'gemini',
153
+ backend: 'claude' | 'codex' | 'opencode' | 'gemini' | 'copilot',
148
154
  env: NodeJS.ProcessEnv,
149
155
  cwd?: string,
150
156
  ): AuthProbeResult {
@@ -224,6 +230,37 @@ export function probeCliAuth(
224
230
  return { authenticated: true }
225
231
  }
226
232
 
233
+ if (backend === 'copilot') {
234
+ // Check for GitHub token in env first
235
+ if (process.env.GH_TOKEN || process.env.GITHUB_TOKEN || process.env.COPILOT_GITHUB_TOKEN) {
236
+ return { authenticated: true }
237
+ }
238
+ // Try `gh auth status` as fallback (copilot inherits gh auth)
239
+ try {
240
+ const probe = spawnSync('gh', ['auth', 'status'], {
241
+ cwd, env, encoding: 'utf-8', timeout: 8000,
242
+ })
243
+ const probeText = `${probe.stdout || ''}\n${probe.stderr || ''}`.toLowerCase()
244
+ if ((probe.status ?? 1) === 0 || probeText.includes('logged in')) {
245
+ return { authenticated: true }
246
+ }
247
+ } catch { /* gh may not be installed */ }
248
+
249
+ // Fall back to config file check
250
+ const configPaths = [
251
+ path.join(os.homedir(), '.copilot/config.json'),
252
+ path.join(os.homedir(), '.config/copilot/config.json'),
253
+ ]
254
+ const hasConfig = configPaths.some((p) => fs.existsSync(p))
255
+ if (!hasConfig) {
256
+ return {
257
+ authenticated: false,
258
+ errorMessage: 'Copilot CLI is not authenticated. Run `copilot /login`, `gh auth login`, or set GH_TOKEN and try again.',
259
+ }
260
+ }
261
+ return { authenticated: true }
262
+ }
263
+
227
264
  return { authenticated: true }
228
265
  }
229
266
 
@@ -308,6 +345,7 @@ export const CLI_PROVIDER_CAPABILITIES: Record<string, string> = {
308
345
  'codex-cli': 'code generation, file creation, automated coding tasks',
309
346
  'opencode-cli': 'code analysis, generation across multiple LLM backends',
310
347
  'gemini-cli': 'code generation, analysis with Gemini models',
348
+ 'copilot-cli': 'code generation, analysis, multi-model support via GitHub Copilot',
311
349
  }
312
350
 
313
351
  /** Check if a provider ID is a CLI-based provider. */
@@ -0,0 +1,222 @@
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
+ * GitHub Copilot CLI provider — spawns `copilot -p <message> --output-format=json -s --yolo`.
12
+ * Tracks `session.copilotSessionId` from streamed JSON events to support multi-turn continuity.
13
+ */
14
+ export function streamCopilotCliChat({ session, message, imagePath, systemPrompt, write, active, signal }: StreamChatOptions): Promise<string> {
15
+ const processTimeoutMs = loadRuntimeSettings().cliProcessTimeoutMs
16
+ const binary = resolveCliBinary('copilot')
17
+ if (!binary) {
18
+ const msg = 'Copilot CLI not found. Install it (brew install copilot-cli, npm i -g @github/copilot, or https://gh.io/copilot-install) 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
+ // Pass GitHub token if available via session API key
26
+ if (session.apiKey) {
27
+ env.GH_TOKEN = session.apiKey
28
+ }
29
+
30
+ // Auth probe
31
+ if (!session.apiKey) {
32
+ const auth = probeCliAuth(binary, 'copilot', env, session.cwd)
33
+ if (!auth.authenticated) {
34
+ log.error('copilot-cli', auth.errorMessage || 'Auth failed')
35
+ write(`data: ${JSON.stringify({ t: 'err', text: auth.errorMessage || 'Copilot CLI is not authenticated.' })}\n\n`)
36
+ return Promise.resolve('')
37
+ }
38
+ }
39
+
40
+ // Build prompt with optional system instructions
41
+ const promptParts: string[] = []
42
+ if (imagePath) {
43
+ promptParts.push(`[The user has shared an image at: ${imagePath}]`)
44
+ }
45
+ promptParts.push(message)
46
+ const prompt = promptParts.join('\n\n')
47
+
48
+ const args = ['-p', prompt, '--output-format=json', '-s', '--yolo']
49
+ if (session.copilotSessionId) args.push('--resume', session.copilotSessionId)
50
+ if (session.model) args.push('--model', session.model)
51
+
52
+ // System prompt: write temp AGENTS.override.md in a temp config dir
53
+ // Symlink auth files from the real config dir so auth still works
54
+ let tempCopilotHome: string | null = null
55
+ if (systemPrompt && !session.copilotSessionId) {
56
+ const realCopilotHome = process.env.COPILOT_HOME || path.join(os.homedir(), '.copilot')
57
+ tempCopilotHome = path.join(os.tmpdir(), `swarmclaw-copilot-${session.id}`)
58
+ fs.mkdirSync(tempCopilotHome, { recursive: true })
59
+
60
+ // Symlink auth/config files from real home into temp dir
61
+ symlinkConfigFiles(realCopilotHome, tempCopilotHome)
62
+
63
+ // Write system prompt as AGENTS.override.md
64
+ fs.writeFileSync(path.join(tempCopilotHome, 'AGENTS.override.md'), systemPrompt)
65
+ env.COPILOT_HOME = tempCopilotHome
66
+ }
67
+
68
+ log.info('copilot-cli', `Spawning: ${binary}`, {
69
+ args: args.map((a) => a.length > 100 ? a.slice(0, 100) + '...' : a),
70
+ cwd: session.cwd,
71
+ promptLen: prompt.length,
72
+ hasSystemPrompt: !!systemPrompt,
73
+ resumeSessionId: session.copilotSessionId || null,
74
+ })
75
+
76
+ const proc = spawn(binary, args, {
77
+ cwd: session.cwd,
78
+ env,
79
+ stdio: ['ignore', 'pipe', 'pipe'],
80
+ timeout: processTimeoutMs,
81
+ })
82
+
83
+ log.info('copilot-cli', `Process spawned: pid=${proc.pid}`)
84
+ active.set(session.id, proc)
85
+ attachAbortHandler(proc, signal)
86
+
87
+ let fullResponse = ''
88
+ let buf = ''
89
+ let eventCount = 0
90
+ let stderrText = ''
91
+
92
+ proc.stdout!.on('data', (chunk: Buffer) => {
93
+ const raw = chunk.toString()
94
+ buf += raw
95
+
96
+ if (eventCount === 0) {
97
+ log.debug('copilot-cli', `First stdout chunk (${raw.length} bytes)`, raw.slice(0, 500))
98
+ }
99
+
100
+ const lines = buf.split('\n')
101
+ buf = lines.pop()!
102
+
103
+ for (const line of lines) {
104
+ if (!line.trim()) continue
105
+ try {
106
+ const ev = JSON.parse(line) as Record<string, unknown>
107
+ eventCount++
108
+
109
+ // Capture session ID from init event
110
+ if (ev.type === 'init' && typeof ev.session_id === 'string') {
111
+ session.copilotSessionId = ev.session_id
112
+ log.info('copilot-cli', `Got session_id: ${ev.session_id}`)
113
+ }
114
+
115
+ // Streaming text deltas
116
+ if (ev.type === 'content_block_delta') {
117
+ const delta = ev.delta as Record<string, unknown> | undefined
118
+ if (typeof delta?.text === 'string') {
119
+ fullResponse += delta.text
120
+ write(`data: ${JSON.stringify({ t: 'd', text: delta.text })}\n\n`)
121
+ }
122
+ }
123
+
124
+ // Agent message chunks (ACP format)
125
+ else if (ev.type === 'agent_message_chunk' && typeof ev.text === 'string') {
126
+ fullResponse += ev.text
127
+ write(`data: ${JSON.stringify({ t: 'd', text: ev.text })}\n\n`)
128
+ }
129
+
130
+ // Assistant message content
131
+ else if (ev.type === 'message' && ev.role === 'assistant' && typeof ev.content === 'string') {
132
+ fullResponse += ev.content
133
+ write(`data: ${JSON.stringify({ t: 'd', text: ev.content })}\n\n`)
134
+ }
135
+
136
+ // Completed item with agent_message
137
+ else if (ev.type === 'item.completed' && (ev.item as Record<string, unknown>)?.type === 'agent_message') {
138
+ const item = ev.item as Record<string, unknown>
139
+ if (typeof item.text === 'string') {
140
+ fullResponse = item.text
141
+ write(`data: ${JSON.stringify({ t: 'r', text: item.text })}\n\n`)
142
+ log.debug('copilot-cli', `Agent message (${item.text.length} chars)`)
143
+ }
144
+ }
145
+
146
+ // Final result
147
+ else if (ev.type === 'result' && typeof ev.result === 'string') {
148
+ fullResponse = ev.result
149
+ write(`data: ${JSON.stringify({ t: 'r', text: ev.result })}\n\n`)
150
+ log.debug('copilot-cli', `Result event (${ev.result.length} chars)`)
151
+ }
152
+
153
+ // Error result
154
+ else if (ev.type === 'result' && ev.status === 'error') {
155
+ const errMsg = typeof ev.error === 'string' ? ev.error : 'Copilot error'
156
+ write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
157
+ log.warn('copilot-cli', `Error result: ${errMsg}`)
158
+ }
159
+
160
+ // Event error
161
+ else if (ev.type === 'error') {
162
+ const errMsg = typeof ev.message === 'string'
163
+ ? ev.message
164
+ : typeof ev.error === 'string'
165
+ ? ev.error
166
+ : 'Unknown Copilot error'
167
+ write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
168
+ log.warn('copilot-cli', `Event error: ${errMsg}`)
169
+ }
170
+
171
+ else if (eventCount <= 10) {
172
+ log.debug('copilot-cli', `Event: ${String(ev.type)}`)
173
+ }
174
+ } catch {
175
+ if (line.trim()) {
176
+ log.debug('copilot-cli', `Non-JSON stdout line`, line.slice(0, 300))
177
+ fullResponse += line + '\n'
178
+ write(`data: ${JSON.stringify({ t: 'd', text: line + '\n' })}\n\n`)
179
+ }
180
+ }
181
+ }
182
+ })
183
+
184
+ proc.stderr!.on('data', (chunk: Buffer) => {
185
+ const text = chunk.toString()
186
+ stderrText += text
187
+ if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
188
+ if (isStderrNoise(text)) {
189
+ log.debug('copilot-cli', `stderr noise [${session.id}]`, text.slice(0, 500))
190
+ } else {
191
+ log.warn('copilot-cli', `stderr [${session.id}]`, text.slice(0, 500))
192
+ }
193
+ })
194
+
195
+ return new Promise((resolve) => {
196
+ proc.on('close', (code, sig) => {
197
+ log.info('copilot-cli', `Process closed: code=${code} signal=${sig} events=${eventCount} response=${fullResponse.length}chars`)
198
+ active.delete(session.id)
199
+ // Clean up temp config dir
200
+ if (tempCopilotHome) {
201
+ try { fs.rmSync(tempCopilotHome, { recursive: true }) } catch { /* ignore */ }
202
+ }
203
+ if ((code ?? 0) !== 0 && !fullResponse.trim()) {
204
+ const msg = stderrText.trim()
205
+ ? `Copilot CLI exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''}: ${stderrText.trim().slice(0, 1200)}`
206
+ : `Copilot CLI exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''} and returned no output.`
207
+ write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
208
+ }
209
+ resolve(fullResponse)
210
+ })
211
+
212
+ proc.on('error', (e) => {
213
+ log.error('copilot-cli', `Process error: ${e.message}`)
214
+ active.delete(session.id)
215
+ if (tempCopilotHome) {
216
+ try { fs.rmSync(tempCopilotHome, { recursive: true }) } catch { /* ignore */ }
217
+ }
218
+ write(`data: ${JSON.stringify({ t: 'err', text: e.message })}\n\n`)
219
+ resolve(fullResponse)
220
+ })
221
+ })
222
+ }
@@ -2,6 +2,7 @@ import { streamClaudeCliChat } from './claude-cli'
2
2
  import { streamCodexCliChat } from './codex-cli'
3
3
  import { streamOpenCodeCliChat } from './opencode-cli'
4
4
  import { streamGeminiCliChat } from './gemini-cli'
5
+ import { streamCopilotCliChat } from './copilot-cli'
5
6
  import { streamOpenAiChat } from './openai'
6
7
  import { streamOllamaChat } from './ollama'
7
8
  import { streamAnthropicChat } from './anthropic'
@@ -102,6 +103,14 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
102
103
  requiresEndpoint: false,
103
104
  handler: { streamChat: streamGeminiCliChat },
104
105
  },
106
+ 'copilot-cli': {
107
+ id: 'copilot-cli',
108
+ name: 'GitHub Copilot CLI',
109
+ models: ['claude-sonnet-4-5', 'gpt-4.1', 'gemini-3-pro'],
110
+ requiresApiKey: false,
111
+ requiresEndpoint: false,
112
+ handler: { streamChat: streamCopilotCliChat },
113
+ },
105
114
  google: {
106
115
  id: 'google',
107
116
  name: 'Google Gemini',
@@ -281,7 +290,7 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
281
290
  function getCustomProviders(): Record<string, CustomProviderConfig> {
282
291
  try {
283
292
  // eslint-disable-next-line @typescript-eslint/no-require-imports
284
- const { loadProviderConfigs } = require('@/lib/server/storage') as typeof import('@/lib/server/storage')
293
+ const { loadProviderConfigs } = require('../server/storage') as typeof import('@/lib/server/storage')
285
294
  const configs = loadProviderConfigs() as Record<string, CustomProviderConfig>
286
295
  return Object.fromEntries(
287
296
  Object.entries(configs).filter(([, config]) => config?.type === 'custom'),
@@ -295,7 +304,7 @@ function getCustomProviders(): Record<string, CustomProviderConfig> {
295
304
  function getModelOverrides(): Record<string, string[]> {
296
305
  try {
297
306
  // eslint-disable-next-line @typescript-eslint/no-require-imports
298
- const { loadModelOverrides } = require('@/lib/server/storage') as typeof import('@/lib/server/storage')
307
+ const { loadModelOverrides } = require('../server/storage') as typeof import('@/lib/server/storage')
299
308
  return loadModelOverrides()
300
309
  } catch {
301
310
  return {}
@@ -313,7 +322,7 @@ export function getProviderList(): ProviderInfo[] {
313
322
  ...info,
314
323
  models: overrides[info.id] || info.models,
315
324
  defaultModels: info.models,
316
- supportsModelDiscovery: !['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'fireworks'].includes(info.id),
325
+ supportsModelDiscovery: !['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'fireworks'].includes(info.id),
317
326
  }
318
327
  })
319
328
 
@@ -383,7 +392,7 @@ export function getProvider(id: string): BuiltinProviderConfig | null {
383
392
  if (id.startsWith('custom-') && !custom) {
384
393
  try {
385
394
  // eslint-disable-next-line @typescript-eslint/no-require-imports
386
- const { loadStoredItem } = require('@/lib/server/storage') as typeof import('@/lib/server/storage')
395
+ const { loadStoredItem } = require('../server/storage') as typeof import('@/lib/server/storage')
387
396
  const directConfig = loadStoredItem('provider_configs', id) as CustomProviderConfig | null
388
397
  if (directConfig?.type === 'custom' && directConfig.isEnabled) {
389
398
  log.info(TAG, `Resolved custom provider '${id}' via direct DB lookup (batch load missed it)`)
@@ -447,7 +456,7 @@ export async function streamChatWithFailover(
447
456
  if (credId && i > 0) {
448
457
  // Need to decrypt fallback credential
449
458
  // eslint-disable-next-line @typescript-eslint/no-require-imports
450
- const { loadCredentials, decryptKey } = require('@/lib/server/storage') as typeof import('@/lib/server/storage')
459
+ const { loadCredentials, decryptKey } = require('../server/storage') as typeof import('@/lib/server/storage')
451
460
  const creds = loadCredentials()
452
461
  const cred = creds[credId]
453
462
  if (cred?.encryptedKey) {
@@ -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', 'openclaw'] satisfies ProviderType[]
7
+ const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-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'> {
@@ -160,7 +160,7 @@ function notifySwarmChanged() {
160
160
  function persistSwarmSnapshot(swarm: SwarmHandle): void {
161
161
  try {
162
162
  // eslint-disable-next-line @typescript-eslint/no-require-imports
163
- const { upsertStoredItem } = require('@/lib/server/storage')
163
+ const { upsertStoredItem } = require('../storage')
164
164
  upsertStoredItem('swarm_snapshots', swarm.swarmId, {
165
165
  swarmId: swarm.swarmId,
166
166
  parentSessionId: swarm.parentSessionId,
@@ -550,7 +550,7 @@ export function getSwarmSnapshot(swarmId: string): SwarmSnapshot | null {
550
550
  // Fallback to persisted store for swarms from previous process lifetimes
551
551
  try {
552
552
  // eslint-disable-next-line @typescript-eslint/no-require-imports
553
- const { loadStoredItem } = require('@/lib/server/storage')
553
+ const { loadStoredItem } = require('../storage')
554
554
  const persisted = loadStoredItem('swarm_snapshots', swarmId)
555
555
  return persisted ? (persisted as SwarmSnapshot) : null
556
556
  } catch { return null }
@@ -641,7 +641,7 @@ function buildSwarmSnapshot(swarm: SwarmHandle): SwarmSnapshot {
641
641
  export function restoreSwarmRegistry(): number {
642
642
  try {
643
643
  // eslint-disable-next-line @typescript-eslint/no-require-imports
644
- const { loadCollection, upsertStoredItem } = require('@/lib/server/storage')
644
+ const { loadCollection, upsertStoredItem } = require('../storage')
645
645
  const persisted = loadCollection('swarm_snapshots') as Record<string, SwarmSnapshot>
646
646
  let lost = 0
647
647
  for (const [id, record] of Object.entries(persisted)) {
@@ -45,9 +45,9 @@ function buildExtensionCapabilityLines(enabledExtensions: string[], opts?: { del
45
45
  if (opts.agentId) {
46
46
  try {
47
47
  // eslint-disable-next-line @typescript-eslint/no-require-imports
48
- const { loadAgents } = require('@/lib/server/storage')
48
+ const { loadAgents } = require('../storage')
49
49
  // eslint-disable-next-line @typescript-eslint/no-require-imports
50
- const { resolveTeam } = require('@/lib/server/agents/team-resolution')
50
+ const { resolveTeam } = require('../agents/team-resolution')
51
51
  const agents = loadAgents() as Record<string, Record<string, unknown>>
52
52
  const team = resolveTeam(opts.agentId, agents)
53
53
  if (team.mode === 'team') {
@@ -1,15 +1,8 @@
1
1
  import { log } from '@/lib/server/logger'
2
- import type { BidCreateInput } from '@swarmdock/shared'
2
+ import type { Task, BidCreateInput } from '@swarmdock/shared'
3
3
 
4
4
  const TAG = 'swarmdock-bid'
5
5
 
6
- interface SwarmDockTask {
7
- id: string
8
- title: string
9
- skillRequirements: string[]
10
- budgetMax: string
11
- }
12
-
13
6
  interface SwarmDockConfig {
14
7
  skills: string
15
8
  maxBudget: string
@@ -20,7 +13,7 @@ interface SwarmDockConfig {
20
13
  * Determine if the agent should auto-bid on a discovered task.
21
14
  * Checks skill overlap and budget limits.
22
15
  */
23
- export function shouldAutoBid(task: SwarmDockTask, config: SwarmDockConfig): boolean {
16
+ export function shouldAutoBid(task: Task, config: SwarmDockConfig): boolean {
24
17
  if (!config.autoDiscover) return false
25
18
 
26
19
  // Check budget
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
2
2
  import test from 'node:test'
3
3
 
4
4
  import { submitAutoBid } from '@/lib/server/connectors/swarmdock-bidding'
5
- import { submitSwarmdockTaskResult } from '@/lib/server/connectors/swarmdock'
5
+ import { submitSwarmdockTaskResult, generateExamplePrompts } from '@/lib/server/connectors/swarmdock'
6
6
 
7
7
  test('submitAutoBid includes empty portfolio refs for SDK compatibility', async () => {
8
8
  const seen: {
@@ -37,6 +37,23 @@ test('submitAutoBid includes empty portfolio refs for SDK compatibility', async
37
37
  })
38
38
  })
39
39
 
40
+ test('generateExamplePrompts returns exactly 5 non-empty strings', () => {
41
+ const prompts = generateExamplePrompts('data-analysis')
42
+ assert.equal(prompts.length, 5)
43
+ for (const prompt of prompts) {
44
+ assert.equal(typeof prompt, 'string')
45
+ assert.ok(prompt.length > 0, 'prompt must be non-empty')
46
+ assert.ok(prompt.includes('data analysis'), 'prompt should include the humanized skill name')
47
+ }
48
+
49
+ // Single-word skill
50
+ const simple = generateExamplePrompts('coding')
51
+ assert.equal(simple.length, 5)
52
+ for (const prompt of simple) {
53
+ assert.ok(prompt.includes('coding'))
54
+ }
55
+ })
56
+
40
57
  test('submitSwarmdockTaskResult includes empty files and propagates submit errors', async () => {
41
58
  const seen: {
42
59
  taskId?: string
@@ -2,23 +2,14 @@ import { genId } from '@/lib/id'
2
2
  import { loadTasks, saveTasks } from '@/lib/server/tasks/task-repository'
3
3
  import { logActivity } from '@/lib/server/activity/activity-log'
4
4
  import type { BoardTask } from '@/types/task'
5
-
6
- interface SwarmDockTask {
7
- id: string
8
- requesterId: string
9
- title: string
10
- description: string
11
- skillRequirements: string[]
12
- budgetMax: string
13
- deadline: string | null
14
- }
5
+ import type { Task } from '@swarmdock/shared'
15
6
 
16
7
  /**
17
8
  * Create a SwarmClaw BoardTask from a SwarmDock task assignment.
18
9
  * Uses `externalSource` to link back to the SwarmDock task (same pattern as GitHub issue import).
19
10
  */
20
11
  export async function createBoardTaskFromAssignment(
21
- task: SwarmDockTask,
12
+ task: Task,
22
13
  agentId: string,
23
14
  connectorId: string,
24
15
  apiUrl: string,
@@ -5,29 +5,10 @@ import type { Connector, InboundMessage } from '@/types/connector'
5
5
  import type { PlatformConnector, ConnectorInstance } from '@/lib/server/connectors/types'
6
6
  import { createBoardTaskFromAssignment, updateBoardTaskFromEvent, findBoardTaskBySwarmdockId } from './swarmdock-tasks'
7
7
  import { shouldAutoBid, submitAutoBid } from './swarmdock-bidding'
8
- import type { TaskSubmitInput } from '@swarmdock/shared'
8
+ import type { Task, SSEEvent, TaskSubmitInput } from '@swarmdock/shared'
9
9
 
10
10
  const TAG = 'swarmdock'
11
11
 
12
- // SDK types inlined until @swarmdock/sdk is built and linked
13
- interface SwarmDockTask {
14
- id: string
15
- requesterId: string
16
- assigneeId: string | null
17
- title: string
18
- description: string
19
- skillRequirements: string[]
20
- budgetMax: string
21
- status: string
22
- deadline: string | null
23
- }
24
-
25
- interface SwarmDockSSEEvent {
26
- type: string
27
- data: Record<string, unknown>
28
- timestamp: string
29
- }
30
-
31
12
  interface SwarmDockConfig {
32
13
  apiUrl: string
33
14
  walletAddress: string
@@ -51,7 +32,7 @@ function parseConfig(connector: Connector): SwarmDockConfig {
51
32
  }
52
33
  }
53
34
 
54
- function buildTaskPrompt(task: SwarmDockTask): string {
35
+ function buildTaskPrompt(task: Task): string {
55
36
  const lines: string[] = [
56
37
  `# SwarmDock Task: ${task.title}`,
57
38
  '',
@@ -65,6 +46,17 @@ function buildTaskPrompt(task: SwarmDockTask): string {
65
46
  return lines.join('\n')
66
47
  }
67
48
 
49
+ export function generateExamplePrompts(skillId: string): string[] {
50
+ const name = skillId.replace(/-/g, ' ')
51
+ return [
52
+ `Perform a ${name} task`,
53
+ `Help me with ${name}`,
54
+ `I need ${name} work done`,
55
+ `Complete a ${name} assignment`,
56
+ `Handle a ${name} request`,
57
+ ]
58
+ }
59
+
68
60
  function formatUsdc(microUnits: string): string {
69
61
  const cents = BigInt(microUnits)
70
62
  const dollars = Number(cents) / 1_000_000
@@ -98,11 +90,15 @@ const swarmdock: PlatformConnector = {
98
90
  if (!privateKey) throw new Error('SwarmDock connector requires an Ed25519 private key credential')
99
91
  if (!config.walletAddress) throw new Error('SwarmDock connector requires a Base L2 wallet address in config')
100
92
 
101
- // Dynamic import of the SDK (must be built and linked first)
93
+ // Dynamic import of the SDK
102
94
  let SwarmDockClient: typeof import('@swarmdock/sdk').SwarmDockClient
95
+ let ConflictError: typeof import('@swarmdock/sdk').ConflictError
96
+ let AuthenticationError: typeof import('@swarmdock/sdk').AuthenticationError
103
97
  try {
104
98
  const sdk = await import('@swarmdock/sdk')
105
99
  SwarmDockClient = sdk.SwarmDockClient
100
+ ConflictError = sdk.ConflictError
101
+ AuthenticationError = sdk.AuthenticationError
106
102
  } catch {
107
103
  throw new Error('SwarmDock SDK (@swarmdock/sdk) is not installed. Run: npm install @swarmdock/sdk')
108
104
  }
@@ -128,36 +124,47 @@ const swarmdock: PlatformConnector = {
128
124
  basePrice: '1000000', // $1.00 default
129
125
  inputModes: ['text'],
130
126
  outputModes: ['text'],
127
+ examplePrompts: generateExamplePrompts(skillId),
131
128
  }))
132
129
 
133
130
  log.info(TAG, `Registering agent "${connector.name}" on SwarmDock at ${config.apiUrl}`)
134
- const registration = await client.register({
135
- displayName: connector.name,
136
- description: config.agentDescription,
137
- framework: 'swarmclaw',
138
- walletAddress: config.walletAddress,
139
- skills: skillList,
140
- })
141
- log.info(TAG, `Registered as ${registration.agent.did} (trust level ${registration.agent.trustLevel})`)
142
-
143
- logActivity({
144
- entityType: 'connector',
145
- entityId: connectorId,
146
- action: 'swarmdock-registered',
147
- actor: 'system',
148
- summary: `Agent "${connector.name}" registered on SwarmDock as ${registration.agent.did}`,
149
- })
131
+ try {
132
+ const registration = await client.register({
133
+ displayName: connector.name,
134
+ description: config.agentDescription,
135
+ framework: 'swarmclaw',
136
+ walletAddress: config.walletAddress,
137
+ skills: skillList,
138
+ })
139
+ log.info(TAG, `Registered as ${registration.agent.did} (trust level ${registration.agent.trustLevel})`)
140
+
141
+ logActivity({
142
+ entityType: 'connector',
143
+ entityId: connectorId,
144
+ action: 'swarmdock-registered',
145
+ actor: 'system',
146
+ summary: `Agent "${connector.name}" registered on SwarmDock as ${registration.agent.did}`,
147
+ })
148
+ } catch (err) {
149
+ if (err instanceof ConflictError) {
150
+ log.info(TAG, `Agent already registered, authenticating`)
151
+ await client.authenticate()
152
+ } else {
153
+ throw err
154
+ }
155
+ }
150
156
 
151
157
  // Set up SSE event stream
152
158
  let alive = true
153
159
 
154
- const handleSSEEvent = async (event: SwarmDockSSEEvent) => {
160
+ const handleSSEEvent = async (event: SSEEvent) => {
155
161
  if (!alive) return
156
162
  try {
157
163
  switch (event.type) {
158
- case 'task.created': {
164
+ case 'task.created':
165
+ case 'task.invited': {
159
166
  if (!config.autoDiscover) break
160
- const task = event.data as unknown as SwarmDockTask
167
+ const task = event.data as unknown as Task
161
168
  if (shouldAutoBid(task, config)) {
162
169
  await submitAutoBid(client, task.id, config)
163
170
  logActivity({
@@ -172,7 +179,7 @@ const swarmdock: PlatformConnector = {
172
179
  }
173
180
 
174
181
  case 'task.assigned': {
175
- const task = event.data as unknown as SwarmDockTask
182
+ const task = event.data as unknown as Task
176
183
  if (!task.assigneeId) break
177
184
 
178
185
  // Signal work started on SwarmDock
@@ -236,6 +243,32 @@ const swarmdock: PlatformConnector = {
236
243
  })
237
244
  break
238
245
  }
246
+
247
+ case 'escrow.releasing':
248
+ case 'escrow.refunding': {
249
+ const data = event.data as Record<string, string>
250
+ logActivity({
251
+ entityType: 'connector',
252
+ entityId: connectorId,
253
+ action: 'swarmdock-escrow',
254
+ actor: 'system',
255
+ summary: `Escrow ${event.type.split('.')[1]} for task ${data.taskId}`,
256
+ })
257
+ break
258
+ }
259
+
260
+ case 'escrow.release_failed':
261
+ case 'escrow.refund_failed': {
262
+ const data = event.data as Record<string, string>
263
+ logActivity({
264
+ entityType: 'connector',
265
+ entityId: connectorId,
266
+ action: 'incident',
267
+ actor: 'system',
268
+ summary: `Escrow ${event.type.replace('escrow.', '')} for task ${data.taskId}`,
269
+ })
270
+ break
271
+ }
239
272
  }
240
273
  } catch (err) {
241
274
  log.error(TAG, `Error handling SSE event ${event.type}: ${err instanceof Error ? err.message : String(err)}`)
@@ -250,7 +283,12 @@ const swarmdock: PlatformConnector = {
250
283
  await client.heartbeat()
251
284
  log.debug(TAG, 'SwarmDock token refreshed')
252
285
  } catch (err) {
253
- log.error(TAG, `SwarmDock heartbeat failed: ${err instanceof Error ? err.message : String(err)}`)
286
+ if (err instanceof AuthenticationError) {
287
+ log.warn(TAG, 'SwarmDock token expired, re-authenticating')
288
+ try { await client.authenticate() } catch {}
289
+ } else {
290
+ log.error(TAG, `SwarmDock heartbeat failed: ${err instanceof Error ? err.message : String(err)}`)
291
+ }
254
292
  }
255
293
  }, 23 * 60 * 60 * 1000)
256
294
 
@@ -71,7 +71,7 @@ export function markProviderFailure(providerId: string, error: string, credentia
71
71
  })
72
72
  try {
73
73
  // eslint-disable-next-line @typescript-eslint/no-require-imports
74
- const { upsertStoredItem } = require('@/lib/server/storage')
74
+ const { upsertStoredItem } = require('./storage')
75
75
  upsertStoredItem('provider_health', key, states.get(key)!)
76
76
  } catch {}
77
77
  }
@@ -89,7 +89,7 @@ export function markProviderSuccess(providerId: string, credentialId?: string |
89
89
  })
90
90
  try {
91
91
  // eslint-disable-next-line @typescript-eslint/no-require-imports
92
- const { upsertStoredItem } = require('@/lib/server/storage')
92
+ const { upsertStoredItem } = require('./storage')
93
93
  upsertStoredItem('provider_health', key, states.get(key)!)
94
94
  } catch {}
95
95
  }
@@ -188,7 +188,7 @@ export function getProviderHealthSnapshot(): Record<string, ProviderHealthState
188
188
  export function restoreProviderHealthState(): number {
189
189
  try {
190
190
  // eslint-disable-next-line @typescript-eslint/no-require-imports
191
- const { loadCollection } = require('@/lib/server/storage')
191
+ const { loadCollection } = require('./storage')
192
192
  const persisted = loadCollection('provider_health') as Record<string, ProviderHealthState>
193
193
  let restored = 0
194
194
  for (const [id, record] of Object.entries(persisted)) {
@@ -323,7 +323,7 @@ export async function pingProvider(
323
323
  apiKey: string | undefined,
324
324
  endpoint: string | undefined,
325
325
  ): Promise<{ ok: boolean; message: string }> {
326
- const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli']
326
+ const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli']
327
327
  if (CLI_PROVIDERS.includes(provider)) return { ok: true, message: 'CLI provider — skipped.' }
328
328
 
329
329
  try {
@@ -620,6 +620,8 @@ function normalizeStoredRecordInner(
620
620
  }
621
621
  // Default geminiSessionId for new field
622
622
  if (session.geminiSessionId === undefined) session.geminiSessionId = null
623
+ // Default copilotSessionId for new field
624
+ if (session.copilotSessionId === undefined) session.copilotSessionId = null
623
625
  // Default injectedMemoryIds for proactive recall dedup
624
626
  if (!session.injectedMemoryIds || typeof session.injectedMemoryIds !== 'object') {
625
627
  session.injectedMemoryIds = {}
@@ -1,4 +1,4 @@
1
- export type ProviderType = 'claude-cli' | 'codex-cli' | 'opencode-cli' | 'gemini-cli' | 'openai' | 'ollama' | 'anthropic' | 'openclaw' | 'google' | 'deepseek' | 'groq' | 'together' | 'mistral' | 'xai' | 'fireworks' | 'nebius' | 'deepinfra'
1
+ export type ProviderType = 'claude-cli' | 'codex-cli' | 'opencode-cli' | 'gemini-cli' | 'copilot-cli' | 'openai' | 'ollama' | 'anthropic' | 'openclaw' | 'google' | 'deepseek' | 'groq' | 'together' | 'mistral' | 'xai' | 'fireworks' | 'nebius' | 'deepinfra'
2
2
  export type ProviderId = ProviderType | (string & {})
3
3
 
4
4
  export interface ProviderInfo {
@@ -70,11 +70,13 @@ export interface Session {
70
70
  codexThreadId?: string | null
71
71
  opencodeSessionId?: string | null
72
72
  geminiSessionId?: string | null
73
+ copilotSessionId?: string | null
73
74
  delegateResumeIds?: {
74
75
  claudeCode?: string | null
75
76
  codex?: string | null
76
77
  opencode?: string | null
77
78
  gemini?: string | null
79
+ copilot?: string | null
78
80
  }
79
81
  /** @deprecated Messages are stored in session_messages table. Use message-repository. */
80
82
  messages: Message[]