@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 +13 -0
- package/package.json +2 -2
- package/src/lib/provider-sets.ts +3 -3
- package/src/lib/providers/cli-utils.ts +39 -1
- package/src/lib/providers/copilot-cli.ts +222 -0
- package/src/lib/providers/index.ts +14 -5
- package/src/lib/server/agents/agent-availability.test.ts +1 -1
- package/src/lib/server/agents/subagent-swarm.ts +3 -3
- package/src/lib/server/chat-execution/prompt-builder.ts +2 -2
- package/src/lib/server/connectors/swarmdock-bidding.ts +2 -9
- package/src/lib/server/connectors/swarmdock-payloads.test.ts +18 -1
- package/src/lib/server/connectors/swarmdock-tasks.ts +2 -11
- package/src/lib/server/connectors/swarmdock.ts +81 -43
- package/src/lib/server/provider-health.ts +4 -4
- package/src/lib/server/storage-normalization.ts +2 -0
- package/src/types/provider.ts +1 -1
- package/src/types/session.ts +2 -0
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.
|
|
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.
|
|
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",
|
package/src/lib/provider-sets.ts
CHANGED
|
@@ -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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
48
|
+
const { loadAgents } = require('../storage')
|
|
49
49
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
|
-
const { resolveTeam } = require('
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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('
|
|
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('
|
|
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('
|
|
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 = {}
|
package/src/types/provider.ts
CHANGED
|
@@ -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 {
|
package/src/types/session.ts
CHANGED
|
@@ -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[]
|