@swarmclawai/swarmclaw 1.3.1 → 1.3.3
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 +10 -0
- package/package.json +1 -1
- package/src/components/connectors/connector-list.tsx +1 -1
- package/src/components/connectors/connector-sheet.tsx +1 -1
- 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/connector-inbound.ts +13 -0
- package/src/lib/server/connectors/connector-service.ts +7 -5
- package/src/lib/server/provider-health.ts +4 -4
- package/src/lib/server/storage-normalization.ts +2 -0
- package/src/types/connector.ts +1 -1
- package/src/types/provider.ts +1 -1
- package/src/types/session.ts +2 -0
package/README.md
CHANGED
|
@@ -204,6 +204,16 @@ Read the full setup guide in [`SWARMDOCK.md`](./SWARMDOCK.md), browse the public
|
|
|
204
204
|
|
|
205
205
|
## Release Notes
|
|
206
206
|
|
|
207
|
+
### v1.3.3 Highlights
|
|
208
|
+
|
|
209
|
+
- **Bug fix — stale connector status after auto-restart (#31)**: connectors that auto-restart via the daemon health monitor now show "Starting" instead of a stale "Stopped" or "Error" status in the UI until the daemon reports runtime state. Added `starting` to the `ConnectorStatus` type and updated both the connector list and detail views.
|
|
210
|
+
- **Bug fix — stale credentialId after credential rotation (#30)**: when a provider credential is deleted and re-created, connector sessions now fall back to resolving any valid credential for the same provider instead of failing with "Missing credentials."
|
|
211
|
+
|
|
212
|
+
### v1.3.2 Highlights
|
|
213
|
+
|
|
214
|
+
- **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.
|
|
215
|
+
- **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).
|
|
216
|
+
|
|
207
217
|
### v1.3.1 Highlights
|
|
208
218
|
|
|
209
219
|
- **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.
|
package/package.json
CHANGED
|
@@ -322,7 +322,7 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
322
322
|
{meta.label}
|
|
323
323
|
</span>
|
|
324
324
|
<span className="text-[11px] text-text-3">
|
|
325
|
-
{isRunning ? 'Connected' : c.status === 'error' ? 'Error' : 'Stopped'}
|
|
325
|
+
{isRunning ? 'Connected' : c.status === 'error' ? 'Error' : c.status === 'starting' ? 'Starting' : 'Stopped'}
|
|
326
326
|
</span>
|
|
327
327
|
</div>
|
|
328
328
|
</div>
|
|
@@ -1270,7 +1270,7 @@ export function ConnectorSheet() {
|
|
|
1270
1270
|
runtimeConnector?.status === 'error' ? 'bg-red-400' : 'bg-white/20'
|
|
1271
1271
|
}`} />
|
|
1272
1272
|
{effectiveRunning ? (waAuthenticated ? 'Connected and listening' : 'Connecting...') :
|
|
1273
|
-
runtimeConnector?.status === 'error' ? 'Error — see below' : 'Not connected'}
|
|
1273
|
+
runtimeConnector?.status === 'error' ? 'Error — see below' : runtimeConnector?.status === 'starting' ? 'Starting...' : 'Not connected'}
|
|
1274
1274
|
</div>
|
|
1275
1275
|
</div>
|
|
1276
1276
|
{effectiveRunning ? (
|
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') {
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
selectChatroomRecipients,
|
|
29
29
|
} from '@/lib/server/chatrooms/chatroom-routing'
|
|
30
30
|
import { markProviderFailure, markProviderSuccess } from '../provider-health'
|
|
31
|
+
import { listCredentialIdsByProvider, resolveCredentialSecret } from '@/lib/server/credentials/credential-service'
|
|
31
32
|
import { buildIdentityContinuityContext } from '../identity-continuity'
|
|
32
33
|
import { buildRuntimeSkillPromptBlocks, resolveRuntimeSkills } from '@/lib/server/skills/runtime-skill-resolver'
|
|
33
34
|
import { getProvider } from '@/lib/providers'
|
|
@@ -1009,6 +1010,18 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
1009
1010
|
}
|
|
1010
1011
|
}
|
|
1011
1012
|
|
|
1013
|
+
// Fallback: session credential was deleted — try any credential for this provider
|
|
1014
|
+
if (!apiKey && session.provider) {
|
|
1015
|
+
const providerCredentialIds = listCredentialIdsByProvider(session.provider)
|
|
1016
|
+
for (const id of providerCredentialIds) {
|
|
1017
|
+
const resolved = resolveCredentialSecret(id)
|
|
1018
|
+
if (resolved) {
|
|
1019
|
+
apiKey = resolved
|
|
1020
|
+
break
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1012
1025
|
// Build system prompt: [identity] \n\n [userPrompt] \n\n [soul] \n\n [systemPrompt]
|
|
1013
1026
|
const settings = loadSettings()
|
|
1014
1027
|
const promptParts: string[] = []
|
|
@@ -62,11 +62,13 @@ function persistConnector(connector: Connector): void {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function applyRuntimeFields(connector: Connector, runtime: DaemonConnectorRuntimeState | null): Connector {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
if (runtime?.status) {
|
|
66
|
+
connector.status = runtime.status
|
|
67
|
+
} else if (connector.isEnabled) {
|
|
68
|
+
connector.status = 'starting'
|
|
69
|
+
} else {
|
|
70
|
+
connector.status = connector.lastError ? 'error' : 'stopped'
|
|
71
|
+
}
|
|
70
72
|
|
|
71
73
|
if (connector.platform === 'whatsapp') {
|
|
72
74
|
connector.authenticated = runtime?.authenticated
|
|
@@ -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/connector.ts
CHANGED
|
@@ -29,7 +29,7 @@ export type ConnectorPlatform =
|
|
|
29
29
|
| 'webchat'
|
|
30
30
|
| 'mockmail'
|
|
31
31
|
| 'swarmdock'
|
|
32
|
-
export type ConnectorStatus = 'stopped' | 'running' | 'error'
|
|
32
|
+
export type ConnectorStatus = 'stopped' | 'running' | 'error' | 'starting'
|
|
33
33
|
|
|
34
34
|
export interface MessageSource {
|
|
35
35
|
platform: ConnectorPlatform
|
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[]
|