claude-ws 0.4.9 → 0.5.1-beta.1
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/locales/de.json +13 -0
- package/locales/en.json +20 -0
- package/locales/es.json +13 -0
- package/locales/fr.json +13 -0
- package/locales/ja.json +13 -0
- package/locales/ko.json +13 -0
- package/locales/vi.json +20 -0
- package/locales/zh.json +13 -0
- package/package.json +1 -1
- package/packages/agentic-sdk/src/agent/claude-sdk-agent-provider.ts +4 -3
- package/packages/agentic-sdk/src/services/task/task-conversation-history-builder.ts +1 -1
- package/packages/agentic-sdk/src/services/task/task-crud-and-reorder.ts +15 -1
- package/server.ts +165 -13
- package/src/app/[locale]/page.tsx +21 -2
- package/src/app/api/butler/schedules/[id]/route.ts +84 -0
- package/src/app/api/butler/schedules/route.ts +81 -0
- package/src/app/api/tasks/[id]/route.ts +2 -1
- package/src/app/api/tasks/reorder/route.ts +2 -1
- package/src/app/api/tasks/route.ts +2 -1
- package/src/app/globals.css +3 -0
- package/src/components/butler/butler-status-indicator.tsx +235 -0
- package/src/components/header.tsx +4 -0
- package/src/components/kanban/board.tsx +5 -2
- package/src/components/kanban/task-card-context-menu.tsx +2 -1
- package/src/components/kanban/task-card.tsx +8 -3
- package/src/components/providers/socket-provider.tsx +100 -0
- package/src/components/sidebar/file-browser/use-file-tab-search.ts +35 -12
- package/src/components/task/attempt-list.tsx +3 -5
- package/src/components/task/conversation-view-historical-user-turn.tsx +4 -1
- package/src/components/task/conversation-view-streaming-prompt-bubble.tsx +1 -1
- package/src/components/task/conversation-view.tsx +23 -10
- package/src/components/task/floating-chat-window.tsx +31 -29
- package/src/components/task/interactive-command/interactive-command-overlay.tsx +2 -6
- package/src/components/task/task-detail-panel.tsx +3 -1
- package/src/components/task/use-task-attempt-stream-handler.ts +14 -2
- package/src/components/task/use-task-stats.ts +11 -7
- package/src/components/team-view/team-view.tsx +3 -3
- package/src/components/ui/detachable-window.tsx +4 -4
- package/src/hooks/use-attempt-questions.ts +5 -9
- package/src/hooks/use-attempt-socket.ts +38 -1
- package/src/hooks/use-attempt-stream-running-attempt-utils.ts +6 -10
- package/src/hooks/use-attempt-stream.ts +51 -2
- package/src/hooks/use-butler-notifications.ts +34 -0
- package/src/hooks/use-butler.ts +25 -0
- package/src/hooks/use-kanban-url-sync-and-deep-links.ts +2 -8
- package/src/lib/agent-manager.ts +10 -6
- package/src/lib/autopilot/autopilot-manager.ts +43 -30
- package/src/lib/butler/butler-action-executor.ts +112 -0
- package/src/lib/butler/butler-attempt-resumption-service.ts +137 -0
- package/src/lib/butler/butler-decision-loop.ts +89 -0
- package/src/lib/butler/butler-event-collector.ts +42 -0
- package/src/lib/butler/butler-lifecycle-service.ts +84 -0
- package/src/lib/butler/butler-manager.ts +231 -0
- package/src/lib/butler/butler-memory-manager.ts +116 -0
- package/src/lib/butler/butler-notification-service.ts +70 -0
- package/src/lib/butler/butler-persona-loader.ts +47 -0
- package/src/lib/butler/butler-project-initializer.ts +179 -0
- package/src/lib/butler/butler-prompt-builder.ts +114 -0
- package/src/lib/butler/butler-rule-engine.ts +242 -0
- package/src/lib/butler/butler-scheduler-service.ts +336 -0
- package/src/lib/butler/butler-session-spawner.ts +134 -0
- package/src/lib/butler/butler-types.ts +110 -0
- package/src/lib/butler/butler-workspace-api.ts +122 -0
- package/src/lib/butler/index.ts +22 -0
- package/src/lib/butler-manager-singleton.ts +21 -0
- package/src/lib/services/task-api-service.ts +252 -0
- package/src/lib/services/task-service-with-socket-emit.ts +51 -0
- package/src/lib/socket-io-server-singleton.ts +15 -0
- package/src/stores/butler-store.ts +68 -0
- package/src/stores/model-store.ts +16 -27
- package/src/stores/task-store-api-actions.ts +9 -23
- package/src/stores/task-store-mutation-api-actions.ts +6 -30
package/locales/de.json
CHANGED
|
@@ -617,6 +617,7 @@
|
|
|
617
617
|
"cancelled": "Abgebrochen",
|
|
618
618
|
"detachToFloating": "In schwebendes Fenster abtrennen",
|
|
619
619
|
"maximizeToPanel": "Als Panel maximieren",
|
|
620
|
+
"sendError": "Senden fehlgeschlagen",
|
|
620
621
|
"clearConversation": "Unterhaltung löschen",
|
|
621
622
|
"clearConversationConfirm": "Sind Sie sicher, dass Sie alle Nachrichten löschen möchten? Dies kann nicht rückgängig gemacht werden.",
|
|
622
623
|
"compactConversation": "Unterhaltung komprimieren",
|
|
@@ -787,6 +788,18 @@
|
|
|
787
788
|
"description": "Die gesuchte Seite existiert nicht.",
|
|
788
789
|
"goHome": "Zurück zur Startseite"
|
|
789
790
|
},
|
|
791
|
+
"butler": {
|
|
792
|
+
"disabled": "Butler deaktiviert",
|
|
793
|
+
"status_idle": "Butler inaktiv",
|
|
794
|
+
"status_initializing": "Butler startet...",
|
|
795
|
+
"status_running": "Butler aktiv",
|
|
796
|
+
"status_reasoning": "Butler denkt nach...",
|
|
797
|
+
"status_shutting_down": "Butler wird gestoppt...",
|
|
798
|
+
"enable": "Butler aktivieren",
|
|
799
|
+
"disable": "Butler deaktivieren",
|
|
800
|
+
"openProject": "Butler-Projekt öffnen",
|
|
801
|
+
"startTask": "Butler Task"
|
|
802
|
+
},
|
|
790
803
|
"globalError": {
|
|
791
804
|
"title": "Etwas ist schiefgelaufen!",
|
|
792
805
|
"fallbackMessage": "Ein unerwarteter Fehler ist aufgetreten",
|
package/locales/en.json
CHANGED
|
@@ -619,6 +619,7 @@
|
|
|
619
619
|
"cancelled": "Cancelled",
|
|
620
620
|
"detachToFloating": "Detach to floating window",
|
|
621
621
|
"maximizeToPanel": "Maximize to panel",
|
|
622
|
+
"sendError": "Failed to send",
|
|
622
623
|
"clearConversation": "Clear Conversation",
|
|
623
624
|
"clearConversationConfirm": "Are you sure you want to clear all messages? This cannot be undone.",
|
|
624
625
|
"compactConversation": "Compact Conversation",
|
|
@@ -792,6 +793,25 @@
|
|
|
792
793
|
"description": "The page you're looking for doesn't exist.",
|
|
793
794
|
"goHome": "Go back home"
|
|
794
795
|
},
|
|
796
|
+
"butler": {
|
|
797
|
+
"disabled": "Butler disabled",
|
|
798
|
+
"status_idle": "Butler idle",
|
|
799
|
+
"status_initializing": "Butler starting...",
|
|
800
|
+
"status_running": "Butler active",
|
|
801
|
+
"status_reasoning": "Butler thinking...",
|
|
802
|
+
"status_shutting_down": "Butler stopping...",
|
|
803
|
+
"enable": "Enable Butler",
|
|
804
|
+
"disable": "Disable Butler",
|
|
805
|
+
"openProject": "Open Butler Project",
|
|
806
|
+
"notifications": "Notifications",
|
|
807
|
+
"noNotifications": "No notifications",
|
|
808
|
+
"clearAll": "Clear all",
|
|
809
|
+
"justNow": "just now",
|
|
810
|
+
"minutesAgo": "{m}m ago",
|
|
811
|
+
"hoursAgo": "{h}h ago",
|
|
812
|
+
"daysAgo": "{d}d ago",
|
|
813
|
+
"startTask": "Butler Task"
|
|
814
|
+
},
|
|
795
815
|
"globalError": {
|
|
796
816
|
"title": "Something went wrong!",
|
|
797
817
|
"fallbackMessage": "An unexpected error occurred",
|
package/locales/es.json
CHANGED
|
@@ -617,6 +617,7 @@
|
|
|
617
617
|
"cancelled": "Cancelado",
|
|
618
618
|
"detachToFloating": "Desacoplar a ventana flotante",
|
|
619
619
|
"maximizeToPanel": "Maximizar al panel",
|
|
620
|
+
"sendError": "Error al enviar",
|
|
620
621
|
"clearConversation": "Limpiar Conversación",
|
|
621
622
|
"clearConversationConfirm": "¿Está seguro de que desea borrar todos los mensajes? Esta acción no se puede deshacer.",
|
|
622
623
|
"compactConversation": "Compactar Conversación",
|
|
@@ -787,6 +788,18 @@
|
|
|
787
788
|
"description": "La página que busca no existe.",
|
|
788
789
|
"goHome": "Volver al inicio"
|
|
789
790
|
},
|
|
791
|
+
"butler": {
|
|
792
|
+
"disabled": "Butler desactivado",
|
|
793
|
+
"status_idle": "Butler inactivo",
|
|
794
|
+
"status_initializing": "Butler iniciando...",
|
|
795
|
+
"status_running": "Butler activo",
|
|
796
|
+
"status_reasoning": "Butler pensando...",
|
|
797
|
+
"status_shutting_down": "Butler deteniéndose...",
|
|
798
|
+
"enable": "Activar Butler",
|
|
799
|
+
"disable": "Desactivar Butler",
|
|
800
|
+
"openProject": "Abrir proyecto Butler",
|
|
801
|
+
"startTask": "Butler Task"
|
|
802
|
+
},
|
|
790
803
|
"globalError": {
|
|
791
804
|
"title": "¡Algo salió mal!",
|
|
792
805
|
"fallbackMessage": "Ocurrió un error inesperado",
|
package/locales/fr.json
CHANGED
|
@@ -617,6 +617,7 @@
|
|
|
617
617
|
"cancelled": "Annulé",
|
|
618
618
|
"detachToFloating": "Détacher en fenêtre flottante",
|
|
619
619
|
"maximizeToPanel": "Maximiser en panneau",
|
|
620
|
+
"sendError": "Échec de l'envoi",
|
|
620
621
|
"clearConversation": "Effacer la conversation",
|
|
621
622
|
"clearConversationConfirm": "Êtes-vous sûr de vouloir effacer tous les messages ? Cette action est irréversible.",
|
|
622
623
|
"compactConversation": "Compacter la conversation",
|
|
@@ -787,6 +788,18 @@
|
|
|
787
788
|
"description": "La page que vous recherchez n'existe pas.",
|
|
788
789
|
"goHome": "Retourner à l'accueil"
|
|
789
790
|
},
|
|
791
|
+
"butler": {
|
|
792
|
+
"disabled": "Butler désactivé",
|
|
793
|
+
"status_idle": "Butler inactif",
|
|
794
|
+
"status_initializing": "Butler en démarrage...",
|
|
795
|
+
"status_running": "Butler actif",
|
|
796
|
+
"status_reasoning": "Butler en réflexion...",
|
|
797
|
+
"status_shutting_down": "Butler en arrêt...",
|
|
798
|
+
"enable": "Activer Butler",
|
|
799
|
+
"disable": "Désactiver Butler",
|
|
800
|
+
"openProject": "Ouvrir le projet Butler",
|
|
801
|
+
"startTask": "Butler Task"
|
|
802
|
+
},
|
|
790
803
|
"globalError": {
|
|
791
804
|
"title": "Quelque chose s'est mal passé !",
|
|
792
805
|
"fallbackMessage": "Une erreur inattendue s'est produite",
|
package/locales/ja.json
CHANGED
|
@@ -617,6 +617,7 @@
|
|
|
617
617
|
"cancelled": "キャンセルされました",
|
|
618
618
|
"detachToFloating": "フローティングウィンドウにデタッチ",
|
|
619
619
|
"maximizeToPanel": "パネルに最大化",
|
|
620
|
+
"sendError": "送信に失敗しました",
|
|
620
621
|
"clearConversation": "会話をクリア",
|
|
621
622
|
"clearConversationConfirm": "すべてのメッセージをクリアしてもよろしいですか?この操作は元に戻せません。",
|
|
622
623
|
"compactConversation": "会話をコンパクト化",
|
|
@@ -787,6 +788,18 @@
|
|
|
787
788
|
"description": "お探しのページは存在しません。",
|
|
788
789
|
"goHome": "ホームに戻る"
|
|
789
790
|
},
|
|
791
|
+
"butler": {
|
|
792
|
+
"disabled": "バトラー無効",
|
|
793
|
+
"status_idle": "バトラー待機中",
|
|
794
|
+
"status_initializing": "バトラー起動中...",
|
|
795
|
+
"status_running": "バトラー稼働中",
|
|
796
|
+
"status_reasoning": "バトラー思考中...",
|
|
797
|
+
"status_shutting_down": "バトラー停止中...",
|
|
798
|
+
"enable": "バトラーを有効にする",
|
|
799
|
+
"disable": "バトラーを無効にする",
|
|
800
|
+
"openProject": "バトラープロジェクトを開く",
|
|
801
|
+
"startTask": "Butler Task"
|
|
802
|
+
},
|
|
790
803
|
"globalError": {
|
|
791
804
|
"title": "問題が発生しました!",
|
|
792
805
|
"fallbackMessage": "予期しないエラーが発生しました",
|
package/locales/ko.json
CHANGED
|
@@ -617,6 +617,7 @@
|
|
|
617
617
|
"cancelled": "취소됨",
|
|
618
618
|
"detachToFloating": "플로팅 창으로 분리",
|
|
619
619
|
"maximizeToPanel": "패널로 최대화",
|
|
620
|
+
"sendError": "전송 실패",
|
|
620
621
|
"clearConversation": "대화 지우기",
|
|
621
622
|
"clearConversationConfirm": "모든 메시지를 지우시겠습니까? 이 작업은 취소할 수 없습니다.",
|
|
622
623
|
"compactConversation": "대화 압축",
|
|
@@ -787,6 +788,18 @@
|
|
|
787
788
|
"description": "찾고 있는 페이지가 존재하지 않습니다.",
|
|
788
789
|
"goHome": "홈으로 돌아가기"
|
|
789
790
|
},
|
|
791
|
+
"butler": {
|
|
792
|
+
"disabled": "버틀러 비활성화",
|
|
793
|
+
"status_idle": "버틀러 대기 중",
|
|
794
|
+
"status_initializing": "버틀러 시작 중...",
|
|
795
|
+
"status_running": "버틀러 활성",
|
|
796
|
+
"status_reasoning": "버틀러 생각 중...",
|
|
797
|
+
"status_shutting_down": "버틀러 종료 중...",
|
|
798
|
+
"enable": "버틀러 활성화",
|
|
799
|
+
"disable": "버틀러 비활성화",
|
|
800
|
+
"openProject": "버틀러 프로젝트 열기",
|
|
801
|
+
"startTask": "Butler Task"
|
|
802
|
+
},
|
|
790
803
|
"globalError": {
|
|
791
804
|
"title": "문제가 발생했습니다!",
|
|
792
805
|
"fallbackMessage": "예기치 않은 오류가 발생했습니다",
|
package/locales/vi.json
CHANGED
|
@@ -619,6 +619,7 @@
|
|
|
619
619
|
"cancelled": "Đã hủy",
|
|
620
620
|
"detachToFloating": "Tách thành cửa sổ nổi",
|
|
621
621
|
"maximizeToPanel": "Phóng to thành bảng",
|
|
622
|
+
"sendError": "Gửi thất bại",
|
|
622
623
|
"clearConversation": "Xóa cuộc trò chuyện",
|
|
623
624
|
"clearConversationConfirm": "Bạn có chắc muốn xóa tất cả tin nhắn? Hành động này không thể hoàn tác.",
|
|
624
625
|
"compactConversation": "Nén cuộc trò chuyện",
|
|
@@ -789,6 +790,25 @@
|
|
|
789
790
|
"description": "Trang bạn đang tìm không tồn tại.",
|
|
790
791
|
"goHome": "Quay về trang chủ"
|
|
791
792
|
},
|
|
793
|
+
"butler": {
|
|
794
|
+
"disabled": "Butler đã tắt",
|
|
795
|
+
"status_idle": "Butler đang rảnh",
|
|
796
|
+
"status_initializing": "Butler đang khởi động...",
|
|
797
|
+
"status_running": "Butler đang hoạt động",
|
|
798
|
+
"status_reasoning": "Butler đang suy nghĩ...",
|
|
799
|
+
"status_shutting_down": "Butler đang dừng...",
|
|
800
|
+
"enable": "Bật Butler",
|
|
801
|
+
"disable": "Tắt Butler",
|
|
802
|
+
"openProject": "Mở dự án Butler",
|
|
803
|
+
"notifications": "Thông báo",
|
|
804
|
+
"noNotifications": "Không có thông báo",
|
|
805
|
+
"clearAll": "Xóa tất cả",
|
|
806
|
+
"justNow": "vừa xong",
|
|
807
|
+
"minutesAgo": "{m} phút trước",
|
|
808
|
+
"hoursAgo": "{h} giờ trước",
|
|
809
|
+
"daysAgo": "{d} ngày trước",
|
|
810
|
+
"startTask": "Butler Task"
|
|
811
|
+
},
|
|
792
812
|
"globalError": {
|
|
793
813
|
"title": "Đã xảy ra lỗi!",
|
|
794
814
|
"fallbackMessage": "Đã xảy ra lỗi không mong đợi",
|
package/locales/zh.json
CHANGED
|
@@ -617,6 +617,7 @@
|
|
|
617
617
|
"cancelled": "已取消",
|
|
618
618
|
"detachToFloating": "分离为浮动窗口",
|
|
619
619
|
"maximizeToPanel": "最大化为面板",
|
|
620
|
+
"sendError": "发送失败",
|
|
620
621
|
"clearConversation": "清除对话",
|
|
621
622
|
"clearConversationConfirm": "您确定要清除所有消息吗?此操作无法撤销。",
|
|
622
623
|
"compactConversation": "压缩对话",
|
|
@@ -787,6 +788,18 @@
|
|
|
787
788
|
"description": "您所查找的页面不存在。",
|
|
788
789
|
"goHome": "返回首页"
|
|
789
790
|
},
|
|
791
|
+
"butler": {
|
|
792
|
+
"disabled": "管家已禁用",
|
|
793
|
+
"status_idle": "管家空闲",
|
|
794
|
+
"status_initializing": "管家启动中...",
|
|
795
|
+
"status_running": "管家运行中",
|
|
796
|
+
"status_reasoning": "管家思考中...",
|
|
797
|
+
"status_shutting_down": "管家停止中...",
|
|
798
|
+
"enable": "启用管家",
|
|
799
|
+
"disable": "禁用管家",
|
|
800
|
+
"openProject": "打开管家项目",
|
|
801
|
+
"startTask": "Butler Task"
|
|
802
|
+
},
|
|
790
803
|
"globalError": {
|
|
791
804
|
"title": "出现错误!",
|
|
792
805
|
"fallbackMessage": "发生了意外错误",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-ws",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1-beta.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "AI-powered workspace for solo CEOs and indie builders — manage your entire business with AI agents, not just code. Kanban board, code editor, Git integration, claw agent hub, local-first SQLite.",
|
|
6
6
|
"keywords": [
|
|
@@ -11,10 +11,11 @@ import { homedir } from 'os';
|
|
|
11
11
|
import { query, type Query } from '@anthropic-ai/claude-agent-sdk';
|
|
12
12
|
import { createRequire } from 'module';
|
|
13
13
|
|
|
14
|
-
// Resolve cli.js path
|
|
15
|
-
//
|
|
14
|
+
// Resolve cli.js path by finding the SDK package directory first, then appending cli.js.
|
|
15
|
+
// Direct subpath resolution fails because cli.js is not in the SDK's exports map.
|
|
16
16
|
const require_ = createRequire(import.meta.url);
|
|
17
|
-
const
|
|
17
|
+
const sdkMainPath = require_.resolve('@anthropic-ai/claude-agent-sdk');
|
|
18
|
+
const CLAUDE_CLI_PATH = join(sdkMainPath, '..', 'cli.js');
|
|
18
19
|
import { adaptSDKMessage, isValidSDKMessage } from './claude-sdk-message-to-output-adapter';
|
|
19
20
|
import type { SDKResultMessage } from './claude-sdk-message-to-output-adapter';
|
|
20
21
|
import { createLogger } from '../lib/pino-logger';
|
|
@@ -36,7 +36,7 @@ export async function buildConversationHistory(db: any, taskId: string): Promise
|
|
|
36
36
|
|
|
37
37
|
turns.push({
|
|
38
38
|
type: 'user',
|
|
39
|
-
prompt: attempt.displayPrompt
|
|
39
|
+
prompt: attempt.displayPrompt ?? attempt.prompt,
|
|
40
40
|
messages: [],
|
|
41
41
|
attemptId: attempt.id,
|
|
42
42
|
timestamp: attempt.createdAt,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Read-heavy query methods (conversation history, stats, running attempt) live in
|
|
4
4
|
* task-attempt-and-conversation-queries.ts and are composed in here.
|
|
5
5
|
*/
|
|
6
|
-
import { eq, and, desc, inArray } from 'drizzle-orm';
|
|
6
|
+
import { eq, and, desc, inArray, gte, ne, sql } from 'drizzle-orm';
|
|
7
7
|
import * as schema from '../../db/database-schema';
|
|
8
8
|
import { generateId } from '../../lib/nanoid-id-generator';
|
|
9
9
|
import { createTaskQueryMethods } from './task-attempt-and-conversation-queries';
|
|
@@ -85,6 +85,20 @@ export function createTaskService(db: any) {
|
|
|
85
85
|
// --- reorder ---
|
|
86
86
|
|
|
87
87
|
async reorder(taskId: string, newPosition: number, newStatus?: string) {
|
|
88
|
+
const task = db.select().from(schema.tasks).where(eq(schema.tasks.id, taskId)).get();
|
|
89
|
+
if (!task) return null;
|
|
90
|
+
|
|
91
|
+
const targetStatus = (newStatus || task.status) as any;
|
|
92
|
+
|
|
93
|
+
// Shift tasks at position >= newPosition up by 1 in the target column
|
|
94
|
+
await db.update(schema.tasks)
|
|
95
|
+
.set({ position: sql`${schema.tasks.position} + 1` })
|
|
96
|
+
.where(and(
|
|
97
|
+
eq(schema.tasks.status, targetStatus),
|
|
98
|
+
gte(schema.tasks.position, newPosition),
|
|
99
|
+
ne(schema.tasks.id, taskId)
|
|
100
|
+
));
|
|
101
|
+
|
|
88
102
|
const updates: any = { position: newPosition, updatedAt: Date.now() };
|
|
89
103
|
if (newStatus) updates.status = newStatus;
|
|
90
104
|
await db.update(schema.tasks).set(updates).where(eq(schema.tasks.id, taskId));
|
package/server.ts
CHANGED
|
@@ -47,6 +47,7 @@ const log = createLogger('Server');
|
|
|
47
47
|
import { eq } from 'drizzle-orm';
|
|
48
48
|
import { nanoid } from 'nanoid';
|
|
49
49
|
import type { AttemptStatus } from './src/types';
|
|
50
|
+
import { setSocketServer } from './src/lib/socket-io-server-singleton';
|
|
50
51
|
import { processAttachments } from './src/lib/file-processor';
|
|
51
52
|
import { usageTracker } from './src/lib/usage-tracker';
|
|
52
53
|
import { workflowTracker } from './src/lib/workflow-tracker';
|
|
@@ -56,6 +57,9 @@ import { getMinioPullQueueWorker } from './src/lib/minio-pull-queue';
|
|
|
56
57
|
import { getMinioPushQueueWorker } from './src/lib/minio-push-queue';
|
|
57
58
|
import { createAutopilotManager, appendQuestionAnswer, appendSubagentEnded, appendTrackedTaskUpdate } from './src/lib/autopilot';
|
|
58
59
|
import type { AutopilotMode } from './src/lib/autopilot';
|
|
60
|
+
import { createButlerManager, type ButlerManager } from './src/lib/butler';
|
|
61
|
+
import { createTaskService } from '@agentic-sdk/services/task/task-crud-and-reorder';
|
|
62
|
+
import { createTaskServiceWithSocketEmit } from './src/lib/services/task-service-with-socket-emit';
|
|
59
63
|
|
|
60
64
|
import { getPort, getHostname } from './src/lib/server-port-configuration';
|
|
61
65
|
|
|
@@ -141,6 +145,9 @@ app.prepare().then(async () => {
|
|
|
141
145
|
|
|
142
146
|
log.info(`[Server] Restored ${shellManager.runningCount} running shells`);
|
|
143
147
|
|
|
148
|
+
// Initialize task service with auto-emit for Socket.IO events
|
|
149
|
+
const taskService = createTaskServiceWithSocketEmit(createTaskService(db));
|
|
150
|
+
|
|
144
151
|
// Initialize Autopilot Manager
|
|
145
152
|
const autopilotManager = createAutopilotManager();
|
|
146
153
|
autopilotManager.registerQuestionListener(agentManager);
|
|
@@ -161,8 +168,24 @@ app.prepare().then(async () => {
|
|
|
161
168
|
},
|
|
162
169
|
});
|
|
163
170
|
|
|
171
|
+
// Expose io singleton for Next.js API routes to emit events
|
|
172
|
+
setSocketServer(io);
|
|
173
|
+
|
|
164
174
|
// Restore autopilot state from DB (needs io for worker callbacks)
|
|
165
|
-
await autopilotManager.restoreFromDb({ db, io, schema, agentManager, sessionManager });
|
|
175
|
+
await autopilotManager.restoreFromDb({ db, io, schema, agentManager, sessionManager, taskService });
|
|
176
|
+
|
|
177
|
+
// Initialize Butler Agent (workspace-wide orchestrator)
|
|
178
|
+
// NOTE: Do not await — Butler spawns SDK sessions that hit our proxy endpoint,
|
|
179
|
+
// which requires httpServer.listen() to complete first. Fire and forget to avoid deadlock.
|
|
180
|
+
const butlerManager = createButlerManager();
|
|
181
|
+
|
|
182
|
+
// Export butlerManager singleton for API routes to access
|
|
183
|
+
const globalKey = '__claude_butler_manager__' as const;
|
|
184
|
+
(globalThis as any)[globalKey] = butlerManager;
|
|
185
|
+
|
|
186
|
+
butlerManager.initialize({ db, io, schema, agentManager, sessionManager }).catch(err => {
|
|
187
|
+
log.error({ err }, '[Butler] Background initialization failed');
|
|
188
|
+
});
|
|
166
189
|
|
|
167
190
|
// Disconnect cleanup timers - keyed by attemptId
|
|
168
191
|
const disconnectTimers = new Map<string, NodeJS.Timeout>();
|
|
@@ -408,25 +431,33 @@ app.prepare().then(async () => {
|
|
|
408
431
|
|
|
409
432
|
// Update task status to in_progress if it was todo
|
|
410
433
|
// Also clear pendingFileIds since they've been processed
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
await
|
|
416
|
-
.update(schema.tasks)
|
|
417
|
-
.set(taskUpdates)
|
|
418
|
-
.where(eq(schema.tasks.id, taskId));
|
|
434
|
+
if (task.status === 'todo' || task.pendingFileIds) {
|
|
435
|
+
const taskUpdates: any = {};
|
|
436
|
+
if (task.status === 'todo') taskUpdates.status = 'in_progress';
|
|
437
|
+
if (task.pendingFileIds) taskUpdates.pendingFileIds = null;
|
|
438
|
+
await taskService.update(taskId, taskUpdates);
|
|
419
439
|
}
|
|
420
440
|
|
|
421
441
|
// Join attempt room
|
|
422
442
|
socket.join(`attempt:${attemptId}`);
|
|
423
443
|
|
|
444
|
+
// Inject Butler persona into prompt when task belongs to Butler's project
|
|
445
|
+
let effectivePrompt = prompt;
|
|
446
|
+
const butlerProjectId = butlerManager.getProjectId();
|
|
447
|
+
if (butlerProjectId && task.projectId === butlerProjectId) {
|
|
448
|
+
const personaPrefix = butlerManager.buildPersonaPrompt();
|
|
449
|
+
if (personaPrefix) {
|
|
450
|
+
effectivePrompt = `${personaPrefix}\n\n---\n\n${prompt}`;
|
|
451
|
+
log.info({ taskId }, '[Butler] Persona context injected into task prompt');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
424
455
|
// Start Claude Agent SDK query
|
|
425
456
|
|
|
426
457
|
agentManager.start({
|
|
427
458
|
attemptId,
|
|
428
459
|
projectPath: project.path,
|
|
429
|
-
prompt,
|
|
460
|
+
prompt: effectivePrompt,
|
|
430
461
|
model: model || undefined,
|
|
431
462
|
provider: provider || undefined,
|
|
432
463
|
sessionOptions: Object.keys(sessionOptions).length > 0 ? sessionOptions : undefined,
|
|
@@ -448,6 +479,11 @@ app.prepare().then(async () => {
|
|
|
448
479
|
|
|
449
480
|
// Global event for all clients to track running tasks
|
|
450
481
|
io.emit('task:started', { taskId });
|
|
482
|
+
|
|
483
|
+
// Forward to butler event collector
|
|
484
|
+
if (butlerManager.isEnabled()) {
|
|
485
|
+
butlerManager.pushEvent({ type: 'task:started', payload: { taskId }, timestamp: Date.now() });
|
|
486
|
+
}
|
|
451
487
|
} catch (error) {
|
|
452
488
|
log.error({ error }, 'Error starting attempt');
|
|
453
489
|
socket.emit('error', {
|
|
@@ -896,7 +932,7 @@ app.prepare().then(async () => {
|
|
|
896
932
|
if (!['off', 'autonomous', 'ask'].includes(mode)) return;
|
|
897
933
|
log.info({ projectId, mode }, '[Autopilot] Setting mode');
|
|
898
934
|
await autopilotManager.setMode(projectId, mode as AutopilotMode, {
|
|
899
|
-
db, io, schema, agentManager, sessionManager,
|
|
935
|
+
db, io, schema, agentManager, sessionManager, taskService,
|
|
900
936
|
});
|
|
901
937
|
});
|
|
902
938
|
|
|
@@ -904,7 +940,7 @@ app.prepare().then(async () => {
|
|
|
904
940
|
socket.on('autopilot:enable', async (data: { projectId: string }) => {
|
|
905
941
|
log.info({ projectId: data.projectId }, '[Autopilot] Enable (compat → autonomous)');
|
|
906
942
|
await autopilotManager.setMode(data.projectId, 'autonomous', {
|
|
907
|
-
db, io, schema, agentManager, sessionManager,
|
|
943
|
+
db, io, schema, agentManager, sessionManager, taskService,
|
|
908
944
|
});
|
|
909
945
|
});
|
|
910
946
|
|
|
@@ -912,7 +948,7 @@ app.prepare().then(async () => {
|
|
|
912
948
|
socket.on('autopilot:disable', async (data: { projectId: string }) => {
|
|
913
949
|
log.info({ projectId: data.projectId }, '[Autopilot] Disable (compat → off)');
|
|
914
950
|
await autopilotManager.setMode(data.projectId, 'off', {
|
|
915
|
-
db, io, schema, agentManager, sessionManager,
|
|
951
|
+
db, io, schema, agentManager, sessionManager, taskService,
|
|
916
952
|
});
|
|
917
953
|
});
|
|
918
954
|
|
|
@@ -924,6 +960,99 @@ app.prepare().then(async () => {
|
|
|
924
960
|
});
|
|
925
961
|
});
|
|
926
962
|
|
|
963
|
+
// Butler handlers
|
|
964
|
+
socket.on('butler:status-request', () => {
|
|
965
|
+
socket.emit('butler:status', butlerManager.getStatus());
|
|
966
|
+
// Replay buffered notifications to late-connecting clients
|
|
967
|
+
const notifService = butlerManager.getNotificationService();
|
|
968
|
+
if (notifService) {
|
|
969
|
+
const buffered = notifService.getRecentNotifications();
|
|
970
|
+
for (const n of buffered) {
|
|
971
|
+
socket.emit('butler:notification', n);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
socket.on('butler:clear-notifications', () => {
|
|
977
|
+
const notifService = butlerManager.getNotificationService();
|
|
978
|
+
if (notifService) notifService.clearBuffer();
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
socket.on('butler:enable', async () => {
|
|
982
|
+
log.info('[Butler] Enable requested');
|
|
983
|
+
await butlerManager.start();
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
socket.on('butler:disable', async () => {
|
|
987
|
+
log.info('[Butler] Disable requested by user');
|
|
988
|
+
await butlerManager.stop(true);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
// Butler scheduler handlers
|
|
992
|
+
socket.on('butler:schedule-create', async (data: { cronExpression: string; actionType: string; actionPayload: any }) => {
|
|
993
|
+
try {
|
|
994
|
+
const scheduler = butlerManager.getSchedulerService();
|
|
995
|
+
if (!scheduler) {
|
|
996
|
+
socket.emit('butler:error', { message: 'Scheduler not available' });
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
const task = await scheduler.createTask(data.cronExpression, data.actionType as any, data.actionPayload);
|
|
1000
|
+
socket.emit('butler:schedule-created', task);
|
|
1001
|
+
} catch (err) {
|
|
1002
|
+
log.error({ err }, '[Butler] Failed to create scheduled task');
|
|
1003
|
+
socket.emit('butler:error', { message: 'Failed to create scheduled task' });
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
socket.on('butler:schedule-list', async () => {
|
|
1008
|
+
try {
|
|
1009
|
+
const scheduler = butlerManager.getSchedulerService();
|
|
1010
|
+
if (!scheduler) {
|
|
1011
|
+
socket.emit('butler:schedule-list', []);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
const tasks = scheduler.listTasks();
|
|
1015
|
+
socket.emit('butler:schedule-list', tasks);
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
log.error({ err }, '[Butler] Failed to list scheduled tasks');
|
|
1018
|
+
socket.emit('butler:error', { message: 'Failed to list scheduled tasks' });
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
socket.on('butler:schedule-delete', async (data: { id: string }) => {
|
|
1023
|
+
try {
|
|
1024
|
+
const scheduler = butlerManager.getSchedulerService();
|
|
1025
|
+
if (!scheduler) {
|
|
1026
|
+
socket.emit('butler:error', { message: 'Scheduler not available' });
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const deleted = await scheduler.deleteTask(data.id);
|
|
1030
|
+
socket.emit('butler:schedule-deleted', { id: data.id, success: deleted });
|
|
1031
|
+
} catch (err) {
|
|
1032
|
+
log.error({ err }, '[Butler] Failed to delete scheduled task');
|
|
1033
|
+
socket.emit('butler:error', { message: 'Failed to delete scheduled task' });
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
socket.on('butler:schedule-update', async (data: { id: string; updates: any }) => {
|
|
1038
|
+
try {
|
|
1039
|
+
const scheduler = butlerManager.getSchedulerService();
|
|
1040
|
+
if (!scheduler) {
|
|
1041
|
+
socket.emit('butler:error', { message: 'Scheduler not available' });
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const task = await scheduler.updateTask(data.id, data.updates);
|
|
1045
|
+
if (!task) {
|
|
1046
|
+
socket.emit('butler:error', { message: 'Task not found' });
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
socket.emit('butler:schedule-updated', task);
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
log.error({ err }, '[Butler] Failed to update scheduled task');
|
|
1052
|
+
socket.emit('butler:error', { message: 'Failed to update scheduled task' });
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
|
|
927
1056
|
socket.on('disconnect', () => {
|
|
928
1057
|
log.info(`Client disconnected: ${socket.id}`);
|
|
929
1058
|
|
|
@@ -1574,6 +1703,25 @@ app.prepare().then(async () => {
|
|
|
1574
1703
|
// Global event for all clients to track completed tasks
|
|
1575
1704
|
if (attempt?.taskId) {
|
|
1576
1705
|
io.emit('task:finished', { taskId: attempt.taskId, status });
|
|
1706
|
+
|
|
1707
|
+
// Forward to butler event collector (include projectId for per-project rule matching)
|
|
1708
|
+
if (butlerManager.isEnabled()) {
|
|
1709
|
+
const taskRow = await db.query.tasks.findFirst({ where: eq(schema.tasks.id, attempt.taskId) });
|
|
1710
|
+
const projectRow = taskRow?.projectId
|
|
1711
|
+
? await db.query.projects.findFirst({ where: eq(schema.projects.id, taskRow.projectId) })
|
|
1712
|
+
: undefined;
|
|
1713
|
+
butlerManager.pushEvent({
|
|
1714
|
+
type: 'task:finished',
|
|
1715
|
+
payload: {
|
|
1716
|
+
taskId: attempt.taskId,
|
|
1717
|
+
status,
|
|
1718
|
+
projectId: taskRow?.projectId,
|
|
1719
|
+
taskTitle: taskRow?.title,
|
|
1720
|
+
projectName: projectRow?.name,
|
|
1721
|
+
},
|
|
1722
|
+
timestamp: Date.now(),
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1577
1725
|
}
|
|
1578
1726
|
|
|
1579
1727
|
const shouldCompact = !!(status === 'completed' && usageStats?.contextHealth?.shouldCompact);
|
|
@@ -1941,6 +2089,10 @@ app.prepare().then(async () => {
|
|
|
1941
2089
|
minioPushQueueWorker.stop();
|
|
1942
2090
|
log.info('> MinIO push queue worker stopped');
|
|
1943
2091
|
|
|
2092
|
+
// Stop butler agent
|
|
2093
|
+
await butlerManager.stop();
|
|
2094
|
+
log.info('> Butler agent stopped');
|
|
2095
|
+
|
|
1944
2096
|
// Cancel all Claude agents first
|
|
1945
2097
|
agentManager.cancelAll();
|
|
1946
2098
|
log.info('> Cancelled all Claude agents');
|
|
@@ -12,6 +12,7 @@ import { AccessAnywhereWizard } from '@/components/access-anywhere';
|
|
|
12
12
|
import { TerminalPanel } from '@/components/terminal/terminal-panel';
|
|
13
13
|
import { useProjectStore } from '@/stores/project-store';
|
|
14
14
|
import { useTaskStore } from '@/stores/task-store';
|
|
15
|
+
import { useButlerStore } from '@/stores/butler-store';
|
|
15
16
|
import { useFloatingWindowsStore } from '@/stores/floating-windows-store';
|
|
16
17
|
import { useTunnelStore } from '@/stores/tunnel-store';
|
|
17
18
|
import { useAgentFactoryUIStore } from '@/stores/agent-factory-ui-store';
|
|
@@ -73,6 +74,8 @@ function KanbanApp() {
|
|
|
73
74
|
|
|
74
75
|
const { projects, selectedProjectIds, loading: projectLoading, error: projectError } = useProjectStore();
|
|
75
76
|
const { selectedTask, setSelectedTask, setPendingAutoStartTask, setSelectedTaskId } = useTaskStore();
|
|
77
|
+
const butlerEnabled = useButlerStore(s => s.enabled);
|
|
78
|
+
const butlerProjectId = useButlerStore(s => s.projectId);
|
|
76
79
|
|
|
77
80
|
const autoShowSetup = !projectLoading && !projectError && projects.length === 0;
|
|
78
81
|
|
|
@@ -88,9 +91,25 @@ function KanbanApp() {
|
|
|
88
91
|
// Fetch tasks when project selection changes
|
|
89
92
|
useEffect(() => {
|
|
90
93
|
if (!projectLoading) {
|
|
91
|
-
|
|
94
|
+
let projectIds = selectedProjectIds;
|
|
95
|
+
// Auto-include butler project when butler is enabled and user has selected specific projects
|
|
96
|
+
// Skip when all-projects mode (empty array) — butler tasks already included in that mode
|
|
97
|
+
if (butlerEnabled && butlerProjectId && projectIds.length > 0 && !projectIds.includes(butlerProjectId)) {
|
|
98
|
+
projectIds = [...projectIds, butlerProjectId];
|
|
99
|
+
}
|
|
100
|
+
useTaskStore.getState().fetchTasks(projectIds);
|
|
101
|
+
}
|
|
102
|
+
}, [selectedProjectIds, projectLoading, butlerEnabled, butlerProjectId]);
|
|
103
|
+
|
|
104
|
+
// Re-fetch when butler status changes (handles delayed socket connection)
|
|
105
|
+
// This fixes timing issue where butler:status arrives after initial render
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!projectLoading && butlerEnabled && butlerProjectId && selectedProjectIds.length > 0) {
|
|
108
|
+
if (!selectedProjectIds.includes(butlerProjectId)) {
|
|
109
|
+
useTaskStore.getState().fetchTasks([...selectedProjectIds, butlerProjectId]);
|
|
110
|
+
}
|
|
92
111
|
}
|
|
93
|
-
}, [
|
|
112
|
+
}, [butlerEnabled, butlerProjectId, projectLoading, selectedProjectIds]);
|
|
94
113
|
|
|
95
114
|
// Mobile: redirect panel selection to floating window
|
|
96
115
|
useEffect(() => {
|