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.
- package/README.md +71 -0
- package/dist/App.d.ts +5 -0
- package/dist/App.js +80 -0
- package/dist/components/HotkeyHint.d.ts +9 -0
- package/dist/components/HotkeyHint.js +5 -0
- package/dist/components/MessageLog.d.ts +12 -0
- package/dist/components/MessageLog.js +10 -0
- package/dist/components/StatusBar.d.ts +7 -0
- package/dist/components/StatusBar.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +19 -0
- package/dist/screens/ConfigScreen.d.ts +7 -0
- package/dist/screens/ConfigScreen.js +114 -0
- package/dist/screens/HomeScreen.d.ts +12 -0
- package/dist/screens/HomeScreen.js +27 -0
- package/dist/screens/LoginScreen.d.ts +7 -0
- package/dist/screens/LoginScreen.js +55 -0
- package/dist/services/chatAgent.d.ts +18 -0
- package/dist/services/chatAgent.js +110 -0
- package/dist/services/chatRouter.d.ts +39 -0
- package/dist/services/chatRouter.js +246 -0
- package/dist/services/configAgent.d.ts +18 -0
- package/dist/services/configAgent.js +125 -0
- package/dist/services/loginService.d.ts +15 -0
- package/dist/services/loginService.js +48 -0
- package/dist/services/sessionCache.d.ts +18 -0
- package/dist/services/sessionCache.js +69 -0
- package/dist/services/theAgent.d.ts +19 -0
- package/dist/services/theAgent.js +139 -0
- package/dist/tools/askUser.d.ts +30 -0
- package/dist/tools/askUser.js +16 -0
- package/dist/tools/finishConfig.d.ts +64 -0
- package/dist/tools/finishConfig.js +20 -0
- package/dist/tools/sendMessage.d.ts +25 -0
- package/dist/tools/sendMessage.js +15 -0
- package/dist/utils/config.d.ts +16 -0
- package/dist/utils/config.js +44 -0
- package/dist/utils/paths.d.ts +4 -0
- package/dist/utils/paths.js +6 -0
- package/package.json +58 -0
- package/patches/@koishijs+loader+4.6.10.patch +13 -0
- package/patches/@moonshot-ai+kimi-agent-sdk+0.0.6.patch +52 -0
- 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
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,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,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
|
+
}
|
package/dist/index.d.ts
ADDED
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,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,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
|
+
}
|