@swarmclawai/swarmclaw 0.4.0 → 0.5.0
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 +21 -4
- package/bin/server-cmd.js +28 -19
- package/next.config.ts +13 -0
- package/package.json +3 -1
- package/src/app/api/agents/[id]/route.ts +39 -22
- package/src/app/api/agents/[id]/thread/route.ts +2 -2
- package/src/app/api/agents/route.ts +3 -2
- package/src/app/api/agents/trash/route.ts +44 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +17 -7
- package/src/app/api/connectors/[id]/webhook/route.ts +103 -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/agent-files/route.ts +57 -0
- package/src/app/api/openclaw/approvals/route.ts +46 -0
- package/src/app/api/openclaw/config-sync/route.ts +33 -0
- package/src/app/api/openclaw/cron/route.ts +52 -0
- package/src/app/api/openclaw/directory/route.ts +27 -0
- package/src/app/api/openclaw/discover/route.ts +62 -0
- package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
- package/src/app/api/openclaw/exec-config/route.ts +41 -0
- package/src/app/api/openclaw/gateway/route.ts +72 -0
- package/src/app/api/openclaw/history/route.ts +109 -0
- package/src/app/api/openclaw/media/route.ts +53 -0
- package/src/app/api/openclaw/models/route.ts +12 -0
- package/src/app/api/openclaw/permissions/route.ts +39 -0
- package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
- package/src/app/api/openclaw/skills/install/route.ts +32 -0
- package/src/app/api/openclaw/skills/remove/route.ts +24 -0
- package/src/app/api/openclaw/skills/route.ts +82 -0
- package/src/app/api/openclaw/sync/route.ts +31 -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]/edit-resend/route.ts +22 -0
- package/src/app/api/sessions/[id]/fork/route.ts +44 -0
- package/src/app/api/sessions/[id]/messages/route.ts +20 -2
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +14 -4
- package/src/app/api/sessions/route.ts +8 -4
- 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/globals.css +14 -0
- package/src/app/layout.tsx +5 -20
- package/src/app/page.tsx +3 -24
- package/src/cli/index.js +60 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +45 -0
- package/src/components/agents/agent-card.tsx +19 -5
- package/src/components/agents/agent-chat-list.tsx +31 -24
- package/src/components/agents/agent-files-editor.tsx +185 -0
- package/src/components/agents/agent-list.tsx +84 -3
- package/src/components/agents/agent-sheet.tsx +147 -14
- package/src/components/agents/cron-job-form.tsx +137 -0
- package/src/components/agents/exec-config-panel.tsx +147 -0
- package/src/components/agents/inspector-panel.tsx +310 -0
- package/src/components/agents/openclaw-skills-panel.tsx +230 -0
- package/src/components/agents/permission-preset-selector.tsx +79 -0
- package/src/components/agents/personality-builder.tsx +111 -0
- package/src/components/agents/sandbox-env-panel.tsx +72 -0
- package/src/components/agents/skill-install-dialog.tsx +102 -0
- package/src/components/agents/trash-list.tsx +109 -0
- package/src/components/chat/chat-area.tsx +41 -6
- package/src/components/chat/chat-header.tsx +305 -29
- package/src/components/chat/chat-preview-panel.tsx +113 -0
- package/src/components/chat/exec-approval-card.tsx +89 -0
- package/src/components/chat/message-bubble.tsx +218 -36
- package/src/components/chat/message-list.tsx +135 -31
- package/src/components/chat/streaming-bubble.tsx +59 -10
- package/src/components/chat/suggestions-bar.tsx +74 -0
- package/src/components/chat/thinking-indicator.tsx +20 -6
- package/src/components/chat/tool-call-bubble.tsx +98 -19
- package/src/components/chat/tool-request-banner.tsx +20 -2
- package/src/components/chat/trace-block.tsx +103 -0
- 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 +123 -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/settings/gateway-connection-panel.tsx +278 -0
- package/src/components/shared/avatar.tsx +13 -2
- 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 +74 -0
- package/src/components/skills/skill-list.tsx +2 -1
- package/src/components/tasks/task-board.tsx +1 -1
- package/src/components/tasks/task-list.tsx +5 -2
- package/src/components/tasks/task-sheet.tsx +12 -12
- package/src/hooks/use-continuous-speech.ts +181 -0
- package/src/hooks/use-openclaw-gateway.ts +63 -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/notification-sounds.ts +58 -0
- package/src/lib/personality-parser.ts +97 -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 +28 -2
- package/src/lib/runtime-loop.ts +2 -2
- 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 +82 -6
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
- package/src/lib/server/connectors/bluebubbles.ts +360 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +51 -8
- package/src/lib/server/connectors/manager.ts +424 -13
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +65 -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/daemon-state.ts +11 -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 +8 -9
- package/src/lib/server/main-session.ts +21 -0
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-config-sync.ts +107 -0
- package/src/lib/server/openclaw-exec-config.ts +52 -0
- package/src/lib/server/openclaw-gateway.ts +291 -0
- package/src/lib/server/openclaw-history-merge.ts +36 -0
- package/src/lib/server/openclaw-models.ts +56 -0
- package/src/lib/server/openclaw-permission-presets.ts +64 -0
- package/src/lib/server/openclaw-sync.ts +497 -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 +24 -11
- 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 +53 -6
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +22 -6
- package/src/lib/server/session-tools/file.ts +192 -19
- package/src/lib/server/session-tools/index.ts +4 -2
- 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 +277 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/web.ts +53 -72
- package/src/lib/server/storage.ts +74 -11
- package/src/lib/server/stream-agent-chat.ts +53 -4
- package/src/lib/server/suggestions.ts +20 -0
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/server/ws-hub.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 +80 -1
- package/src/stores/use-approval-store.ts +78 -0
- package/src/stores/use-chat-store.ts +162 -6
- package/src/types/index.ts +154 -3
- package/tsconfig.json +13 -4
|
@@ -63,6 +63,18 @@ 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
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
69
|
+
const { getSharedDeviceToken } = require('../server/openclaw-sync')
|
|
70
|
+
const sharedToken = getSharedDeviceToken()
|
|
71
|
+
if (sharedToken) {
|
|
72
|
+
// Shared token exists — the connector has already paired.
|
|
73
|
+
// Still need the keypair, so continue to identity resolution below.
|
|
74
|
+
// The token will be used during WS connect.
|
|
75
|
+
}
|
|
76
|
+
} catch { /* openclaw-sync not available */ }
|
|
77
|
+
|
|
66
78
|
// 1. Prefer the openclaw CLI's identity — it's likely already paired with the gateway
|
|
67
79
|
const cliIdentityPath = path.join(resolveCliStateDir(), 'identity', 'device.json')
|
|
68
80
|
const cliIdentity = tryLoadIdentityFile(cliIdentityPath)
|
|
@@ -333,8 +345,22 @@ export function streamOpenClawChat({ session, message, imagePath, apiKey, write,
|
|
|
333
345
|
for (const p of payloads) {
|
|
334
346
|
const text = typeof p.text === 'string' ? p.text.trimEnd() : ''
|
|
335
347
|
if (text) {
|
|
336
|
-
|
|
337
|
-
|
|
348
|
+
// Detect [[trace]], [[tool]], [[tool-result]], [[meta]] prefixes
|
|
349
|
+
const traceMatch = text.match(/^\[\[(thinking|tool|tool-result|trace|meta)\]\]/)
|
|
350
|
+
if (traceMatch) {
|
|
351
|
+
const traceType = traceMatch[1]
|
|
352
|
+
const traceContent = text.slice(traceMatch[0].length)
|
|
353
|
+
if (traceType === 'meta') {
|
|
354
|
+
write(`data: ${JSON.stringify({ t: 'md', text: traceContent })}\n\n`)
|
|
355
|
+
} else {
|
|
356
|
+
// Include as text (client-side will parse trace markers)
|
|
357
|
+
fullResponse += text
|
|
358
|
+
write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
fullResponse += text
|
|
362
|
+
write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
|
|
363
|
+
}
|
|
338
364
|
}
|
|
339
365
|
}
|
|
340
366
|
if (!fullResponse && msg.payload?.summary) {
|
package/src/lib/runtime-loop.ts
CHANGED
|
@@ -11,5 +11,5 @@ export const DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES = 60
|
|
|
11
11
|
|
|
12
12
|
// Tool/process timeouts
|
|
13
13
|
export const DEFAULT_SHELL_COMMAND_TIMEOUT_SEC = 30
|
|
14
|
-
export const DEFAULT_CLAUDE_CODE_TIMEOUT_SEC =
|
|
15
|
-
export const DEFAULT_CLI_PROCESS_TIMEOUT_SEC =
|
|
14
|
+
export const DEFAULT_CLAUDE_CODE_TIMEOUT_SEC = 1800
|
|
15
|
+
export const DEFAULT_CLI_PROCESS_TIMEOUT_SEC = 1800
|
|
@@ -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 {
|
|
@@ -342,13 +343,27 @@ function resolveApiKeyForSession(session: SessionWithCredentials, provider: Prov
|
|
|
342
343
|
|
|
343
344
|
function classifyHeartbeatResponse(text: string, ackMaxChars: number): 'suppress' | 'strip' | 'keep' {
|
|
344
345
|
const trimmed = text.trim()
|
|
345
|
-
if (trimmed === 'HEARTBEAT_OK') return 'suppress'
|
|
346
|
-
const stripped = trimmed.replace(/HEARTBEAT_OK/gi, '').trim()
|
|
346
|
+
if (trimmed === 'HEARTBEAT_OK' || trimmed === 'NO_MESSAGE') return 'suppress'
|
|
347
|
+
const stripped = trimmed.replace(/HEARTBEAT_OK/gi, '').replace(/NO_MESSAGE/gi, '').trim()
|
|
347
348
|
if (!stripped) return 'suppress'
|
|
348
349
|
if (stripped.length <= ackMaxChars) return 'suppress'
|
|
349
350
|
return stripped.length < trimmed.length ? 'strip' : 'keep'
|
|
350
351
|
}
|
|
351
352
|
|
|
353
|
+
function estimateConversationTone(text: string): string {
|
|
354
|
+
const t = text || ''
|
|
355
|
+
// Technical: code blocks, function signatures, technical terms
|
|
356
|
+
if (/```/.test(t) || /\b(function|const|let|var|import|export|class|interface|async|await|return)\b/.test(t)) return 'technical'
|
|
357
|
+
if (/\b(error|bug|debug|stack trace|exception|null|undefined|TypeError)\b/i.test(t)) return 'technical'
|
|
358
|
+
// Empathetic: emotional/supportive language
|
|
359
|
+
if (/\b(understand|feel|sorry|empathize|appreciate|grateful|tough|difficult|challenging)\b/i.test(t)) return 'empathetic'
|
|
360
|
+
// Formal: academic/business language
|
|
361
|
+
if (/\b(furthermore|regarding|consequently|therefore|henceforth|pursuant|accordingly|notwithstanding)\b/i.test(t)) return 'formal'
|
|
362
|
+
// Casual: contractions, exclamations, informal language
|
|
363
|
+
if (/\b(gonna|wanna|gotta|yeah|hey|awesome|cool|lol|btw|tbh)\b/i.test(t) || /!{2,}/.test(t)) return 'casual'
|
|
364
|
+
return 'neutral'
|
|
365
|
+
}
|
|
366
|
+
|
|
352
367
|
const AUTO_MEMORY_MIN_INTERVAL_MS = 45 * 60 * 1000
|
|
353
368
|
|
|
354
369
|
function normalizeMemoryText(value: string): string {
|
|
@@ -572,8 +587,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
572
587
|
kill: () => abortController.abort(),
|
|
573
588
|
})
|
|
574
589
|
|
|
590
|
+
// Capture provider-reported usage for the direct (non-tools) path.
|
|
591
|
+
// Uses a mutable object because TS can't track callback mutations on plain variables.
|
|
592
|
+
const directUsage = { inputTokens: 0, outputTokens: 0, received: false }
|
|
593
|
+
const hasTools = !!sessionForRun.tools?.length && !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
|
|
594
|
+
|
|
575
595
|
try {
|
|
576
|
-
const hasTools = !!sessionForRun.tools?.length && !CLI_PROVIDER_IDS.has(providerType)
|
|
577
596
|
// Heartbeat runs are self-contained — skip conversation history to avoid
|
|
578
597
|
// blowing past the context window on long-lived sessions.
|
|
579
598
|
const heartbeatHistory = isAutoRunNoHistory ? [] : undefined
|
|
@@ -601,6 +620,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
601
620
|
write: (raw: string) => parseAndEmit(raw),
|
|
602
621
|
active,
|
|
603
622
|
loadHistory: isAutoRunNoHistory ? () => [] : getSessionMessages,
|
|
623
|
+
onUsage: (u) => { directUsage.inputTokens = u.inputTokens; directUsage.outputTokens = u.outputTokens; directUsage.received = true },
|
|
604
624
|
})
|
|
605
625
|
} catch (err: any) {
|
|
606
626
|
errorMessage = err?.message || String(err)
|
|
@@ -622,6 +642,34 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
622
642
|
markProviderSuccess(providerType)
|
|
623
643
|
}
|
|
624
644
|
|
|
645
|
+
// Record usage for the direct (non-tools) streamChat path.
|
|
646
|
+
// streamAgentChat already calls appendUsage internally for the tools path.
|
|
647
|
+
if (!hasTools && fullResponse && !errorMessage) {
|
|
648
|
+
const inputTokens = directUsage.received ? directUsage.inputTokens : Math.ceil(message.length / 4)
|
|
649
|
+
const outputTokens = directUsage.received ? directUsage.outputTokens : Math.ceil(fullResponse.length / 4)
|
|
650
|
+
const totalTokens = inputTokens + outputTokens
|
|
651
|
+
if (totalTokens > 0) {
|
|
652
|
+
const cost = estimateCost(sessionForRun.model, inputTokens, outputTokens)
|
|
653
|
+
const history = getSessionMessages(sessionId)
|
|
654
|
+
const usageRecord: UsageRecord = {
|
|
655
|
+
sessionId,
|
|
656
|
+
messageIndex: history.length,
|
|
657
|
+
model: sessionForRun.model,
|
|
658
|
+
provider: providerType,
|
|
659
|
+
inputTokens,
|
|
660
|
+
outputTokens,
|
|
661
|
+
totalTokens,
|
|
662
|
+
estimatedCost: cost,
|
|
663
|
+
timestamp: Date.now(),
|
|
664
|
+
}
|
|
665
|
+
appendUsage(sessionId, usageRecord)
|
|
666
|
+
emit({
|
|
667
|
+
t: 'md',
|
|
668
|
+
text: JSON.stringify({ usage: { inputTokens, outputTokens, totalTokens, estimatedCost: cost } }),
|
|
669
|
+
})
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
625
673
|
const requestedToolNames = (!internal && source === 'chat')
|
|
626
674
|
? requestedToolNamesFromMessage(message)
|
|
627
675
|
: []
|
|
@@ -782,6 +830,26 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
782
830
|
const finalText = (fullResponse || '').trim() || (!internal && errorMessage ? `Error: ${errorMessage}` : '')
|
|
783
831
|
const textForPersistence = stripMainLoopMetaForPersistence(finalText, internal)
|
|
784
832
|
|
|
833
|
+
// Emit status SSE event from [MAIN_LOOP_META] if present
|
|
834
|
+
if (internal && finalText) {
|
|
835
|
+
const metaMatch = finalText.match(/\[MAIN_LOOP_META\]\s*(\{[^\n]*\})/i)
|
|
836
|
+
if (metaMatch) {
|
|
837
|
+
try {
|
|
838
|
+
const meta = JSON.parse(metaMatch[1])
|
|
839
|
+
const statusPayload: Record<string, string | undefined> = {}
|
|
840
|
+
if (meta.goal) statusPayload.goal = String(meta.goal)
|
|
841
|
+
if (meta.status) statusPayload.status = String(meta.status)
|
|
842
|
+
if (meta.summary) statusPayload.summary = String(meta.summary)
|
|
843
|
+
if (meta.next_action) statusPayload.nextAction = String(meta.next_action)
|
|
844
|
+
if (Object.keys(statusPayload).length > 0) {
|
|
845
|
+
emit({ t: 'status', text: JSON.stringify(statusPayload) })
|
|
846
|
+
}
|
|
847
|
+
} catch {
|
|
848
|
+
// ignore malformed meta JSON
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
785
853
|
// HEARTBEAT_OK suppression
|
|
786
854
|
const heartbeatConfig = input.heartbeatConfig
|
|
787
855
|
let heartbeatClassification: 'suppress' | 'strip' | 'keep' | null = null
|
|
@@ -841,6 +909,14 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
841
909
|
})
|
|
842
910
|
changed = true
|
|
843
911
|
|
|
912
|
+
// Conversation tone detection
|
|
913
|
+
if (!internal) {
|
|
914
|
+
const tone = estimateConversationTone(persistedText)
|
|
915
|
+
if (tone !== current.conversationTone) {
|
|
916
|
+
current.conversationTone = tone
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
844
920
|
// Target routing for non-suppressed heartbeat alerts
|
|
845
921
|
if (isHeartbeatRun && heartbeatConfig?.target && heartbeatConfig.target !== 'none' && heartbeatConfig.showAlerts !== false) {
|
|
846
922
|
try {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import bluebubbles from './bluebubbles.ts'
|
|
4
|
+
import type { Connector } from '@/types'
|
|
5
|
+
import type { InboundMessage } from './types'
|
|
6
|
+
|
|
7
|
+
type FetchCall = {
|
|
8
|
+
url: string
|
|
9
|
+
init?: RequestInit
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type MockResponse = {
|
|
13
|
+
ok: boolean
|
|
14
|
+
status: number
|
|
15
|
+
json: () => Promise<unknown>
|
|
16
|
+
text: () => Promise<string>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function jsonResponse(status: number, body: unknown): MockResponse {
|
|
20
|
+
return {
|
|
21
|
+
ok: status >= 200 && status < 300,
|
|
22
|
+
status,
|
|
23
|
+
json: async () => body,
|
|
24
|
+
text: async () => JSON.stringify(body),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function textResponse(status: number, text: string): MockResponse {
|
|
29
|
+
return {
|
|
30
|
+
ok: status >= 200 && status < 300,
|
|
31
|
+
status,
|
|
32
|
+
json: async () => {
|
|
33
|
+
throw new Error('not json')
|
|
34
|
+
},
|
|
35
|
+
text: async () => text,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const originalFetch = globalThis.fetch
|
|
40
|
+
|
|
41
|
+
test.afterEach(() => {
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
;(globalThis as any).fetch = originalFetch
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('bluebubbles connector processes inbound webhook payloads and sends replies', async () => {
|
|
47
|
+
const calls: FetchCall[] = []
|
|
48
|
+
const queue: MockResponse[] = [
|
|
49
|
+
jsonResponse(200, { ok: true }), // ping
|
|
50
|
+
jsonResponse(200, { data: { guid: 'msg-1' } }), // send reply
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
;(globalThis as any).fetch = async (url: string, init?: RequestInit) => {
|
|
55
|
+
calls.push({ url: String(url), init })
|
|
56
|
+
const next = queue.shift()
|
|
57
|
+
assert.ok(next, 'unexpected fetch call')
|
|
58
|
+
return next
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const received: InboundMessage[] = []
|
|
62
|
+
const connector = {
|
|
63
|
+
id: 'bb-1',
|
|
64
|
+
name: 'BlueBubbles Test',
|
|
65
|
+
platform: 'bluebubbles',
|
|
66
|
+
agentId: 'agent-1',
|
|
67
|
+
credentialId: null,
|
|
68
|
+
config: {
|
|
69
|
+
serverUrl: 'http://127.0.0.1:1234',
|
|
70
|
+
},
|
|
71
|
+
isEnabled: true,
|
|
72
|
+
status: 'running',
|
|
73
|
+
createdAt: Date.now(),
|
|
74
|
+
updatedAt: Date.now(),
|
|
75
|
+
} as unknown as Connector
|
|
76
|
+
|
|
77
|
+
const instance = await bluebubbles.start(connector, 'pw-test', async (msg) => {
|
|
78
|
+
received.push(msg)
|
|
79
|
+
return 'pong'
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
84
|
+
const handler = (globalThis as any).__swarmclaw_bluebubbles_handler_bb_1__
|
|
85
|
+
assert.equal(typeof handler, 'undefined', 'sanity: wrong handler key should be undefined')
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
87
|
+
const validHandler = (globalThis as any)[`__swarmclaw_bluebubbles_handler_${connector.id}__`]
|
|
88
|
+
assert.equal(typeof validHandler, 'function')
|
|
89
|
+
|
|
90
|
+
await validHandler({
|
|
91
|
+
type: 'new-message',
|
|
92
|
+
data: {
|
|
93
|
+
guid: 'm-123',
|
|
94
|
+
text: 'hello there',
|
|
95
|
+
isFromMe: false,
|
|
96
|
+
isGroup: false,
|
|
97
|
+
handle: { address: '+15551234567', displayName: 'Alice' },
|
|
98
|
+
chatGuid: 'iMessage;-;+15551234567',
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
assert.equal(received.length, 1)
|
|
103
|
+
assert.equal(received[0].text, 'hello there')
|
|
104
|
+
assert.equal(received[0].senderId, '+15551234567')
|
|
105
|
+
assert.equal(received[0].channelId, 'iMessage;-;+15551234567')
|
|
106
|
+
|
|
107
|
+
assert.equal(calls.length, 2)
|
|
108
|
+
assert.ok(calls[0].url.includes('/api/v1/ping'))
|
|
109
|
+
assert.ok(calls[1].url.includes('/api/v1/message/text'))
|
|
110
|
+
|
|
111
|
+
const body = JSON.parse(String(calls[1].init?.body || '{}'))
|
|
112
|
+
assert.equal(body.chatGuid, 'iMessage;-;+15551234567')
|
|
113
|
+
assert.equal(body.message, 'pong')
|
|
114
|
+
} finally {
|
|
115
|
+
await instance.stop()
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('bluebubbles connector supports array-wrapped webhook payload and NO_MESSAGE suppression', async () => {
|
|
120
|
+
const calls: FetchCall[] = []
|
|
121
|
+
const queue: MockResponse[] = [
|
|
122
|
+
jsonResponse(200, { ok: true }), // ping
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
126
|
+
;(globalThis as any).fetch = async (url: string, init?: RequestInit) => {
|
|
127
|
+
calls.push({ url: String(url), init })
|
|
128
|
+
const next = queue.shift()
|
|
129
|
+
assert.ok(next, 'unexpected fetch call')
|
|
130
|
+
return next
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const connector = {
|
|
134
|
+
id: 'bb-2',
|
|
135
|
+
name: 'BlueBubbles Test 2',
|
|
136
|
+
platform: 'bluebubbles',
|
|
137
|
+
agentId: 'agent-2',
|
|
138
|
+
credentialId: null,
|
|
139
|
+
config: {
|
|
140
|
+
serverUrl: 'http://127.0.0.1:1234',
|
|
141
|
+
},
|
|
142
|
+
isEnabled: true,
|
|
143
|
+
status: 'running',
|
|
144
|
+
createdAt: Date.now(),
|
|
145
|
+
updatedAt: Date.now(),
|
|
146
|
+
} as unknown as Connector
|
|
147
|
+
|
|
148
|
+
const instance = await bluebubbles.start(connector, 'pw-test', async () => 'NO_MESSAGE')
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
152
|
+
const handler = (globalThis as any)[`__swarmclaw_bluebubbles_handler_${connector.id}__`]
|
|
153
|
+
assert.equal(typeof handler, 'function')
|
|
154
|
+
|
|
155
|
+
await handler({
|
|
156
|
+
type: 'new-message',
|
|
157
|
+
data: [
|
|
158
|
+
{
|
|
159
|
+
guid: 'm-124',
|
|
160
|
+
text: '',
|
|
161
|
+
isFromMe: false,
|
|
162
|
+
handle: { address: 'test@example.com', displayName: 'Taylor' },
|
|
163
|
+
chatGuid: 'iMessage;-;test@example.com',
|
|
164
|
+
attachments: [{ mimeType: 'image/png', transferName: 'a.png', totalBytes: 128 }],
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
assert.equal(calls.length, 1, 'should not call send endpoint when NO_MESSAGE is returned')
|
|
170
|
+
} finally {
|
|
171
|
+
await instance.stop()
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('bluebubbles sendMessage posts to message/text endpoint', async () => {
|
|
176
|
+
const calls: FetchCall[] = []
|
|
177
|
+
const queue: MockResponse[] = [
|
|
178
|
+
jsonResponse(200, { ok: true }), // ping
|
|
179
|
+
textResponse(200, ''), // send
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
183
|
+
;(globalThis as any).fetch = async (url: string, init?: RequestInit) => {
|
|
184
|
+
calls.push({ url: String(url), init })
|
|
185
|
+
const next = queue.shift()
|
|
186
|
+
assert.ok(next, 'unexpected fetch call')
|
|
187
|
+
return next
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const connector = {
|
|
191
|
+
id: 'bb-3',
|
|
192
|
+
name: 'BlueBubbles Test 3',
|
|
193
|
+
platform: 'bluebubbles',
|
|
194
|
+
agentId: 'agent-3',
|
|
195
|
+
credentialId: null,
|
|
196
|
+
config: {
|
|
197
|
+
serverUrl: 'http://127.0.0.1:1234',
|
|
198
|
+
},
|
|
199
|
+
isEnabled: true,
|
|
200
|
+
status: 'running',
|
|
201
|
+
createdAt: Date.now(),
|
|
202
|
+
updatedAt: Date.now(),
|
|
203
|
+
} as unknown as Connector
|
|
204
|
+
|
|
205
|
+
const instance = await bluebubbles.start(connector, 'pw-test', async () => 'ok')
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await instance.sendMessage?.('iMessage;-;+15550001111', 'hello outbound')
|
|
209
|
+
assert.equal(calls.length, 2)
|
|
210
|
+
assert.ok(calls[1].url.includes('/api/v1/message/text'))
|
|
211
|
+
const body = JSON.parse(String(calls[1].init?.body || '{}'))
|
|
212
|
+
assert.equal(body.chatGuid, 'iMessage;-;+15550001111')
|
|
213
|
+
assert.equal(body.message, 'hello outbound')
|
|
214
|
+
} finally {
|
|
215
|
+
await instance.stop()
|
|
216
|
+
}
|
|
217
|
+
})
|