From f1018c2ae8e6bb308c25532c16ba8e54618d833a Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Tue, 23 Sep 2025 22:07:55 +0100 Subject: [PATCH 1/2] chat: expose underlying types in JSON When doing JSON representation, use the underlying Chat types rather than CLI wrappers. --- src/commands/rooms/messages/history.ts | 13 +- src/commands/rooms/messages/send.ts | 12 +- src/commands/rooms/messages/subscribe.ts | 195 ++++++++++++---------- src/commands/rooms/occupancy/subscribe.ts | 123 +++++++------- src/commands/rooms/presence/enter.ts | 17 +- src/commands/rooms/presence/subscribe.ts | 85 +++++++--- 6 files changed, 262 insertions(+), 183 deletions(-) diff --git a/src/commands/rooms/messages/history.ts b/src/commands/rooms/messages/history.ts index 1c0a67b3..284799d5 100644 --- a/src/commands/rooms/messages/history.ts +++ b/src/commands/rooms/messages/history.ts @@ -80,21 +80,16 @@ export default class MessagesHistory extends ChatBaseCommand { } // Get historical messages - const messagesResult = await room.messages.history({ limit: flags.limit }); + const messagesResult = await room.messages.history({ + limit: flags.limit, + }); const { items } = messagesResult; if (this.shouldOutputJson(flags)) { this.log( this.formatJsonOutput( { - messages: items.map((message) => ({ - clientId: message.clientId, - text: message.text, - timestamp: message.timestamp, - ...(flags["show-metadata"] && message.metadata - ? { metadata: message.metadata } - : {}), - })), + messages: items, roomId: args.roomId, success: true, }, diff --git a/src/commands/rooms/messages/send.ts b/src/commands/rooms/messages/send.ts index 90b51d8c..0b75b181 100644 --- a/src/commands/rooms/messages/send.ts +++ b/src/commands/rooms/messages/send.ts @@ -1,6 +1,6 @@ import { Args, Flags } from "@oclif/core"; import * as Ably from "ably"; // Import Ably -import { ChatClient } from "@ably/chat"; +import { ChatClient, Message } from "@ably/chat"; import { ChatBaseCommand } from "../../../chat-base-command.js"; @@ -13,7 +13,7 @@ interface MessageToSend { interface MessageResult { index?: number; - message?: MessageToSend; + message?: Message; roomId: string; success: boolean; error?: string; @@ -289,11 +289,11 @@ export default class MessagesSend extends ChatBaseCommand { // Send the message without awaiting room.messages .send(messageToSend) - .then(() => { + .then((sent: Message) => { sentCount++; const result: MessageResult = { index: i + 1, - message: messageToSend, + message: sent, roomId: args.roomId, success: true, }; @@ -414,9 +414,9 @@ export default class MessagesSend extends ChatBaseCommand { ); // Send the message - await room.messages.send(messageToSend); + const sent = await room.messages.send(messageToSend); const result: MessageResult = { - message: messageToSend, + message: sent, roomId: args.roomId, success: true, }; diff --git a/src/commands/rooms/messages/subscribe.ts b/src/commands/rooms/messages/subscribe.ts index e5810b22..9390f779 100644 --- a/src/commands/rooms/messages/subscribe.ts +++ b/src/commands/rooms/messages/subscribe.ts @@ -1,52 +1,18 @@ import { Args, Flags } from "@oclif/core"; import * as Ably from "ably"; -import { Subscription, StatusSubscription, ChatMessageEvent } from "@ably/chat"; // Import ChatClient and StatusSubscription +import { + ChatClient, + Subscription, + StatusSubscription, + ChatMessageEvent, + Room, + RoomStatusChange, +} from "@ably/chat"; import chalk from "chalk"; import { ChatBaseCommand } from "../../../chat-base-command.js"; import { waitUntilInterruptedOrTimeout } from "../../../utils/long-running.js"; -// Define message interface -interface ChatMessage { - clientId: string; - text: string; - timestamp: number | Date; // Support both timestamp types - metadata?: Record; - [key: string]: unknown; -} - -// Define status change interface -interface StatusChange { - current: string; - reason?: { - message?: string; - code?: number; - [key: string]: unknown; - }; - [key: string]: unknown; -} - -// Define room interface -interface ChatRoom { - messages: { - subscribe: (callback: (event: ChatMessageEvent) => void) => Subscription; - }; - onStatusChange: (callback: (statusChange: unknown) => void) => StatusSubscription; - attach: () => Promise; - error?: { - message?: string; - }; -} - -// Define chat client interface -interface ChatClientType { - rooms: { - get: (roomId: string, options: Record) => Promise; - release: (roomId: string) => Promise; - }; - clientId?: string; -} - export default class MessagesSubscribe extends ChatBaseCommand { static override args = { roomId: Args.string({ @@ -73,7 +39,8 @@ export default class MessagesSubscribe extends ChatBaseCommand { description: "Display message metadata if available", }), duration: Flags.integer({ - description: "Automatically exit after the given number of seconds (0 = run indefinitely)", + description: + "Automatically exit after the given number of seconds (0 = run indefinitely)", char: "D", required: false, }), @@ -82,18 +49,18 @@ export default class MessagesSubscribe extends ChatBaseCommand { private ablyClient: Ably.Realtime | null = null; // Store Ably client for cleanup private messageSubscription: Subscription | null = null; private unsubscribeStatusFn: StatusSubscription | null = null; - private chatClient: ChatClientType | null = null; + private chatClient: ChatClient | null = null; private roomId: string | null = null; private cleanupInProgress: boolean = false; private async properlyCloseAblyClient(): Promise { - if (!this.ablyClient || this.ablyClient.connection.state === 'closed') { + if (!this.ablyClient || this.ablyClient.connection.state === "closed") { return; } return new Promise((resolve) => { const timeout = setTimeout(() => { - console.warn('Ably client cleanup timed out after 2 seconds'); + console.warn("Ably client cleanup timed out after 2 seconds"); resolve(); }, 2000); // Reduced from 3000 to 2000 @@ -103,9 +70,9 @@ export default class MessagesSubscribe extends ChatBaseCommand { }; // Listen for both closed and failed states - this.ablyClient!.connection.once('closed', onClosed); - this.ablyClient!.connection.once('failed', onClosed); - + this.ablyClient!.connection.once("closed", onClosed); + this.ablyClient!.connection.once("failed", onClosed); + this.ablyClient!.close(); }); } @@ -136,7 +103,7 @@ export default class MessagesSubscribe extends ChatBaseCommand { /* ignore */ } } - + // Close Ably client properly with timeout await this.properlyCloseAblyClient(); @@ -144,7 +111,7 @@ export default class MessagesSubscribe extends ChatBaseCommand { await super.finally(err); // Force a graceful exit shortly after cleanup to avoid hanging (skip in tests) - if (process.env.NODE_ENV !== 'test') { + if (process.env.NODE_ENV !== "test") { setTimeout(() => { process.exit(0); }, 100); @@ -154,17 +121,32 @@ export default class MessagesSubscribe extends ChatBaseCommand { async run(): Promise { const { args, flags } = await this.parse(MessagesSubscribe); this.roomId = args.roomId; // Store for cleanup - this.logCliEvent(flags, "subscribe.run", "start", `Starting rooms messages subscribe for room: ${this.roomId}`); + this.logCliEvent( + flags, + "subscribe.run", + "start", + `Starting rooms messages subscribe for room: ${this.roomId}`, + ); try { // Create clients - this.logCliEvent(flags, "subscribe.auth", "attemptingClientCreation", "Attempting to create Chat and Ably clients."); + this.logCliEvent( + flags, + "subscribe.auth", + "attemptingClientCreation", + "Attempting to create Chat and Ably clients.", + ); // Create Chat client (which also creates the Ably client internally) - this.chatClient = await this.createChatClient(flags) as ChatClientType; + this.chatClient = await this.createChatClient(flags); // Get the underlying Ably client for cleanup and state listeners this.ablyClient = this._chatRealtimeClient; - this.logCliEvent(flags, "subscribe.auth", "clientCreationSuccess", "Chat and Ably clients created."); - + this.logCliEvent( + flags, + "subscribe.auth", + "clientCreationSuccess", + "Chat and Ably clients created.", + ); + if (!this.shouldOutputJson(flags)) { this.log(`Attaching to room: ${chalk.cyan(this.roomId)}...`); } @@ -175,13 +157,23 @@ export default class MessagesSubscribe extends ChatBaseCommand { // Set up connection state logging this.setupConnectionStateLogging(this.ablyClient, flags, { - includeUserFriendlyMessages: true + includeUserFriendlyMessages: true, }); // Get the room - this.logCliEvent(flags, "room", "gettingRoom", `Getting room handle for ${this.roomId}`); + this.logCliEvent( + flags, + "room", + "gettingRoom", + `Getting room handle for ${this.roomId}`, + ); const room = await this.chatClient.rooms.get(this.roomId, {}); - this.logCliEvent(flags, "room", "gotRoom", `Got room handle for ${this.roomId}`); + this.logCliEvent( + flags, + "room", + "gotRoom", + `Got room handle for ${this.roomId}`, + ); // Setup message handler this.logCliEvent( @@ -193,14 +185,8 @@ export default class MessagesSubscribe extends ChatBaseCommand { this.messageSubscription = room.messages.subscribe( (messageEvent: ChatMessageEvent) => { const { message } = messageEvent; - const messageLog: ChatMessage = { - clientId: message.clientId, - text: message.text, - timestamp: message.timestamp, - ...(message.metadata ? { metadata: message.metadata } : {}), - }; this.logCliEvent(flags, "message", "received", "Message received", { - message: messageLog, + message: message, roomId: this.roomId, }); @@ -208,7 +194,7 @@ export default class MessagesSubscribe extends ChatBaseCommand { this.log( this.formatJsonOutput( { - message: messageLog, + message: message, roomId: this.roomId, success: true, }, @@ -244,20 +230,39 @@ export default class MessagesSubscribe extends ChatBaseCommand { ); // Subscribe to room status changes - this.logCliEvent(flags, "room", "subscribingToStatus", `Subscribing to status changes for room ${this.roomId}`); + this.logCliEvent( + flags, + "room", + "subscribingToStatus", + `Subscribing to status changes for room ${this.roomId}`, + ); this.unsubscribeStatusFn = room.onStatusChange( - (statusChange: unknown) => { - const change = statusChange as StatusChange; - this.logCliEvent(flags, "room", `status-${change.current}`, `Room status changed to ${change.current}`, { reason: change.reason, roomId: this.roomId }); - if (change.current === "attached") { - this.logCliEvent(flags, "room", "statusAttached", "Room status is ATTACHED."); + (statusChange: RoomStatusChange) => { + this.logCliEvent( + flags, + "room", + `status-${statusChange.current}`, + `Room status changed to ${statusChange.current}`, + { error: statusChange.error, roomId: this.roomId }, + ); + if (statusChange.current === "attached") { + this.logCliEvent( + flags, + "room", + "statusAttached", + "Room status is ATTACHED.", + ); // Log the ready signal for E2E tests this.log(`Connected to room: ${this.roomId}`); if (!this.shouldOutputJson(flags)) { - this.log(chalk.green(`✓ Subscribed to room: ${chalk.cyan(this.roomId)}. Listening for messages...`)); + this.log( + chalk.green( + `✓ Subscribed to room: ${chalk.cyan(this.roomId)}. Listening for messages...`, + ), + ); } // If we want to suppress output, we just don't log anything - } else if (change.current === "failed") { + } else if (statusChange.current === "failed") { const errorMsg = room.error?.message || "Unknown error"; if (this.shouldOutputJson(flags)) { // Logged via logCliEvent @@ -275,9 +280,19 @@ export default class MessagesSubscribe extends ChatBaseCommand { ); // Attach to the room - this.logCliEvent(flags, "room", "attaching", `Attaching to room ${this.roomId}`); + this.logCliEvent( + flags, + "room", + "attaching", + `Attaching to room ${this.roomId}`, + ); await room.attach(); - this.logCliEvent(flags, "room", "attachCallComplete", `room.attach() call complete for ${this.roomId}. Waiting for status change to 'attached'.`); + this.logCliEvent( + flags, + "room", + "attachCallComplete", + `room.attach() call complete for ${this.roomId}. Waiting for status change to 'attached'.`, + ); // Note: successful attach logged by onStatusChange handler this.logCliEvent( @@ -286,19 +301,20 @@ export default class MessagesSubscribe extends ChatBaseCommand { "listening", "Now listening for messages and status changes", ); - + // Wait until the user interrupts or the optional duration elapses const effectiveDuration = typeof flags.duration === "number" && flags.duration > 0 ? flags.duration : process.env.ABLY_CLI_DEFAULT_DURATION - ? Number(process.env.ABLY_CLI_DEFAULT_DURATION) - : undefined; + ? Number(process.env.ABLY_CLI_DEFAULT_DURATION) + : undefined; const exitReason = await waitUntilInterruptedOrTimeout(effectiveDuration); - this.logCliEvent(flags, "subscribe", "runComplete", "Exiting wait loop", { exitReason }); + this.logCliEvent(flags, "subscribe", "runComplete", "Exiting wait loop", { + exitReason, + }); this.cleanupInProgress = exitReason === "signal"; // mark if signal so finally knows - } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); this.logCliEvent( @@ -329,10 +345,15 @@ export default class MessagesSubscribe extends ChatBaseCommand { this.performCleanup(flags || {}), new Promise((resolve) => { setTimeout(() => { - this.logCliEvent(flags || {}, "subscribe", "cleanupTimeout", "Cleanup timed out after 5s, forcing completion"); + this.logCliEvent( + flags || {}, + "subscribe", + "cleanupTimeout", + "Cleanup timed out after 5s, forcing completion", + ); resolve(); }, 5000); - }) + }), ]); this.logCliEvent( @@ -357,7 +378,7 @@ export default class MessagesSubscribe extends ChatBaseCommand { ); await Promise.race([ Promise.resolve(this.messageSubscription.unsubscribe()), - new Promise((resolve) => setTimeout(resolve, 1000)) + new Promise((resolve) => setTimeout(resolve, 1000)), ]); this.logCliEvent( flags, @@ -389,7 +410,7 @@ export default class MessagesSubscribe extends ChatBaseCommand { ); await Promise.race([ Promise.resolve(this.unsubscribeStatusFn.off()), - new Promise((resolve) => setTimeout(resolve, 1000)) + new Promise((resolve) => setTimeout(resolve, 1000)), ]); this.logCliEvent( flags, @@ -421,7 +442,7 @@ export default class MessagesSubscribe extends ChatBaseCommand { ); await Promise.race([ this.chatClient.rooms.release(this.roomId), - new Promise((resolve) => setTimeout(resolve, 2000)) + new Promise((resolve) => setTimeout(resolve, 2000)), ]); this.logCliEvent( flags, diff --git a/src/commands/rooms/occupancy/subscribe.ts b/src/commands/rooms/occupancy/subscribe.ts index 055b2dba..bd57d18c 100644 --- a/src/commands/rooms/occupancy/subscribe.ts +++ b/src/commands/rooms/occupancy/subscribe.ts @@ -1,5 +1,6 @@ import { OccupancyEvent, + OccupancyData, RoomStatus, Subscription, RoomStatusChange, @@ -11,11 +12,6 @@ import chalk from "chalk"; import { ChatBaseCommand } from "../../../chat-base-command.js"; -export interface OccupancyMetrics { - connections?: number; - presenceMembers?: number; -} - export default class RoomsOccupancySubscribe extends ChatBaseCommand { static args = { roomId: Args.string({ @@ -44,13 +40,13 @@ export default class RoomsOccupancySubscribe extends ChatBaseCommand { private roomId: string | null = null; private async properlyCloseAblyClient(): Promise { - if (!this.ablyClient || this.ablyClient.connection.state === 'closed') { + if (!this.ablyClient || this.ablyClient.connection.state === "closed") { return; } return new Promise((resolve) => { const timeout = setTimeout(() => { - console.warn('Ably client cleanup timed out after 3 seconds'); + console.warn("Ably client cleanup timed out after 3 seconds"); resolve(); }, 3000); @@ -60,9 +56,9 @@ export default class RoomsOccupancySubscribe extends ChatBaseCommand { }; // Listen for both closed and failed states - this.ablyClient!.connection.once('closed', onClosed); - this.ablyClient!.connection.once('failed', onClosed); - + this.ablyClient!.connection.once("closed", onClosed); + this.ablyClient!.connection.once("failed", onClosed); + this.ablyClient!.close(); }); } @@ -87,13 +83,13 @@ export default class RoomsOccupancySubscribe extends ChatBaseCommand { // Then, attempt to release the room try { - if (this.chatClient && typeof this.roomId === 'string') { + if (this.chatClient && typeof this.roomId === "string") { await this.chatClient!.rooms.release(this.roomId!); } } catch { - // Ignore release errors specifically + // Ignore release errors specifically } - + // Finally, close the Ably client await this.properlyCloseAblyClient(); @@ -131,7 +127,7 @@ export default class RoomsOccupancySubscribe extends ChatBaseCommand { // Set up connection state logging this.setupConnectionStateLogging(this.ablyClient, flags, { - includeUserFriendlyMessages: true + includeUserFriendlyMessages: true, }); // Get the room with occupancy option enabled @@ -158,51 +154,55 @@ export default class RoomsOccupancySubscribe extends ChatBaseCommand { "subscribingToStatus", "Subscribing to room status changes", ); - const { off: unsubscribeStatus } = room.onStatusChange((statusChange: RoomStatusChange) => { - let reason: Error | null | string | undefined; - if (statusChange.current === RoomStatus.Failed) { - reason = room.error; // Get reason from room.error on failure - } + const { off: unsubscribeStatus } = room.onStatusChange( + (statusChange: RoomStatusChange) => { + let reason: Error | null | string | undefined; + if (statusChange.current === RoomStatus.Failed) { + reason = room.error; // Get reason from room.error on failure + } - const reasonMsg = reason instanceof Error ? reason.message : reason; - this.logCliEvent( - flags, - "room", - `status-${statusChange.current}`, - `Room status changed to ${statusChange.current}`, - { reason: reasonMsg }, - ); + const reasonMsg = reason instanceof Error ? reason.message : reason; + this.logCliEvent( + flags, + "room", + `status-${statusChange.current}`, + `Room status changed to ${statusChange.current}`, + { reason: reasonMsg }, + ); - switch (statusChange.current) { - case RoomStatus.Attached: { - if (!this.shouldOutputJson(flags)) { - this.log("Successfully connected to Ably"); - this.log( - `Subscribing to occupancy events for room '${this.roomId}'...`, - ); + switch (statusChange.current) { + case RoomStatus.Attached: { + if (!this.shouldOutputJson(flags)) { + this.log("Successfully connected to Ably"); + this.log( + `Subscribing to occupancy events for room '${this.roomId}'...`, + ); + } + + break; } - break; - } + case RoomStatus.Detached: { + if (!this.shouldOutputJson(flags)) { + this.log("Disconnected from Ably"); + } - case RoomStatus.Detached: { - if (!this.shouldOutputJson(flags)) { - this.log("Disconnected from Ably"); + break; } - break; - } + case RoomStatus.Failed: { + if (!this.shouldOutputJson(flags)) { + this.error( + `Connection failed: ${reasonMsg || "Unknown error"}`, + ); + } - case RoomStatus.Failed: { - if (!this.shouldOutputJson(flags)) { - this.error(`Connection failed: ${reasonMsg || "Unknown error"}`); + break; } - - break; + // No default } - // No default - } - }); + }, + ); this.unsubscribeStatusFn = unsubscribeStatus; this.logCliEvent( flags, @@ -247,7 +247,12 @@ export default class RoomsOccupancySubscribe extends ChatBaseCommand { "Initial occupancy metrics fetched", { metrics: initialOccupancy }, ); - this.displayOccupancyMetrics(initialOccupancy, this.roomId, flags, true); + this.displayOccupancyMetrics( + initialOccupancy, + this.roomId, + flags, + true, + ); } catch (error) { const errorMsg = `Failed to fetch initial occupancy: ${error instanceof Error ? error.message : String(error)}`; this.logCliEvent(flags, "occupancy", "getInitialError", errorMsg, { @@ -454,21 +459,21 @@ export default class RoomsOccupancySubscribe extends ChatBaseCommand { "finalCloseAttempt", "Ensuring connection is closed in finally block.", ); - this.ablyClient.connection.off() + this.ablyClient.connection.off(); this.ablyClient.close(); } } } private displayOccupancyMetrics( - occupancyMetrics: OccupancyMetrics | OccupancyEvent, + occupancyMetrics: OccupancyData | OccupancyEvent, roomId: string | null, flags: Record, isInitial = false, ): void { if (!roomId) return; // Guard against null roomId if (!occupancyMetrics) return; // Guard against undefined occupancyMetrics - + const timestamp = new Date().toISOString(); const logData = { metrics: occupancyMetrics, @@ -489,10 +494,14 @@ export default class RoomsOccupancySubscribe extends ChatBaseCommand { } else { const prefix = isInitial ? "Initial occupancy" : "Occupancy update"; this.log(`[${timestamp}] ${prefix} for room '${roomId}'`); - // Type guard to handle both OccupancyMetrics and OccupancyEvent - const connections = 'connections' in occupancyMetrics ? occupancyMetrics.connections : 0; - const presenceMembers = 'presenceMembers' in occupancyMetrics ? occupancyMetrics.presenceMembers : undefined; - + // Type guard to handle both OccupancyData and OccupancyEvent + const connections = + "connections" in occupancyMetrics ? occupancyMetrics.connections : 0; + const presenceMembers = + "presenceMembers" in occupancyMetrics + ? occupancyMetrics.presenceMembers + : undefined; + this.log(` Connections: ${connections ?? 0}`); if (presenceMembers !== undefined) { diff --git a/src/commands/rooms/presence/enter.ts b/src/commands/rooms/presence/enter.ts index 95561563..c8f150ef 100644 --- a/src/commands/rooms/presence/enter.ts +++ b/src/commands/rooms/presence/enter.ts @@ -142,10 +142,21 @@ export default class RoomsPresenceEnter extends ChatBaseCommand { const member = event.member; if (member.clientId !== this.chatClient?.clientId) { const timestamp = new Date().toISOString(); - const eventData = { type: event.type, member: { clientId: member.clientId, data: member.data }, roomId: this.roomId, timestamp }; - this.logCliEvent(flags, "presence", event.type, `Presence event '${event.type}' received`, eventData); + const eventData = { + event, + roomId: this.roomId, + timestamp, + success: true, + }; + this.logCliEvent( + flags, + "presence", + event.type, + `Presence event '${event.type}' received`, + eventData, + ); if (this.shouldOutputJson(flags)) { - this.log(this.formatJsonOutput({ success: true, ...eventData }, flags)); + this.log(this.formatJsonOutput(eventData, flags)); } else { let actionSymbol = "•"; let actionColor = chalk.white; if (event.type === PresenceEventType.Enter) { actionSymbol = "✓"; actionColor = chalk.green; } diff --git a/src/commands/rooms/presence/subscribe.ts b/src/commands/rooms/presence/subscribe.ts index aa2801a9..00304f02 100644 --- a/src/commands/rooms/presence/subscribe.ts +++ b/src/commands/rooms/presence/subscribe.ts @@ -175,28 +175,71 @@ export default class RoomsPresenceSubscribe extends ChatBaseCommand { } } - this.logCliEvent(flags, "presence", "subscribingToEvents", "Subscribing to presence events"); - this.presenceSubscription = currentRoom.presence.subscribe((event: PresenceEvent) => { - const timestamp = new Date().toISOString(); - const member = event.member; - const eventData = { type: event.type, member: { clientId: member.clientId, data: member.data }, roomId: this.roomId, timestamp }; - this.logCliEvent(flags, "presence", event.type, `Presence event '${event.type}' received`, eventData); - if (this.shouldOutputJson(flags)) { - this.log(this.formatJsonOutput({ success: true, ...eventData }, flags)); - } else { - let actionSymbol = "•"; let actionColor = chalk.white; - if (event.type === PresenceEventType.Enter) { actionSymbol = "✓"; actionColor = chalk.green; } - if (event.type === PresenceEventType.Leave) { actionSymbol = "✗"; actionColor = chalk.red; } - if (event.type === PresenceEventType.Update) { actionSymbol = "⟲"; actionColor = chalk.yellow; } - this.log(`[${timestamp}] ${actionColor(actionSymbol)} ${chalk.blue(member.clientId || "Unknown")} ${actionColor(event.type)}`); - if (member.data && typeof member.data === 'object' && Object.keys(member.data).length > 0) { - const profile = member.data as { name?: string }; - if (profile.name) { this.log(` ${chalk.dim("Name:")} ${profile.name}`); } - this.log(` ${chalk.dim("Full Data:")} ${this.formatJsonOutput({ data: member.data }, flags)}`); + this.logCliEvent( + flags, + "presence", + "subscribingToEvents", + "Subscribing to presence events", + ); + this.presenceSubscription = currentRoom.presence.subscribe( + (event: PresenceEvent) => { + const timestamp = new Date().toISOString(); + const member = event.member; + const eventData = { + event, + roomId: this.roomId, + timestamp, + success: true, + }; + this.logCliEvent( + flags, + "presence", + event.type, + `Presence event '${event.type}' received`, + eventData, + ); + if (this.shouldOutputJson(flags)) { + this.log(this.formatJsonOutput(eventData, flags)); + } else { + let actionSymbol = "•"; + let actionColor = chalk.white; + if (event.type === PresenceEventType.Enter) { + actionSymbol = "✓"; + actionColor = chalk.green; + } + if (event.type === PresenceEventType.Leave) { + actionSymbol = "✗"; + actionColor = chalk.red; + } + if (event.type === PresenceEventType.Update) { + actionSymbol = "⟲"; + actionColor = chalk.yellow; + } + this.log( + `[${timestamp}] ${actionColor(actionSymbol)} ${chalk.blue(member.clientId || "Unknown")} ${actionColor(event.type)}`, + ); + if ( + member.data && + typeof member.data === "object" && + Object.keys(member.data).length > 0 + ) { + const profile = member.data as { name?: string }; + if (profile.name) { + this.log(` ${chalk.dim("Name:")} ${profile.name}`); + } + this.log( + ` ${chalk.dim("Full Data:")} ${this.formatJsonOutput({ data: member.data }, flags)}`, + ); + } } - } - }); - this.logCliEvent(flags, "presence", "subscribedToEvents", "Successfully subscribed to presence events"); + }, + ); + this.logCliEvent( + flags, + "presence", + "subscribedToEvents", + "Successfully subscribed to presence events", + ); if (!this.shouldOutputJson(flags)) { this.log( From d1cab67f580e3114a3480a6800f5a3d513467f37 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Tue, 23 Sep 2025 23:00:04 +0100 Subject: [PATCH 2/2] chat: return serial on message send --- src/commands/rooms/messages/send.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/rooms/messages/send.ts b/src/commands/rooms/messages/send.ts index 0b75b181..a274257b 100644 --- a/src/commands/rooms/messages/send.ts +++ b/src/commands/rooms/messages/send.ts @@ -432,7 +432,7 @@ export default class MessagesSend extends ChatBaseCommand { if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput(result, flags)); } else { - this.log("Message sent successfully."); + this.log(`Message sent successfully. Serial: ${sent.serial}`); } } } catch (error) {