@swarmclawai/swarmclaw 0.2.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 +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { WebSocket } from 'ws'
|
|
2
|
+
import crypto, { randomUUID } from 'crypto'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import type { StreamChatOptions } from './index'
|
|
6
|
+
|
|
7
|
+
// --- Device Identity (Ed25519 keypair for gateway auth) ---
|
|
8
|
+
|
|
9
|
+
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex')
|
|
10
|
+
|
|
11
|
+
function base64UrlEncode(buf: Buffer): string {
|
|
12
|
+
return buf.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/g, '')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function derivePublicKeyRaw(publicKeyPem: string): Buffer {
|
|
16
|
+
const spki = crypto.createPublicKey(publicKeyPem).export({ type: 'spki', format: 'der' })
|
|
17
|
+
if (spki.length === ED25519_SPKI_PREFIX.length + 32 && spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
|
|
18
|
+
return spki.subarray(ED25519_SPKI_PREFIX.length)
|
|
19
|
+
}
|
|
20
|
+
return spki
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fingerprintPublicKey(publicKeyPem: string): string {
|
|
24
|
+
return crypto.createHash('sha256').update(derivePublicKeyRaw(publicKeyPem)).digest('hex')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface DeviceIdentity {
|
|
28
|
+
deviceId: string
|
|
29
|
+
publicKeyPem: string
|
|
30
|
+
privateKeyPem: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getIdentityPath(): string {
|
|
34
|
+
const dataDir = path.join(process.cwd(), 'data')
|
|
35
|
+
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
|
|
36
|
+
return path.join(dataDir, 'openclaw-device.json')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function loadOrCreateDeviceIdentity(): DeviceIdentity {
|
|
40
|
+
const filePath = getIdentityPath()
|
|
41
|
+
try {
|
|
42
|
+
if (fs.existsSync(filePath)) {
|
|
43
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
44
|
+
if (parsed?.deviceId && parsed?.publicKeyPem && parsed?.privateKeyPem) {
|
|
45
|
+
return parsed
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
|
|
50
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519')
|
|
51
|
+
const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }) as string
|
|
52
|
+
const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }) as string
|
|
53
|
+
const identity: DeviceIdentity = {
|
|
54
|
+
deviceId: fingerprintPublicKey(publicKeyPem),
|
|
55
|
+
publicKeyPem,
|
|
56
|
+
privateKeyPem,
|
|
57
|
+
}
|
|
58
|
+
fs.writeFileSync(filePath, JSON.stringify({ version: 1, ...identity }, null, 2) + '\n', { mode: 0o600 })
|
|
59
|
+
return identity
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Get the device ID that SwarmClaw would use for pairing. */
|
|
63
|
+
export function getDeviceId(): string {
|
|
64
|
+
return loadOrCreateDeviceIdentity().deviceId
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- Protocol helpers ---
|
|
68
|
+
|
|
69
|
+
function normalizeWsUrl(raw: string): string {
|
|
70
|
+
let url = raw.replace(/\/+$/, '').replace(/\/v1$/i, '')
|
|
71
|
+
if (!/^(https?|wss?):\/\//i.test(url)) url = `http://${url}`
|
|
72
|
+
url = url.replace(/^ws:/i, 'http:').replace(/^wss:/i, 'https:')
|
|
73
|
+
return url.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build connect params for the OpenClaw gateway protocol.
|
|
78
|
+
*
|
|
79
|
+
* The gateway allows operators with a valid token to skip device identity
|
|
80
|
+
* (roleCanSkipDeviceIdentity). When useDeviceAuth is true, includes an
|
|
81
|
+
* Ed25519-signed device identity for gateways that require device pairing.
|
|
82
|
+
*/
|
|
83
|
+
export function buildOpenClawConnectParams(
|
|
84
|
+
token: string | undefined,
|
|
85
|
+
nonce: string | undefined,
|
|
86
|
+
opts?: { useDeviceAuth?: boolean },
|
|
87
|
+
) {
|
|
88
|
+
const clientId = 'gateway-client'
|
|
89
|
+
const clientMode = 'backend'
|
|
90
|
+
const platform = process.platform
|
|
91
|
+
const role = 'operator'
|
|
92
|
+
const scopes = ['operator.admin']
|
|
93
|
+
|
|
94
|
+
const params: Record<string, unknown> = {
|
|
95
|
+
minProtocol: 1,
|
|
96
|
+
maxProtocol: 3,
|
|
97
|
+
auth: token ? { token } : undefined,
|
|
98
|
+
client: {
|
|
99
|
+
id: clientId,
|
|
100
|
+
version: '1.0.0',
|
|
101
|
+
platform,
|
|
102
|
+
mode: clientMode,
|
|
103
|
+
instanceId: randomUUID(),
|
|
104
|
+
},
|
|
105
|
+
caps: [],
|
|
106
|
+
role,
|
|
107
|
+
scopes,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (opts?.useDeviceAuth) {
|
|
111
|
+
const identity = loadOrCreateDeviceIdentity()
|
|
112
|
+
const signedAtMs = Date.now()
|
|
113
|
+
|
|
114
|
+
const payload = [
|
|
115
|
+
'v3', identity.deviceId, clientId, clientMode, role,
|
|
116
|
+
scopes.join(','), String(signedAtMs), token ?? '', nonce ?? '',
|
|
117
|
+
platform, '', // deviceFamily
|
|
118
|
+
].join('|')
|
|
119
|
+
const signature = base64UrlEncode(
|
|
120
|
+
crypto.sign(null, Buffer.from(payload, 'utf8'), crypto.createPrivateKey(identity.privateKeyPem)),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
params.device = {
|
|
124
|
+
id: identity.deviceId,
|
|
125
|
+
publicKey: base64UrlEncode(derivePublicKeyRaw(identity.publicKeyPem)),
|
|
126
|
+
signature,
|
|
127
|
+
signedAt: signedAtMs,
|
|
128
|
+
nonce: nonce ?? '',
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return params
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Gateway connection ---
|
|
136
|
+
|
|
137
|
+
interface ConnectResult {
|
|
138
|
+
ok: boolean
|
|
139
|
+
message: string
|
|
140
|
+
errorCode?: string
|
|
141
|
+
ws?: InstanceType<typeof WebSocket>
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Open a WebSocket and complete the connect handshake.
|
|
146
|
+
* Resolves with { ok, ws } on success or { ok: false, message, errorCode } on failure.
|
|
147
|
+
*/
|
|
148
|
+
function wsConnect(
|
|
149
|
+
wsUrl: string,
|
|
150
|
+
token: string | undefined,
|
|
151
|
+
useDeviceAuth: boolean,
|
|
152
|
+
timeoutMs = 15_000,
|
|
153
|
+
): Promise<ConnectResult> {
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
let settled = false
|
|
156
|
+
const done = (result: ConnectResult) => {
|
|
157
|
+
if (settled) return
|
|
158
|
+
settled = true
|
|
159
|
+
clearTimeout(timer)
|
|
160
|
+
if (!result.ok) try { ws.close() } catch {}
|
|
161
|
+
resolve(result)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const timer = setTimeout(() => {
|
|
165
|
+
done({ ok: false, message: 'Connection timed out. Verify the gateway URL and network access.' })
|
|
166
|
+
}, timeoutMs)
|
|
167
|
+
|
|
168
|
+
const ws = new WebSocket(wsUrl)
|
|
169
|
+
let connectId: string | null = null
|
|
170
|
+
|
|
171
|
+
ws.on('message', (data) => {
|
|
172
|
+
try {
|
|
173
|
+
const msg = JSON.parse(data.toString())
|
|
174
|
+
if (msg.event === 'connect.challenge') {
|
|
175
|
+
connectId = randomUUID()
|
|
176
|
+
ws.send(JSON.stringify({
|
|
177
|
+
type: 'req',
|
|
178
|
+
id: connectId,
|
|
179
|
+
method: 'connect',
|
|
180
|
+
params: buildOpenClawConnectParams(token, msg.payload?.nonce, { useDeviceAuth }),
|
|
181
|
+
}))
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
if (msg.type === 'res' && msg.id === connectId) {
|
|
185
|
+
if (msg.ok) {
|
|
186
|
+
done({ ok: true, message: 'Connected.', ws })
|
|
187
|
+
} else {
|
|
188
|
+
done({
|
|
189
|
+
ok: false,
|
|
190
|
+
message: msg.error?.message || 'Gateway connect failed.',
|
|
191
|
+
errorCode: msg.error?.details?.code as string | undefined,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
done({ ok: false, message: 'Unexpected response from gateway.' })
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
ws.on('error', (err) => {
|
|
201
|
+
done({ ok: false, message: `Connection failed: ${err.message}` })
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
ws.on('close', (code, reason) => {
|
|
205
|
+
if (code === 1008) {
|
|
206
|
+
done({ ok: false, message: `Unauthorized: ${reason?.toString() || 'invalid token'}` })
|
|
207
|
+
} else {
|
|
208
|
+
done({ ok: false, message: `Connection closed unexpectedly (${code})` })
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Connect to the gateway with device identity.
|
|
216
|
+
*
|
|
217
|
+
* Always includes Ed25519 device auth — the gateway may accept the initial
|
|
218
|
+
* connect handshake with token-only but still require device identity for
|
|
219
|
+
* agent operations. Sending device auth unconditionally avoids that mismatch.
|
|
220
|
+
*/
|
|
221
|
+
async function connectToGateway(
|
|
222
|
+
wsUrl: string,
|
|
223
|
+
token: string | undefined,
|
|
224
|
+
timeoutMs = 15_000,
|
|
225
|
+
): Promise<ConnectResult> {
|
|
226
|
+
return wsConnect(wsUrl, token, true, timeoutMs)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// --- Provider ---
|
|
230
|
+
|
|
231
|
+
export function streamOpenClawChat({ session, message, imagePath, write, active }: StreamChatOptions): Promise<string> {
|
|
232
|
+
let prompt = message
|
|
233
|
+
if (imagePath) {
|
|
234
|
+
prompt = `[The user has shared an image at: ${imagePath}]\n\n${message}`
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const wsUrl = session.apiEndpoint ? normalizeWsUrl(session.apiEndpoint) : 'ws://127.0.0.1:18789'
|
|
238
|
+
const token = session.apiKey || undefined
|
|
239
|
+
|
|
240
|
+
return new Promise((resolve) => {
|
|
241
|
+
let fullResponse = ''
|
|
242
|
+
let settled = false
|
|
243
|
+
|
|
244
|
+
const finish = (errMsg?: string) => {
|
|
245
|
+
if (settled) return
|
|
246
|
+
settled = true
|
|
247
|
+
active.delete(session.id)
|
|
248
|
+
if (errMsg && !fullResponse.trim()) {
|
|
249
|
+
write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
|
|
250
|
+
}
|
|
251
|
+
resolve(fullResponse)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
connectToGateway(wsUrl, token).then((result) => {
|
|
255
|
+
if (!result.ok || !result.ws) {
|
|
256
|
+
finish(result.message)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const ws = result.ws
|
|
261
|
+
const timeout = setTimeout(() => {
|
|
262
|
+
ws.close()
|
|
263
|
+
finish('OpenClaw gateway timed out after 120s.')
|
|
264
|
+
}, 120_000)
|
|
265
|
+
|
|
266
|
+
active.set(session.id, { kill: () => { ws.close(); clearTimeout(timeout); finish('Aborted.') } })
|
|
267
|
+
|
|
268
|
+
const agentReqId = randomUUID()
|
|
269
|
+
ws.send(JSON.stringify({
|
|
270
|
+
type: 'req',
|
|
271
|
+
id: agentReqId,
|
|
272
|
+
method: 'agent',
|
|
273
|
+
params: {
|
|
274
|
+
message: prompt,
|
|
275
|
+
agentId: 'main',
|
|
276
|
+
timeout: 120,
|
|
277
|
+
idempotencyKey: randomUUID(),
|
|
278
|
+
},
|
|
279
|
+
}))
|
|
280
|
+
|
|
281
|
+
ws.on('message', (data) => {
|
|
282
|
+
try {
|
|
283
|
+
const msg = JSON.parse(data.toString())
|
|
284
|
+
if (msg.type === 'res' && msg.id === agentReqId) {
|
|
285
|
+
if (!msg.ok) {
|
|
286
|
+
ws.close()
|
|
287
|
+
clearTimeout(timeout)
|
|
288
|
+
finish(msg.error?.message || 'Agent request failed.')
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
if (msg.payload?.status === 'accepted') return
|
|
292
|
+
|
|
293
|
+
const payloads = msg.payload?.result?.payloads ?? []
|
|
294
|
+
for (const p of payloads) {
|
|
295
|
+
const text = typeof p.text === 'string' ? p.text.trimEnd() : ''
|
|
296
|
+
if (text) {
|
|
297
|
+
fullResponse += text
|
|
298
|
+
write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!fullResponse && msg.payload?.summary) {
|
|
302
|
+
const text = String(msg.payload.summary)
|
|
303
|
+
fullResponse = text
|
|
304
|
+
write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
|
|
305
|
+
}
|
|
306
|
+
ws.close()
|
|
307
|
+
clearTimeout(timeout)
|
|
308
|
+
finish()
|
|
309
|
+
}
|
|
310
|
+
} catch {}
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
ws.on('error', (err) => {
|
|
314
|
+
clearTimeout(timeout)
|
|
315
|
+
finish(`OpenClaw connection failed: ${err.message}`)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
ws.on('close', (code, reason) => {
|
|
319
|
+
clearTimeout(timeout)
|
|
320
|
+
if (code === 1008) {
|
|
321
|
+
finish(`Unauthorized: ${reason?.toString() || 'invalid token'}`)
|
|
322
|
+
} else {
|
|
323
|
+
finish()
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
}).catch((err) => {
|
|
327
|
+
finish(`OpenClaw error: ${err?.message || 'unknown error'}`)
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
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 '../server/runtime-settings'
|
|
8
|
+
|
|
9
|
+
function findOpencode(): string {
|
|
10
|
+
const locations = [
|
|
11
|
+
path.join(os.homedir(), '.local/bin/opencode'),
|
|
12
|
+
'/usr/local/bin/opencode',
|
|
13
|
+
'/opt/homebrew/bin/opencode',
|
|
14
|
+
]
|
|
15
|
+
// Check nvm paths
|
|
16
|
+
const nvmDir = process.env.NVM_DIR || path.join(os.homedir(), '.nvm')
|
|
17
|
+
try {
|
|
18
|
+
const versions = fs.readdirSync(path.join(nvmDir, 'versions/node'))
|
|
19
|
+
for (const v of versions) {
|
|
20
|
+
locations.push(path.join(nvmDir, 'versions/node', v, 'bin/opencode'))
|
|
21
|
+
}
|
|
22
|
+
} catch { /* nvm not installed */ }
|
|
23
|
+
for (const loc of locations) {
|
|
24
|
+
if (fs.existsSync(loc)) {
|
|
25
|
+
log.info('opencode-cli', `Found opencode at: ${loc}`)
|
|
26
|
+
return loc
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
log.warn('opencode-cli', 'opencode binary not found in known locations, falling back to PATH')
|
|
30
|
+
return 'opencode'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const OPENCODE = findOpencode()
|
|
34
|
+
|
|
35
|
+
function extractSessionId(raw: unknown): string | null {
|
|
36
|
+
if (!raw) return null
|
|
37
|
+
const text = String(raw).trim()
|
|
38
|
+
return text ? text : null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* OpenCode CLI provider — spawns `opencode run <message> --format json` for non-interactive usage.
|
|
43
|
+
* Tracks `session.opencodeSessionId` from streamed JSON events to support multi-turn continuity.
|
|
44
|
+
*/
|
|
45
|
+
export function streamOpenCodeCliChat({ session, message, imagePath, systemPrompt, write, active }: StreamChatOptions): Promise<string> {
|
|
46
|
+
const processTimeoutMs = loadRuntimeSettings().cliProcessTimeoutMs
|
|
47
|
+
const cwd = session.cwd || process.cwd()
|
|
48
|
+
const promptParts: string[] = []
|
|
49
|
+
if (systemPrompt && !session.opencodeSessionId) {
|
|
50
|
+
promptParts.push(`[System instructions]\n${systemPrompt}`)
|
|
51
|
+
}
|
|
52
|
+
promptParts.push(message)
|
|
53
|
+
const prompt = promptParts.join('\n\n')
|
|
54
|
+
|
|
55
|
+
const env: NodeJS.ProcessEnv = {
|
|
56
|
+
...process.env,
|
|
57
|
+
TERM: 'dumb',
|
|
58
|
+
NO_COLOR: '1',
|
|
59
|
+
}
|
|
60
|
+
// Set model via env if specified
|
|
61
|
+
if (session.model) {
|
|
62
|
+
env.OPENCODE_MODEL = session.model
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const args = ['run', prompt, '--format', 'json']
|
|
66
|
+
if (session.opencodeSessionId) args.push('--session', session.opencodeSessionId)
|
|
67
|
+
if (session.model) args.push('--model', session.model)
|
|
68
|
+
if (imagePath) args.push('--file', imagePath)
|
|
69
|
+
|
|
70
|
+
log.info('opencode-cli', `Spawning: ${OPENCODE}`, {
|
|
71
|
+
args: args.map((a, i) => {
|
|
72
|
+
if (i === 1) return `(${prompt.length} chars)`
|
|
73
|
+
if (a.length > 120) return `${a.slice(0, 120)}...`
|
|
74
|
+
return a
|
|
75
|
+
}),
|
|
76
|
+
cwd,
|
|
77
|
+
hasSystemPrompt: !!systemPrompt,
|
|
78
|
+
hasImage: !!imagePath,
|
|
79
|
+
resumeSessionId: session.opencodeSessionId || null,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const proc = spawn(OPENCODE, args, {
|
|
83
|
+
cwd,
|
|
84
|
+
env,
|
|
85
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
86
|
+
timeout: processTimeoutMs,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
log.info('opencode-cli', `Process spawned: pid=${proc.pid}`)
|
|
90
|
+
active.set(session.id, proc)
|
|
91
|
+
|
|
92
|
+
let fullResponse = ''
|
|
93
|
+
let stderrText = ''
|
|
94
|
+
let stdoutBuf = ''
|
|
95
|
+
let eventCount = 0
|
|
96
|
+
const eventErrors: string[] = []
|
|
97
|
+
|
|
98
|
+
proc.stdout!.on('data', (chunk: Buffer) => {
|
|
99
|
+
const text = chunk.toString()
|
|
100
|
+
stdoutBuf += text
|
|
101
|
+
const lines = stdoutBuf.split('\n')
|
|
102
|
+
stdoutBuf = lines.pop() || ''
|
|
103
|
+
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
const trimmed = line.trim()
|
|
106
|
+
if (!trimmed) continue
|
|
107
|
+
try {
|
|
108
|
+
const ev = JSON.parse(trimmed) as any
|
|
109
|
+
eventCount += 1
|
|
110
|
+
const discoveredSessionId = extractSessionId(ev?.sessionID ?? ev?.sessionId)
|
|
111
|
+
if (discoveredSessionId) session.opencodeSessionId = discoveredSessionId
|
|
112
|
+
|
|
113
|
+
if (ev?.type === 'text' && typeof ev?.part?.text === 'string') {
|
|
114
|
+
fullResponse += ev.part.text
|
|
115
|
+
write(`data: ${JSON.stringify({ t: 'd', text: ev.part.text })}\n\n`)
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (ev?.type === 'error') {
|
|
120
|
+
const msg = typeof ev?.error === 'string'
|
|
121
|
+
? ev.error
|
|
122
|
+
: typeof ev?.message === 'string'
|
|
123
|
+
? ev.message
|
|
124
|
+
: 'Unknown OpenCode event error'
|
|
125
|
+
eventErrors.push(msg)
|
|
126
|
+
write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// Raw fallback line from the CLI.
|
|
131
|
+
fullResponse += `${line}\n`
|
|
132
|
+
write(`data: ${JSON.stringify({ t: 'd', text: `${line}\n` })}\n\n`)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
proc.stderr!.on('data', (chunk: Buffer) => {
|
|
138
|
+
const text = chunk.toString()
|
|
139
|
+
stderrText += text
|
|
140
|
+
if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
|
|
141
|
+
log.warn('opencode-cli', `stderr [${session.id}]`, text.slice(0, 500))
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
proc.on('close', (code, signal) => {
|
|
146
|
+
log.info('opencode-cli', `Process closed: code=${code} signal=${signal} events=${eventCount} response=${fullResponse.length}chars`)
|
|
147
|
+
active.delete(session.id)
|
|
148
|
+
if ((code ?? 0) !== 0 && !fullResponse.trim() && eventErrors.length === 0) {
|
|
149
|
+
const msg = stderrText.trim()
|
|
150
|
+
? `OpenCode CLI exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}: ${stderrText.trim().slice(0, 1200)}`
|
|
151
|
+
: `OpenCode CLI exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''} and returned no output.`
|
|
152
|
+
write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
|
|
153
|
+
}
|
|
154
|
+
resolve(fullResponse.trim())
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
proc.on('error', (e) => {
|
|
158
|
+
log.error('opencode-cli', `Process error: ${e.message}`)
|
|
159
|
+
active.delete(session.id)
|
|
160
|
+
write(`data: ${JSON.stringify({ t: 'err', text: e.message })}\n\n`)
|
|
161
|
+
resolve(fullResponse)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { LoopMode } from '@/types'
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_LOOP_MODE: LoopMode = 'bounded'
|
|
4
|
+
|
|
5
|
+
// Loop limits
|
|
6
|
+
export const DEFAULT_AGENT_LOOP_RECURSION_LIMIT = 15
|
|
7
|
+
export const DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT = 25
|
|
8
|
+
export const DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS = 10
|
|
9
|
+
export const DEFAULT_ONGOING_LOOP_MAX_ITERATIONS = 250
|
|
10
|
+
export const DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES = 60
|
|
11
|
+
|
|
12
|
+
// Tool/process timeouts
|
|
13
|
+
export const DEFAULT_SHELL_COMMAND_TIMEOUT_SEC = 30
|
|
14
|
+
export const DEFAULT_CLAUDE_CODE_TIMEOUT_SEC = 120
|
|
15
|
+
export const DEFAULT_CLI_PROCESS_TIMEOUT_SEC = 300
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import { findDuplicateSchedule, getScheduleSignatureKey, type ScheduleLike } from './schedule-dedupe.ts'
|
|
4
|
+
|
|
5
|
+
test('findDuplicateSchedule matches active interval schedules with normalized prompts', () => {
|
|
6
|
+
const schedules: Record<string, ScheduleLike> = {
|
|
7
|
+
a1: {
|
|
8
|
+
id: 'a1',
|
|
9
|
+
agentId: 'assistant',
|
|
10
|
+
taskPrompt: 'Take a screenshot of Wikipedia homepage',
|
|
11
|
+
scheduleType: 'interval',
|
|
12
|
+
intervalMs: 60_000,
|
|
13
|
+
status: 'active',
|
|
14
|
+
createdAt: 1,
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const duplicate = findDuplicateSchedule(schedules, {
|
|
19
|
+
agentId: 'assistant',
|
|
20
|
+
taskPrompt: 'take a screenshot of wikipedia homepage',
|
|
21
|
+
scheduleType: 'interval',
|
|
22
|
+
intervalMs: 60_000,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
assert.ok(duplicate)
|
|
26
|
+
assert.equal(duplicate?.id, 'a1')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('findDuplicateSchedule ignores completed/failed schedules by default', () => {
|
|
30
|
+
const schedules: Record<string, ScheduleLike> = {
|
|
31
|
+
done1: {
|
|
32
|
+
id: 'done1',
|
|
33
|
+
agentId: 'assistant',
|
|
34
|
+
taskPrompt: 'Run report',
|
|
35
|
+
scheduleType: 'interval',
|
|
36
|
+
intervalMs: 300_000,
|
|
37
|
+
status: 'completed',
|
|
38
|
+
createdAt: 1,
|
|
39
|
+
},
|
|
40
|
+
fail1: {
|
|
41
|
+
id: 'fail1',
|
|
42
|
+
agentId: 'assistant',
|
|
43
|
+
taskPrompt: 'Run report',
|
|
44
|
+
scheduleType: 'interval',
|
|
45
|
+
intervalMs: 300_000,
|
|
46
|
+
status: 'failed',
|
|
47
|
+
createdAt: 2,
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const duplicate = findDuplicateSchedule(schedules, {
|
|
52
|
+
agentId: 'assistant',
|
|
53
|
+
taskPrompt: 'run report',
|
|
54
|
+
scheduleType: 'interval',
|
|
55
|
+
intervalMs: 300_000,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
assert.equal(duplicate, null)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('getScheduleSignatureKey is stable for equivalent schedules', () => {
|
|
62
|
+
const keyA = getScheduleSignatureKey({
|
|
63
|
+
agentId: 'assistant',
|
|
64
|
+
taskPrompt: ' Check status ',
|
|
65
|
+
scheduleType: 'cron',
|
|
66
|
+
cron: '*/5 * * * *',
|
|
67
|
+
})
|
|
68
|
+
const keyB = getScheduleSignatureKey({
|
|
69
|
+
agentId: 'assistant',
|
|
70
|
+
taskPrompt: 'check status',
|
|
71
|
+
scheduleType: 'cron',
|
|
72
|
+
cron: '*/5 * * * *',
|
|
73
|
+
})
|
|
74
|
+
const keyC = getScheduleSignatureKey({
|
|
75
|
+
agentId: 'assistant',
|
|
76
|
+
taskPrompt: 'check status',
|
|
77
|
+
scheduleType: 'cron',
|
|
78
|
+
cron: '*/10 * * * *',
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
assert.ok(keyA)
|
|
82
|
+
assert.equal(keyA, keyB)
|
|
83
|
+
assert.notEqual(keyA, keyC)
|
|
84
|
+
})
|