hakimi 0.1.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.
Files changed (43) hide show
  1. package/README.md +71 -0
  2. package/dist/App.d.ts +5 -0
  3. package/dist/App.js +80 -0
  4. package/dist/components/HotkeyHint.d.ts +9 -0
  5. package/dist/components/HotkeyHint.js +5 -0
  6. package/dist/components/MessageLog.d.ts +12 -0
  7. package/dist/components/MessageLog.js +10 -0
  8. package/dist/components/StatusBar.d.ts +7 -0
  9. package/dist/components/StatusBar.js +5 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +19 -0
  12. package/dist/screens/ConfigScreen.d.ts +7 -0
  13. package/dist/screens/ConfigScreen.js +114 -0
  14. package/dist/screens/HomeScreen.d.ts +12 -0
  15. package/dist/screens/HomeScreen.js +27 -0
  16. package/dist/screens/LoginScreen.d.ts +7 -0
  17. package/dist/screens/LoginScreen.js +55 -0
  18. package/dist/services/chatAgent.d.ts +18 -0
  19. package/dist/services/chatAgent.js +110 -0
  20. package/dist/services/chatRouter.d.ts +39 -0
  21. package/dist/services/chatRouter.js +246 -0
  22. package/dist/services/configAgent.d.ts +18 -0
  23. package/dist/services/configAgent.js +125 -0
  24. package/dist/services/loginService.d.ts +15 -0
  25. package/dist/services/loginService.js +48 -0
  26. package/dist/services/sessionCache.d.ts +18 -0
  27. package/dist/services/sessionCache.js +69 -0
  28. package/dist/services/theAgent.d.ts +19 -0
  29. package/dist/services/theAgent.js +139 -0
  30. package/dist/tools/askUser.d.ts +30 -0
  31. package/dist/tools/askUser.js +16 -0
  32. package/dist/tools/finishConfig.d.ts +64 -0
  33. package/dist/tools/finishConfig.js +20 -0
  34. package/dist/tools/sendMessage.d.ts +25 -0
  35. package/dist/tools/sendMessage.js +15 -0
  36. package/dist/utils/config.d.ts +16 -0
  37. package/dist/utils/config.js +44 -0
  38. package/dist/utils/paths.d.ts +4 -0
  39. package/dist/utils/paths.js +6 -0
  40. package/package.json +58 -0
  41. package/patches/@koishijs+loader+4.6.10.patch +13 -0
  42. package/patches/@moonshot-ai+kimi-agent-sdk+0.0.6.patch +52 -0
  43. package/prompts/config-agent.md +76 -0
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # Hakimi
2
+
3
+ Hakimi lets you chat with an AI assistant via Telegram/Slack/Feishu to remotely control your computer.
4
+
5
+ ## Quick Start
6
+
7
+ ### 1. Install
8
+
9
+ ```bash
10
+ npm install
11
+ ```
12
+
13
+ ### 2. Run
14
+
15
+ ```bash
16
+ npm run dev
17
+ ```
18
+
19
+ Debug mode (show detailed logs):
20
+
21
+ ```bash
22
+ npm run dev -- --debug
23
+ ```
24
+
25
+ ### 3. Login to Kimi Code
26
+
27
+ Press `L` to login to your Kimi Code account. Follow the prompts to complete authorization in your browser.
28
+
29
+ ### 4. Configure
30
+
31
+ Press `C` to start the configuration wizard. The AI assistant will guide you through:
32
+ - Naming your AI assistant
33
+ - Setting up chat platforms (Telegram/Slack/Feishu)
34
+
35
+ ### 5. Start Using
36
+
37
+ Press `S` to start the service, then send messages to your Bot on the chat platform.
38
+
39
+ ## Supported Platforms
40
+
41
+ ### Telegram
42
+
43
+ 1. Search for @BotFather on Telegram
44
+ 2. Send `/newbot` to create a bot
45
+ 3. Get the Bot Token
46
+
47
+ ### Slack
48
+
49
+ 1. Go to https://api.slack.com/apps to create an app
50
+ 2. Get the App-Level Token (`xapp-...`)
51
+ 3. Get the Bot User OAuth Token (`xoxb-...`)
52
+
53
+ ### Feishu (Lark)
54
+
55
+ 1. Go to https://open.feishu.cn to create an app
56
+ 2. Get the App ID and App Secret
57
+
58
+ ## Config File Locations
59
+
60
+ - Kimi Code: `~/.kimi/config.toml`
61
+ - Hakimi: `~/.hakimi/config.toml`
62
+
63
+ ## Hotkeys
64
+
65
+ | Key | Function |
66
+ |-----|----------|
67
+ | L | Login to Kimi Code |
68
+ | C | Configuration wizard |
69
+ | S | Start/Stop service |
70
+ | Q | Quit |
71
+ | Esc | Back/Cancel |
package/dist/App.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ interface AppProps {
2
+ debug?: boolean;
3
+ }
4
+ export declare function App({ debug }: AppProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
package/dist/App.js ADDED
@@ -0,0 +1,80 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect, useCallback, useRef } from 'react';
3
+ import { Box, Text, useApp } from 'ink';
4
+ import { HomeScreen } from './screens/HomeScreen.js';
5
+ import { LoginScreen } from './screens/LoginScreen.js';
6
+ import { ConfigScreen } from './screens/ConfigScreen.js';
7
+ import { isLoggedIn as checkLoggedIn, readHakimiConfig } from './utils/config.js';
8
+ import { ChatRouter } from './services/chatRouter.js';
9
+ export function App({ debug = false }) {
10
+ const { exit } = useApp();
11
+ const [screen, setScreen] = useState('home');
12
+ const [loggedIn, setLoggedIn] = useState(false);
13
+ const [adaptersConfigured, setAdaptersConfigured] = useState(0);
14
+ const [chatActive, setChatActive] = useState(false);
15
+ const [logs, setLogs] = useState([]);
16
+ const [lastMessage, setLastMessage] = useState(null);
17
+ const logsRef = useRef(logs);
18
+ logsRef.current = logs;
19
+ const addLog = useCallback((message) => {
20
+ const timestamp = new Date().toLocaleTimeString();
21
+ setLogs((prev) => [...prev.slice(-9), `[${timestamp}] ${message}`]);
22
+ }, []);
23
+ const [chatRouter] = useState(() => new ChatRouter({
24
+ onMessage: (sessionId, content) => {
25
+ setLastMessage({ sessionId, content });
26
+ if (debug)
27
+ addLog(`Message [${sessionId}]: ${content}`);
28
+ },
29
+ onSessionStart: (session) => {
30
+ addLog(`Session started: ${session.sessionId}`);
31
+ },
32
+ onSessionEnd: (sessionId) => {
33
+ addLog(`Session ended: ${sessionId}`);
34
+ },
35
+ onLog: (message) => {
36
+ if (debug)
37
+ addLog(message);
38
+ },
39
+ }));
40
+ const refreshStatus = useCallback(async () => {
41
+ const isLogged = await checkLoggedIn();
42
+ setLoggedIn(isLogged);
43
+ const config = await readHakimiConfig();
44
+ setAdaptersConfigured(config?.adapters?.length ?? 0);
45
+ }, []);
46
+ useEffect(() => {
47
+ refreshStatus();
48
+ }, [refreshStatus]);
49
+ const handleLoginSuccess = useCallback(() => {
50
+ setLoggedIn(true);
51
+ setScreen('home');
52
+ refreshStatus();
53
+ }, [refreshStatus]);
54
+ const handleConfigFinished = useCallback(() => {
55
+ setScreen('home');
56
+ refreshStatus();
57
+ }, [refreshStatus]);
58
+ const handleToggleChat = useCallback(async () => {
59
+ if (chatActive) {
60
+ await chatRouter.stop();
61
+ setChatActive(false);
62
+ }
63
+ else {
64
+ try {
65
+ await chatRouter.start();
66
+ setChatActive(true);
67
+ }
68
+ catch (error) {
69
+ console.error('Failed to start chat:', error);
70
+ }
71
+ }
72
+ }, [chatActive, chatRouter]);
73
+ const handleQuit = useCallback(async () => {
74
+ if (chatRouter.running) {
75
+ await chatRouter.stop();
76
+ }
77
+ exit();
78
+ }, [chatRouter, exit]);
79
+ return (_jsxs(Box, { flexDirection: "column", children: [screen === 'home' && (_jsx(HomeScreen, { isLoggedIn: loggedIn, adaptersConfigured: adaptersConfigured, chatActive: chatActive, onLogin: () => setScreen('login'), onConfig: () => setScreen('config'), onChat: handleToggleChat, onQuit: handleQuit, isActive: screen === 'home' })), screen === 'login' && (_jsx(LoginScreen, { onSuccess: handleLoginSuccess, onCancel: () => setScreen('home'), isActive: screen === 'login' })), screen === 'config' && (_jsx(ConfigScreen, { onFinished: handleConfigFinished, onCancel: () => setScreen('home'), isActive: screen === 'config' })), chatActive && debug && logs.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingX: 1, children: [_jsx(Text, { color: "gray", children: "\u2500\u2500\u2500 Debug Logs \u2500\u2500\u2500" }), logs.map((log, i) => (_jsx(Text, { color: "gray", dimColor: true, children: log }, i)))] })), chatActive && !debug && lastMessage && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsx(Text, { color: "gray", children: "\u4E0A\u6B21\u6536\u5230: " }), _jsxs(Text, { color: "cyan", children: ["[", lastMessage.sessionId, "] "] }), _jsx(Text, { children: lastMessage.content.length > 50 ? lastMessage.content.slice(0, 50) + '...' : lastMessage.content })] }))] }));
80
+ }
@@ -0,0 +1,9 @@
1
+ interface HotkeyHintProps {
2
+ hints: Array<{
3
+ key: string;
4
+ label: string;
5
+ disabled?: boolean;
6
+ }>;
7
+ }
8
+ export declare function HotkeyHint({ hints }: HotkeyHintProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
@@ -0,0 +1,5 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function HotkeyHint({ hints }) {
4
+ return (_jsx(Box, { gap: 2, marginTop: 1, children: hints.map(({ key, label, disabled }) => (_jsxs(Box, { children: [_jsxs(Text, { color: disabled ? 'gray' : 'cyan', bold: true, children: ["[", key, "]"] }), _jsxs(Text, { color: disabled ? 'gray' : undefined, children: [" ", label] })] }, key))) }));
5
+ }
@@ -0,0 +1,12 @@
1
+ export interface Message {
2
+ id: string;
3
+ role: 'user' | 'assistant' | 'system';
4
+ content: string;
5
+ timestamp: Date;
6
+ }
7
+ interface MessageLogProps {
8
+ messages: Message[];
9
+ maxHeight?: number;
10
+ }
11
+ export declare function MessageLog({ messages, maxHeight }: MessageLogProps): import("react/jsx-runtime").JSX.Element;
12
+ export {};
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function MessageLog({ messages, maxHeight = 10 }) {
4
+ const visibleMessages = messages.slice(-maxHeight);
5
+ return (_jsx(Box, { flexDirection: "column", children: visibleMessages.length === 0 ? (_jsx(Text, { color: "gray", children: "No messages yet..." })) : (visibleMessages.map((msg) => (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: msg.role === 'user'
6
+ ? 'blue'
7
+ : msg.role === 'assistant'
8
+ ? 'green'
9
+ : 'yellow', bold: true, children: [msg.role === 'user' ? 'You' : msg.role === 'assistant' ? 'Hakimi' : 'System', ":"] }), _jsxs(Text, { children: [" ", msg.content] })] }, msg.id)))) }));
10
+ }
@@ -0,0 +1,7 @@
1
+ interface StatusBarProps {
2
+ isLoggedIn: boolean;
3
+ adaptersConfigured: number;
4
+ chatActive: boolean;
5
+ }
6
+ export declare function StatusBar({ isLoggedIn, adaptersConfigured, chatActive }: StatusBarProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function StatusBar({ isLoggedIn, adaptersConfigured, chatActive }) {
4
+ return (_jsxs(Box, { borderStyle: "single", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Hakimi Status" }), _jsxs(Box, { children: [_jsx(Text, { children: "Kimi: " }), isLoggedIn ? (_jsx(Text, { color: "green", children: "Logged in" })) : (_jsx(Text, { color: "yellow", children: "Not logged in" }))] }), _jsxs(Box, { children: [_jsx(Text, { children: "Adapters: " }), adaptersConfigured > 0 ? (_jsxs(Text, { color: "green", children: [adaptersConfigured, " configured"] })) : (_jsx(Text, { color: "yellow", children: "None configured" }))] }), _jsxs(Box, { children: [_jsx(Text, { children: "Chat: " }), chatActive ? (_jsx(Text, { color: "green", children: "Active" })) : (_jsx(Text, { color: "gray", children: "Inactive" }))] })] }));
5
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { render } from 'ink';
4
+ import { App } from './App.js';
5
+ const debug = process.argv.includes('--debug');
6
+ // Global error handlers to prevent crash
7
+ process.on('uncaughtException', (error) => {
8
+ if (debug) {
9
+ console.error('[Uncaught Exception]', error);
10
+ }
11
+ // Don't exit - let the app continue
12
+ });
13
+ process.on('unhandledRejection', (reason) => {
14
+ if (debug) {
15
+ console.error('[Unhandled Rejection]', reason);
16
+ }
17
+ // Don't exit - let the app continue
18
+ });
19
+ render(_jsx(App, { debug: debug }));
@@ -0,0 +1,7 @@
1
+ interface ConfigScreenProps {
2
+ onFinished: () => void;
3
+ onCancel: () => void;
4
+ isActive: boolean;
5
+ }
6
+ export declare function ConfigScreen({ onFinished, onCancel, isActive }: ConfigScreenProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,114 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useCallback, useEffect, useRef } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import Spinner from 'ink-spinner';
6
+ import { MessageLog } from '../components/MessageLog.js';
7
+ import { ConfigAgent } from '../services/configAgent.js';
8
+ export function ConfigScreen({ onFinished, onCancel, isActive }) {
9
+ const [messages, setMessages] = useState([]);
10
+ const [inputValue, setInputValue] = useState('');
11
+ const [inputMode, setInputMode] = useState('chat');
12
+ const [pendingQuestion, setPendingQuestion] = useState(null);
13
+ const [isProcessing, setIsProcessing] = useState(true);
14
+ const [isFinished, setIsFinished] = useState(false);
15
+ const [error, setError] = useState(null);
16
+ const agentRef = useRef(null);
17
+ const currentResponseRef = useRef('');
18
+ const addMessage = useCallback((role, content) => {
19
+ setMessages((prev) => [
20
+ ...prev,
21
+ {
22
+ id: `msg-${Date.now()}-${Math.random()}`,
23
+ role,
24
+ content,
25
+ timestamp: new Date(),
26
+ },
27
+ ]);
28
+ }, []);
29
+ const flushCurrentResponse = useCallback(() => {
30
+ if (currentResponseRef.current.trim()) {
31
+ addMessage('assistant', currentResponseRef.current.trim());
32
+ currentResponseRef.current = '';
33
+ }
34
+ }, [addMessage]);
35
+ useEffect(() => {
36
+ const agent = new ConfigAgent({
37
+ onText: (text) => {
38
+ currentResponseRef.current += text;
39
+ },
40
+ onAskUser: async (question) => {
41
+ flushCurrentResponse();
42
+ return new Promise((resolve) => {
43
+ setPendingQuestion({ question, resolve });
44
+ setInputMode('ask_user');
45
+ setInputValue('');
46
+ addMessage('assistant', question);
47
+ setIsProcessing(false);
48
+ });
49
+ },
50
+ onFinished: () => {
51
+ flushCurrentResponse();
52
+ setIsFinished(true);
53
+ addMessage('system', 'Configuration saved successfully!');
54
+ setIsProcessing(false);
55
+ },
56
+ onError: (err) => {
57
+ flushCurrentResponse();
58
+ setError(err.message);
59
+ setIsProcessing(false);
60
+ },
61
+ });
62
+ agentRef.current = agent;
63
+ agent.start().then(() => {
64
+ flushCurrentResponse();
65
+ setIsProcessing(false);
66
+ }).catch((err) => {
67
+ setError(err.message);
68
+ setIsProcessing(false);
69
+ });
70
+ return () => {
71
+ agent.close();
72
+ };
73
+ }, [addMessage, flushCurrentResponse]);
74
+ const handleSubmit = useCallback(async (value) => {
75
+ if (!value.trim())
76
+ return;
77
+ const agent = agentRef.current;
78
+ if (!agent)
79
+ return;
80
+ if (inputMode === 'ask_user' && pendingQuestion) {
81
+ addMessage('user', value);
82
+ pendingQuestion.resolve(value);
83
+ setPendingQuestion(null);
84
+ setInputMode('chat');
85
+ setIsProcessing(true);
86
+ }
87
+ else {
88
+ addMessage('user', value);
89
+ setIsProcessing(true);
90
+ flushCurrentResponse();
91
+ try {
92
+ await agent.sendMessage(value);
93
+ flushCurrentResponse();
94
+ }
95
+ catch (err) {
96
+ setError(err instanceof Error ? err.message : String(err));
97
+ }
98
+ setIsProcessing(false);
99
+ }
100
+ setInputValue('');
101
+ }, [inputMode, pendingQuestion, addMessage, flushCurrentResponse]);
102
+ useInput((input, key) => {
103
+ if (key.escape) {
104
+ agentRef.current?.close();
105
+ if (isFinished) {
106
+ onFinished();
107
+ }
108
+ else {
109
+ onCancel();
110
+ }
111
+ }
112
+ }, { isActive });
113
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Adapter Configuration" }), isFinished && _jsx(Text, { color: "green", children: " (Complete)" })] }), error && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) })), _jsx(MessageLog, { messages: messages, maxHeight: 15 }), _jsx(Box, { marginTop: 1, children: isProcessing ? (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { color: "gray", children: " Thinking..." })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: inputMode === 'ask_user' ? 'yellow' : 'blue', children: inputMode === 'ask_user' ? '? ' : '> ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleSubmit })] })) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", children: ["[Esc] ", isFinished ? 'Done' : 'Cancel'] }) })] }));
114
+ }
@@ -0,0 +1,12 @@
1
+ interface HomeScreenProps {
2
+ isLoggedIn: boolean;
3
+ adaptersConfigured: number;
4
+ chatActive: boolean;
5
+ onLogin: () => void;
6
+ onConfig: () => void;
7
+ onChat: () => void;
8
+ onQuit: () => void;
9
+ isActive: boolean;
10
+ }
11
+ export declare function HomeScreen({ isLoggedIn, adaptersConfigured, chatActive, onLogin, onConfig, onChat, onQuit, isActive, }: HomeScreenProps): import("react/jsx-runtime").JSX.Element;
12
+ export {};
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { StatusBar } from '../components/StatusBar.js';
4
+ import { HotkeyHint } from '../components/HotkeyHint.js';
5
+ export function HomeScreen({ isLoggedIn, adaptersConfigured, chatActive, onLogin, onConfig, onChat, onQuit, isActive, }) {
6
+ useInput((input) => {
7
+ if (input === 'l' || input === 'L') {
8
+ onLogin();
9
+ }
10
+ else if ((input === 'c' || input === 'C') && isLoggedIn) {
11
+ onConfig();
12
+ }
13
+ else if ((input === 's' || input === 'S') && isLoggedIn && adaptersConfigured > 0) {
14
+ onChat();
15
+ }
16
+ else if (input === 'q' || input === 'Q') {
17
+ onQuit();
18
+ }
19
+ }, { isActive });
20
+ const hints = [
21
+ { key: 'L', label: 'Login', disabled: isLoggedIn },
22
+ { key: 'C', label: 'Configure', disabled: !isLoggedIn },
23
+ { key: 'S', label: chatActive ? 'Stop Chat' : 'Start Chat', disabled: !isLoggedIn || adaptersConfigured === 0 },
24
+ { key: 'Q', label: 'Quit' },
25
+ ];
26
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "magenta", children: "Hakimi - Kimi Chat Router" }) }), _jsx(StatusBar, { isLoggedIn: isLoggedIn, adaptersConfigured: adaptersConfigured, chatActive: chatActive }), _jsx(HotkeyHint, { hints: hints }), !isLoggedIn && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "Press L to login to Kimi first" }) })), isLoggedIn && adaptersConfigured === 0 && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "Press C to configure chat adapters" }) }))] }));
27
+ }
@@ -0,0 +1,7 @@
1
+ interface LoginScreenProps {
2
+ onSuccess: () => void;
3
+ onCancel: () => void;
4
+ isActive: boolean;
5
+ }
6
+ export declare function LoginScreen({ onSuccess, onCancel, isActive }: LoginScreenProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import Spinner from 'ink-spinner';
5
+ import { startLogin } from '../services/loginService.js';
6
+ export function LoginScreen({ onSuccess, onCancel, isActive }) {
7
+ const [state, setState] = useState('starting');
8
+ const [verificationUrl, setVerificationUrl] = useState('');
9
+ const [userCode, setUserCode] = useState('');
10
+ const [message, setMessage] = useState('Starting login...');
11
+ const [error, setError] = useState('');
12
+ useInput((input, key) => {
13
+ if (key.escape || input === 'q' || input === 'Q') {
14
+ onCancel();
15
+ }
16
+ if (state === 'success' && (key.return || input === ' ')) {
17
+ onSuccess();
18
+ }
19
+ }, { isActive });
20
+ useEffect(() => {
21
+ const emitter = startLogin();
22
+ emitter.on('event', (event) => {
23
+ switch (event.type) {
24
+ case 'verification_url':
25
+ setState('awaiting_auth');
26
+ setVerificationUrl(event.data?.verification_url || '');
27
+ setUserCode(event.data?.user_code || '');
28
+ setMessage('Please authorize in your browser');
29
+ break;
30
+ case 'waiting':
31
+ setMessage(event.message);
32
+ break;
33
+ case 'success':
34
+ setState('success');
35
+ setMessage(event.message);
36
+ break;
37
+ case 'error':
38
+ setState('error');
39
+ setError(event.message);
40
+ break;
41
+ case 'info':
42
+ setMessage(event.message);
43
+ break;
44
+ }
45
+ });
46
+ emitter.on('error', (err) => {
47
+ setState('error');
48
+ setError(err.message);
49
+ });
50
+ return () => {
51
+ emitter.removeAllListeners();
52
+ };
53
+ }, []);
54
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Kimi Login" }) }), state === 'starting' && (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsxs(Text, { children: [" ", message] })] })), state === 'awaiting_auth' && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", padding: 1, children: [_jsx(Text, { children: "Open this URL in your browser:" }), _jsx(Box, { marginY: 1, children: _jsx(Text, { bold: true, color: "blue", children: verificationUrl }) }), _jsx(Text, { children: "Enter this code:" }), _jsx(Box, { marginY: 1, children: _jsxs(Text, { bold: true, color: "green", inverse: true, children: [' ', userCode, ' '] }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Spinner, { type: "dots" }), _jsxs(Text, { color: "gray", children: [" ", message] })] })] })), state === 'success' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", children: message }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "Press Enter to continue..." }) })] })), state === 'error' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "Press Esc to go back" }) })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "[Esc] Cancel" }) })] }));
55
+ }
@@ -0,0 +1,18 @@
1
+ export interface ChatAgentCallbacks {
2
+ onSend: (message: string) => Promise<void>;
3
+ onLog?: (message: string) => void;
4
+ }
5
+ export declare class ChatAgent {
6
+ private session;
7
+ private currentTurn;
8
+ private callbacks;
9
+ private sessionId;
10
+ private didSendMessage;
11
+ constructor(sessionId: string, callbacks: ChatAgentCallbacks);
12
+ private log;
13
+ start(): Promise<void>;
14
+ sendMessage(content: string): Promise<void>;
15
+ private runPrompt;
16
+ interrupt(): Promise<void>;
17
+ close(): Promise<void>;
18
+ }
@@ -0,0 +1,110 @@
1
+ import { createSession, createExternalTool } from '@moonshot-ai/kimi-agent-sdk';
2
+ import { z } from 'zod';
3
+ import { KIMCHI_DIR } from '../utils/paths.js';
4
+ const CHAT_SYSTEM_PROMPT = `You are a helpful assistant chatting with a user via a messaging platform.
5
+
6
+ IMPORTANT: You MUST use the SendMessage tool to reply to the user. Do NOT put your reply in the assistant message content - it will be ignored. Only messages sent via SendMessage will reach the user.
7
+
8
+ You can call SendMessage multiple times if needed (e.g., for long responses or multiple parts).`;
9
+ export class ChatAgent {
10
+ session = null;
11
+ currentTurn = null;
12
+ callbacks;
13
+ sessionId;
14
+ didSendMessage = false;
15
+ constructor(sessionId, callbacks) {
16
+ this.sessionId = sessionId;
17
+ this.callbacks = callbacks;
18
+ }
19
+ log(message) {
20
+ this.callbacks.onLog?.(message);
21
+ }
22
+ async start() {
23
+ const sendMessageTool = createExternalTool({
24
+ name: 'SendMessage',
25
+ description: 'Send a message to the user. You MUST use this tool to reply. Your assistant message content will NOT be shown to the user.',
26
+ parameters: z.object({
27
+ message: z.string().describe('The message to send to the user'),
28
+ }),
29
+ handler: async (params) => {
30
+ this.didSendMessage = true;
31
+ this.log(`Sending: ${params.message.slice(0, 50)}...`);
32
+ await this.callbacks.onSend(params.message);
33
+ return { output: 'Message sent successfully', message: '' };
34
+ },
35
+ });
36
+ this.session = createSession({
37
+ workDir: KIMCHI_DIR,
38
+ sessionId: `chat-${this.sessionId}`,
39
+ thinking: false,
40
+ yoloMode: true,
41
+ externalTools: [sendMessageTool],
42
+ });
43
+ // Initialize with system prompt
44
+ await this.runPrompt(CHAT_SYSTEM_PROMPT);
45
+ }
46
+ async sendMessage(content) {
47
+ if (!this.session) {
48
+ throw new Error('Session not started');
49
+ }
50
+ // Reset send flag
51
+ this.didSendMessage = false;
52
+ // Send user message
53
+ await this.runPrompt(`User message: ${content}`);
54
+ // If agent didn't use SendMessage, prompt again
55
+ let retries = 0;
56
+ while (!this.didSendMessage && retries < 3) {
57
+ retries++;
58
+ this.log(`Agent did not send message, prompting again (retry ${retries})...`);
59
+ await this.runPrompt('You did not send a message to the user. Please use the SendMessage tool to reply.');
60
+ }
61
+ if (!this.didSendMessage) {
62
+ this.log('Agent failed to send message after retries');
63
+ await this.callbacks.onSend('Sorry, I encountered an error processing your message.');
64
+ }
65
+ }
66
+ async runPrompt(content) {
67
+ if (!this.session)
68
+ return;
69
+ const turn = this.session.prompt(content);
70
+ this.currentTurn = turn;
71
+ try {
72
+ for await (const event of turn) {
73
+ // Handle approval requests automatically
74
+ if (event.type === 'ApprovalRequest' && this.currentTurn) {
75
+ this.currentTurn.approve(event.payload.id, 'approve').catch(() => { });
76
+ }
77
+ // Ignore text content - only SendMessage matters
78
+ }
79
+ }
80
+ catch (error) {
81
+ if (error instanceof Error && error.message.includes('interrupted')) {
82
+ return;
83
+ }
84
+ throw error;
85
+ }
86
+ finally {
87
+ if (this.currentTurn === turn) {
88
+ this.currentTurn = null;
89
+ }
90
+ }
91
+ }
92
+ async interrupt() {
93
+ if (this.currentTurn) {
94
+ try {
95
+ await this.currentTurn.interrupt();
96
+ }
97
+ catch {
98
+ // Ignore interrupt errors
99
+ }
100
+ this.currentTurn = null;
101
+ }
102
+ }
103
+ async close() {
104
+ await this.interrupt();
105
+ if (this.session) {
106
+ await this.session.close();
107
+ this.session = null;
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,39 @@
1
+ import { TheAgent } from './theAgent.js';
2
+ export interface ChatSession {
3
+ sessionId: string;
4
+ platform: string;
5
+ userId: string;
6
+ botId: string;
7
+ isProcessing: boolean;
8
+ sendFn: (message: string) => Promise<void>;
9
+ agent: TheAgent | null;
10
+ pendingMessage: string | null;
11
+ }
12
+ export interface ChatRouterOptions {
13
+ onMessage: (sessionId: string, content: string) => void;
14
+ onSessionStart: (session: ChatSession) => void;
15
+ onSessionEnd: (sessionId: string) => void;
16
+ onLog?: (message: string) => void;
17
+ }
18
+ export declare class ChatRouter {
19
+ private ctx;
20
+ private sessionCache;
21
+ private options;
22
+ private isRunning;
23
+ private agentName;
24
+ private retryTimeouts;
25
+ constructor(options: ChatRouterOptions);
26
+ private log;
27
+ private sleep;
28
+ private sendWithRetry;
29
+ start(): Promise<void>;
30
+ private startWithRetry;
31
+ private scheduleReconnect;
32
+ private loadAdapter;
33
+ private handleMessage;
34
+ private processMessage;
35
+ getSession(sessionId: string): ChatSession | undefined;
36
+ stop(): Promise<void>;
37
+ get running(): boolean;
38
+ get activeSessions(): number;
39
+ }