vibekit-agent 1.0.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 +112 -0
- package/dist/agent.d.ts +105 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +741 -0
- package/dist/agent.js.map +1 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +137 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +164 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/dist/agent.js
ADDED
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.AgentClient = void 0;
|
|
40
|
+
const ws_1 = __importDefault(require("ws"));
|
|
41
|
+
const child_process_1 = require("child_process");
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
/**
|
|
46
|
+
* Find the claude binary by checking common installation locations
|
|
47
|
+
*/
|
|
48
|
+
function findClaudeBinary() {
|
|
49
|
+
// Check for CLAUDE_PATH environment variable first (allows user override)
|
|
50
|
+
if (process.env.CLAUDE_PATH && fs.existsSync(process.env.CLAUDE_PATH)) {
|
|
51
|
+
return process.env.CLAUDE_PATH;
|
|
52
|
+
}
|
|
53
|
+
const homedir = os.homedir();
|
|
54
|
+
// Build list of possible paths
|
|
55
|
+
const possiblePaths = [
|
|
56
|
+
// Homebrew on macOS (Apple Silicon and Intel)
|
|
57
|
+
'/opt/homebrew/bin/claude',
|
|
58
|
+
'/usr/local/bin/claude',
|
|
59
|
+
// npm global installs with common prefixes
|
|
60
|
+
path.join(homedir, '.npm-global', 'bin', 'claude'),
|
|
61
|
+
'/usr/local/lib/node_modules/.bin/claude',
|
|
62
|
+
// Claude desktop app location on macOS
|
|
63
|
+
'/Applications/Claude.app/Contents/Resources/claude',
|
|
64
|
+
];
|
|
65
|
+
// Try to get npm global bin directory dynamically
|
|
66
|
+
try {
|
|
67
|
+
const npmBin = (0, child_process_1.execSync)('npm bin -g 2>/dev/null', {
|
|
68
|
+
encoding: 'utf-8',
|
|
69
|
+
env: { ...process.env, PATH: '/usr/local/bin:/opt/homebrew/bin:/usr/bin:' + (process.env.PATH || '') }
|
|
70
|
+
}).trim();
|
|
71
|
+
if (npmBin) {
|
|
72
|
+
possiblePaths.unshift(path.join(npmBin, 'claude'));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Ignore
|
|
77
|
+
}
|
|
78
|
+
// Check nvm paths
|
|
79
|
+
const nvmDir = process.env.NVM_DIR || path.join(homedir, '.nvm');
|
|
80
|
+
if (fs.existsSync(nvmDir)) {
|
|
81
|
+
// Check common node versions
|
|
82
|
+
const versionsDir = path.join(nvmDir, 'versions', 'node');
|
|
83
|
+
if (fs.existsSync(versionsDir)) {
|
|
84
|
+
try {
|
|
85
|
+
const versions = fs.readdirSync(versionsDir);
|
|
86
|
+
for (const v of versions) {
|
|
87
|
+
possiblePaths.push(path.join(versionsDir, v, 'bin', 'claude'));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Ignore
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Also check fnm (Fast Node Manager) paths
|
|
96
|
+
const fnmDir = process.env.FNM_MULTISHELL_PATH || path.join(homedir, '.fnm');
|
|
97
|
+
if (fs.existsSync(fnmDir)) {
|
|
98
|
+
possiblePaths.push(path.join(fnmDir, 'bin', 'claude'));
|
|
99
|
+
}
|
|
100
|
+
// Check each path
|
|
101
|
+
for (const p of possiblePaths) {
|
|
102
|
+
try {
|
|
103
|
+
if (fs.existsSync(p)) {
|
|
104
|
+
// Verify it's executable
|
|
105
|
+
fs.accessSync(p, fs.constants.X_OK);
|
|
106
|
+
console.log(`Found claude at: ${p}`);
|
|
107
|
+
return p;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Not accessible, continue
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Last resort: try which command with enhanced PATH
|
|
115
|
+
try {
|
|
116
|
+
const enhancedPath = [
|
|
117
|
+
'/opt/homebrew/bin',
|
|
118
|
+
'/usr/local/bin',
|
|
119
|
+
path.join(homedir, '.npm-global', 'bin'),
|
|
120
|
+
process.env.PATH || ''
|
|
121
|
+
].join(':');
|
|
122
|
+
const result = (0, child_process_1.execSync)('which claude', {
|
|
123
|
+
encoding: 'utf-8',
|
|
124
|
+
env: { ...process.env, PATH: enhancedPath }
|
|
125
|
+
}).trim();
|
|
126
|
+
if (result && fs.existsSync(result)) {
|
|
127
|
+
console.log(`Found claude via which: ${result}`);
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Not found
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
// Strip ANSI escape codes
|
|
137
|
+
function stripAnsi(str) {
|
|
138
|
+
// eslint-disable-next-line no-control-regex
|
|
139
|
+
return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
|
|
140
|
+
}
|
|
141
|
+
class AgentClient {
|
|
142
|
+
config;
|
|
143
|
+
ws = null;
|
|
144
|
+
claudeProcess = null;
|
|
145
|
+
reconnectTimeout = null;
|
|
146
|
+
reconnectDelay = 1000;
|
|
147
|
+
isConnected = false;
|
|
148
|
+
currentChatId = null;
|
|
149
|
+
currentTelegramId = null;
|
|
150
|
+
currentReplyToMessageId = null;
|
|
151
|
+
outputBuffer = '';
|
|
152
|
+
streamingInterval = null;
|
|
153
|
+
lastFlushedLength = 0;
|
|
154
|
+
currentMessageId = null;
|
|
155
|
+
workingDirectory = process.cwd();
|
|
156
|
+
// Conversation memory: track if we should continue the previous conversation
|
|
157
|
+
hasActiveConversation = false;
|
|
158
|
+
constructor(config) {
|
|
159
|
+
this.config = config;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Link this computer to a Telegram account
|
|
163
|
+
*/
|
|
164
|
+
async link() {
|
|
165
|
+
console.log('Connecting to VibeKit...\n');
|
|
166
|
+
// Request a link from the server
|
|
167
|
+
const serverUrl = process.env.VIBEKIT_SERVER || 'https://vibekit.bot';
|
|
168
|
+
try {
|
|
169
|
+
// Generate a temporary link code request
|
|
170
|
+
console.log('To link your computer:');
|
|
171
|
+
console.log('');
|
|
172
|
+
console.log('1. Go to @the_vibe_kit_bot on Telegram');
|
|
173
|
+
console.log('2. Send: /remote');
|
|
174
|
+
console.log('3. Copy the 6-character code it gives you');
|
|
175
|
+
console.log('');
|
|
176
|
+
// Wait for user to enter the code
|
|
177
|
+
const readline = await Promise.resolve().then(() => __importStar(require('readline')));
|
|
178
|
+
const rl = readline.createInterface({
|
|
179
|
+
input: process.stdin,
|
|
180
|
+
output: process.stdout,
|
|
181
|
+
});
|
|
182
|
+
const code = await new Promise((resolve) => {
|
|
183
|
+
rl.question('Enter the code from Telegram: ', (answer) => {
|
|
184
|
+
rl.close();
|
|
185
|
+
resolve(answer.trim().toUpperCase());
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
if (!code || code.length !== 6) {
|
|
189
|
+
console.error('Invalid code. Please try again.');
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
// Submit the code to the server
|
|
193
|
+
console.log('\nValidating code...');
|
|
194
|
+
const response = await fetch(`${serverUrl}/api/agent/link`, {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: { 'Content-Type': 'application/json' },
|
|
197
|
+
body: JSON.stringify({ code }),
|
|
198
|
+
});
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
const errorData = (await response.json().catch(() => ({ error: 'Unknown error' })));
|
|
201
|
+
console.error(`Link failed: ${errorData.error || 'Invalid code'}`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
const result = (await response.json());
|
|
205
|
+
this.config.setCredentials(result.token, result.wsUrl);
|
|
206
|
+
console.log('\nLinked successfully!');
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log('Start the agent with:');
|
|
209
|
+
console.log(' npx vibekit-agent start');
|
|
210
|
+
console.log('');
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
214
|
+
console.error('Link failed:', message);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Start the agent and connect to VibeKit
|
|
220
|
+
*/
|
|
221
|
+
async start(directory, autoMode = false) {
|
|
222
|
+
this.workingDirectory = path.resolve(directory);
|
|
223
|
+
// Don't persist directory in auto mode (ephemeral container)
|
|
224
|
+
if (!autoMode) {
|
|
225
|
+
this.config.setLastDirectory(this.workingDirectory);
|
|
226
|
+
}
|
|
227
|
+
console.log(`Starting VibeKit Remote Agent...`);
|
|
228
|
+
console.log(`Working directory: ${this.workingDirectory}`);
|
|
229
|
+
// In auto mode, set up Claude credentials from the credentials file
|
|
230
|
+
if (autoMode) {
|
|
231
|
+
await this.setupAutoModeCredentials();
|
|
232
|
+
}
|
|
233
|
+
// Check for claude binary at startup
|
|
234
|
+
const claudePath = findClaudeBinary();
|
|
235
|
+
if (claudePath) {
|
|
236
|
+
console.log(`Claude Code found: ${claudePath}`);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
console.error('\nClaude Code not found!');
|
|
240
|
+
console.error('Please install it: npm install -g @anthropic-ai/claude-code');
|
|
241
|
+
console.error('Then restart this agent.\n');
|
|
242
|
+
}
|
|
243
|
+
console.log('');
|
|
244
|
+
// Connect to VibeKit WebSocket
|
|
245
|
+
this.connect();
|
|
246
|
+
// Handle process signals
|
|
247
|
+
process.on('SIGINT', () => this.shutdown());
|
|
248
|
+
process.on('SIGTERM', () => this.shutdown());
|
|
249
|
+
// Keep the process running
|
|
250
|
+
await new Promise(() => { });
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Run claude command with the given prompt
|
|
254
|
+
*/
|
|
255
|
+
runClaude(prompt) {
|
|
256
|
+
// Find the claude binary
|
|
257
|
+
const claudeBinary = findClaudeBinary();
|
|
258
|
+
if (!claudeBinary) {
|
|
259
|
+
console.error('\nClaude Code not found!');
|
|
260
|
+
console.error('Searched in common locations but could not find the claude binary.');
|
|
261
|
+
console.error('\nPlease ensure Claude Code is installed:');
|
|
262
|
+
console.error(' npm install -g @anthropic-ai/claude-code');
|
|
263
|
+
console.error('\nOr specify the path by running:');
|
|
264
|
+
console.error(' which claude');
|
|
265
|
+
if (this.currentChatId && this.currentTelegramId) {
|
|
266
|
+
this.sendResponse(this.currentChatId, `Error: Claude Code not found on this computer.\n\nPlease install it:\n\`npm install -g @anthropic-ai/claude-code\`\n\nThen restart the agent.`, 'complete');
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// Kill any existing process
|
|
271
|
+
if (this.claudeProcess) {
|
|
272
|
+
this.claudeProcess.kill();
|
|
273
|
+
}
|
|
274
|
+
// Clear streaming state
|
|
275
|
+
this.stopStreaming();
|
|
276
|
+
this.outputBuffer = '';
|
|
277
|
+
this.lastFlushedLength = 0;
|
|
278
|
+
this.currentMessageId = null;
|
|
279
|
+
// Build command arguments
|
|
280
|
+
const allowedTools = this.config.getAllowedTools();
|
|
281
|
+
const args = ['-p', prompt];
|
|
282
|
+
// Use --continue flag if we have an active conversation (for context memory)
|
|
283
|
+
if (this.hasActiveConversation) {
|
|
284
|
+
args.push('--continue');
|
|
285
|
+
}
|
|
286
|
+
if (allowedTools.length === 0) {
|
|
287
|
+
// No restrictions - allow everything
|
|
288
|
+
args.push('--dangerously-skip-permissions');
|
|
289
|
+
console.log(`\nRunning: ${claudeBinary} -p "..."${this.hasActiveConversation ? ' --continue' : ''} --dangerously-skip-permissions`);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
// Use specific allowed tools
|
|
293
|
+
args.push('--allowedTools', allowedTools.join(','));
|
|
294
|
+
console.log(`\nRunning: ${claudeBinary} -p "..."${this.hasActiveConversation ? ' --continue' : ''} --allowedTools ${allowedTools.join(',')}`);
|
|
295
|
+
}
|
|
296
|
+
// Spawn claude directly with the full path (no shell needed)
|
|
297
|
+
this.claudeProcess = (0, child_process_1.spawn)(claudeBinary, args, {
|
|
298
|
+
cwd: this.workingDirectory,
|
|
299
|
+
env: process.env,
|
|
300
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
301
|
+
});
|
|
302
|
+
// Start streaming interval to send partial output periodically
|
|
303
|
+
this.startStreaming();
|
|
304
|
+
this.claudeProcess.stdout?.on('data', (data) => {
|
|
305
|
+
const text = data.toString();
|
|
306
|
+
process.stdout.write(text);
|
|
307
|
+
this.outputBuffer += text;
|
|
308
|
+
});
|
|
309
|
+
this.claudeProcess.stderr?.on('data', (data) => {
|
|
310
|
+
const text = data.toString();
|
|
311
|
+
process.stderr.write(text);
|
|
312
|
+
this.outputBuffer += text;
|
|
313
|
+
});
|
|
314
|
+
this.claudeProcess.on('close', (code) => {
|
|
315
|
+
console.log(`\nClaude exited with code ${code}`);
|
|
316
|
+
this.stopStreaming();
|
|
317
|
+
this.flushOutput();
|
|
318
|
+
this.claudeProcess = null;
|
|
319
|
+
// Mark that we now have an active conversation for --continue on next message
|
|
320
|
+
if (code === 0) {
|
|
321
|
+
this.hasActiveConversation = true;
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
this.claudeProcess.on('error', (err) => {
|
|
325
|
+
console.error('Failed to start claude:', err.message);
|
|
326
|
+
this.stopStreaming();
|
|
327
|
+
if (this.currentChatId && this.currentTelegramId) {
|
|
328
|
+
this.sendResponse(this.currentChatId, `Error: Could not start Claude Code.\n\nPath: ${claudeBinary}\nError: ${err.message}`, 'complete');
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Start streaming interval to send partial output
|
|
334
|
+
*/
|
|
335
|
+
startStreaming() {
|
|
336
|
+
// Clear any existing interval
|
|
337
|
+
this.stopStreaming();
|
|
338
|
+
// Send updates every 500ms
|
|
339
|
+
this.streamingInterval = setInterval(() => {
|
|
340
|
+
this.flushPartialOutput();
|
|
341
|
+
}, 500);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Stop streaming interval
|
|
345
|
+
*/
|
|
346
|
+
stopStreaming() {
|
|
347
|
+
if (this.streamingInterval) {
|
|
348
|
+
clearInterval(this.streamingInterval);
|
|
349
|
+
this.streamingInterval = null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Flush partial output if buffer has grown since last flush
|
|
354
|
+
*/
|
|
355
|
+
flushPartialOutput() {
|
|
356
|
+
if (!this.currentChatId || !this.currentTelegramId) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
// Check if buffer has new content
|
|
360
|
+
if (this.outputBuffer.length <= this.lastFlushedLength) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// Update last flushed length
|
|
364
|
+
this.lastFlushedLength = this.outputBuffer.length;
|
|
365
|
+
// Clean the output
|
|
366
|
+
let cleaned = stripAnsi(this.outputBuffer);
|
|
367
|
+
// Truncate if too long for Telegram
|
|
368
|
+
if (cleaned.length > 4000) {
|
|
369
|
+
cleaned = '...\n' + cleaned.slice(-3900);
|
|
370
|
+
}
|
|
371
|
+
if (cleaned.trim().length > 0) {
|
|
372
|
+
this.sendStreaming(this.currentChatId, cleaned.trim());
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Flush final buffered output to Telegram
|
|
377
|
+
*/
|
|
378
|
+
flushOutput() {
|
|
379
|
+
if (!this.outputBuffer.trim() || !this.currentChatId || !this.currentTelegramId) {
|
|
380
|
+
this.outputBuffer = '';
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
// Clean the output
|
|
384
|
+
let cleaned = stripAnsi(this.outputBuffer);
|
|
385
|
+
// Truncate if too long for Telegram
|
|
386
|
+
if (cleaned.length > 4000) {
|
|
387
|
+
cleaned = cleaned.slice(-4000) + '\n\n(output truncated)';
|
|
388
|
+
}
|
|
389
|
+
if (cleaned.trim().length > 0) {
|
|
390
|
+
this.sendResponse(this.currentChatId, cleaned.trim(), 'complete');
|
|
391
|
+
}
|
|
392
|
+
this.outputBuffer = '';
|
|
393
|
+
this.lastFlushedLength = 0;
|
|
394
|
+
this.currentMessageId = null;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Connect to VibeKit WebSocket server
|
|
398
|
+
*/
|
|
399
|
+
connect() {
|
|
400
|
+
const wsUrl = this.config.getWsUrl();
|
|
401
|
+
const token = this.config.getToken();
|
|
402
|
+
if (!wsUrl || !token) {
|
|
403
|
+
console.error('No credentials found. Run "vibekit-agent link" first.');
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
console.log('Connecting to VibeKit...');
|
|
407
|
+
this.ws = new ws_1.default(wsUrl);
|
|
408
|
+
this.ws.on('open', () => {
|
|
409
|
+
console.log('Connected to VibeKit');
|
|
410
|
+
this.isConnected = true;
|
|
411
|
+
this.reconnectDelay = 1000;
|
|
412
|
+
// Authenticate
|
|
413
|
+
this.send({
|
|
414
|
+
type: 'auth',
|
|
415
|
+
payload: {
|
|
416
|
+
token,
|
|
417
|
+
agentVersion: '1.0.8',
|
|
418
|
+
platform: os.platform(),
|
|
419
|
+
workingDirectory: this.workingDirectory,
|
|
420
|
+
},
|
|
421
|
+
timestamp: Date.now(),
|
|
422
|
+
messageId: this.generateId(),
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
this.ws.on('message', (data) => {
|
|
426
|
+
try {
|
|
427
|
+
const message = JSON.parse(data.toString());
|
|
428
|
+
this.handleMessage(message);
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
console.error('Error parsing message:', error);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
this.ws.on('close', () => {
|
|
435
|
+
console.log('Disconnected from VibeKit');
|
|
436
|
+
this.isConnected = false;
|
|
437
|
+
this.scheduleReconnect();
|
|
438
|
+
});
|
|
439
|
+
this.ws.on('error', (error) => {
|
|
440
|
+
console.error('WebSocket error:', error.message);
|
|
441
|
+
this.isConnected = false;
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Handle incoming WebSocket messages
|
|
446
|
+
*/
|
|
447
|
+
handleMessage(message) {
|
|
448
|
+
switch (message.type) {
|
|
449
|
+
case 'auth_success':
|
|
450
|
+
console.log('Authenticated successfully');
|
|
451
|
+
console.log('');
|
|
452
|
+
console.log('Ready! Send messages to @the_vibe_kit_bot on Telegram.');
|
|
453
|
+
console.log('Press Ctrl+C to stop.');
|
|
454
|
+
console.log('');
|
|
455
|
+
this.sendStatus('idle');
|
|
456
|
+
break;
|
|
457
|
+
case 'auth_error':
|
|
458
|
+
console.error('Authentication failed:', message.payload.message);
|
|
459
|
+
this.config.clear();
|
|
460
|
+
process.exit(1);
|
|
461
|
+
break;
|
|
462
|
+
case 'message':
|
|
463
|
+
this.handleUserMessage(message);
|
|
464
|
+
break;
|
|
465
|
+
case 'ping':
|
|
466
|
+
this.send({ type: 'pong', timestamp: Date.now(), messageId: this.generateId() });
|
|
467
|
+
break;
|
|
468
|
+
case 'cd':
|
|
469
|
+
this.handleCdCommand(message.payload.path);
|
|
470
|
+
break;
|
|
471
|
+
case 'new_conversation':
|
|
472
|
+
this.handleNewConversation();
|
|
473
|
+
break;
|
|
474
|
+
case 'streaming_ack':
|
|
475
|
+
// Server acknowledged streaming message and sent back Telegram messageId
|
|
476
|
+
const ack = message.payload;
|
|
477
|
+
if (ack.messageId) {
|
|
478
|
+
this.currentMessageId = ack.messageId;
|
|
479
|
+
}
|
|
480
|
+
break;
|
|
481
|
+
default:
|
|
482
|
+
// Ignore unknown messages
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Handle user message from Telegram
|
|
488
|
+
*/
|
|
489
|
+
handleUserMessage(message) {
|
|
490
|
+
const { chatId, text, telegramId, messageId, attachments } = message.payload;
|
|
491
|
+
this.currentChatId = chatId;
|
|
492
|
+
this.currentTelegramId = telegramId;
|
|
493
|
+
this.currentReplyToMessageId = messageId;
|
|
494
|
+
let prompt = text || '';
|
|
495
|
+
const attachmentInfo = attachments?.length ? ` with ${attachments.length} attachment(s)` : '';
|
|
496
|
+
console.log(`\nReceived from Telegram: ${text || '(no text)'}${attachmentInfo}`);
|
|
497
|
+
// Process attachments if present
|
|
498
|
+
if (attachments && attachments.length > 0) {
|
|
499
|
+
const savedFiles = this.saveAttachments(attachments);
|
|
500
|
+
if (savedFiles.length > 0) {
|
|
501
|
+
// Build prompt with attachment references
|
|
502
|
+
for (const file of savedFiles) {
|
|
503
|
+
if (file.type === 'voice') {
|
|
504
|
+
// For voice, ask Claude to listen and respond
|
|
505
|
+
prompt = `[Voice message saved to: ${file.path}]\n\nPlease listen to this voice message and respond to it.\n\n${prompt}`.trim();
|
|
506
|
+
}
|
|
507
|
+
else if (file.type === 'image') {
|
|
508
|
+
// For images, ask Claude to analyze
|
|
509
|
+
prompt = `[Image saved to: ${file.path}]\n\nPlease analyze this image.\n\n${prompt}`.trim();
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
// For documents, mention the file
|
|
513
|
+
prompt = `[File saved to: ${file.path}]\n\n${prompt}`.trim();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Ensure we have something to process
|
|
519
|
+
if (!prompt.trim()) {
|
|
520
|
+
this.sendResponse(chatId, 'No message content received.', 'complete');
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
this.sendStatus('busy');
|
|
524
|
+
// Send initial streaming status to trigger loading animation
|
|
525
|
+
this.sendResponse(chatId, '', 'streaming');
|
|
526
|
+
// Run claude with the prompt (streaming will send updates)
|
|
527
|
+
this.runClaude(prompt);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Save attachments to temp directory and return file paths
|
|
531
|
+
*/
|
|
532
|
+
saveAttachments(attachments) {
|
|
533
|
+
const savedFiles = [];
|
|
534
|
+
// Create temp directory in working directory
|
|
535
|
+
const attachmentsDir = path.join(this.workingDirectory, '.vibekit-attachments');
|
|
536
|
+
if (!fs.existsSync(attachmentsDir)) {
|
|
537
|
+
fs.mkdirSync(attachmentsDir, { recursive: true });
|
|
538
|
+
}
|
|
539
|
+
for (const attachment of attachments) {
|
|
540
|
+
try {
|
|
541
|
+
// Decode base64 data
|
|
542
|
+
const buffer = Buffer.from(attachment.data, 'base64');
|
|
543
|
+
// Generate unique filename
|
|
544
|
+
const timestamp = Date.now();
|
|
545
|
+
const filename = `${timestamp}-${attachment.filename}`;
|
|
546
|
+
const filePath = path.join(attachmentsDir, filename);
|
|
547
|
+
// Write file
|
|
548
|
+
fs.writeFileSync(filePath, buffer);
|
|
549
|
+
console.log(`Saved ${attachment.type}: ${filePath}`);
|
|
550
|
+
savedFiles.push({
|
|
551
|
+
type: attachment.type,
|
|
552
|
+
path: filePath,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
console.error(`Failed to save attachment ${attachment.filename}:`, error);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return savedFiles;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Handle new conversation command - reset conversation memory
|
|
563
|
+
*/
|
|
564
|
+
handleNewConversation() {
|
|
565
|
+
this.hasActiveConversation = false;
|
|
566
|
+
console.log('Conversation reset - next message will start fresh');
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Handle change directory command
|
|
570
|
+
*/
|
|
571
|
+
handleCdCommand(newPath) {
|
|
572
|
+
const resolved = path.resolve(this.workingDirectory, newPath);
|
|
573
|
+
try {
|
|
574
|
+
const fs = require('fs');
|
|
575
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
|
|
576
|
+
this.send({
|
|
577
|
+
type: 'cd_result',
|
|
578
|
+
payload: { success: false, error: 'Directory not found' },
|
|
579
|
+
timestamp: Date.now(),
|
|
580
|
+
messageId: this.generateId(),
|
|
581
|
+
});
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
this.workingDirectory = resolved;
|
|
585
|
+
this.config.setLastDirectory(resolved);
|
|
586
|
+
this.send({
|
|
587
|
+
type: 'cd_result',
|
|
588
|
+
payload: { success: true, path: resolved },
|
|
589
|
+
timestamp: Date.now(),
|
|
590
|
+
messageId: this.generateId(),
|
|
591
|
+
});
|
|
592
|
+
console.log(`Changed directory to: ${resolved}`);
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
this.send({
|
|
596
|
+
type: 'cd_result',
|
|
597
|
+
payload: { success: false, error: 'Failed to change directory' },
|
|
598
|
+
timestamp: Date.now(),
|
|
599
|
+
messageId: this.generateId(),
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Send streaming update to Telegram
|
|
605
|
+
*/
|
|
606
|
+
sendStreaming(chatId, text) {
|
|
607
|
+
this.send({
|
|
608
|
+
type: 'streaming',
|
|
609
|
+
payload: {
|
|
610
|
+
chatId,
|
|
611
|
+
messageId: this.currentMessageId,
|
|
612
|
+
text,
|
|
613
|
+
workingDirectory: this.workingDirectory,
|
|
614
|
+
},
|
|
615
|
+
timestamp: Date.now(),
|
|
616
|
+
messageId: this.generateId(),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Send a response back to Telegram
|
|
621
|
+
*/
|
|
622
|
+
sendResponse(chatId, text, status) {
|
|
623
|
+
if (!this.currentTelegramId) {
|
|
624
|
+
console.error('Cannot send response: no telegram ID');
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
this.send({
|
|
628
|
+
type: 'response',
|
|
629
|
+
payload: {
|
|
630
|
+
telegramId: this.currentTelegramId,
|
|
631
|
+
chatId,
|
|
632
|
+
text,
|
|
633
|
+
status,
|
|
634
|
+
replyToMessageId: this.currentReplyToMessageId || undefined,
|
|
635
|
+
},
|
|
636
|
+
timestamp: Date.now(),
|
|
637
|
+
messageId: this.generateId(),
|
|
638
|
+
});
|
|
639
|
+
if (status === 'complete') {
|
|
640
|
+
this.sendStatus('idle');
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Send status update
|
|
645
|
+
*/
|
|
646
|
+
sendStatus(status) {
|
|
647
|
+
this.send({
|
|
648
|
+
type: 'status',
|
|
649
|
+
payload: { status, workingDirectory: this.workingDirectory },
|
|
650
|
+
timestamp: Date.now(),
|
|
651
|
+
messageId: this.generateId(),
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Send a message to the WebSocket
|
|
656
|
+
*/
|
|
657
|
+
send(message) {
|
|
658
|
+
if (this.ws?.readyState === ws_1.default.OPEN) {
|
|
659
|
+
this.ws.send(JSON.stringify(message));
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Schedule reconnection
|
|
664
|
+
*/
|
|
665
|
+
scheduleReconnect() {
|
|
666
|
+
if (this.reconnectTimeout)
|
|
667
|
+
return;
|
|
668
|
+
console.log(`Reconnecting in ${this.reconnectDelay / 1000}s...`);
|
|
669
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
670
|
+
this.reconnectTimeout = null;
|
|
671
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
|
|
672
|
+
this.connect();
|
|
673
|
+
}, this.reconnectDelay);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Generate a unique message ID
|
|
677
|
+
*/
|
|
678
|
+
generateId() {
|
|
679
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Shutdown the agent
|
|
683
|
+
*/
|
|
684
|
+
shutdown() {
|
|
685
|
+
console.log('\nShutting down...');
|
|
686
|
+
this.stopStreaming();
|
|
687
|
+
if (this.claudeProcess) {
|
|
688
|
+
this.claudeProcess.kill();
|
|
689
|
+
}
|
|
690
|
+
if (this.ws) {
|
|
691
|
+
this.ws.close();
|
|
692
|
+
}
|
|
693
|
+
process.exit(0);
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Set up Claude credentials for auto mode
|
|
697
|
+
* In auto mode, credentials come from CLAUDE_OAUTH_JSON env var or credentials file
|
|
698
|
+
*/
|
|
699
|
+
async setupAutoModeCredentials() {
|
|
700
|
+
const credentialsFile = this.config.getCredentialsFile();
|
|
701
|
+
const oauthJson = process.env.CLAUDE_OAUTH_JSON;
|
|
702
|
+
// Determine where to get credentials from
|
|
703
|
+
let credentialsData = null;
|
|
704
|
+
if (credentialsFile && fs.existsSync(credentialsFile)) {
|
|
705
|
+
// Read from file
|
|
706
|
+
console.log(`[AutoMode] Loading credentials from ${credentialsFile}`);
|
|
707
|
+
credentialsData = fs.readFileSync(credentialsFile, 'utf-8');
|
|
708
|
+
}
|
|
709
|
+
else if (oauthJson) {
|
|
710
|
+
// Decode from environment variable (base64)
|
|
711
|
+
console.log('[AutoMode] Loading credentials from CLAUDE_OAUTH_JSON env var');
|
|
712
|
+
credentialsData = Buffer.from(oauthJson, 'base64').toString('utf-8');
|
|
713
|
+
}
|
|
714
|
+
if (!credentialsData) {
|
|
715
|
+
console.error('[AutoMode] No credentials found. Set CLAUDE_OAUTH_JSON or --credentials-file');
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
// Write credentials to Claude's expected location
|
|
719
|
+
const homeDir = process.env.HOME || '/home/agent';
|
|
720
|
+
const claudeDir = path.join(homeDir, '.claude');
|
|
721
|
+
if (!fs.existsSync(claudeDir)) {
|
|
722
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
723
|
+
}
|
|
724
|
+
// Write credentials
|
|
725
|
+
const credentialsPath = path.join(claudeDir, 'credentials.json');
|
|
726
|
+
fs.writeFileSync(credentialsPath, credentialsData, { mode: 0o600 });
|
|
727
|
+
// Also write settings to indicate onboarding is complete
|
|
728
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
729
|
+
if (!fs.existsSync(settingsPath)) {
|
|
730
|
+
const settings = {
|
|
731
|
+
hasCompletedOnboarding: true,
|
|
732
|
+
acceptedTerms: true,
|
|
733
|
+
permissions: { defaultMode: 'acceptEdits' }
|
|
734
|
+
};
|
|
735
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
736
|
+
}
|
|
737
|
+
console.log('[AutoMode] Claude credentials configured');
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
exports.AgentClient = AgentClient;
|
|
741
|
+
//# sourceMappingURL=agent.js.map
|