@swarmclawai/swarmclaw 0.4.0 → 0.4.5
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 -2
- package/next.config.ts +8 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +20 -21
- package/src/app/api/agents/[id]/thread/route.ts +2 -2
- package/src/app/api/agents/route.ts +3 -2
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +10 -3
- package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
- package/src/app/api/connectors/route.ts +6 -3
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -2
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +2 -2
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- package/src/app/api/openclaw/directory/route.ts +26 -0
- package/src/app/api/openclaw/discover/route.ts +61 -0
- package/src/app/api/openclaw/sync/route.ts +30 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -15
- package/src/app/api/providers/route.ts +2 -2
- package/src/app/api/schedules/[id]/route.ts +16 -18
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +2 -2
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +2 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
- package/src/app/api/sessions/[id]/messages/route.ts +2 -1
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +2 -1
- package/src/app/api/sessions/route.ts +2 -2
- package/src/app/api/skills/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +2 -2
- package/src/app/api/tasks/[id]/approve/route.ts +2 -1
- package/src/app/api/tasks/[id]/route.ts +6 -5
- package/src/app/api/tasks/route.ts +2 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- package/src/app/api/webhooks/[id]/route.ts +29 -31
- package/src/app/api/webhooks/route.ts +2 -2
- package/src/app/page.tsx +3 -24
- package/src/cli/index.js +28 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/spec.js +2 -0
- package/src/components/agents/agent-list.tsx +3 -1
- package/src/components/agents/agent-sheet.tsx +116 -14
- package/src/components/chat/chat-area.tsx +27 -4
- package/src/components/chat/chat-header.tsx +141 -29
- package/src/components/chat/tool-call-bubble.tsx +9 -3
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +6 -2
- package/src/components/connectors/connector-sheet.tsx +31 -7
- package/src/components/layout/app-layout.tsx +47 -25
- package/src/components/projects/project-list.tsx +122 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/schedules/schedule-list.tsx +3 -1
- package/src/components/sessions/new-session-sheet.tsx +6 -6
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/shared/connector-platform-icon.tsx +4 -0
- package/src/components/shared/settings/section-heartbeat.tsx +1 -1
- package/src/components/shared/settings/section-orchestrator.tsx +1 -2
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +73 -0
- package/src/components/skills/skill-list.tsx +2 -1
- package/src/components/tasks/task-list.tsx +5 -2
- package/src/hooks/use-continuous-speech.ts +144 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/lib/id.ts +6 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +14 -1
- package/src/lib/providers/index.ts +6 -0
- package/src/lib/providers/ollama.ts +9 -1
- package/src/lib/providers/openai.ts +9 -1
- package/src/lib/providers/openclaw.ts +11 -0
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +38 -4
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
- package/src/lib/server/connectors/bluebubbles.ts +357 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +46 -7
- package/src/lib/server/connectors/manager.ts +392 -3
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +64 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +1 -1
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/main-agent-loop.ts +6 -6
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-sync.ts +496 -0
- package/src/lib/server/orchestrator-lg.ts +30 -9
- package/src/lib/server/orchestrator.ts +4 -4
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +22 -10
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +2 -2
- package/src/lib/server/session-tools/connector.ts +51 -4
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +3 -3
- package/src/lib/server/session-tools/file.ts +176 -3
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +33 -0
- package/src/lib/server/session-tools/search-providers.ts +270 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/web.ts +47 -66
- package/src/lib/server/storage.ts +12 -0
- package/src/lib/server/stream-agent-chat.ts +29 -0
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/tool-definitions.ts +5 -3
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/view-routes.ts +28 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +28 -1
- package/src/stores/use-chat-store.ts +9 -1
- package/src/types/index.ts +27 -2
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { VIEW_TO_PATH, PATH_TO_VIEW, DEFAULT_VIEW } from '@/lib/view-routes'
|
|
6
|
+
|
|
7
|
+
export function useViewRouter() {
|
|
8
|
+
const fromPopstate = useRef(false)
|
|
9
|
+
|
|
10
|
+
// Mount: read pathname → set active view
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const view = PATH_TO_VIEW[window.location.pathname]
|
|
13
|
+
if (view) {
|
|
14
|
+
useAppStore.getState().setActiveView(view)
|
|
15
|
+
} else {
|
|
16
|
+
useAppStore.getState().setActiveView(DEFAULT_VIEW)
|
|
17
|
+
window.history.replaceState(null, '', VIEW_TO_PATH[DEFAULT_VIEW])
|
|
18
|
+
}
|
|
19
|
+
}, [])
|
|
20
|
+
|
|
21
|
+
// State→URL: push new path when activeView changes
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
let prev = useAppStore.getState().activeView
|
|
24
|
+
const unsub = useAppStore.subscribe((state) => {
|
|
25
|
+
const next = state.activeView
|
|
26
|
+
if (next === prev) return
|
|
27
|
+
prev = next
|
|
28
|
+
if (fromPopstate.current) {
|
|
29
|
+
fromPopstate.current = false
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
const targetPath = VIEW_TO_PATH[next]
|
|
33
|
+
if (targetPath && window.location.pathname !== targetPath) {
|
|
34
|
+
window.history.pushState(null, '', targetPath)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
return unsub
|
|
38
|
+
}, [])
|
|
39
|
+
|
|
40
|
+
// Popstate: browser back/forward → update view
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const onPopstate = () => {
|
|
43
|
+
const view = PATH_TO_VIEW[window.location.pathname]
|
|
44
|
+
if (view) {
|
|
45
|
+
fromPopstate.current = true
|
|
46
|
+
useAppStore.getState().setActiveView(view)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
window.addEventListener('popstate', onPopstate)
|
|
50
|
+
return () => window.removeEventListener('popstate', onPopstate)
|
|
51
|
+
}, [])
|
|
52
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useRef, useState } from 'react'
|
|
4
|
+
import { useContinuousSpeech } from './use-continuous-speech'
|
|
5
|
+
import { SentenceAccumulator, AudioChunkQueue, fetchStreamTts } from '@/lib/tts-stream'
|
|
6
|
+
import { useChatStore } from '@/stores/use-chat-store'
|
|
7
|
+
|
|
8
|
+
export type VoiceConversationState = 'idle' | 'listening' | 'processing' | 'speaking'
|
|
9
|
+
|
|
10
|
+
export function useVoiceConversation() {
|
|
11
|
+
const [active, setActive] = useState(false)
|
|
12
|
+
const [voiceState, setVoiceState] = useState<VoiceConversationState>('idle')
|
|
13
|
+
const accumulatorRef = useRef<SentenceAccumulator | null>(null)
|
|
14
|
+
const queueRef = useRef<AudioChunkQueue | null>(null)
|
|
15
|
+
const sendMessage = useChatStore((s) => s.sendMessage)
|
|
16
|
+
|
|
17
|
+
const speech = useContinuousSpeech({
|
|
18
|
+
onUtterance: useCallback((text: string) => {
|
|
19
|
+
setVoiceState('processing')
|
|
20
|
+
// Send the transcribed text as a chat message
|
|
21
|
+
sendMessage(text)
|
|
22
|
+
}, [sendMessage]),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// Called by the chat store's onStreamEvent callback
|
|
26
|
+
const handleStreamEvent = useCallback((event: { t: string; text?: string }) => {
|
|
27
|
+
if (!active) return
|
|
28
|
+
|
|
29
|
+
if (event.t === 'd' && event.text) {
|
|
30
|
+
setVoiceState('speaking')
|
|
31
|
+
if (!accumulatorRef.current) {
|
|
32
|
+
const queue = new AudioChunkQueue()
|
|
33
|
+
queueRef.current = queue
|
|
34
|
+
queue.onComplete = () => {
|
|
35
|
+
// Resume listening after TTS playback finishes
|
|
36
|
+
setVoiceState('listening')
|
|
37
|
+
speech.resume()
|
|
38
|
+
}
|
|
39
|
+
accumulatorRef.current = new SentenceAccumulator((sentence) => {
|
|
40
|
+
queue.enqueue(fetchStreamTts(sentence))
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
accumulatorRef.current.push(event.text)
|
|
44
|
+
} else if (event.t === 'done') {
|
|
45
|
+
// Flush remaining text to TTS
|
|
46
|
+
if (accumulatorRef.current) {
|
|
47
|
+
accumulatorRef.current.flush()
|
|
48
|
+
accumulatorRef.current = null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}, [active, speech])
|
|
52
|
+
|
|
53
|
+
const start = useCallback(() => {
|
|
54
|
+
setActive(true)
|
|
55
|
+
setVoiceState('listening')
|
|
56
|
+
// Register the stream event handler on the chat store
|
|
57
|
+
useChatStore.setState({ onStreamEvent: handleStreamEvent, voiceConversationActive: true })
|
|
58
|
+
speech.start()
|
|
59
|
+
}, [speech, handleStreamEvent])
|
|
60
|
+
|
|
61
|
+
const stop = useCallback(() => {
|
|
62
|
+
setActive(false)
|
|
63
|
+
setVoiceState('idle')
|
|
64
|
+
speech.stop()
|
|
65
|
+
queueRef.current?.stop()
|
|
66
|
+
queueRef.current = null
|
|
67
|
+
accumulatorRef.current = null
|
|
68
|
+
useChatStore.setState({ onStreamEvent: null, voiceConversationActive: false })
|
|
69
|
+
}, [speech])
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
active,
|
|
73
|
+
state: voiceState,
|
|
74
|
+
interimText: speech.interimText,
|
|
75
|
+
transcript: speech.transcript,
|
|
76
|
+
supported: speech.supported,
|
|
77
|
+
start,
|
|
78
|
+
stop,
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/lib/id.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { api } from './api-client'
|
|
2
|
+
import type { Project } from '../types'
|
|
3
|
+
|
|
4
|
+
export const fetchProjects = () => api<Record<string, Project>>('GET', '/projects')
|
|
5
|
+
|
|
6
|
+
export const createProject = (data: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>) =>
|
|
7
|
+
api<Project>('POST', '/projects', data)
|
|
8
|
+
|
|
9
|
+
export const updateProject = (id: string, data: Partial<Project>) =>
|
|
10
|
+
api<Project>('PUT', `/projects/${id}`, data)
|
|
11
|
+
|
|
12
|
+
export const deleteProject = (id: string) =>
|
|
13
|
+
api<string>('DELETE', `/projects/${id}`)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** CLI providers that use their own tool execution — incompatible with LangGraph orchestration. */
|
|
2
|
+
export const NON_LANGGRAPH_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli'])
|
|
3
|
+
|
|
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', 'openclaw'])
|
|
@@ -23,10 +23,12 @@ function fileToContentBlocks(filePath: string): any[] {
|
|
|
23
23
|
return [{ type: 'text', text: `[Attached file: ${filePath.split('/').pop()}]` }]
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export function streamAnthropicChat({ session, message, imagePath, apiKey, systemPrompt, write, active, loadHistory }: StreamChatOptions): Promise<string> {
|
|
26
|
+
export function streamAnthropicChat({ session, message, imagePath, apiKey, systemPrompt, write, active, loadHistory, onUsage }: StreamChatOptions): Promise<string> {
|
|
27
27
|
return new Promise((resolve) => {
|
|
28
28
|
const messages = buildMessages(session, message, imagePath, loadHistory)
|
|
29
29
|
const model = session.model || 'claude-sonnet-4-6'
|
|
30
|
+
let usageInput = 0
|
|
31
|
+
let usageOutput = 0
|
|
30
32
|
|
|
31
33
|
const body: Record<string, unknown> = {
|
|
32
34
|
model,
|
|
@@ -86,11 +88,22 @@ export function streamAnthropicChat({ session, message, imagePath, apiKey, syste
|
|
|
86
88
|
fullResponse += parsed.delta.text
|
|
87
89
|
write(`data: ${JSON.stringify({ t: 'd', text: parsed.delta.text })}\n\n`)
|
|
88
90
|
}
|
|
91
|
+
// message_start carries input token count
|
|
92
|
+
if (parsed.type === 'message_start' && parsed.message?.usage) {
|
|
93
|
+
usageInput = parsed.message.usage.input_tokens || 0
|
|
94
|
+
}
|
|
95
|
+
// message_delta carries output token count
|
|
96
|
+
if (parsed.type === 'message_delta' && parsed.usage) {
|
|
97
|
+
usageOutput = parsed.usage.output_tokens || 0
|
|
98
|
+
}
|
|
89
99
|
} catch {}
|
|
90
100
|
}
|
|
91
101
|
})
|
|
92
102
|
|
|
93
103
|
apiRes.on('end', () => {
|
|
104
|
+
if (onUsage && (usageInput > 0 || usageOutput > 0)) {
|
|
105
|
+
onUsage({ inputTokens: usageInput, outputTokens: usageOutput })
|
|
106
|
+
}
|
|
94
107
|
active.delete(session.id)
|
|
95
108
|
resolve(fullResponse)
|
|
96
109
|
})
|
|
@@ -13,6 +13,11 @@ export interface ProviderHandler {
|
|
|
13
13
|
streamChat: (opts: StreamChatOptions) => Promise<string>
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export interface StreamChatUsage {
|
|
17
|
+
inputTokens: number
|
|
18
|
+
outputTokens: number
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
export interface StreamChatOptions {
|
|
17
22
|
session: any
|
|
18
23
|
message: string
|
|
@@ -22,6 +27,7 @@ export interface StreamChatOptions {
|
|
|
22
27
|
write: (data: string) => void
|
|
23
28
|
active: Map<string, any>
|
|
24
29
|
loadHistory: (sessionId: string) => any[]
|
|
30
|
+
onUsage?: (usage: StreamChatUsage) => void
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
interface BuiltinProviderConfig extends ProviderInfo {
|
|
@@ -6,7 +6,7 @@ import type { StreamChatOptions } from './index'
|
|
|
6
6
|
const IMAGE_EXTS = /\.(png|jpg|jpeg|gif|webp|bmp)$/i
|
|
7
7
|
const TEXT_EXTS = /\.(txt|md|csv|json|xml|html|js|ts|tsx|jsx|py|go|rs|java|c|cpp|h|yml|yaml|toml|env|log|sh|sql|css|scss)$/i
|
|
8
8
|
|
|
9
|
-
export function streamOllamaChat({ session, message, imagePath, apiKey, write, active, loadHistory }: StreamChatOptions): Promise<string> {
|
|
9
|
+
export function streamOllamaChat({ session, message, imagePath, apiKey, write, active, loadHistory, onUsage }: StreamChatOptions): Promise<string> {
|
|
10
10
|
return new Promise((resolve) => {
|
|
11
11
|
const messages = buildMessages(session, message, imagePath, loadHistory)
|
|
12
12
|
const model = session.model || 'llama3'
|
|
@@ -69,6 +69,14 @@ export function streamOllamaChat({ session, message, imagePath, apiKey, write, a
|
|
|
69
69
|
fullResponse += content
|
|
70
70
|
write(`data: ${JSON.stringify({ t: 'd', text: content })}\n\n`)
|
|
71
71
|
}
|
|
72
|
+
// Final chunk (done: true) carries token counts
|
|
73
|
+
if (parsed.done && onUsage) {
|
|
74
|
+
const input = parsed.prompt_eval_count || 0
|
|
75
|
+
const output = parsed.eval_count || 0
|
|
76
|
+
if (input > 0 || output > 0) {
|
|
77
|
+
onUsage({ inputTokens: input, outputTokens: output })
|
|
78
|
+
}
|
|
79
|
+
}
|
|
72
80
|
} catch {}
|
|
73
81
|
}
|
|
74
82
|
})
|
|
@@ -43,7 +43,7 @@ async function fileToContentParts(filePath: string): Promise<any[]> {
|
|
|
43
43
|
return [{ type: 'text', text: `[Attached file: ${name}]` }]
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export function streamOpenAiChat({ session, message, imagePath, apiKey, systemPrompt, write, active, loadHistory }: StreamChatOptions): Promise<string> {
|
|
46
|
+
export function streamOpenAiChat({ session, message, imagePath, apiKey, systemPrompt, write, active, loadHistory, onUsage }: StreamChatOptions): Promise<string> {
|
|
47
47
|
return new Promise(async (resolve) => {
|
|
48
48
|
const messages = await buildMessages(session, message, imagePath, systemPrompt, loadHistory)
|
|
49
49
|
const model = session.model || 'gpt-4o'
|
|
@@ -52,6 +52,7 @@ export function streamOpenAiChat({ session, message, imagePath, apiKey, systemPr
|
|
|
52
52
|
model,
|
|
53
53
|
messages,
|
|
54
54
|
stream: true,
|
|
55
|
+
stream_options: { include_usage: true },
|
|
55
56
|
})
|
|
56
57
|
|
|
57
58
|
let fullResponse = ''
|
|
@@ -136,6 +137,13 @@ export function streamOpenAiChat({ session, message, imagePath, apiKey, systemPr
|
|
|
136
137
|
fullResponse += delta
|
|
137
138
|
write(`data: ${JSON.stringify({ t: 'd', text: delta })}\n\n`)
|
|
138
139
|
}
|
|
140
|
+
// Extract usage from the final chunk (stream_options: include_usage)
|
|
141
|
+
if (parsed.usage && onUsage) {
|
|
142
|
+
onUsage({
|
|
143
|
+
inputTokens: parsed.usage.prompt_tokens || 0,
|
|
144
|
+
outputTokens: parsed.usage.completion_tokens || 0,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
139
147
|
} catch {}
|
|
140
148
|
}
|
|
141
149
|
}
|
|
@@ -63,6 +63,17 @@ function tryLoadIdentityFile(filePath: string): DeviceIdentity | null {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function loadOrCreateDeviceIdentity(): DeviceIdentity {
|
|
66
|
+
// 0. Check shared device token for cross-synced identity
|
|
67
|
+
try {
|
|
68
|
+
const { getSharedDeviceToken } = require('../server/openclaw-sync')
|
|
69
|
+
const sharedToken = getSharedDeviceToken()
|
|
70
|
+
if (sharedToken) {
|
|
71
|
+
// Shared token exists — the connector has already paired.
|
|
72
|
+
// Still need the keypair, so continue to identity resolution below.
|
|
73
|
+
// The token will be used during WS connect.
|
|
74
|
+
}
|
|
75
|
+
} catch { /* openclaw-sync not available */ }
|
|
76
|
+
|
|
66
77
|
// 1. Prefer the openclaw CLI's identity — it's likely already paired with the gateway
|
|
67
78
|
const cliIdentityPath = path.join(resolveCliStateDir(), 'identity', 'device.json')
|
|
68
79
|
const cliIdentity = tryLoadIdentityFile(cliIdentityPath)
|
|
@@ -346,17 +346,16 @@ describe('MCP Server API contract', () => {
|
|
|
346
346
|
assert.match(src, /export\s+async\s+function\s+DELETE/)
|
|
347
347
|
})
|
|
348
348
|
|
|
349
|
-
it('MCP POST route assigns an id via
|
|
349
|
+
it('MCP POST route assigns an id via genId helper', () => {
|
|
350
350
|
const src = readRoute('mcp-servers', 'route.ts')
|
|
351
|
-
assert.match(src, /
|
|
351
|
+
assert.match(src, /const\s+id\s*=\s*genId\(/)
|
|
352
352
|
})
|
|
353
353
|
|
|
354
|
-
it('MCP PUT route preserves id and sets updatedAt', () => {
|
|
354
|
+
it('MCP PUT route preserves id and sets updatedAt via mutateItem', () => {
|
|
355
355
|
const src = readRoute('mcp-servers', '[id]', 'route.ts')
|
|
356
|
+
assert.match(src, /mutateItem\(/)
|
|
356
357
|
assert.match(src, /updatedAt:\s*Date\.now\(\)/)
|
|
357
|
-
|
|
358
|
-
assert.match(src, /\.\.\.servers\[id\]/)
|
|
359
|
-
assert.match(src, /\bid\b,/)
|
|
358
|
+
assert.match(src, /\.\.\.server,\s*\.\.\.body,\s*id,/)
|
|
360
359
|
})
|
|
361
360
|
})
|
|
362
361
|
})
|
|
@@ -3,10 +3,10 @@ import { ChatOpenAI } from '@langchain/openai'
|
|
|
3
3
|
import { loadCredentials, decryptKey, loadAgents, loadSettings } from './storage'
|
|
4
4
|
import { getProviderList } from '../providers'
|
|
5
5
|
import { normalizeOpenClawEndpoint } from '../openclaw-endpoint'
|
|
6
|
+
import { NON_LANGGRAPH_PROVIDER_IDS } from '../provider-sets'
|
|
6
7
|
|
|
7
8
|
const OLLAMA_CLOUD_URL = 'https://ollama.com/v1'
|
|
8
9
|
const OLLAMA_LOCAL_URL = 'http://localhost:11434/v1'
|
|
9
|
-
const NON_LANGGRAPH_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli'])
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Build a LangChain chat model from provider config.
|
|
@@ -18,8 +18,9 @@ export function buildChatModel(opts: {
|
|
|
18
18
|
model: string
|
|
19
19
|
apiKey: string | null
|
|
20
20
|
apiEndpoint?: string | null
|
|
21
|
+
thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high'
|
|
21
22
|
}) {
|
|
22
|
-
const { provider, model, apiKey, apiEndpoint } = opts
|
|
23
|
+
const { provider, model, apiKey, apiEndpoint, thinkingLevel } = opts
|
|
23
24
|
const providers = getProviderList()
|
|
24
25
|
const providerInfo = providers.find((p) => p.id === provider)
|
|
25
26
|
const endpointRaw = apiEndpoint || providerInfo?.defaultEndpoint || null
|
|
@@ -28,11 +29,18 @@ export function buildChatModel(opts: {
|
|
|
28
29
|
: endpointRaw
|
|
29
30
|
|
|
30
31
|
if (provider === 'anthropic') {
|
|
31
|
-
|
|
32
|
+
const anthropicOpts: Record<string, unknown> = {
|
|
32
33
|
model: model || 'claude-sonnet-4-6',
|
|
33
34
|
anthropicApiKey: apiKey || undefined,
|
|
34
35
|
maxTokens: 8192,
|
|
35
|
-
}
|
|
36
|
+
}
|
|
37
|
+
if (thinkingLevel) {
|
|
38
|
+
const budgetMap = { minimal: 1024, low: 4096, medium: 8192, high: 16384 }
|
|
39
|
+
anthropicOpts.thinking = { type: 'enabled', budget_tokens: budgetMap[thinkingLevel] }
|
|
40
|
+
// Extended thinking requires higher maxTokens (budget + output)
|
|
41
|
+
anthropicOpts.maxTokens = budgetMap[thinkingLevel] + 8192
|
|
42
|
+
}
|
|
43
|
+
return new ChatAnthropic(anthropicOpts as ConstructorParameters<typeof ChatAnthropic>[0])
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
if (provider === 'ollama') {
|
|
@@ -48,6 +56,11 @@ export function buildChatModel(opts: {
|
|
|
48
56
|
|
|
49
57
|
// All other providers — OpenAI-compatible with their registered endpoint
|
|
50
58
|
const config: any = { model: model || 'gpt-4o', apiKey: apiKey || undefined }
|
|
59
|
+
// Map thinking level to reasoning_effort for OpenAI o-series models
|
|
60
|
+
if (thinkingLevel && provider === 'openai' && /^o\d/.test(model || '')) {
|
|
61
|
+
const effortMap = { minimal: 'low', low: 'low', medium: 'medium', high: 'high' }
|
|
62
|
+
config.modelKwargs = { reasoning_effort: effortMap[thinkingLevel] }
|
|
63
|
+
}
|
|
51
64
|
if (endpoint) {
|
|
52
65
|
config.configuration = { baseURL: endpoint }
|
|
53
66
|
// OpenClaw endpoints behind Hostinger's proxy use express.json() middleware
|
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
loadSkills,
|
|
10
10
|
loadSettings,
|
|
11
11
|
loadUsage,
|
|
12
|
+
appendUsage,
|
|
12
13
|
active,
|
|
13
14
|
} from './storage'
|
|
14
15
|
import { getProvider } from '@/lib/providers'
|
|
16
|
+
import { estimateCost } from './cost'
|
|
15
17
|
import { log } from './logger'
|
|
16
18
|
import { logExecution } from './execution-log'
|
|
17
19
|
import { streamAgentChat } from './stream-agent-chat'
|
|
@@ -22,10 +24,9 @@ import { getMemoryDb } from './memory-db'
|
|
|
22
24
|
import { routeTaskIntent } from './capability-router'
|
|
23
25
|
import { notify } from './ws-hub'
|
|
24
26
|
import { resolveConcreteToolPolicyBlock, resolveSessionToolPolicy } from './tool-capability-policy'
|
|
25
|
-
import type { MessageToolEvent, SSEEvent } from '@/types'
|
|
27
|
+
import type { MessageToolEvent, SSEEvent, UsageRecord } from '@/types'
|
|
26
28
|
import { markProviderFailure, markProviderSuccess, rankDelegatesByHealth } from './provider-health'
|
|
27
|
-
|
|
28
|
-
const CLI_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli'])
|
|
29
|
+
import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
29
30
|
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli'
|
|
30
31
|
|
|
31
32
|
interface SessionWithTools {
|
|
@@ -572,8 +573,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
572
573
|
kill: () => abortController.abort(),
|
|
573
574
|
})
|
|
574
575
|
|
|
576
|
+
// Capture provider-reported usage for the direct (non-tools) path.
|
|
577
|
+
// Uses a mutable object because TS can't track callback mutations on plain variables.
|
|
578
|
+
const directUsage = { inputTokens: 0, outputTokens: 0, received: false }
|
|
579
|
+
const hasTools = !!sessionForRun.tools?.length && !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
|
|
580
|
+
|
|
575
581
|
try {
|
|
576
|
-
const hasTools = !!sessionForRun.tools?.length && !CLI_PROVIDER_IDS.has(providerType)
|
|
577
582
|
// Heartbeat runs are self-contained — skip conversation history to avoid
|
|
578
583
|
// blowing past the context window on long-lived sessions.
|
|
579
584
|
const heartbeatHistory = isAutoRunNoHistory ? [] : undefined
|
|
@@ -601,6 +606,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
601
606
|
write: (raw: string) => parseAndEmit(raw),
|
|
602
607
|
active,
|
|
603
608
|
loadHistory: isAutoRunNoHistory ? () => [] : getSessionMessages,
|
|
609
|
+
onUsage: (u) => { directUsage.inputTokens = u.inputTokens; directUsage.outputTokens = u.outputTokens; directUsage.received = true },
|
|
604
610
|
})
|
|
605
611
|
} catch (err: any) {
|
|
606
612
|
errorMessage = err?.message || String(err)
|
|
@@ -622,6 +628,34 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
622
628
|
markProviderSuccess(providerType)
|
|
623
629
|
}
|
|
624
630
|
|
|
631
|
+
// Record usage for the direct (non-tools) streamChat path.
|
|
632
|
+
// streamAgentChat already calls appendUsage internally for the tools path.
|
|
633
|
+
if (!hasTools && fullResponse && !errorMessage) {
|
|
634
|
+
const inputTokens = directUsage.received ? directUsage.inputTokens : Math.ceil(message.length / 4)
|
|
635
|
+
const outputTokens = directUsage.received ? directUsage.outputTokens : Math.ceil(fullResponse.length / 4)
|
|
636
|
+
const totalTokens = inputTokens + outputTokens
|
|
637
|
+
if (totalTokens > 0) {
|
|
638
|
+
const cost = estimateCost(sessionForRun.model, inputTokens, outputTokens)
|
|
639
|
+
const history = getSessionMessages(sessionId)
|
|
640
|
+
const usageRecord: UsageRecord = {
|
|
641
|
+
sessionId,
|
|
642
|
+
messageIndex: history.length,
|
|
643
|
+
model: sessionForRun.model,
|
|
644
|
+
provider: providerType,
|
|
645
|
+
inputTokens,
|
|
646
|
+
outputTokens,
|
|
647
|
+
totalTokens,
|
|
648
|
+
estimatedCost: cost,
|
|
649
|
+
timestamp: Date.now(),
|
|
650
|
+
}
|
|
651
|
+
appendUsage(sessionId, usageRecord)
|
|
652
|
+
emit({
|
|
653
|
+
t: 'md',
|
|
654
|
+
text: JSON.stringify({ usage: { inputTokens, outputTokens, totalTokens, estimatedCost: cost } }),
|
|
655
|
+
})
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
625
659
|
const requestedToolNames = (!internal && source === 'chat')
|
|
626
660
|
? requestedToolNamesFromMessage(message)
|
|
627
661
|
: []
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { notify } from './ws-hub'
|
|
3
|
+
|
|
4
|
+
export interface CollectionOps<T> {
|
|
5
|
+
load: () => Record<string, T>
|
|
6
|
+
save: (data: Record<string, T>) => void
|
|
7
|
+
deleteFn?: (id: string) => void
|
|
8
|
+
topic?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load → 404 check → mutate → save → notify.
|
|
13
|
+
* `fn` receives the item and the full collection, returns the updated item.
|
|
14
|
+
*/
|
|
15
|
+
export function mutateItem<T>(
|
|
16
|
+
ops: CollectionOps<T>,
|
|
17
|
+
id: string,
|
|
18
|
+
fn: (item: T, all: Record<string, T>) => T,
|
|
19
|
+
): T | null {
|
|
20
|
+
const all = ops.load()
|
|
21
|
+
if (!all[id]) return null
|
|
22
|
+
all[id] = fn(all[id], all)
|
|
23
|
+
ops.save(all)
|
|
24
|
+
if (ops.topic) notify(ops.topic)
|
|
25
|
+
return all[id]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load → 404 check → delete → notify.
|
|
30
|
+
* Uses `ops.deleteFn` if provided, otherwise inline `delete` + `save`.
|
|
31
|
+
*/
|
|
32
|
+
export function deleteItem<T>(
|
|
33
|
+
ops: CollectionOps<T>,
|
|
34
|
+
id: string,
|
|
35
|
+
): boolean {
|
|
36
|
+
const all = ops.load()
|
|
37
|
+
if (!all[id]) return false
|
|
38
|
+
if (ops.deleteFn) {
|
|
39
|
+
ops.deleteFn(id)
|
|
40
|
+
} else {
|
|
41
|
+
delete all[id]
|
|
42
|
+
ops.save(all)
|
|
43
|
+
}
|
|
44
|
+
if (ops.topic) notify(ops.topic)
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function notFound(msg = 'Not found') {
|
|
49
|
+
return NextResponse.json({ error: msg }, { status: 404 })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function badRequest(msg: string) {
|
|
53
|
+
return NextResponse.json({ error: msg }, { status: 400 })
|
|
54
|
+
}
|