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
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { readHakimiConfig } from '../utils/config.js';
|
|
2
|
+
import { SessionCache } from './sessionCache.js';
|
|
3
|
+
import { TheAgent } from './theAgent.js';
|
|
4
|
+
export class ChatRouter {
|
|
5
|
+
ctx = null;
|
|
6
|
+
sessionCache;
|
|
7
|
+
options;
|
|
8
|
+
isRunning = false;
|
|
9
|
+
agentName = 'Hakimi';
|
|
10
|
+
retryTimeouts = new Map();
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.sessionCache = new SessionCache((sessionId) => {
|
|
14
|
+
this.options.onSessionEnd(sessionId);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
log(message) {
|
|
18
|
+
this.options.onLog?.(message);
|
|
19
|
+
}
|
|
20
|
+
async sleep(ms) {
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
async sendWithRetry(fn, maxRetries) {
|
|
24
|
+
let lastError = null;
|
|
25
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
26
|
+
try {
|
|
27
|
+
await fn();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
lastError = error;
|
|
32
|
+
const delay = Math.min(3000 * (i + 1), 30000); // 3s, 6s, 9s, ... up to 30s
|
|
33
|
+
this.log(`Send failed (${i + 1}/${maxRetries}), retry in ${delay / 1000}s...`);
|
|
34
|
+
await this.sleep(delay);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
this.log(`Send failed after ${maxRetries} retries: ${lastError?.message}`);
|
|
38
|
+
}
|
|
39
|
+
async start() {
|
|
40
|
+
const config = await readHakimiConfig();
|
|
41
|
+
if (!config?.adapters || config.adapters.length === 0) {
|
|
42
|
+
throw new Error('No adapters configured');
|
|
43
|
+
}
|
|
44
|
+
this.agentName = config.agentName || 'Hakimi';
|
|
45
|
+
// Dynamic import koishi to avoid Node.js v24 compatibility issues at load time
|
|
46
|
+
const { Context, HTTP } = await import('koishi');
|
|
47
|
+
this.ctx = new Context();
|
|
48
|
+
// Register HTTP service (required by bot adapters)
|
|
49
|
+
this.ctx.plugin(HTTP);
|
|
50
|
+
for (const adapter of config.adapters) {
|
|
51
|
+
await this.loadAdapter(adapter);
|
|
52
|
+
}
|
|
53
|
+
// Listen for bot status updates
|
|
54
|
+
this.ctx.on('bot-status-updated', (bot) => {
|
|
55
|
+
this.log(`Bot ${bot.selfId || 'unknown'} status: ${bot.status}`);
|
|
56
|
+
// Status 3 = offline, try to reconnect
|
|
57
|
+
if (bot.status === 3) {
|
|
58
|
+
this.scheduleReconnect(bot);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
// Listen for errors - don't crash, just log
|
|
62
|
+
this.ctx.on('internal/error', (error) => {
|
|
63
|
+
this.log(`Error: ${error.message || error}`);
|
|
64
|
+
});
|
|
65
|
+
this.ctx.on('message', (session) => {
|
|
66
|
+
this.log(`Message from ${session.userId}: ${session.content}`);
|
|
67
|
+
this.handleMessage(session);
|
|
68
|
+
});
|
|
69
|
+
this.log('Starting Koishi context...');
|
|
70
|
+
// Start with retry
|
|
71
|
+
await this.startWithRetry();
|
|
72
|
+
this.isRunning = true;
|
|
73
|
+
}
|
|
74
|
+
async startWithRetry(maxRetries = Infinity) {
|
|
75
|
+
let retries = 0;
|
|
76
|
+
while (retries < maxRetries) {
|
|
77
|
+
try {
|
|
78
|
+
await this.ctx.start();
|
|
79
|
+
// Log created bots
|
|
80
|
+
const bots = this.ctx.bots || [];
|
|
81
|
+
this.log(`Koishi started with ${bots.length} bot(s)`);
|
|
82
|
+
for (const bot of bots) {
|
|
83
|
+
this.log(`Bot: ${bot.platform}/${bot.selfId || 'connecting...'} - ${bot.status}`);
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
retries++;
|
|
89
|
+
const delay = Math.min(5000 * retries, 30000); // Max 30s delay
|
|
90
|
+
this.log(`Start failed (${retries}), retrying in ${delay / 1000}s: ${error}`);
|
|
91
|
+
await this.sleep(delay);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
scheduleReconnect(bot) {
|
|
96
|
+
const botKey = `${bot.platform}-${bot.selfId}`;
|
|
97
|
+
// Clear existing retry timeout
|
|
98
|
+
const existing = this.retryTimeouts.get(botKey);
|
|
99
|
+
if (existing) {
|
|
100
|
+
clearTimeout(existing);
|
|
101
|
+
}
|
|
102
|
+
// Schedule reconnect
|
|
103
|
+
const timeout = setTimeout(async () => {
|
|
104
|
+
this.retryTimeouts.delete(botKey);
|
|
105
|
+
this.log(`Attempting to reconnect bot ${botKey}...`);
|
|
106
|
+
try {
|
|
107
|
+
await bot.start();
|
|
108
|
+
this.log(`Bot ${botKey} reconnected`);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
this.log(`Reconnect failed for ${botKey}: ${error}`);
|
|
112
|
+
// Schedule another retry
|
|
113
|
+
this.scheduleReconnect(bot);
|
|
114
|
+
}
|
|
115
|
+
}, 5000);
|
|
116
|
+
this.retryTimeouts.set(botKey, timeout);
|
|
117
|
+
}
|
|
118
|
+
async loadAdapter(adapter) {
|
|
119
|
+
if (!this.ctx)
|
|
120
|
+
return;
|
|
121
|
+
this.log(`Loading ${adapter.type} adapter...`);
|
|
122
|
+
switch (adapter.type) {
|
|
123
|
+
case 'telegram': {
|
|
124
|
+
const mod = await import('@koishijs/plugin-adapter-telegram');
|
|
125
|
+
const TelegramBot = mod.TelegramBot || mod.default;
|
|
126
|
+
this.ctx.plugin(TelegramBot, adapter.config);
|
|
127
|
+
this.log(`Telegram adapter loaded`);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
case 'slack': {
|
|
131
|
+
const mod = await import('@koishijs/plugin-adapter-slack');
|
|
132
|
+
const SlackBot = mod.SlackBot || mod.default;
|
|
133
|
+
this.ctx.plugin(SlackBot, adapter.config);
|
|
134
|
+
this.log(`Slack adapter loaded`);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'feishu': {
|
|
138
|
+
const mod = await import('@koishijs/plugin-adapter-lark');
|
|
139
|
+
const LarkBot = mod.LarkBot || mod.default;
|
|
140
|
+
this.ctx.plugin(LarkBot, adapter.config);
|
|
141
|
+
this.log(`Feishu adapter loaded`);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
handleMessage(koishiSession) {
|
|
147
|
+
// Only handle private messages
|
|
148
|
+
if (koishiSession.guildId)
|
|
149
|
+
return;
|
|
150
|
+
const sessionId = `${koishiSession.platform}-${koishiSession.selfId}-${koishiSession.userId}`;
|
|
151
|
+
const content = koishiSession.content || '';
|
|
152
|
+
let chatSession = this.sessionCache.get(sessionId);
|
|
153
|
+
if (!chatSession) {
|
|
154
|
+
chatSession = {
|
|
155
|
+
sessionId,
|
|
156
|
+
platform: koishiSession.platform,
|
|
157
|
+
userId: koishiSession.userId || '',
|
|
158
|
+
botId: koishiSession.selfId || '',
|
|
159
|
+
isProcessing: false,
|
|
160
|
+
sendFn: async (message) => {
|
|
161
|
+
await this.sendWithRetry(() => koishiSession.send(message), 10);
|
|
162
|
+
},
|
|
163
|
+
agent: null,
|
|
164
|
+
pendingMessage: null,
|
|
165
|
+
};
|
|
166
|
+
this.sessionCache.set(sessionId, chatSession);
|
|
167
|
+
this.options.onSessionStart(chatSession);
|
|
168
|
+
}
|
|
169
|
+
this.options.onMessage(sessionId, content);
|
|
170
|
+
// Process message with agent
|
|
171
|
+
this.processMessage(chatSession, content);
|
|
172
|
+
}
|
|
173
|
+
async processMessage(session, content) {
|
|
174
|
+
// If already processing, interrupt and queue the new message
|
|
175
|
+
if (session.isProcessing) {
|
|
176
|
+
this.log(`Interrupting current turn for ${session.sessionId}`);
|
|
177
|
+
session.pendingMessage = content;
|
|
178
|
+
if (session.agent) {
|
|
179
|
+
await session.agent.interrupt();
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
session.isProcessing = true;
|
|
184
|
+
try {
|
|
185
|
+
// Create agent if not exists
|
|
186
|
+
if (!session.agent) {
|
|
187
|
+
this.log(`Creating agent for ${session.sessionId}`);
|
|
188
|
+
session.agent = new TheAgent(session.sessionId, this.agentName, {
|
|
189
|
+
onSend: async (message) => {
|
|
190
|
+
await session.sendFn(message);
|
|
191
|
+
},
|
|
192
|
+
onLog: (msg) => this.log(msg),
|
|
193
|
+
});
|
|
194
|
+
await session.agent.start();
|
|
195
|
+
}
|
|
196
|
+
// Process the message
|
|
197
|
+
await session.agent.sendMessage(content);
|
|
198
|
+
// Check for pending messages (new message came in while processing)
|
|
199
|
+
while (session.pendingMessage) {
|
|
200
|
+
const pending = session.pendingMessage;
|
|
201
|
+
session.pendingMessage = null;
|
|
202
|
+
this.log(`Processing pending message: ${pending}`);
|
|
203
|
+
await session.agent.sendMessage(pending);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
this.log(`Error processing message: ${error}`);
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
session.isProcessing = false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
getSession(sessionId) {
|
|
214
|
+
return this.sessionCache.get(sessionId);
|
|
215
|
+
}
|
|
216
|
+
async stop() {
|
|
217
|
+
// Clear all retry timeouts
|
|
218
|
+
for (const timeout of this.retryTimeouts.values()) {
|
|
219
|
+
clearTimeout(timeout);
|
|
220
|
+
}
|
|
221
|
+
this.retryTimeouts.clear();
|
|
222
|
+
// Close all agents
|
|
223
|
+
for (const session of this.sessionCache.values()) {
|
|
224
|
+
if (session.agent) {
|
|
225
|
+
await session.agent.close();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
this.sessionCache.clear();
|
|
229
|
+
if (this.ctx) {
|
|
230
|
+
try {
|
|
231
|
+
await this.ctx.stop();
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// Ignore stop errors
|
|
235
|
+
}
|
|
236
|
+
this.ctx = null;
|
|
237
|
+
}
|
|
238
|
+
this.isRunning = false;
|
|
239
|
+
}
|
|
240
|
+
get running() {
|
|
241
|
+
return this.isRunning;
|
|
242
|
+
}
|
|
243
|
+
get activeSessions() {
|
|
244
|
+
return this.sessionCache.size;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ConfigAgentCallbacks {
|
|
2
|
+
onText: (text: string) => void;
|
|
3
|
+
onAskUser: (question: string) => Promise<string>;
|
|
4
|
+
onFinished: () => void;
|
|
5
|
+
onError: (error: Error) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function loadConfigPrompt(): Promise<string>;
|
|
8
|
+
export declare class ConfigAgent {
|
|
9
|
+
private session;
|
|
10
|
+
private currentTurn;
|
|
11
|
+
private callbacks;
|
|
12
|
+
private systemPrompt;
|
|
13
|
+
constructor(callbacks: ConfigAgentCallbacks);
|
|
14
|
+
start(): Promise<void>;
|
|
15
|
+
sendMessage(content: string): Promise<void>;
|
|
16
|
+
private handleEvent;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { readFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createSession, createExternalTool } from '@moonshot-ai/kimi-agent-sdk';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { writeHakimiConfig } from '../utils/config.js';
|
|
8
|
+
import { HAKIMI_DIR } from '../utils/paths.js';
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const CONFIG_SESSION_ID = 'hakimi-config-wizard';
|
|
11
|
+
export async function loadConfigPrompt() {
|
|
12
|
+
const promptPath = join(__dirname, '../../prompts/config-agent.md');
|
|
13
|
+
return readFile(promptPath, 'utf-8');
|
|
14
|
+
}
|
|
15
|
+
export class ConfigAgent {
|
|
16
|
+
session = null;
|
|
17
|
+
currentTurn = null;
|
|
18
|
+
callbacks;
|
|
19
|
+
systemPrompt = '';
|
|
20
|
+
constructor(callbacks) {
|
|
21
|
+
this.callbacks = callbacks;
|
|
22
|
+
}
|
|
23
|
+
async start() {
|
|
24
|
+
this.systemPrompt = await loadConfigPrompt();
|
|
25
|
+
// Ensure work directory exists
|
|
26
|
+
if (!existsSync(HAKIMI_DIR)) {
|
|
27
|
+
await mkdir(HAKIMI_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
const askUserTool = createExternalTool({
|
|
30
|
+
name: 'AskUser',
|
|
31
|
+
description: 'Ask user for input like API tokens or configuration values',
|
|
32
|
+
parameters: z.object({
|
|
33
|
+
question: z.string().describe('The question to ask the user'),
|
|
34
|
+
}),
|
|
35
|
+
handler: async (params) => {
|
|
36
|
+
const answer = await this.callbacks.onAskUser(params.question);
|
|
37
|
+
return { output: answer, message: '' };
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const finishConfigTool = createExternalTool({
|
|
41
|
+
name: 'FinishConfig',
|
|
42
|
+
description: 'Save configuration and finish the wizard',
|
|
43
|
+
parameters: z.object({
|
|
44
|
+
agentName: z.string().optional().describe('Name for the AI assistant (default: Hakimi)'),
|
|
45
|
+
adapters: z.array(z.object({
|
|
46
|
+
type: z.enum(['telegram', 'slack', 'feishu']),
|
|
47
|
+
config: z.record(z.any()),
|
|
48
|
+
})).describe('List of adapter configurations to save'),
|
|
49
|
+
}),
|
|
50
|
+
handler: async (params) => {
|
|
51
|
+
await writeHakimiConfig({
|
|
52
|
+
agentName: params.agentName || 'Hakimi',
|
|
53
|
+
adapters: params.adapters,
|
|
54
|
+
});
|
|
55
|
+
this.callbacks.onFinished();
|
|
56
|
+
return { output: `Saved configuration: agent "${params.agentName || 'Hakimi'}" with ${params.adapters.length} adapter(s)`, message: '' };
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
this.session = createSession({
|
|
60
|
+
workDir: HAKIMI_DIR,
|
|
61
|
+
sessionId: CONFIG_SESSION_ID,
|
|
62
|
+
thinking: false,
|
|
63
|
+
yoloMode: true,
|
|
64
|
+
externalTools: [askUserTool, finishConfigTool],
|
|
65
|
+
});
|
|
66
|
+
// Clear previous session context
|
|
67
|
+
await this.sendMessage('/clear');
|
|
68
|
+
// Send initial prompt with system instructions
|
|
69
|
+
await this.sendMessage(this.systemPrompt + '\n\nPlease start by asking the user what they want to name their AI assistant.');
|
|
70
|
+
}
|
|
71
|
+
async sendMessage(content) {
|
|
72
|
+
if (!this.session) {
|
|
73
|
+
throw new Error('Session not started');
|
|
74
|
+
}
|
|
75
|
+
const turn = this.session.prompt(content);
|
|
76
|
+
this.currentTurn = turn;
|
|
77
|
+
try {
|
|
78
|
+
for await (const event of turn) {
|
|
79
|
+
this.handleEvent(event);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof Error && error.message.includes('interrupted')) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.callbacks.onError(error instanceof Error ? error : new Error(String(error)));
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
// Clear turn reference after completion
|
|
90
|
+
if (this.currentTurn === turn) {
|
|
91
|
+
this.currentTurn = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
handleEvent(event) {
|
|
96
|
+
switch (event.type) {
|
|
97
|
+
case 'ContentPart':
|
|
98
|
+
if (event.payload.type === 'text') {
|
|
99
|
+
this.callbacks.onText(event.payload.text);
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
case 'ApprovalRequest':
|
|
103
|
+
// Auto-approve any pending requests
|
|
104
|
+
if (this.currentTurn) {
|
|
105
|
+
this.currentTurn.approve(event.payload.id, 'approve').catch(() => { });
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async close() {
|
|
111
|
+
if (this.currentTurn) {
|
|
112
|
+
try {
|
|
113
|
+
await this.currentTurn.interrupt();
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Ignore interrupt errors on close
|
|
117
|
+
}
|
|
118
|
+
this.currentTurn = null;
|
|
119
|
+
}
|
|
120
|
+
if (this.session) {
|
|
121
|
+
await this.session.close();
|
|
122
|
+
this.session = null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
export interface LoginEvent {
|
|
3
|
+
type: 'info' | 'verification_url' | 'waiting' | 'success' | 'error';
|
|
4
|
+
message: string;
|
|
5
|
+
data?: {
|
|
6
|
+
verification_url?: string;
|
|
7
|
+
user_code?: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export interface LoginEmitter extends EventEmitter {
|
|
11
|
+
on(event: 'event', listener: (data: LoginEvent) => void): this;
|
|
12
|
+
on(event: 'error', listener: (error: Error) => void): this;
|
|
13
|
+
on(event: 'close', listener: () => void): this;
|
|
14
|
+
}
|
|
15
|
+
export declare function startLogin(): LoginEmitter;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
export function startLogin() {
|
|
4
|
+
const emitter = new EventEmitter();
|
|
5
|
+
const proc = spawn('kimi', ['login', '--json'], {
|
|
6
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
7
|
+
});
|
|
8
|
+
let buffer = '';
|
|
9
|
+
proc.stdout.on('data', (chunk) => {
|
|
10
|
+
buffer += chunk.toString();
|
|
11
|
+
const lines = buffer.split('\n');
|
|
12
|
+
buffer = lines.pop() || '';
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
if (!line.trim())
|
|
15
|
+
continue;
|
|
16
|
+
try {
|
|
17
|
+
const event = JSON.parse(line);
|
|
18
|
+
emitter.emit('event', event);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Ignore non-JSON lines
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
proc.stderr.on('data', (chunk) => {
|
|
26
|
+
const message = chunk.toString().trim();
|
|
27
|
+
if (message) {
|
|
28
|
+
emitter.emit('event', { type: 'error', message });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
proc.on('error', (err) => {
|
|
32
|
+
emitter.emit('error', err);
|
|
33
|
+
});
|
|
34
|
+
proc.on('close', () => {
|
|
35
|
+
// Process remaining buffer
|
|
36
|
+
if (buffer.trim()) {
|
|
37
|
+
try {
|
|
38
|
+
const event = JSON.parse(buffer);
|
|
39
|
+
emitter.emit('event', event);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Ignore
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
emitter.emit('close');
|
|
46
|
+
});
|
|
47
|
+
return emitter;
|
|
48
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface CachedSession<T> {
|
|
2
|
+
session: T;
|
|
3
|
+
lastActivity: number;
|
|
4
|
+
timer: ReturnType<typeof setTimeout>;
|
|
5
|
+
}
|
|
6
|
+
export declare class SessionCache<T> {
|
|
7
|
+
private sessions;
|
|
8
|
+
private onExpire;
|
|
9
|
+
constructor(onExpire: (sessionId: string, session: T) => void);
|
|
10
|
+
get(sessionId: string): T | undefined;
|
|
11
|
+
set(sessionId: string, session: T): void;
|
|
12
|
+
touch(sessionId: string): void;
|
|
13
|
+
private expire;
|
|
14
|
+
delete(sessionId: string): T | undefined;
|
|
15
|
+
clear(): void;
|
|
16
|
+
get size(): number;
|
|
17
|
+
values(): T[];
|
|
18
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
2
|
+
export class SessionCache {
|
|
3
|
+
sessions = new Map();
|
|
4
|
+
onExpire;
|
|
5
|
+
constructor(onExpire) {
|
|
6
|
+
this.onExpire = onExpire;
|
|
7
|
+
}
|
|
8
|
+
get(sessionId) {
|
|
9
|
+
const cached = this.sessions.get(sessionId);
|
|
10
|
+
if (cached) {
|
|
11
|
+
this.touch(sessionId);
|
|
12
|
+
return cached.session;
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
set(sessionId, session) {
|
|
17
|
+
const existing = this.sessions.get(sessionId);
|
|
18
|
+
if (existing) {
|
|
19
|
+
clearTimeout(existing.timer);
|
|
20
|
+
}
|
|
21
|
+
const timer = setTimeout(() => {
|
|
22
|
+
this.expire(sessionId);
|
|
23
|
+
}, SESSION_TTL_MS);
|
|
24
|
+
this.sessions.set(sessionId, {
|
|
25
|
+
session,
|
|
26
|
+
lastActivity: Date.now(),
|
|
27
|
+
timer,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
touch(sessionId) {
|
|
31
|
+
const cached = this.sessions.get(sessionId);
|
|
32
|
+
if (cached) {
|
|
33
|
+
clearTimeout(cached.timer);
|
|
34
|
+
cached.lastActivity = Date.now();
|
|
35
|
+
cached.timer = setTimeout(() => {
|
|
36
|
+
this.expire(sessionId);
|
|
37
|
+
}, SESSION_TTL_MS);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
expire(sessionId) {
|
|
41
|
+
const cached = this.sessions.get(sessionId);
|
|
42
|
+
if (cached) {
|
|
43
|
+
this.onExpire(sessionId, cached.session);
|
|
44
|
+
this.sessions.delete(sessionId);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
delete(sessionId) {
|
|
48
|
+
const cached = this.sessions.get(sessionId);
|
|
49
|
+
if (cached) {
|
|
50
|
+
clearTimeout(cached.timer);
|
|
51
|
+
this.sessions.delete(sessionId);
|
|
52
|
+
return cached.session;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
clear() {
|
|
57
|
+
for (const [sessionId, cached] of this.sessions) {
|
|
58
|
+
clearTimeout(cached.timer);
|
|
59
|
+
this.onExpire(sessionId, cached.session);
|
|
60
|
+
}
|
|
61
|
+
this.sessions.clear();
|
|
62
|
+
}
|
|
63
|
+
get size() {
|
|
64
|
+
return this.sessions.size;
|
|
65
|
+
}
|
|
66
|
+
values() {
|
|
67
|
+
return Array.from(this.sessions.values()).map((cached) => cached.session);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface TheAgentCallbacks {
|
|
2
|
+
onSend: (message: string) => Promise<void>;
|
|
3
|
+
onLog?: (message: string) => void;
|
|
4
|
+
}
|
|
5
|
+
export declare class TheAgent {
|
|
6
|
+
private session;
|
|
7
|
+
private currentTurn;
|
|
8
|
+
private callbacks;
|
|
9
|
+
private sessionId;
|
|
10
|
+
private agentName;
|
|
11
|
+
private didSendMessage;
|
|
12
|
+
constructor(sessionId: string, agentName: string, callbacks: TheAgentCallbacks);
|
|
13
|
+
private log;
|
|
14
|
+
start(): Promise<void>;
|
|
15
|
+
sendMessage(content: string): Promise<void>;
|
|
16
|
+
private runPrompt;
|
|
17
|
+
interrupt(): Promise<void>;
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
}
|