@shykaruu/jarvis-brain 0.4.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/LICENSE +153 -0
- package/README.md +428 -0
- package/bin/jarvis.ts +449 -0
- package/package.json +79 -0
- package/roles/activity-observer.yaml +60 -0
- package/roles/ceo-founder.yaml +144 -0
- package/roles/chief-of-staff.yaml +158 -0
- package/roles/dev-lead.yaml +182 -0
- package/roles/executive-assistant.yaml +77 -0
- package/roles/marketing-director.yaml +168 -0
- package/roles/personal-assistant.yaml +266 -0
- package/roles/research-specialist.yaml +60 -0
- package/roles/specialists/content-writer.yaml +53 -0
- package/roles/specialists/customer-support.yaml +57 -0
- package/roles/specialists/data-analyst.yaml +57 -0
- package/roles/specialists/financial-analyst.yaml +56 -0
- package/roles/specialists/hr-specialist.yaml +55 -0
- package/roles/specialists/legal-advisor.yaml +58 -0
- package/roles/specialists/marketing-strategist.yaml +56 -0
- package/roles/specialists/project-coordinator.yaml +55 -0
- package/roles/specialists/research-analyst.yaml +58 -0
- package/roles/specialists/software-engineer.yaml +57 -0
- package/roles/specialists/system-administrator.yaml +57 -0
- package/roles/system-admin.yaml +76 -0
- package/scripts/ensure-bun.cjs +16 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +261 -0
- package/src/actions/browser/session.ts +506 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +363 -0
- package/src/actions/tools/builtin.ts +950 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/documents.ts +169 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +31 -0
- package/src/actions/tools/registry.ts +173 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +592 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +309 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +301 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +417 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +238 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +827 -0
- package/src/cli/uninstall.test.ts +37 -0
- package/src/cli/uninstall.ts +202 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/dashboard-auth.ts +75 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +149 -0
- package/src/comms/voice.test.ts +504 -0
- package/src/comms/voice.ts +341 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +669 -0
- package/src/config/README.md +389 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +183 -0
- package/src/config/loader.ts +148 -0
- package/src/config/types.ts +293 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +667 -0
- package/src/daemon/api-routes.ts +3067 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/dashboard-auth.test.ts +170 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/flock.c +7 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1070 -0
- package/src/daemon/llm-settings.test.ts +78 -0
- package/src/daemon/llm-settings.ts +450 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.test.ts +283 -0
- package/src/daemon/pid.ts +224 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +926 -0
- package/src/global.d.ts +4 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +407 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +400 -0
- package/src/llm/gemini.ts +380 -0
- package/src/llm/groq.ts +406 -0
- package/src/llm/history.ts +147 -0
- package/src/llm/index.ts +21 -0
- package/src/llm/manager.ts +226 -0
- package/src/llm/ollama.ts +316 -0
- package/src/llm/openai.ts +411 -0
- package/src/llm/openrouter.ts +390 -0
- package/src/llm/provider.test.ts +487 -0
- package/src/llm/provider.ts +61 -0
- package/src/llm/test.ts +88 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +120 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +218 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +195 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/sites/builder-tools.ts +215 -0
- package/src/sites/dev-server-manager.ts +286 -0
- package/src/sites/fixtures/security-test-site/.jarvis-project.json +6 -0
- package/src/sites/fixtures/security-test-site/Makefile +15 -0
- package/src/sites/fixtures/security-test-site/README.md +18 -0
- package/src/sites/fixtures/security-test-site/index.html +12 -0
- package/src/sites/fixtures/security-test-site/index.ts +16 -0
- package/src/sites/fixtures/security-test-site/package.json +13 -0
- package/src/sites/fixtures/security-test-site/src/app.tsx +780 -0
- package/src/sites/fixtures/security-test-site/tsconfig.json +10 -0
- package/src/sites/git-manager.ts +240 -0
- package/src/sites/github-manager.ts +355 -0
- package/src/sites/index.ts +25 -0
- package/src/sites/project-manager.ts +389 -0
- package/src/sites/proxy.ts +133 -0
- package/src/sites/service.ts +136 -0
- package/src/sites/templates.ts +169 -0
- package/src/sites/types.ts +89 -0
- package/src/user/profile-followup.test.ts +84 -0
- package/src/user/profile-followup.ts +185 -0
- package/src/user/profile.ts +224 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +270 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/dashboard-sessions.ts +44 -0
- package/src/vault/documents.ts +130 -0
- package/src/vault/entities.ts +185 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +139 -0
- package/src/vault/retrieval.ts +258 -0
- package/src/vault/schema.ts +709 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/user-profile.test.ts +113 -0
- package/src/vault/user-profile.ts +176 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/webapp-template-seeds.ts +116 -0
- package/src/vault/webapp-templates.ts +244 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-3gr23jt9.js +112614 -0
- package/ui/dist/index-9vmj8127.css +14239 -0
- package/ui/dist/index-hy9pc1gm.js +112873 -0
- package/ui/dist/index-j2ep5d1w.js +112374 -0
- package/ui/dist/index-jt00vjqs.js +112858 -0
- package/ui/dist/index-k9ymx5qb.js +112374 -0
- package/ui/dist/index.html +16 -0
- package/ui/public/audio/pcm-capture-processor.js +11 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Service — The Mouth
|
|
3
|
+
*
|
|
4
|
+
* Wraps WebSocketServer and StreamRelay. Routes incoming messages
|
|
5
|
+
* to the AgentService and relays streamed responses back to clients.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ServerWebSocket } from 'bun';
|
|
9
|
+
import type { Service, ServiceStatus } from './services.ts';
|
|
10
|
+
import type { AgentService } from './agent-service.ts';
|
|
11
|
+
import type { CommitmentExecutor } from './commitment-executor.ts';
|
|
12
|
+
import type { ChannelService } from './channel-service.ts';
|
|
13
|
+
import type { Commitment } from '../vault/commitments.ts';
|
|
14
|
+
import type { ContentItem } from '../vault/content-pipeline.ts';
|
|
15
|
+
import type { STTProvider, TTSProvider } from '../comms/voice.ts';
|
|
16
|
+
import { setDefaultCwd } from '../actions/tools/builtin.ts';
|
|
17
|
+
import type { ApprovalRequest } from '../authority/approval.ts';
|
|
18
|
+
import type { EmergencyState } from '../authority/emergency.ts';
|
|
19
|
+
import { createCommitment, updateCommitmentStatus, updateCommitmentAssignee } from '../vault/commitments.ts';
|
|
20
|
+
import { WebSocketServer, type WSMessage } from '../comms/websocket.ts';
|
|
21
|
+
import { StreamRelay } from '../comms/streaming.ts';
|
|
22
|
+
import { getOrCreateConversation, addMessage } from '../vault/conversations.ts';
|
|
23
|
+
import { maybeCreateUserProfileFollowupPrompt, recordUserProfileTurn } from '../user/profile-followup.ts';
|
|
24
|
+
|
|
25
|
+
type VoiceSession = {
|
|
26
|
+
requestId: string;
|
|
27
|
+
chunks: Buffer[];
|
|
28
|
+
startedAt: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export class WebSocketService implements Service {
|
|
32
|
+
name = 'websocket';
|
|
33
|
+
private _status: ServiceStatus = 'stopped';
|
|
34
|
+
private port: number;
|
|
35
|
+
private agentService: AgentService;
|
|
36
|
+
private wsServer: WebSocketServer;
|
|
37
|
+
private streamRelay: StreamRelay;
|
|
38
|
+
/** Tracks the commitment ID for the currently processing chat message */
|
|
39
|
+
private activeTaskId: string | null = null;
|
|
40
|
+
private commitmentExecutor: CommitmentExecutor | null = null;
|
|
41
|
+
private channelService: ChannelService | null = null;
|
|
42
|
+
private ttsProvider: TTSProvider | null = null;
|
|
43
|
+
private sttProvider: STTProvider | null = null;
|
|
44
|
+
private voiceSessions = new Map<ServerWebSocket<unknown>, VoiceSession>();
|
|
45
|
+
private siteBuilderService: import('../sites/service.ts').SiteBuilderService | null = null;
|
|
46
|
+
|
|
47
|
+
constructor(port: number, agentService: AgentService) {
|
|
48
|
+
this.port = port;
|
|
49
|
+
this.agentService = agentService;
|
|
50
|
+
this.wsServer = new WebSocketServer(port);
|
|
51
|
+
this.streamRelay = new StreamRelay(this.wsServer);
|
|
52
|
+
|
|
53
|
+
// Wire delegation callback: when PA delegates to a specialist,
|
|
54
|
+
// update the active task's assigned_to on the task board
|
|
55
|
+
this.agentService.setDelegationCallback((specialistName) => {
|
|
56
|
+
if (!this.activeTaskId) return;
|
|
57
|
+
try {
|
|
58
|
+
const updated = updateCommitmentAssignee(this.activeTaskId, specialistName);
|
|
59
|
+
if (updated) this.broadcastTaskUpdate(updated, 'updated');
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error('[WSService] Failed to update task assignee:', err);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Set the site builder service for project-scoped chat.
|
|
68
|
+
*/
|
|
69
|
+
setSiteBuilderService(svc: import('../sites/service.ts').SiteBuilderService): void {
|
|
70
|
+
this.siteBuilderService = svc;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set the commitment executor for handling cancel commands.
|
|
75
|
+
*/
|
|
76
|
+
setCommitmentExecutor(executor: CommitmentExecutor): void {
|
|
77
|
+
this.commitmentExecutor = executor;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Set the channel service for cross-channel broadcasts.
|
|
82
|
+
*/
|
|
83
|
+
setChannelService(channelService: ChannelService): void {
|
|
84
|
+
this.channelService = channelService;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Set the TTS provider for voice responses.
|
|
89
|
+
*/
|
|
90
|
+
setTTSProvider(provider: TTSProvider): void {
|
|
91
|
+
this.ttsProvider = provider;
|
|
92
|
+
console.log('[WSService] TTS provider set');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Set the STT provider for voice input transcription.
|
|
97
|
+
*/
|
|
98
|
+
setSTTProvider(provider: STTProvider): void {
|
|
99
|
+
this.sttProvider = provider;
|
|
100
|
+
console.log('[WSService] STT provider set');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get the underlying WebSocket server for direct broadcasting.
|
|
105
|
+
*/
|
|
106
|
+
getServer(): WebSocketServer {
|
|
107
|
+
return this.wsServer;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Register API route handlers on the underlying WebSocket server.
|
|
112
|
+
* Must be called before start().
|
|
113
|
+
*/
|
|
114
|
+
setApiRoutes(routes: Record<string, any>): void {
|
|
115
|
+
this.wsServer.setApiRoutes(routes);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Set directory for serving pre-built dashboard files.
|
|
120
|
+
* Must be called before start().
|
|
121
|
+
*/
|
|
122
|
+
setStaticDir(dir: string): void {
|
|
123
|
+
this.wsServer.setStaticDir(dir);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
setPublicDir(dir: string): void {
|
|
127
|
+
this.wsServer.setPublicDir(dir);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
setAuthToken(token: string): void {
|
|
131
|
+
this.wsServer.setAuthToken(token);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
setDashboardPasswordHash(passwordHash?: string | null): void {
|
|
135
|
+
this.wsServer.setDashboardPasswordHash(passwordHash);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async start(): Promise<void> {
|
|
139
|
+
this._status = 'starting';
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// Set up message handler
|
|
143
|
+
this.wsServer.setHandler({
|
|
144
|
+
onMessage: (msg, ws) => this.routeMessage(msg, ws),
|
|
145
|
+
onBinaryMessage: (data, ws) => this.handleVoiceAudio(data, ws),
|
|
146
|
+
onConnect: (_ws) => {
|
|
147
|
+
console.log('[WSService] Client connected');
|
|
148
|
+
},
|
|
149
|
+
onDisconnect: (ws) => {
|
|
150
|
+
// Clean up any pending voice session for this client
|
|
151
|
+
this.voiceSessions.delete(ws);
|
|
152
|
+
console.log('[WSService] Client disconnected');
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Start the server
|
|
157
|
+
this.wsServer.start();
|
|
158
|
+
this._status = 'running';
|
|
159
|
+
console.log(`[WSService] Started on port ${this.port}`);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
this._status = 'error';
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async stop(): Promise<void> {
|
|
167
|
+
this._status = 'stopping';
|
|
168
|
+
this.wsServer.stop();
|
|
169
|
+
this._status = 'stopped';
|
|
170
|
+
console.log('[WSService] Stopped');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
status(): ServiceStatus {
|
|
174
|
+
return this._status;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Broadcast a proactive heartbeat message to all connected clients
|
|
179
|
+
* and external channels.
|
|
180
|
+
*/
|
|
181
|
+
broadcastHeartbeat(text: string): void {
|
|
182
|
+
const message: WSMessage = {
|
|
183
|
+
type: 'chat',
|
|
184
|
+
payload: {
|
|
185
|
+
text,
|
|
186
|
+
source: 'heartbeat',
|
|
187
|
+
},
|
|
188
|
+
priority: 'normal',
|
|
189
|
+
timestamp: Date.now(),
|
|
190
|
+
};
|
|
191
|
+
this.wsServer.broadcast(message);
|
|
192
|
+
|
|
193
|
+
// Also push to external channels
|
|
194
|
+
if (this.channelService) {
|
|
195
|
+
this.channelService.broadcastToAll(text).catch(err =>
|
|
196
|
+
console.error('[WSService] Channel heartbeat broadcast error:', err)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Broadcast a notification with priority level.
|
|
203
|
+
* Used by EventReactor for immediate event reactions.
|
|
204
|
+
* Urgent notifications are also pushed to all external channels.
|
|
205
|
+
*/
|
|
206
|
+
broadcastNotification(text: string, priority: 'urgent' | 'normal' | 'low'): void {
|
|
207
|
+
const message: WSMessage = {
|
|
208
|
+
type: 'chat',
|
|
209
|
+
payload: {
|
|
210
|
+
text,
|
|
211
|
+
source: 'proactive',
|
|
212
|
+
},
|
|
213
|
+
priority,
|
|
214
|
+
timestamp: Date.now(),
|
|
215
|
+
};
|
|
216
|
+
this.wsServer.broadcast(message);
|
|
217
|
+
|
|
218
|
+
// Push urgent notifications to external channels (Telegram, Discord)
|
|
219
|
+
if (priority === 'urgent' && this.channelService) {
|
|
220
|
+
this.channelService.broadcastToAll(`[URGENT] ${text}`).catch(err =>
|
|
221
|
+
console.error('[WSService] Channel broadcast error:', err)
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Broadcast task (commitment) changes to all connected clients.
|
|
228
|
+
* Used for real-time task board updates.
|
|
229
|
+
*/
|
|
230
|
+
broadcastTaskUpdate(task: Commitment, action: 'created' | 'updated' | 'deleted'): void {
|
|
231
|
+
const message: WSMessage = {
|
|
232
|
+
type: 'notification',
|
|
233
|
+
payload: {
|
|
234
|
+
source: 'task_update',
|
|
235
|
+
action,
|
|
236
|
+
task,
|
|
237
|
+
},
|
|
238
|
+
timestamp: Date.now(),
|
|
239
|
+
};
|
|
240
|
+
this.wsServer.broadcast(message);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private broadcastAssistantMessage(text: string, requestId?: string): void {
|
|
244
|
+
const message: WSMessage = {
|
|
245
|
+
type: 'notification',
|
|
246
|
+
payload: {
|
|
247
|
+
source: 'assistant_message',
|
|
248
|
+
text,
|
|
249
|
+
},
|
|
250
|
+
id: requestId,
|
|
251
|
+
timestamp: Date.now(),
|
|
252
|
+
};
|
|
253
|
+
this.wsServer.broadcast(message);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Broadcast content pipeline changes to all connected clients.
|
|
258
|
+
* Used for real-time content pipeline updates.
|
|
259
|
+
*/
|
|
260
|
+
broadcastContentUpdate(item: ContentItem, action: 'created' | 'updated' | 'deleted'): void {
|
|
261
|
+
const message: WSMessage = {
|
|
262
|
+
type: 'notification',
|
|
263
|
+
payload: {
|
|
264
|
+
source: 'content_update',
|
|
265
|
+
action,
|
|
266
|
+
item,
|
|
267
|
+
},
|
|
268
|
+
timestamp: Date.now(),
|
|
269
|
+
};
|
|
270
|
+
this.wsServer.broadcast(message);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Broadcast sub-agent progress events to all connected clients.
|
|
275
|
+
* Used by the delegation system for real-time visibility.
|
|
276
|
+
*/
|
|
277
|
+
broadcastSubAgentProgress(event: {
|
|
278
|
+
type: 'text' | 'tool_call' | 'done';
|
|
279
|
+
agentName: string;
|
|
280
|
+
agentId: string;
|
|
281
|
+
data: unknown;
|
|
282
|
+
}): void {
|
|
283
|
+
const message: WSMessage = {
|
|
284
|
+
type: 'stream',
|
|
285
|
+
payload: {
|
|
286
|
+
...event,
|
|
287
|
+
source: 'sub-agent',
|
|
288
|
+
},
|
|
289
|
+
timestamp: Date.now(),
|
|
290
|
+
};
|
|
291
|
+
this.wsServer.broadcast(message);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Broadcast an approval request to all connected dashboard clients.
|
|
296
|
+
* Always pushed via WS; urgent requests are also sent to external channels.
|
|
297
|
+
*/
|
|
298
|
+
broadcastApprovalRequest(request: ApprovalRequest): void {
|
|
299
|
+
const shortId = request.id.slice(0, 8);
|
|
300
|
+
const message: WSMessage = {
|
|
301
|
+
type: 'notification',
|
|
302
|
+
payload: {
|
|
303
|
+
source: 'approval_request',
|
|
304
|
+
request,
|
|
305
|
+
shortId,
|
|
306
|
+
},
|
|
307
|
+
priority: request.urgency === 'urgent' ? 'urgent' : 'normal',
|
|
308
|
+
timestamp: Date.now(),
|
|
309
|
+
};
|
|
310
|
+
this.wsServer.broadcast(message);
|
|
311
|
+
|
|
312
|
+
// Push urgent approvals to external channels
|
|
313
|
+
if (request.urgency === 'urgent' && this.channelService) {
|
|
314
|
+
const text = `[APPROVAL NEEDED] ${request.agent_name} wants to run ${request.tool_name} (${request.action_category}).\nReason: ${request.reason}\nReply: approve ${shortId} / deny ${shortId}`;
|
|
315
|
+
this.channelService.broadcastToAll(text).catch(err =>
|
|
316
|
+
console.error('[WSService] Approval channel broadcast error:', err)
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Broadcast emergency state changes to all connected clients.
|
|
323
|
+
*/
|
|
324
|
+
broadcastEmergencyState(state: EmergencyState): void {
|
|
325
|
+
const message: WSMessage = {
|
|
326
|
+
type: 'notification',
|
|
327
|
+
payload: {
|
|
328
|
+
source: 'emergency_state',
|
|
329
|
+
state,
|
|
330
|
+
},
|
|
331
|
+
priority: 'urgent',
|
|
332
|
+
timestamp: Date.now(),
|
|
333
|
+
};
|
|
334
|
+
this.wsServer.broadcast(message);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Broadcast an approval resolution (approved/denied/executed) to all clients.
|
|
339
|
+
*/
|
|
340
|
+
/**
|
|
341
|
+
* Broadcast an awareness event to all connected clients.
|
|
342
|
+
*/
|
|
343
|
+
/**
|
|
344
|
+
* Broadcast a sidecar event to all connected clients.
|
|
345
|
+
*/
|
|
346
|
+
broadcastSidecarEvent(sidecarId: string, event: { type: string; data: Record<string, unknown>; timestamp: number }): void {
|
|
347
|
+
const message: WSMessage = {
|
|
348
|
+
type: 'notification',
|
|
349
|
+
payload: {
|
|
350
|
+
source: 'sidecar_event',
|
|
351
|
+
sidecarId,
|
|
352
|
+
event,
|
|
353
|
+
},
|
|
354
|
+
timestamp: event.timestamp,
|
|
355
|
+
};
|
|
356
|
+
this.wsServer.broadcast(message);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
broadcastAwarenessEvent(event: { type: string; data: Record<string, unknown>; timestamp: number }): void {
|
|
360
|
+
const message: WSMessage = {
|
|
361
|
+
type: 'notification',
|
|
362
|
+
payload: {
|
|
363
|
+
source: 'awareness_event',
|
|
364
|
+
event,
|
|
365
|
+
},
|
|
366
|
+
timestamp: event.timestamp,
|
|
367
|
+
};
|
|
368
|
+
this.wsServer.broadcast(message);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Synthesize TTS for a proactive message and broadcast audio to all clients.
|
|
373
|
+
* Used for awareness suggestions and other unsolicited voice notifications.
|
|
374
|
+
*/
|
|
375
|
+
/**
|
|
376
|
+
* Synthesize TTS for a proactive message and broadcast audio to all clients.
|
|
377
|
+
* Used for awareness suggestions and other unsolicited voice notifications.
|
|
378
|
+
*/
|
|
379
|
+
async broadcastProactiveVoice(text: string): Promise<void> {
|
|
380
|
+
if (!this.ttsProvider || !text) {
|
|
381
|
+
console.log(`[WSService] Proactive TTS skipped: ${!this.ttsProvider ? 'no TTS provider' : 'empty text'}`);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (this.wsServer.getClientCount() === 0) {
|
|
386
|
+
console.log('[WSService] Proactive TTS skipped: no connected clients');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
const requestId = `proactive-${Date.now()}`;
|
|
392
|
+
|
|
393
|
+
// Signal TTS start to all clients
|
|
394
|
+
const startMsg: WSMessage = {
|
|
395
|
+
type: 'tts_start',
|
|
396
|
+
payload: { requestId },
|
|
397
|
+
timestamp: Date.now(),
|
|
398
|
+
};
|
|
399
|
+
this.wsServer.broadcast(startMsg);
|
|
400
|
+
|
|
401
|
+
let chunkCount = 0;
|
|
402
|
+
for await (const chunk of this.ttsProvider.synthesizeStream(text)) {
|
|
403
|
+
// Send binary audio to all connected clients
|
|
404
|
+
for (const ws of this.wsServer.getClients()) {
|
|
405
|
+
try {
|
|
406
|
+
ws.sendBinary(chunk);
|
|
407
|
+
} catch { /* client may have disconnected */ }
|
|
408
|
+
}
|
|
409
|
+
chunkCount++;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Signal TTS end
|
|
413
|
+
const endMsg: WSMessage = {
|
|
414
|
+
type: 'tts_end',
|
|
415
|
+
payload: { requestId },
|
|
416
|
+
timestamp: Date.now(),
|
|
417
|
+
};
|
|
418
|
+
this.wsServer.broadcast(endMsg);
|
|
419
|
+
console.log(`[WSService] Proactive TTS complete: "${text.slice(0, 60)}..." (${chunkCount} chunks)`);
|
|
420
|
+
} catch (err) {
|
|
421
|
+
console.error('[WSService] Proactive TTS error:', err instanceof Error ? err.message : err);
|
|
422
|
+
// Still send tts_end so client doesn't get stuck
|
|
423
|
+
try {
|
|
424
|
+
this.wsServer.broadcast({ type: 'tts_end', payload: {}, timestamp: Date.now() });
|
|
425
|
+
} catch { /* ignore */ }
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Broadcast a workflow execution event to all connected clients.
|
|
431
|
+
*/
|
|
432
|
+
broadcastWorkflowEvent(event: { type: string; workflowId: string; executionId?: string; nodeId?: string; data: Record<string, unknown>; timestamp: number }): void {
|
|
433
|
+
const message: WSMessage = {
|
|
434
|
+
type: 'workflow_event',
|
|
435
|
+
payload: event,
|
|
436
|
+
timestamp: event.timestamp,
|
|
437
|
+
};
|
|
438
|
+
this.wsServer.broadcast(message);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Broadcast a goal event to all connected clients.
|
|
443
|
+
*/
|
|
444
|
+
broadcastGoalEvent(event: { type: string; goalId?: string; data: Record<string, unknown>; timestamp: number }): void {
|
|
445
|
+
const message: WSMessage = {
|
|
446
|
+
type: 'goal_event',
|
|
447
|
+
payload: event,
|
|
448
|
+
timestamp: event.timestamp,
|
|
449
|
+
};
|
|
450
|
+
this.wsServer.broadcast(message);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
broadcastSiteEvent(event: { type: string; projectId: string; data: Record<string, unknown>; timestamp: number }): void {
|
|
454
|
+
const message: WSMessage = {
|
|
455
|
+
type: 'site_event',
|
|
456
|
+
payload: event,
|
|
457
|
+
timestamp: event.timestamp,
|
|
458
|
+
};
|
|
459
|
+
this.wsServer.broadcast(message);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Format a FileEntry tree into a compact text listing.
|
|
464
|
+
*/
|
|
465
|
+
private formatFileTree(entry: { name: string; path: string; type: 'file' | 'directory'; children?: { name: string; type: 'file' | 'directory' }[] }): string {
|
|
466
|
+
const lines: string[] = [];
|
|
467
|
+
if (entry.children) {
|
|
468
|
+
for (const child of entry.children) {
|
|
469
|
+
lines.push(child.type === 'directory' ? `${child.name}/` : child.name);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return lines.join('\n') + '\n';
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
broadcastApprovalUpdate(request: ApprovalRequest): void {
|
|
476
|
+
const message: WSMessage = {
|
|
477
|
+
type: 'notification',
|
|
478
|
+
payload: {
|
|
479
|
+
source: 'approval_update',
|
|
480
|
+
request,
|
|
481
|
+
},
|
|
482
|
+
timestamp: Date.now(),
|
|
483
|
+
};
|
|
484
|
+
this.wsServer.broadcast(message);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Route incoming WebSocket messages to the appropriate handler.
|
|
489
|
+
*/
|
|
490
|
+
private async routeMessage(msg: WSMessage, ws: ServerWebSocket<unknown>): Promise<WSMessage | void> {
|
|
491
|
+
switch (msg.type) {
|
|
492
|
+
case 'chat':
|
|
493
|
+
return this.handleChat(msg, ws);
|
|
494
|
+
|
|
495
|
+
case 'command':
|
|
496
|
+
return this.handleCommand(msg);
|
|
497
|
+
|
|
498
|
+
case 'status':
|
|
499
|
+
return this.handleStatus();
|
|
500
|
+
|
|
501
|
+
case 'voice_start': {
|
|
502
|
+
const { requestId } = msg.payload as { requestId: string };
|
|
503
|
+
this.voiceSessions.set(ws, { requestId, chunks: [], startedAt: Date.now() });
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
case 'voice_end': {
|
|
508
|
+
const session = this.voiceSessions.get(ws);
|
|
509
|
+
if (!session) return undefined;
|
|
510
|
+
this.voiceSessions.delete(ws);
|
|
511
|
+
// Fire-and-forget: transcribe → process → TTS response
|
|
512
|
+
this.handleVoiceSession(session, ws).catch(err =>
|
|
513
|
+
console.error('[WSService] Voice session error:', err)
|
|
514
|
+
);
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
default:
|
|
519
|
+
return {
|
|
520
|
+
type: 'error',
|
|
521
|
+
payload: { message: `Unknown message type: ${msg.type}` },
|
|
522
|
+
timestamp: Date.now(),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Handle chat messages — stream response via StreamRelay.
|
|
529
|
+
* Auto-creates a task for non-trivial messages so the task board tracks agent work.
|
|
530
|
+
*/
|
|
531
|
+
private async handleChat(msg: WSMessage, ws?: ServerWebSocket<unknown>): Promise<WSMessage | void> {
|
|
532
|
+
const payload = msg.payload as { text?: string; channel?: string; projectId?: string };
|
|
533
|
+
const text = payload?.text;
|
|
534
|
+
const projectId = payload?.projectId ?? null;
|
|
535
|
+
|
|
536
|
+
if (!text) {
|
|
537
|
+
return {
|
|
538
|
+
type: 'error',
|
|
539
|
+
payload: { message: 'Missing text in chat payload' },
|
|
540
|
+
id: msg.id,
|
|
541
|
+
timestamp: Date.now(),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const channel = payload.channel ?? 'websocket';
|
|
546
|
+
const requestId = msg.id ?? crypto.randomUUID();
|
|
547
|
+
|
|
548
|
+
// Build site builder system prompt context (injected into system prompt, not user message)
|
|
549
|
+
let siteContext: string | undefined;
|
|
550
|
+
if (projectId && this.siteBuilderService) {
|
|
551
|
+
// Project-scoped chat (from the Site Builder page)
|
|
552
|
+
const project = await this.siteBuilderService.getProjectWithStatus(projectId);
|
|
553
|
+
if (project) {
|
|
554
|
+
let fileTreeText = '';
|
|
555
|
+
try {
|
|
556
|
+
const tree = this.siteBuilderService.projectManager.getFileTree(projectId, 1);
|
|
557
|
+
fileTreeText = this.formatFileTree(tree);
|
|
558
|
+
} catch { /* ignore */ }
|
|
559
|
+
|
|
560
|
+
siteContext = `# Site Builder Context
|
|
561
|
+
|
|
562
|
+
You are working on project "${project.name}" (${project.framework}).
|
|
563
|
+
- Path: ${project.path}
|
|
564
|
+
- Branch: ${project.gitBranch ?? 'main'}
|
|
565
|
+
- Dev server: ${project.status}
|
|
566
|
+
${project.githubUrl ? `- GitHub: ${project.githubUrl}` : ''}
|
|
567
|
+
${fileTreeText ? `\n## Project Structure\n\`\`\`\n${fileTreeText}\`\`\`` : ''}
|
|
568
|
+
|
|
569
|
+
## Rules
|
|
570
|
+
- Use site_read_file, site_write_file, site_list_files, site_run_command, site_git_commit, site_github_push tools with project_id="${projectId}".
|
|
571
|
+
- Do NOT use regular read_file, write_file, or run_command — always use the site_* variants.
|
|
572
|
+
- Do NOT start dev servers via site_run_command. The dev server is managed by the dashboard (make dev runs automatically).
|
|
573
|
+
- Changes are auto-committed after this conversation turn completes.
|
|
574
|
+
- For the "bun-react" framework: the server uses Bun.serve() with HTML imports (import from "./index.html"). Run with "bun --hot index.ts", NOT vite or webpack.`;
|
|
575
|
+
}
|
|
576
|
+
} else if (this.siteBuilderService) {
|
|
577
|
+
// General chat (main dashboard) — give the LLM awareness of site builder projects
|
|
578
|
+
try {
|
|
579
|
+
const projects = await this.siteBuilderService.listProjectsWithStatus();
|
|
580
|
+
if (projects.length > 0) {
|
|
581
|
+
const projectList = projects.map(p =>
|
|
582
|
+
` - "${p.name}" (id: ${p.id}, framework: ${p.framework}, branch: ${p.gitBranch ?? 'main'}${p.githubUrl ? `, github: ${p.githubUrl}` : ''})`
|
|
583
|
+
).join('\n');
|
|
584
|
+
|
|
585
|
+
siteContext = `# Site Builder
|
|
586
|
+
|
|
587
|
+
You have access to the Site Builder feature with ${projects.length} project(s):
|
|
588
|
+
${projectList}
|
|
589
|
+
|
|
590
|
+
You can work on any of these projects using site builder tools (site_read_file, site_write_file, site_list_files, site_run_command, site_git_commit, site_github_push) by passing the project's id as project_id.
|
|
591
|
+
When the user asks you to build, edit, or work on a website/app, use these tools to make changes directly in the project files.
|
|
592
|
+
If the user wants to create a new project, tell them to use the Site Builder page (Sites tab in the sidebar) to create one first.`;
|
|
593
|
+
}
|
|
594
|
+
} catch { /* ignore — site builder may not be fully started */ }
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Auto-create a task for non-trivial messages
|
|
598
|
+
const isTrivial = text.trim().length < 10;
|
|
599
|
+
let taskCommitment: Commitment | null = null;
|
|
600
|
+
|
|
601
|
+
if (!isTrivial) {
|
|
602
|
+
try {
|
|
603
|
+
const taskLabel = text.length > 80 ? text.slice(0, 77) + '...' : text;
|
|
604
|
+
taskCommitment = createCommitment(taskLabel, {
|
|
605
|
+
assigned_to: 'jarvis',
|
|
606
|
+
created_from: 'user',
|
|
607
|
+
});
|
|
608
|
+
updateCommitmentStatus(taskCommitment.id, 'active');
|
|
609
|
+
taskCommitment.status = 'active';
|
|
610
|
+
this.activeTaskId = taskCommitment.id;
|
|
611
|
+
this.broadcastTaskUpdate(taskCommitment, 'created');
|
|
612
|
+
} catch (err) {
|
|
613
|
+
console.error('[WSService] Failed to auto-create task:', err);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Persist user message
|
|
618
|
+
try {
|
|
619
|
+
const conversation = getOrCreateConversation(channel);
|
|
620
|
+
recordUserProfileTurn(text);
|
|
621
|
+
addMessage(conversation.id, { role: 'user', content: text });
|
|
622
|
+
|
|
623
|
+
// Set default cwd for general tools (run_command, read_file, etc.)
|
|
624
|
+
// so they operate in the project directory during site builder conversations
|
|
625
|
+
if (projectId && this.siteBuilderService) {
|
|
626
|
+
const projectPath = this.siteBuilderService.projectManager.getProjectPath(projectId);
|
|
627
|
+
setDefaultCwd(projectPath);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const { stream, onComplete } = this.agentService.streamMessage(text, channel, siteContext);
|
|
631
|
+
|
|
632
|
+
// Set up streaming TTS: speak sentences as they arrive
|
|
633
|
+
const ttsActive = !!(this.ttsProvider && ws);
|
|
634
|
+
let ttsSentenceQueue: string[] = [];
|
|
635
|
+
let ttsSpeaking = false;
|
|
636
|
+
let ttsStartSent = false;
|
|
637
|
+
let ttsStreamFullyDone = false; // set AFTER relayStream returns, not per-turn 'done'
|
|
638
|
+
let ttsSentenceCount = 0;
|
|
639
|
+
let ttsChunkCount = 0;
|
|
640
|
+
|
|
641
|
+
const speakNextSentence = async () => {
|
|
642
|
+
if (ttsSpeaking || !ttsActive || !ws) return;
|
|
643
|
+
const sentence = ttsSentenceQueue.shift();
|
|
644
|
+
if (!sentence) {
|
|
645
|
+
// Queue empty — send tts_end only if stream is fully done
|
|
646
|
+
if (ttsStreamFullyDone && ttsStartSent) {
|
|
647
|
+
console.log(`[WSService] TTS complete: ${ttsSentenceCount} sentences, ${ttsChunkCount} audio chunks`);
|
|
648
|
+
this.wsServer.sendToClient(ws, {
|
|
649
|
+
type: 'tts_end',
|
|
650
|
+
payload: { requestId },
|
|
651
|
+
id: requestId,
|
|
652
|
+
timestamp: Date.now(),
|
|
653
|
+
});
|
|
654
|
+
ttsStartSent = false; // prevent duplicate tts_end
|
|
655
|
+
}
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Send tts_start exactly once before the first audio chunk
|
|
660
|
+
if (!ttsStartSent) {
|
|
661
|
+
ttsStartSent = true;
|
|
662
|
+
this.wsServer.sendToClient(ws, {
|
|
663
|
+
type: 'tts_start',
|
|
664
|
+
payload: { requestId },
|
|
665
|
+
id: requestId,
|
|
666
|
+
timestamp: Date.now(),
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
ttsSpeaking = true;
|
|
671
|
+
ttsSentenceCount++;
|
|
672
|
+
try {
|
|
673
|
+
if (this.ttsProvider) {
|
|
674
|
+
for await (const chunk of this.ttsProvider.synthesizeStream(sentence)) {
|
|
675
|
+
ttsChunkCount++;
|
|
676
|
+
this.wsServer.sendBinary(ws, chunk);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
} catch (err) {
|
|
680
|
+
console.error('[WSService] TTS sentence error:', err);
|
|
681
|
+
}
|
|
682
|
+
ttsSpeaking = false;
|
|
683
|
+
speakNextSentence();
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
// Relay stream to all WebSocket clients, collect full text.
|
|
687
|
+
// onSentence fires for each complete sentence during streaming.
|
|
688
|
+
// NOTE: onTextDone fires per LLM turn (tool loop), NOT once at the end.
|
|
689
|
+
// We ignore onTextDone and use the relayStream return to mark stream completion.
|
|
690
|
+
const fullText = await this.streamRelay.relayStream(stream, requestId, ttsActive ? {
|
|
691
|
+
onSentence: (sentence) => {
|
|
692
|
+
ttsSentenceQueue.push(sentence);
|
|
693
|
+
speakNextSentence();
|
|
694
|
+
},
|
|
695
|
+
} : undefined);
|
|
696
|
+
|
|
697
|
+
// Stream is now fully done (all tool loop turns complete)
|
|
698
|
+
ttsStreamFullyDone = true;
|
|
699
|
+
if (ttsActive) {
|
|
700
|
+
if (!ttsSpeaking && ttsSentenceQueue.length === 0 && ttsStartSent) {
|
|
701
|
+
// Everything already played, send tts_end now
|
|
702
|
+
this.wsServer.sendToClient(ws!, {
|
|
703
|
+
type: 'tts_end',
|
|
704
|
+
payload: { requestId },
|
|
705
|
+
id: requestId,
|
|
706
|
+
timestamp: Date.now(),
|
|
707
|
+
});
|
|
708
|
+
ttsStartSent = false;
|
|
709
|
+
}
|
|
710
|
+
// Otherwise speakNextSentence will send tts_end when queue drains
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Persist assistant response
|
|
714
|
+
addMessage(conversation.id, { role: 'assistant', content: fullText });
|
|
715
|
+
|
|
716
|
+
const followupPrompt = maybeCreateUserProfileFollowupPrompt();
|
|
717
|
+
if (followupPrompt) {
|
|
718
|
+
this.broadcastAssistantMessage(followupPrompt);
|
|
719
|
+
addMessage(conversation.id, { role: 'assistant', content: followupPrompt });
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Mark task as completed
|
|
723
|
+
if (taskCommitment) {
|
|
724
|
+
try {
|
|
725
|
+
const resultSummary = fullText.length > 200 ? fullText.slice(0, 197) + '...' : fullText;
|
|
726
|
+
const updated = updateCommitmentStatus(taskCommitment.id, 'completed', resultSummary);
|
|
727
|
+
if (updated) this.broadcastTaskUpdate(updated, 'updated');
|
|
728
|
+
} catch (err) {
|
|
729
|
+
console.error('[WSService] Failed to complete task:', err);
|
|
730
|
+
} finally {
|
|
731
|
+
this.activeTaskId = null;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Clear site builder default cwd now that the turn is done
|
|
736
|
+
setDefaultCwd(null);
|
|
737
|
+
|
|
738
|
+
// Fire-and-forget: run post-processing (extraction, personality)
|
|
739
|
+
onComplete(fullText).catch((err) =>
|
|
740
|
+
console.error('[WSService] onComplete error:', err)
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
// Auto-commit site builder changes after chat turn
|
|
744
|
+
if (projectId && this.siteBuilderService) {
|
|
745
|
+
try {
|
|
746
|
+
const projectPath = this.siteBuilderService.projectManager.getProjectPath(projectId);
|
|
747
|
+
if (projectPath) {
|
|
748
|
+
const commitMsg = text.length > 60 ? text.slice(0, 57) + '...' : text;
|
|
749
|
+
const commit = await this.siteBuilderService.gitManager.autoCommit(projectPath, commitMsg);
|
|
750
|
+
if (commit) {
|
|
751
|
+
this.broadcastSiteEvent({
|
|
752
|
+
type: 'git_commit',
|
|
753
|
+
projectId,
|
|
754
|
+
data: { commit },
|
|
755
|
+
timestamp: Date.now(),
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
} catch (err) {
|
|
760
|
+
console.error('[WSService] Site builder auto-commit error:', err);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Don't return a direct response — StreamRelay already broadcast everything
|
|
765
|
+
return undefined;
|
|
766
|
+
} catch (error) {
|
|
767
|
+
console.error('[WSService] Chat error:', error);
|
|
768
|
+
|
|
769
|
+
// Mark task as failed
|
|
770
|
+
if (taskCommitment) {
|
|
771
|
+
try {
|
|
772
|
+
const reason = error instanceof Error ? error.message : 'Processing failed';
|
|
773
|
+
const updated = updateCommitmentStatus(taskCommitment.id, 'failed', reason);
|
|
774
|
+
if (updated) this.broadcastTaskUpdate(updated, 'updated');
|
|
775
|
+
} catch (err) {
|
|
776
|
+
console.error('[WSService] Failed to fail task:', err);
|
|
777
|
+
} finally {
|
|
778
|
+
this.activeTaskId = null;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return {
|
|
783
|
+
type: 'error',
|
|
784
|
+
payload: {
|
|
785
|
+
message: error instanceof Error ? error.message : 'Chat processing failed',
|
|
786
|
+
},
|
|
787
|
+
id: requestId,
|
|
788
|
+
timestamp: Date.now(),
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Handle binary audio data from voice recording.
|
|
795
|
+
* Accumulates chunks into the active voice session for this client.
|
|
796
|
+
*/
|
|
797
|
+
private async handleVoiceAudio(data: Buffer, ws: ServerWebSocket<unknown>): Promise<void> {
|
|
798
|
+
const session = this.voiceSessions.get(ws);
|
|
799
|
+
if (!session) {
|
|
800
|
+
console.warn('[WSService] Binary audio received with no active voice session');
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
session.chunks.push(data);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Process a completed voice session: STT → chat → TTS response.
|
|
808
|
+
*/
|
|
809
|
+
private async handleVoiceSession(session: VoiceSession, ws: ServerWebSocket<unknown>): Promise<void> {
|
|
810
|
+
if (!this.sttProvider) {
|
|
811
|
+
this.wsServer.sendToClient(ws, {
|
|
812
|
+
type: 'error',
|
|
813
|
+
payload: { message: 'STT not configured. Enable it in Settings > Channels.' },
|
|
814
|
+
timestamp: Date.now(),
|
|
815
|
+
});
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const audioBuffer = Buffer.concat(session.chunks);
|
|
820
|
+
if (audioBuffer.length === 0) return;
|
|
821
|
+
|
|
822
|
+
try {
|
|
823
|
+
const transcript = await this.sttProvider.transcribe(audioBuffer);
|
|
824
|
+
if (!transcript.trim()) return;
|
|
825
|
+
|
|
826
|
+
console.log('[WSService] Voice transcript:', transcript);
|
|
827
|
+
|
|
828
|
+
// Echo transcript back so the UI shows it as a user message
|
|
829
|
+
this.wsServer.sendToClient(ws, {
|
|
830
|
+
type: 'chat',
|
|
831
|
+
payload: { text: transcript, source: 'voice_transcript' },
|
|
832
|
+
id: session.requestId,
|
|
833
|
+
timestamp: Date.now(),
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// Reuse existing chat flow
|
|
837
|
+
await this.handleChat({
|
|
838
|
+
type: 'chat',
|
|
839
|
+
payload: { text: transcript },
|
|
840
|
+
id: session.requestId,
|
|
841
|
+
timestamp: Date.now(),
|
|
842
|
+
}, ws);
|
|
843
|
+
} catch (err) {
|
|
844
|
+
console.error('[WSService] STT error:', err);
|
|
845
|
+
const message = err instanceof Error ? err.message : 'Voice transcription failed';
|
|
846
|
+
this.wsServer.sendToClient(ws, {
|
|
847
|
+
type: 'error',
|
|
848
|
+
payload: { message },
|
|
849
|
+
timestamp: Date.now(),
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Handle system commands.
|
|
856
|
+
*/
|
|
857
|
+
private async handleCommand(msg: WSMessage): Promise<WSMessage> {
|
|
858
|
+
const payload = msg.payload as { command?: string };
|
|
859
|
+
const command = payload?.command;
|
|
860
|
+
|
|
861
|
+
switch (command) {
|
|
862
|
+
case 'health':
|
|
863
|
+
return {
|
|
864
|
+
type: 'status',
|
|
865
|
+
payload: {
|
|
866
|
+
status: 'ok',
|
|
867
|
+
service: this.name,
|
|
868
|
+
clients: this.wsServer.getClientCount(),
|
|
869
|
+
},
|
|
870
|
+
id: msg.id,
|
|
871
|
+
timestamp: Date.now(),
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
case 'ping':
|
|
875
|
+
return {
|
|
876
|
+
type: 'status',
|
|
877
|
+
payload: { pong: true },
|
|
878
|
+
id: msg.id,
|
|
879
|
+
timestamp: Date.now(),
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
case 'cancel_execution': {
|
|
883
|
+
const commitmentId = (msg.payload as any)?.commitmentId;
|
|
884
|
+
if (this.commitmentExecutor && commitmentId) {
|
|
885
|
+
const cancelled = this.commitmentExecutor.cancelExecution(commitmentId);
|
|
886
|
+
return {
|
|
887
|
+
type: 'status',
|
|
888
|
+
payload: { cancelled, commitmentId },
|
|
889
|
+
id: msg.id,
|
|
890
|
+
timestamp: Date.now(),
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
return {
|
|
894
|
+
type: 'error',
|
|
895
|
+
payload: { message: 'No executor available or missing commitmentId' },
|
|
896
|
+
id: msg.id,
|
|
897
|
+
timestamp: Date.now(),
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
default:
|
|
902
|
+
return {
|
|
903
|
+
type: 'error',
|
|
904
|
+
payload: { message: `Unknown command: ${command}` },
|
|
905
|
+
id: msg.id,
|
|
906
|
+
timestamp: Date.now(),
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Handle status requests.
|
|
913
|
+
*/
|
|
914
|
+
private handleStatus(): WSMessage {
|
|
915
|
+
return {
|
|
916
|
+
type: 'status',
|
|
917
|
+
payload: {
|
|
918
|
+
service: this.name,
|
|
919
|
+
status: this._status,
|
|
920
|
+
clients: this.wsServer.getClientCount(),
|
|
921
|
+
port: this.port,
|
|
922
|
+
},
|
|
923
|
+
timestamp: Date.now(),
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
}
|