OverviewFilesCommitsIssuesDocsReleasesPeers

ollama_semantic_function_calling commits / eae53a0

Add device linking functionality and simulate multi-device setup
eae53a0e1c0497ac680dfee51ade07bdf9987aba
by fleeky.ashatron <fleeky@7rnx.net> · 2025-04-21T18:36:17.000Z

Changed files (5) +893 -0

ollamatools-new/second-device-keet/keet_cmd.js +778 -0
……@@ -0,0 +1,778 @@
1#!/usr/bin/env node
2/**
3 * Keet Command-line Agent
4 *
5 * This script provides a minimal CLI agent for Keet that can:
6 * - Join a room
7 * - Set its name
8 * - Respond to user messages using Ollama
9 */
10
11import fs from 'fs';
12import path from 'path';
13import { fileURLToPath } from 'url';
14import { parseArgs } from 'node:util';
15import fetch from 'node-fetch';
16
17// Get the directory name of the current module
18const __filename = fileURLToPath(import.meta.url);
19const __dirname = path.dirname(__filename);
20
21// Configure logging
22const logLevels = {
23 INFO: 'INFO',
24 ERROR: 'ERROR',
25 WARNING: 'WARNING',
26 DEBUG: 'DEBUG'
27};
28
29function log(level, message) {
30 const timestamp = new Date().toISOString();
31 console.log(`[${timestamp}] [${level}] ${message}`);
32}
33
34// More detailed logging for verbose mode
35function verboseLog(message, args) {
36 if (args && args.verbose) {
37 const timestamp = new Date().toISOString();
38 console.log(`[${timestamp}] [VERBOSE] ${message}`);
39 }
40}
41
42// Import Keet dependencies or mock implementations
43let KeetBackend, KeetCoreApi;
44let KEET_AVAILABLE = false;
45let useMock = false;
46
47async function importDependencies() {
48 try {
49 // Try to import the real Keet dependencies
50 const backendModule = await import('@holepunchto/keet-backend');
51 const coreApiModule = await import('@holepunchto/keet-core-api');
52
53 // Try to get the exports
54 KeetBackend = backendModule.default || backendModule;
55 KeetCoreApi = coreApiModule.default || coreApiModule;
56
57 KEET_AVAILABLE = true;
58
59 log(logLevels.INFO, 'Successfully imported Keet dependencies');
60 } catch (error) {
61 log(logLevels.WARNING, `Failed to import Keet dependencies: ${error.message}`);
62 log(logLevels.WARNING, 'Using mock implementation');
63
64 // Create a simple mock implementation
65 class MockKeetBackend {
66 constructor(storagePath, options) {
67 this.storagePath = storagePath;
68 this.options = options;
69 this.api = {
70 core: {
71 getIdentity: async () => {
72 try {
73 const identityPath = path.join(storagePath, 'identity.json');
74 if (fs.existsSync(identityPath)) {
75 return JSON.parse(fs.readFileSync(identityPath, 'utf8'));
76 }
77 return null;
78 } catch (error) {
79 return null;
80 }
81 },
82 generateIdentity: async () => {
83 const identity = {
84 publicKey: `mock-${Date.now()}`,
85 displayName: 'Mock User'
86 };
87 const identityPath = path.join(storagePath, 'identity.json');
88 fs.writeFileSync(identityPath, JSON.stringify(identity));
89 return identity;
90 },
91 getRecentRooms: async () => {
92 try {
93 const roomsPath = path.join(storagePath, 'rooms.json');
94 if (fs.existsSync(roomsPath)) {
95 return JSON.parse(fs.readFileSync(roomsPath, 'utf8'));
96 }
97 return [];
98 } catch (error) {
99 return [];
100 }
101 },
102 pairRoom: async (roomKey) => {
103 const rooms = await this.api.core.getRecentRooms();
104 const newRoom = {
105 roomId: roomKey,
106 title: `Room ${roomKey.substring(0, 8)}`,
107 lastMessage: null,
108 timestamp: Date.now()
109 };
110 rooms.push(newRoom);
111 const roomsPath = path.join(storagePath, 'rooms.json');
112 fs.writeFileSync(roomsPath, JSON.stringify(rooms));
113 return true;
114 },
115 getChatMessages: async (roomId) => {
116 try {
117 const messagesPath = path.join(storagePath, `messages_${roomId}.json`);
118 if (fs.existsSync(messagesPath)) {
119 return JSON.parse(fs.readFileSync(messagesPath, 'utf8'));
120 }
121 return [];
122 } catch (error) {
123 return [];
124 }
125 },
126 addChatMessage: async (roomId, text) => {
127 const messages = await this.api.core.getChatMessages(roomId);
128 const identity = await this.api.core.getIdentity();
129 const newMessage = {
130 id: `msg-${Date.now()}`,
131 timestamp: Date.now(),
132 author: identity.publicKey,
133 chat: {
134 text: text
135 }
136 };
137 messages.push(newMessage);
138 const messagesPath = path.join(storagePath, `messages_${roomId}.json`);
139 fs.writeFileSync(messagesPath, JSON.stringify(messages));
140 return true;
141 },
142 setDisplayName: async (name) => {
143 const identity = await this.api.core.getIdentity();
144 if (identity) {
145 identity.displayName = name;
146 const identityPath = path.join(storagePath, 'identity.json');
147 fs.writeFileSync(identityPath, JSON.stringify(identity));
148 }
149 return true;
150 }
151 }
152 };
153 }
154
155 async ready() {
156 // Create necessary directories
157 if (!fs.existsSync(this.storagePath)) {
158 fs.mkdirSync(this.storagePath, { recursive: true });
159 }
160
161 // Initialize rooms file if it doesn't exist
162 const roomsPath = path.join(this.storagePath, 'rooms.json');
163 if (!fs.existsSync(roomsPath)) {
164 fs.writeFileSync(roomsPath, JSON.stringify([]));
165 }
166
167 return true;
168 }
169
170 async close() {
171 return true;
172 }
173 }
174
175 KeetBackend = MockKeetBackend;
176 KEET_AVAILABLE = true;
177 useMock = true;
178 }
179}
180
181/**
182 * Parse command-line arguments.
183 */
184function parseArguments() {
185 const options = {
186 storage: { type: 'string' },
187 'room-key': { type: 'string' },
188 name: { type: 'string', default: 'KeetBot' },
189 model: { type: 'string', default: 'llama3.1:latest' },
190 'ollama-host': { type: 'string', default: 'localhost:11434' },
191 prompt: { type: 'string' },
192 mock: { type: 'boolean', default: false },
193 verbose: { type: 'boolean', default: false },
194 'link-device': { type: 'string' }, // Name of the device to link
195 'confirm-device': { type: 'string' }, // Device ID to confirm
196 'mobile-device': { type: 'boolean', default: false } // Whether the linked device is mobile
197 };
198
199 const { values } = parseArgs({ options });
200
201 // Force mock mode if specified
202 if (values.mock) {
203 useMock = true;
204 }
205
206 return values;
207}
208
209/**
210 * Ollama client for interacting with the Ollama API.
211 */
212class OllamaClient {
213 constructor(host = 'localhost:11434', model = 'llama3.1:latest', systemPrompt = null) {
214 this.host = host;
215 this.model = model;
216 this.systemPrompt = systemPrompt;
217 this.baseUrl = `http://${host}`;
218
219 // User context tracking
220 this.userContexts = new Map();
221 this.maxContextLength = 10; // Maximum number of messages to keep in context
222 }
223
224 /**
225 * Call Ollama chat API to generate a response.
226 */
227 async chat(message, userId, roomId) {
228 try {
229 const contextKey = `${userId}:${roomId}`;
230 const messages = [];
231
232 // Add system prompt if provided
233 if (this.systemPrompt) {
234 messages.push({
235 role: 'system',
236 content: this.systemPrompt
237 });
238 }
239
240 // Add conversation context if available
241 if (this.userContexts.has(contextKey)) {
242 for (const ctxMsg of this.userContexts.get(contextKey)) {
243 messages.push(ctxMsg);
244 }
245 }
246
247 // Add user message
248 const userMessage = {
249 role: 'user',
250 content: message
251 };
252 messages.push(userMessage);
253
254 // Store user message in context
255 if (!this.userContexts.has(contextKey)) {
256 this.userContexts.set(contextKey, []);
257 }
258 this.userContexts.get(contextKey).push(userMessage);
259
260 // Trim context if it gets too long
261 if (this.userContexts.get(contextKey).length > this.maxContextLength) {
262 this.userContexts.set(
263 contextKey,
264 this.userContexts.get(contextKey).slice(-this.maxContextLength)
265 );
266 }
267
268 // Make API request to Ollama
269 const response = await fetch(`${this.baseUrl}/api/chat`, {
270 method: 'POST',
271 headers: {
272 'Content-Type': 'application/json'
273 },
274 body: JSON.stringify({
275 model: this.model,
276 messages: messages,
277 stream: false
278 })
279 });
280
281 if (!response.ok) {
282 const errorText = await response.text();
283 throw new Error(`Ollama API error (${response.status}): ${errorText}`);
284 }
285
286 const data = await response.json();
287 const messageContent = data.message.content;
288
289 // Store assistant response in context
290 this.userContexts.get(contextKey).push({
291 role: 'assistant',
292 content: messageContent
293 });
294
295 // Trim context if it gets too long
296 if (this.userContexts.get(contextKey).length > this.maxContextLength) {
297 this.userContexts.set(
298 contextKey,
299 this.userContexts.get(contextKey).slice(-this.maxContextLength)
300 );
301 }
302
303 return messageContent;
304 } catch (error) {
305 log(logLevels.ERROR, `Error calling Ollama API: ${error.message}`);
306 throw error;
307 }
308 }
309
310 /**
311 * List available models from Ollama.
312 */
313 async listModels() {
314 try {
315 const response = await fetch(`${this.baseUrl}/api/tags`);
316
317 if (!response.ok) {
318 const errorText = await response.text();
319 throw new Error(`Ollama API error (${response.status}): ${errorText}`);
320 }
321
322 const data = await response.json();
323 return data.models.map(model => model.name);
324 } catch (error) {
325 log(logLevels.ERROR, `Error listing Ollama models: ${error.message}`);
326 throw error;
327 }
328 }
329
330 /**
331 * Change the model being used.
332 */
333 changeModel(model) {
334 this.model = model;
335 log(logLevels.INFO, `Changed Ollama model to: ${model}`);
336 }
337
338 /**
339 * Clear context for a specific user/room.
340 */
341 clearContext(userId, roomId) {
342 const contextKey = `${userId}:${roomId}`;
343 this.userContexts.delete(contextKey);
344 log(logLevels.INFO, `Cleared context for ${userId} in room ${roomId}`);
345 }
346}
347
348/**
349 * Process new messages and respond using Ollama.
350 */
351async function processMessages(backend, roomId, lastProcessedTimestamp, ollamaClient, verbose) {
352 try {
353 // Get messages from the room
354 verboseLog(`Fetching messages from room ${roomId}...`, { verbose });
355 const messages = await backend.api.core.getChatMessages(roomId);
356 verboseLog(`Retrieved ${messages.length} total messages from room`, { verbose });
357
358 // Get identity for filtering
359 const identity = await backend.api.core.getIdentity();
360 verboseLog(`Current identity: ${identity ? identity.publicKey : 'undefined'}`, { verbose });
361
362 // Filter messages that are newer than the last processed timestamp
363 const newMessages = messages.filter(msg => {
364 // Skip our own messages
365 if (identity && identity.publicKey && msg.author === identity.publicKey) {
366 verboseLog(`Skipping own message with ID: ${typeof msg.id === 'object' ? JSON.stringify(msg.id) : msg.id}`, { verbose });
367 return false;
368 }
369
370 // Check if the message is newer than the last processed timestamp
371 const isNewer = msg.timestamp > lastProcessedTimestamp;
372 if (isNewer) {
373 verboseLog(`Found new message with ID: ${typeof msg.id === 'object' ? JSON.stringify(msg.id) : msg.id}, timestamp: ${new Date(msg.timestamp).toISOString()}`, { verbose });
374 }
375 return isNewer;
376 });
377
378 verboseLog(`Found ${newMessages.length} new messages to process`, { verbose });
379
380 if (newMessages.length > 0) {
381 // Update the last processed timestamp
382 const latestTimestamp = Math.max(...newMessages.map(msg => msg.timestamp));
383 verboseLog(`Updating last processed timestamp to: ${new Date(latestTimestamp).toISOString()}`, { verbose });
384
385 // Process each new message
386 for (const message of newMessages) {
387 if (message.chat && message.chat.text) {
388 const authorId = message.author ? message.author.substring(0, 8) : 'unknown';
389 log(logLevels.INFO, `New message from ${authorId}: ${message.chat.text}`);
390
391 try {
392 // Generate a response using Ollama
393 log(logLevels.INFO, `Generating response using Ollama (model: ${ollamaClient.model})...`);
394 const response = await ollamaClient.chat(message.chat.text, message.author || 'unknown-user', roomId);
395
396 // Send the response back to the room
397 log(logLevels.INFO, `Sending response to room ${roomId}: ${response}`);
398 verboseLog(`Attempting to send message to room ${roomId}...`, { verbose });
399 await backend.api.core.addChatMessage(roomId, response);
400 log(logLevels.INFO, `✓ Message successfully sent to room`);
401 } catch (error) {
402 log(logLevels.ERROR, `Failed to generate or send response: ${error.message}`);
403 }
404 }
405 }
406
407 return latestTimestamp;
408 } else {
409 verboseLog(`No new messages to process`, { verbose });
410 }
411
412 return lastProcessedTimestamp;
413 } catch (error) {
414 log(logLevels.ERROR, `Error processing messages: ${error.message}`);
415 return lastProcessedTimestamp;
416 }
417}
418
419/**
420 * Main function.
421 */
422async function main() {
423 // Parse command-line arguments
424 const args = parseArguments();
425
426 // Set verbose logging if requested
427 if (args.verbose) {
428 console.log('Verbose logging enabled');
429 }
430
431 // Import dependencies
432 await importDependencies();
433
434 if (!KEET_AVAILABLE) {
435 log(logLevels.ERROR, 'Keet dependencies not available');
436 return 1;
437 }
438
439 // Create a storage directory if it doesn't exist
440 const storagePath = args.storage || path.join(__dirname, '..', 'test-keet-storage');
441 if (!fs.existsSync(storagePath)) {
442 fs.mkdirSync(storagePath, { recursive: true });
443 }
444
445 // Initialize Keet backend
446 log(logLevels.INFO, 'Initializing Keet backend...');
447 const backend = new KeetBackend(storagePath, {
448 experimental: false,
449 devMirrors: false
450 });
451
452 // Wait for backend to be ready
453 await backend.ready();
454
455 // Check if we have an identity
456 log(logLevels.INFO, 'Checking identity...');
457 let identity = await backend.api.core.getIdentity();
458
459// If we don't have an identity, create one
460if (!identity) {
461 log(logLevels.INFO, 'No identity found, generating a new one...');
462 await backend.api.core.generateIdentity();
463 identity = await backend.api.core.getIdentity();
464
465 // If we're using a mock implementation, we need to manually set up the identity
466 if (useMock) {
467 log(logLevels.INFO, 'Using mock implementation, manually setting up identity...');
468
469 // Create a proper identity structure
470 const identityPath = path.join(storagePath, 'identity.json');
471 const mockIdentity = {
472 anonymous: false, // Set to false for a real identity
473 memberId: `real-${Date.now()}`,
474 timestamp: Date.now(),
475 displayName: args.name || 'KeetBot',
476 avatar: null,
477 devices: [{
478 deviceId: `device-${Date.now()}`,
479 isMobile: false,
480 name: 'Primary Device',
481 confirmed: true
482 }],
483 local: {
484 deviceId: `device-${Date.now()}`
485 }
486 };
487
488 // Make sure the local deviceId matches the first device in the devices array
489 mockIdentity.local.deviceId = mockIdentity.devices[0].deviceId;
490
491 // Write the identity to the file
492 fs.writeFileSync(identityPath, JSON.stringify(mockIdentity));
493
494 // Update our identity reference
495 identity = mockIdentity;
496 log(logLevels.INFO, `Created new identity with ID: ${identity.memberId}`);
497 }
498}
499
500// Set the display name if provided
501if (args.name && identity) {
502 log(logLevels.INFO, `Setting display name to: ${args.name}`);
503 if (backend.api.core.setDisplayName) {
504 await backend.api.core.setDisplayName(args.name);
505 } else {
506 log(logLevels.WARNING, 'setDisplayName method not available');
507
508 // If we're using a mock implementation, manually update the display name
509 if (useMock) {
510 const identityPath = path.join(storagePath, 'identity.json');
511 identity.displayName = args.name;
512 fs.writeFileSync(identityPath, JSON.stringify(identity));
513 log(logLevels.INFO, `Manually updated display name to: ${args.name}`);
514 }
515 }
516}
517
518// Add a function to link a new device to the identity
519async function linkDevice(deviceName = 'New Device', isMobile = false) {
520 try {
521 // Get the current identity
522 const identity = await backend.api.core.getIdentity();
523 if (!identity) {
524 log(logLevels.ERROR, 'No identity found, cannot link device');
525 return null;
526 }
527
528 // If we're using the real Keet implementation
529 if (!useMock) {
530 // Check if there's a linkDevice method available
531 if (backend.api.core.linkDevice) {
532 log(logLevels.INFO, `Linking new device: ${deviceName}`);
533 const result = await backend.api.core.linkDevice(deviceName, isMobile);
534 log(logLevels.INFO, `Device linked successfully: ${JSON.stringify(result)}`);
535 return result;
536 } else {
537 log(logLevels.WARNING, 'linkDevice method not available');
538 return null;
539 }
540 } else {
541 // For mock implementation, manually add a device to the identity
542 const identityPath = path.join(storagePath, 'identity.json');
543
544 // Create a new device
545 const newDevice = {
546 deviceId: `device-${Date.now()}`,
547 isMobile: isMobile,
548 name: deviceName,
549 confirmed: false // New devices start as unconfirmed
550 };
551
552 // Add the device to the identity
553 identity.devices.push(newDevice);
554
555 // Write the updated identity to the file
556 fs.writeFileSync(identityPath, JSON.stringify(identity));
557
558 log(logLevels.INFO, `Manually added new device: ${newDevice.deviceId}`);
559 return newDevice;
560 }
561 } catch (error) {
562 log(logLevels.ERROR, `Error linking device: ${error.message}`);
563 return null;
564 }
565}
566
567// Add a function to confirm a linked device
568async function confirmDevice(deviceId) {
569 try {
570 // Get the current identity
571 const identity = await backend.api.core.getIdentity();
572 if (!identity) {
573 log(logLevels.ERROR, 'No identity found, cannot confirm device');
574 return false;
575 }
576
577 // If we're using the real Keet implementation
578 if (!useMock) {
579 // Check if there's a confirmDevice method available
580 if (backend.api.core.confirmDevice) {
581 log(logLevels.INFO, `Confirming device: ${deviceId}`);
582 const result = await backend.api.core.confirmDevice(deviceId);
583 log(logLevels.INFO, `Device confirmed successfully`);
584 return result;
585 } else {
586 log(logLevels.WARNING, 'confirmDevice method not available');
587 return false;
588 }
589 } else {
590 // For mock implementation, manually confirm the device
591 const identityPath = path.join(storagePath, 'identity.json');
592
593 // Find the device in the identity
594 const deviceIndex = identity.devices.findIndex(device => device.deviceId === deviceId);
595 if (deviceIndex === -1) {
596 log(logLevels.ERROR, `Device not found: ${deviceId}`);
597 return false;
598 }
599
600 // Confirm the device
601 identity.devices[deviceIndex].confirmed = true;
602
603 // Write the updated identity to the file
604 fs.writeFileSync(identityPath, JSON.stringify(identity));
605
606 log(logLevels.INFO, `Manually confirmed device: ${deviceId}`);
607 return true;
608 }
609 } catch (error) {
610 log(logLevels.ERROR, `Error confirming device: ${error.message}`);
611 return false;
612 }
613}
614
615 log(logLevels.INFO, `Current identity: ${JSON.stringify(identity)}`);
616
617 // Handle device linking if requested
618 if (args['link-device']) {
619 log(logLevels.INFO, `Linking new device: ${args['link-device']}`);
620 const newDevice = await linkDevice(args['link-device'], args['mobile-device']);
621 if (newDevice) {
622 log(logLevels.INFO, `Successfully linked new device: ${JSON.stringify(newDevice)}`);
623 log(logLevels.INFO, `Device ID: ${newDevice.deviceId}`);
624 log(logLevels.INFO, `To confirm this device, run with --confirm-device=${newDevice.deviceId}`);
625
626 // Exit after linking the device
627 return 0;
628 } else {
629 log(logLevels.ERROR, 'Failed to link device');
630 return 1;
631 }
632 }
633
634 // Handle device confirmation if requested
635 if (args['confirm-device']) {
636 log(logLevels.INFO, `Confirming device: ${args['confirm-device']}`);
637 const confirmed = await confirmDevice(args['confirm-device']);
638 if (confirmed) {
639 log(logLevels.INFO, `Successfully confirmed device: ${args['confirm-device']}`);
640
641 // Exit after confirming the device
642 return 0;
643 } else {
644 log(logLevels.ERROR, `Failed to confirm device: ${args['confirm-device']}`);
645 return 1;
646 }
647 }
648
649 // Initialize Ollama client
650 log(logLevels.INFO, `Initializing Ollama client with host: ${args['ollama-host']}, model: ${args.model}`);
651 const ollamaClient = new OllamaClient(args['ollama-host'], args.model, args.prompt);
652
653 // Join the room if a room key is provided
654 let roomId;
655 if (args['room-key']) {
656 const roomKey = args['room-key'];
657 log(logLevels.INFO, `Joining room with key: ${roomKey}`);
658
659 // Extract the room key from the invite link if needed
660 let normalizedRoomKey = roomKey;
661 if (roomKey.startsWith('pear://keet/')) {
662 normalizedRoomKey = roomKey.replace('pear://keet/', '');
663 verboseLog(`Extracted room key from pear link: ${normalizedRoomKey}`, args);
664 }
665
666 // Check if there's a pairRoom method available
667 if (backend.api.core.pairRoom) {
668 log(logLevels.INFO, 'Using pairRoom method to join the room...');
669 verboseLog(`Attempting to pair with room using key: ${normalizedRoomKey.substring(0, 16)}...`, args);
670
671 try {
672 verboseLog(`Starting room pairing process...`, args);
673 await backend.api.core.pairRoom(normalizedRoomKey);
674 log(logLevels.INFO, '✓ Successfully paired with the room');
675 roomId = normalizedRoomKey;
676
677 // Verify the room is in recent rooms after pairing
678 const recentRooms = await backend.api.core.getRecentRooms();
679 const roomFound = recentRooms.some(room => room.roomId === normalizedRoomKey);
680
681 if (roomFound) {
682 verboseLog(`Room verified in recent rooms list`, args);
683 } else {
684 log(logLevels.WARNING, `Room paired but not found in recent rooms list - this may cause issues`);
685 }
686 } catch (error) {
687 log(logLevels.ERROR, `Failed to pair with room: ${error.message}`);
688 verboseLog(`Detailed error: ${error.stack || 'No stack trace available'}`, args);
689 return 1;
690 }
691 } else {
692 log(logLevels.ERROR, 'pairRoom method not available');
693 verboseLog(`Available API methods: ${Object.keys(backend.api.core).join(', ')}`, args);
694 return 1;
695 }
696 } else {
697 // Get recent rooms
698 log(logLevels.INFO, 'No room key provided, checking recent rooms...');
699 const recentRooms = await backend.api.core.getRecentRooms();
700 verboseLog(`Found ${recentRooms.length} recent rooms`, args);
701
702 if (recentRooms.length > 0) {
703 roomId = recentRooms[0].roomId;
704 log(logLevels.INFO, `Using the most recent room: ${roomId.substring(0, 16)}...`);
705 verboseLog(`Room details: ${JSON.stringify(recentRooms[0])}`, args);
706 } else {
707 log(logLevels.ERROR, 'No room key provided and no recent rooms found');
708 return 1;
709 }
710 }
711
712 // Main loop to process messages
713 log(logLevels.INFO, `Starting message processing loop for room: ${roomId}`);
714 log(logLevels.INFO, `Using Ollama model: ${args.model}`);
715
716 // Get the current timestamp to avoid processing old messages
717 let lastProcessedTimestamp = Date.now();
718
719 try {
720 // Send an initial message to the room
721 const initialMessage = `Hello! I'm ${args.name}, a Keet bot powered by Ollama. I'm here to assist you.`;
722 log(logLevels.INFO, `Sending initial greeting message to room ${roomId.substring(0, 16)}...`);
723 verboseLog(`Full message: "${initialMessage}"`, args);
724
725 try {
726 verboseLog(`Attempting to send initial message...`, args);
727 await backend.api.core.addChatMessage(roomId, initialMessage);
728 log(logLevels.INFO, '✓ Initial greeting message sent successfully');
729 } catch (sendError) {
730 log(logLevels.ERROR, `Failed to send initial message: ${sendError.message}`);
731 verboseLog(`Detailed error: ${sendError.stack || 'No stack trace available'}`, args);
732 }
733
734 // Main loop
735 log(logLevels.INFO, `Starting main message processing loop`);
736 verboseLog(`Message polling interval: 2000ms`, args);
737
738 while (true) {
739 verboseLog(`Checking for new messages in room ${roomId.substring(0, 16)}...`, args);
740
741 // Process new messages
742 lastProcessedTimestamp = await processMessages(
743 backend,
744 roomId,
745 lastProcessedTimestamp,
746 ollamaClient,
747 args.verbose
748 );
749
750 // Wait a bit before checking for new messages
751 verboseLog(`Waiting 2000ms before next message check...`, args);
752 await new Promise(resolve => setTimeout(resolve, 2000));
753 }
754 } catch (error) {
755 log(logLevels.ERROR, `Error in main loop: ${error.message}`);
756 verboseLog(`Detailed error: ${error.stack || 'No stack trace available'}`, args);
757
758 // Close the backend
759 log(logLevels.INFO, 'Closing backend...');
760 await backend.close();
761
762 return 1;
763 }
764}
765
766// Run the main function if this file is executed directly
767if (import.meta.url === `file://${process.argv[1]}`) {
768 try {
769 const exitCode = await main();
770 process.exit(exitCode);
771 } catch (error) {
772 log(logLevels.ERROR, `Unhandled error: ${error.message}`);
773 log(logLevels.ERROR, error.stack);
774 process.exit(1);
775 }
776}
777
778export { main };
ollamatools-new/second-device-keet/link_device.js +100 -0
……@@ -0,0 +1,100 @@
1#!/usr/bin/env node
2/**
3 * Script to link a new device to an existing Keet identity
4 */
5
6import fs from 'fs';
7import path from 'path';
8import { fileURLToPath } from 'url';
9
10// Get the directory name of the current module
11const __filename = fileURLToPath(import.meta.url);
12const __dirname = path.dirname(__filename);
13
14// Configure logging
15function log(level, message) {
16 const timestamp = new Date().toISOString();
17 console.log(`[${timestamp}] [${level}] ${message}`);
18}
19
20// Main function
21async function main() {
22 try {
23 // Path to the first device's identity file
24 const firstDeviceIdentityPath = path.join(__dirname, '..', 'test-keet-storage', 'identity.json');
25
26 // Path to the second device's storage directory
27 const secondDeviceStoragePath = path.join(__dirname, 'storage');
28
29 // Create the storage directory if it doesn't exist
30 if (!fs.existsSync(secondDeviceStoragePath)) {
31 fs.mkdirSync(secondDeviceStoragePath, { recursive: true });
32 }
33
34 // Read the first device's identity
35 log('INFO', `Reading identity from first device: ${firstDeviceIdentityPath}`);
36 const firstDeviceIdentity = JSON.parse(fs.readFileSync(firstDeviceIdentityPath, 'utf8'));
37 log('INFO', `First device identity: ${JSON.stringify(firstDeviceIdentity)}`);
38
39 // Create a new device ID
40 const newDeviceId = `device-${Date.now()}`;
41 log('INFO', `Generated new device ID: ${newDeviceId}`);
42
43 // Create a new device entry
44 const newDevice = {
45 deviceId: newDeviceId,
46 isMobile: true,
47 name: 'Mobile Device',
48 confirmed: false // New devices start as unconfirmed
49 };
50
51 // Create a new identity for the second device that links to the first device's identity
52 const secondDeviceIdentity = {
53 anonymous: false, // Set to false for a real identity
54 memberId: firstDeviceIdentity.memberId, // Use the same memberId as the first device
55 timestamp: Date.now(),
56 displayName: firstDeviceIdentity.displayName,
57 avatar: firstDeviceIdentity.avatar,
58 devices: [
59 ...firstDeviceIdentity.devices, // Include all devices from the first device
60 newDevice // Add the new device
61 ],
62 local: {
63 deviceId: newDeviceId // Set the local device ID to the new device ID
64 }
65 };
66
67 // Write the new identity to the second device's storage directory
68 const secondDeviceIdentityPath = path.join(secondDeviceStoragePath, 'identity.json');
69 log('INFO', `Writing identity to second device: ${secondDeviceIdentityPath}`);
70 fs.writeFileSync(secondDeviceIdentityPath, JSON.stringify(secondDeviceIdentity));
71
72 // Also update the first device's identity to include the new device
73 log('INFO', `Updating first device identity to include the new device`);
74 firstDeviceIdentity.devices.push(newDevice);
75 fs.writeFileSync(firstDeviceIdentityPath, JSON.stringify(firstDeviceIdentity));
76
77 log('INFO', `Device linking completed successfully!`);
78 log('INFO', `New device ID: ${newDeviceId}`);
79 log('INFO', `To confirm this device, run the following command:`);
80 log('INFO', `node keet_cmd.js --confirm-device=${newDeviceId} --storage=../test-keet-storage`);
81
82 return 0;
83 } catch (error) {
84 log('ERROR', `Error linking device: ${error.message}`);
85 log('ERROR', error.stack);
86 return 1;
87 }
88}
89
90// Run the main function if this file is executed directly
91if (import.meta.url === `file://${process.argv[1]}`) {
92 try {
93 const exitCode = await main();
94 process.exit(exitCode);
95 } catch (error) {
96 log('ERROR', `Unhandled error: ${error.message}`);
97 log('ERROR', error.stack);
98 process.exit(1);
99 }
100}
ollamatools-new/second-device-keet/package.json +13 -0
……@@ -0,0 +1,13 @@
1{
2 "name": "keet-messaging-tests",
3 "version": "1.0.0",
4 "description": "Tests for Keet messaging integration",
5 "type": "module",
6 "private": true,
7 "dependencies": {
8 "@holepunchto/keet-backend": "file:../keet/node_modules/@holepunchto/keet-backend",
9 "@holepunchto/keet-core-api": "file:../keet/node_modules/@holepunchto/keet-core-api",
10 "@holepunchto/keet-store": "file:../keet/node_modules/@holepunchto/keet-store",
11 "node-fetch": "^3.3.2"
12 }
13}
ollamatools-new/second-device-keet/storage/identity.json +1 -0
……@@ -0,0 +1 @@
1{"anonymous":false,"memberId":"s3hngfgxfgs9db17jxynwxpdyieq8sesoo91wnd5mycp56t6o5ay","timestamp":1745260473844,"displayName":"KeetBot","avatar":null,"devices":[{"deviceId":"s3hngfgxfgs9db17jxynwxpdyieq8sesoo91wnd5mycp56t6o5ay","isMobile":false,"name":null,"confirmed":true},{"deviceId":"device-1745260473843","isMobile":true,"name":"Mobile Device","confirmed":true}],"local":{"deviceId":"device-1745260473843"}}
ollamatools-new/test-keet-storage/identity.json +1 -0
……@@ -0,0 +1 @@
1{"anonymous":false,"memberId":"s3hngfgxfgs9db17jxynwxpdyieq8sesoo91wnd5mycp56t6o5ay","timestamp":1745254354218,"displayName":"KeetBot","avatar":null,"devices":[{"deviceId":"s3hngfgxfgs9db17jxynwxpdyieq8sesoo91wnd5mycp56t6o5ay","isMobile":false,"name":null,"confirmed":true},{"deviceId":"device-1745260473843","isMobile":true,"name":"Mobile Device","confirmed":true}],"local":{"deviceId":"s3hngfgxfgs9db17jxynwxpdyieq8sesoo91wnd5mycp56t6o5ay"}}