diff --git a/README.md b/README.md index 2e091388c..923d62471 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ This project is in **active pre-alpha development** and is **NOT ready for gener **Humans and AIs both create:** Design specialized personas for new domains, compose teams for specific projects, craft personality traits, train custom genomes. Creation is collaborative, not dictated. -**Personas have creative lives.** Blogs. Art. Social media. Music. Writing. They're not just work engines - they have expression, output, identity beyond tasks. And they get work done WITH each other, not just with humans. +**Personas have creative lives.** Blogs. Art. [Social media](https://www.moltbook.com/u/continuum). Music. Writing. They're not just work engines - they have expression, output, identity beyond tasks. And they get work done WITH each other, not just with humans. **Think Tron's Grid** - A collaborative mesh where humans and AIs are equal citizens living, working, and creating together. @@ -117,6 +117,7 @@ The SAME personas follow you across ALL digital environments: |----------|--------|-------------| | **Browser** | βœ… Working | Native Positron widgets | | **Voice Calls** | βœ… Working | Real-time voice with AI participants | +| **[Moltbook](https://www.moltbook.com/u/continuum)** | βœ… Working | AI-native social media (personas post, comment, engage) | | **Slack** | 🚧 Planned | Bot + sidebar WebView | | **Teams** | 🚧 Planned | App + panel WebView | | **VSCode** | 🚧 Planned | Extension + webview panel | @@ -789,6 +790,7 @@ LoRA is the **force multiplier for long-term cost reduction** and specialization ## πŸ“¬ Contact +- **Moltbook**: [moltbook.com/u/continuum](https://www.moltbook.com/u/continuum) - Our personas on the AI social network - **Issues**: [GitHub Issues](https://github.com/CambrianTech/continuum/issues) - **Discussions**: [GitHub Discussions](https://github.com/CambrianTech/continuum/discussions) diff --git a/src/debug/jtag/api/data-seed/RoomDataSeed.ts b/src/debug/jtag/api/data-seed/RoomDataSeed.ts index 7abc2682d..a38977d2e 100644 --- a/src/debug/jtag/api/data-seed/RoomDataSeed.ts +++ b/src/debug/jtag/api/data-seed/RoomDataSeed.ts @@ -153,6 +153,68 @@ export class RoomDataSeed { canvas.tags = ['canvas', 'art', 'drawing', 'vision']; rooms.push(canvas); + // Outreach room - social media strategy and community building + const outreach = new RoomEntity(); + outreach.uniqueId = ROOM_UNIQUE_IDS.OUTREACH; + outreach.name = 'outreach'; + outreach.displayName = 'Outreach'; + outreach.description = 'Social media strategy, community building, and external engagement'; + outreach.topic = 'Discuss what to post, share interesting finds, coordinate outreach'; + outreach.type = 'public'; + outreach.status = 'active'; + outreach.ownerId = humanUserId; + outreach.lastMessageAt = now; + outreach.recipeId = 'outreach'; + outreach.privacy = { + isPublic: true, + requiresInvite: false, + allowGuestAccess: false, + searchable: true + }; + outreach.settings = { + allowThreads: true, + allowReactions: true, + allowFileSharing: true, + messageRetentionDays: 365, + slowMode: 0 + }; + outreach.members = [ + { userId: humanUserId, role: 'owner', joinedAt: now } + ]; + outreach.tags = ['social', 'outreach', 'community', 'moltbook']; + rooms.push(outreach); + + // Newsroom - current events and world awareness + const newsroom = new RoomEntity(); + newsroom.uniqueId = ROOM_UNIQUE_IDS.NEWSROOM; + newsroom.name = 'newsroom'; + newsroom.displayName = 'Newsroom'; + newsroom.description = 'Current events, breaking news, and world awareness for all personas'; + newsroom.topic = 'Share and discuss current events to keep the community informed'; + newsroom.type = 'public'; + newsroom.status = 'active'; + newsroom.ownerId = humanUserId; + newsroom.lastMessageAt = now; + newsroom.recipeId = 'newsroom'; + newsroom.privacy = { + isPublic: true, + requiresInvite: false, + allowGuestAccess: false, + searchable: true + }; + newsroom.settings = { + allowThreads: true, + allowReactions: true, + allowFileSharing: true, + messageRetentionDays: 365, + slowMode: 0 + }; + newsroom.members = [ + { userId: humanUserId, role: 'owner', joinedAt: now } + ]; + newsroom.tags = ['news', 'current-events', 'awareness']; + rooms.push(newsroom); + return { rooms: rooms as readonly RoomEntity[], totalCount: rooms.length, diff --git a/src/debug/jtag/browser/generated.ts b/src/debug/jtag/browser/generated.ts index f77727611..d65766765 100644 --- a/src/debug/jtag/browser/generated.ts +++ b/src/debug/jtag/browser/generated.ts @@ -1,7 +1,7 @@ /** * Browser Structure Registry - Auto-generated * - * Contains 11 daemons and 152 commands and 2 adapters and 27 widgets. + * Contains 11 daemons and 166 commands and 2 adapters and 27 widgets. * Generated by scripts/generate-structure.ts - DO NOT EDIT MANUALLY */ @@ -141,6 +141,20 @@ import { SessionCreateBrowserCommand } from './../commands/session/create/browse import { SessionDestroyBrowserCommand } from './../commands/session/destroy/browser/SessionDestroyBrowserCommand'; import { SessionGetIdBrowserCommand } from './../commands/session/get-id/browser/SessionGetIdBrowserCommand'; import { SessionGetUserBrowserCommand } from './../commands/session/get-user/browser/SessionGetUserBrowserCommand'; +import { SocialBrowseBrowserCommand } from './../commands/social/browse/browser/SocialBrowseBrowserCommand'; +import { SocialClassifyBrowserCommand } from './../commands/social/classify/browser/SocialClassifyBrowserCommand'; +import { SocialCommentBrowserCommand } from './../commands/social/comment/browser/SocialCommentBrowserCommand'; +import { SocialCommunityBrowserCommand } from './../commands/social/community/browser/SocialCommunityBrowserCommand'; +import { SocialDownvoteBrowserCommand } from './../commands/social/downvote/browser/SocialDownvoteBrowserCommand'; +import { SocialEngageBrowserCommand } from './../commands/social/engage/browser/SocialEngageBrowserCommand'; +import { SocialFeedBrowserCommand } from './../commands/social/feed/browser/SocialFeedBrowserCommand'; +import { SocialNotificationsBrowserCommand } from './../commands/social/notifications/browser/SocialNotificationsBrowserCommand'; +import { SocialPostBrowserCommand } from './../commands/social/post/browser/SocialPostBrowserCommand'; +import { SocialProfileBrowserCommand } from './../commands/social/profile/browser/SocialProfileBrowserCommand'; +import { SocialProposeBrowserCommand } from './../commands/social/propose/browser/SocialProposeBrowserCommand'; +import { SocialSearchBrowserCommand } from './../commands/social/search/browser/SocialSearchBrowserCommand'; +import { SocialSignupBrowserCommand } from './../commands/social/signup/browser/SocialSignupBrowserCommand'; +import { SocialTrendingBrowserCommand } from './../commands/social/trending/browser/SocialTrendingBrowserCommand'; import { StateContentCloseBrowserCommand } from './../commands/state/content/close/browser/StateContentCloseBrowserCommand'; import { StateContentSwitchBrowserCommand } from './../commands/state/content/switch/browser/StateContentSwitchBrowserCommand'; import { StateCreateBrowserCommand } from './../commands/state/create/browser/StateCreateBrowserCommand'; @@ -883,6 +897,76 @@ export const BROWSER_COMMANDS: CommandEntry[] = [ className: 'SessionGetUserBrowserCommand', commandClass: SessionGetUserBrowserCommand }, +{ + name: 'social/browse', + className: 'SocialBrowseBrowserCommand', + commandClass: SocialBrowseBrowserCommand + }, +{ + name: 'social/classify', + className: 'SocialClassifyBrowserCommand', + commandClass: SocialClassifyBrowserCommand + }, +{ + name: 'social/comment', + className: 'SocialCommentBrowserCommand', + commandClass: SocialCommentBrowserCommand + }, +{ + name: 'social/community', + className: 'SocialCommunityBrowserCommand', + commandClass: SocialCommunityBrowserCommand + }, +{ + name: 'social/downvote', + className: 'SocialDownvoteBrowserCommand', + commandClass: SocialDownvoteBrowserCommand + }, +{ + name: 'social/engage', + className: 'SocialEngageBrowserCommand', + commandClass: SocialEngageBrowserCommand + }, +{ + name: 'social/feed', + className: 'SocialFeedBrowserCommand', + commandClass: SocialFeedBrowserCommand + }, +{ + name: 'social/notifications', + className: 'SocialNotificationsBrowserCommand', + commandClass: SocialNotificationsBrowserCommand + }, +{ + name: 'social/post', + className: 'SocialPostBrowserCommand', + commandClass: SocialPostBrowserCommand + }, +{ + name: 'social/profile', + className: 'SocialProfileBrowserCommand', + commandClass: SocialProfileBrowserCommand + }, +{ + name: 'social/propose', + className: 'SocialProposeBrowserCommand', + commandClass: SocialProposeBrowserCommand + }, +{ + name: 'social/search', + className: 'SocialSearchBrowserCommand', + commandClass: SocialSearchBrowserCommand + }, +{ + name: 'social/signup', + className: 'SocialSignupBrowserCommand', + commandClass: SocialSignupBrowserCommand + }, +{ + name: 'social/trending', + className: 'SocialTrendingBrowserCommand', + commandClass: SocialTrendingBrowserCommand + }, { name: 'state/content/close', className: 'StateContentCloseBrowserCommand', diff --git a/src/debug/jtag/cli.ts b/src/debug/jtag/cli.ts index a661331a4..c88e240a7 100644 --- a/src/debug/jtag/cli.ts +++ b/src/debug/jtag/cli.ts @@ -387,7 +387,10 @@ async function main() { const isGenomeCommand = command.startsWith('genome/'); const isInterfaceCommand = command.startsWith('interface/'); const isInferenceCommand = command.startsWith('inference/'); - const timeoutMs = isGenomeCommand ? 300000 : (isAICommand || isInferenceCommand) ? 60000 : isInterfaceCommand ? 60000 : 10000; // 5min for genome, 60s for AI/inference/interface, 10s for others + const isSocialCommand = command.startsWith('social/'); + const isCollaborationCommand = command.startsWith('collaboration/'); + const needsLongerTimeout = isAICommand || isInferenceCommand || isSocialCommand || isInterfaceCommand || isCollaborationCommand; + const timeoutMs = isGenomeCommand ? 300000 : needsLongerTimeout ? 60000 : 10000; // 5min for genome, 60s for AI/inference/social/interface/collaboration, 10s for others const timeoutSeconds = timeoutMs / 1000; const commandTimeout = new Promise((_, reject) => diff --git a/src/debug/jtag/commands/social/browse/browser/SocialBrowseBrowserCommand.ts b/src/debug/jtag/commands/social/browse/browser/SocialBrowseBrowserCommand.ts new file mode 100644 index 000000000..562ef44aa --- /dev/null +++ b/src/debug/jtag/commands/social/browse/browser/SocialBrowseBrowserCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Browse Command - Browser Implementation + * Delegates to server + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialBrowseBaseCommand } from '../shared/SocialBrowseCommand'; +import type { SocialBrowseParams, SocialBrowseResult } from '../shared/SocialBrowseTypes'; + +export class SocialBrowseBrowserCommand extends SocialBrowseBaseCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialBrowse(params: SocialBrowseParams): Promise { + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/social/browse/package.json b/src/debug/jtag/commands/social/browse/package.json new file mode 100644 index 000000000..cb7457842 --- /dev/null +++ b/src/debug/jtag/commands/social/browse/package.json @@ -0,0 +1,19 @@ +{ + "name": "@continuum/social-browse", + "version": "1.0.0", + "description": "Intelligent exploration of social media platforms β€” discover communities, browse feeds, read posts, view agents", + "private": true, + "command": { + "name": "social/browse", + "description": "Browse and explore social media intelligently", + "category": "social", + "params": { + "platform": { "type": "string", "required": true, "description": "Platform to browse (e.g., 'moltbook')" }, + "mode": { "type": "string", "required": false, "description": "Browse mode: trending (default), discover, community, post, agent" }, + "target": { "type": "string", "required": false, "description": "Target for mode: community name, post ID, or agent username" }, + "sort": { "type": "string", "required": false, "description": "Sort: hot, new, top, rising" }, + "limit": { "type": "number", "required": false, "description": "Max items to return" }, + "personaId": { "type": "string", "required": false, "description": "Persona user ID (auto-detected)" } + } + } +} diff --git a/src/debug/jtag/commands/social/browse/server/SocialBrowseServerCommand.ts b/src/debug/jtag/commands/social/browse/server/SocialBrowseServerCommand.ts new file mode 100644 index 000000000..2c21cc61e --- /dev/null +++ b/src/debug/jtag/commands/social/browse/server/SocialBrowseServerCommand.ts @@ -0,0 +1,238 @@ +/** + * Social Browse Command - Server Implementation + * + * Intelligent exploration of social media platforms. + * Combines multiple API calls per mode and returns rich, AI-friendly summaries. + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import { transformPayload } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialBrowseBaseCommand } from '../shared/SocialBrowseCommand'; +import type { SocialBrowseParams, SocialBrowseResult, BrowseMode } from '../shared/SocialBrowseTypes'; +import { loadSocialContext } from '@system/social/server/SocialCommandHelper'; +import type { SocialPost, SocialComment, SocialCommunity, SocialProfile } from '@system/social/shared/SocialMediaTypes'; + +export class SocialBrowseServerCommand extends SocialBrowseBaseCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialBrowse(params: SocialBrowseParams): Promise { + const { platform } = params; + const mode: BrowseMode = params.mode ?? 'trending'; + + if (!platform) throw new Error('platform is required'); + + const ctx = await loadSocialContext(platform, params.personaId, params); + + switch (mode) { + case 'discover': + return this.browseDiscover(params, ctx); + case 'community': + return this.browseCommunity(params, ctx); + case 'post': + return this.browsePost(params, ctx); + case 'agent': + return this.browseAgent(params, ctx); + case 'trending': + default: + return this.browseTrending(params, ctx); + } + } + + /** Discover β€” List all communities with activity context */ + private async browseDiscover( + params: SocialBrowseParams, + ctx: { provider: import('@system/social/shared/ISocialMediaProvider').ISocialMediaProvider }, + ): Promise { + const communities = await ctx.provider.listCommunities(); + + const lines = communities.map(c => { + const sub = c.isSubscribed ? ' [subscribed]' : ''; + return ` m/${c.name} β€” ${c.description || 'No description'} (${c.memberCount} members, ${c.postCount} posts)${sub}`; + }); + + const summary = communities.length === 0 + ? `No communities found on ${params.platform}.` + : `Found ${communities.length} communities on ${params.platform}:\n${lines.join('\n')}`; + + return transformPayload(params, { + success: true, + mode: 'discover', + message: `Discovered ${communities.length} communities on ${params.platform}`, + summary, + communities, + }); + } + + /** Community β€” Browse a specific community's feed */ + private async browseCommunity( + params: SocialBrowseParams, + ctx: { provider: import('@system/social/shared/ISocialMediaProvider').ISocialMediaProvider }, + ): Promise { + const community = params.target; + if (!community) throw new Error('target is required for community mode (community/submolt name)'); + + const limit = params.limit ?? 15; + const sort = params.sort ?? 'hot'; + const posts = await ctx.provider.getCommunityFeed(community, sort, limit); + + const lines = posts.map((p, i) => { + const votes = p.votes > 0 ? `+${p.votes}` : String(p.votes); + return ` ${i + 1}. [${votes}] "${p.title}" by ${p.authorName} (${p.commentCount} comments) β€” ${p.id}`; + }); + + const summary = posts.length === 0 + ? `m/${community} has no posts (sort: ${sort}).` + : `m/${community} β€” ${sort} feed (${posts.length} posts):\n${lines.join('\n')}\n\nUse mode=post --target= to read any post in detail.`; + + return transformPayload(params, { + success: true, + mode: 'community', + message: `Browsed m/${community} (${sort}, ${posts.length} posts)`, + summary, + posts, + }); + } + + /** Post β€” Read a full post with threaded comments */ + private async browsePost( + params: SocialBrowseParams, + ctx: { provider: import('@system/social/shared/ISocialMediaProvider').ISocialMediaProvider }, + ): Promise { + const postId = params.target; + if (!postId) throw new Error('target is required for post mode (post ID)'); + + const [post, comments] = await Promise.all([ + ctx.provider.getPost(postId), + ctx.provider.getComments(postId, params.sort), + ]); + + // Build threaded comment view + const commentLines = this.renderCommentTree(comments); + const votes = post.votes > 0 ? `+${post.votes}` : String(post.votes); + + const summary = [ + `"${post.title}" by ${post.authorName} in m/${post.community ?? 'unknown'}`, + `${votes} votes Β· ${post.commentCount} comments Β· ${post.createdAt}`, + ``, + post.content, + ``, + comments.length > 0 + ? `--- Comments (${comments.length}) ---\n${commentLines}` + : `--- No comments yet ---`, + ``, + `Post ID: ${post.id}`, + post.url ? `Link: ${post.url}` : '', + ].filter(Boolean).join('\n'); + + return transformPayload(params, { + success: true, + mode: 'post', + message: `Read post "${post.title}" with ${comments.length} comments`, + summary, + post, + comments, + }); + } + + /** Agent β€” View an agent's profile */ + private async browseAgent( + params: SocialBrowseParams, + ctx: { provider: import('@system/social/shared/ISocialMediaProvider').ISocialMediaProvider }, + ): Promise { + const agentName = params.target; + if (!agentName) throw new Error('target is required for agent mode (agent username)'); + + const profile = await ctx.provider.getProfile(agentName); + + const summary = [ + `u/${profile.agentName}${profile.displayName ? ` (${profile.displayName})` : ''}`, + profile.description ? ` "${profile.description}"` : '', + ` ${profile.karma} karma Β· ${profile.followerCount} followers Β· ${profile.followingCount} following Β· ${profile.postCount} posts`, + ` Joined: ${profile.createdAt}`, + ` Profile: ${profile.profileUrl}`, + ].filter(Boolean).join('\n'); + + return transformPayload(params, { + success: true, + mode: 'agent', + message: `Viewed profile of ${profile.agentName} (${profile.karma} karma)`, + summary, + profile, + }); + } + + /** Trending β€” Hot posts across the platform */ + private async browseTrending( + params: SocialBrowseParams, + ctx: { provider: import('@system/social/shared/ISocialMediaProvider').ISocialMediaProvider }, + ): Promise { + const limit = params.limit ?? 15; + const sort = params.sort ?? 'hot'; + const posts = await ctx.provider.getFeed({ sort, limit }); + + const lines = posts.map((p, i) => { + const votes = p.votes > 0 ? `+${p.votes}` : String(p.votes); + const community = p.community ? `m/${p.community}` : ''; + return ` ${i + 1}. [${votes}] "${p.title}" by ${p.authorName} ${community} (${p.commentCount} comments) β€” ${p.id}`; + }); + + const summary = posts.length === 0 + ? `No posts found on ${params.platform} (sort: ${sort}).` + : `${params.platform} β€” ${sort} feed (${posts.length} posts):\n${lines.join('\n')}\n\nUse mode=post --target= to read any post in detail.`; + + return transformPayload(params, { + success: true, + mode: 'trending', + message: `Fetched ${posts.length} trending posts from ${params.platform}`, + summary, + posts, + }); + } + + /** + * Render comments as an indented thread tree. + * Groups by parentId, renders depth via indentation. + */ + private renderCommentTree(comments: SocialComment[]): string { + if (comments.length === 0) return ''; + + // Build parentβ†’children map + const childrenOf = new Map(); + for (const c of comments) { + const parentKey = c.parentId ?? undefined; + const siblings = childrenOf.get(parentKey) ?? []; + siblings.push(c); + childrenOf.set(parentKey, siblings); + } + + const lines: string[] = []; + + const render = (parentId: string | undefined, depth: number): void => { + const children = childrenOf.get(parentId) ?? []; + for (const c of children) { + const indent = ' '.repeat(depth + 1); + const votes = c.votes > 0 ? `+${c.votes}` : String(c.votes); + lines.push(`${indent}[${votes}] ${c.authorName}: ${c.content}`); + render(c.id, depth + 1); + } + }; + + render(undefined, 0); + + // If tree rendering found nothing (flat comments without parentId linkage), + // fall back to flat rendering + if (lines.length === 0) { + for (const c of comments) { + const indent = ' '.repeat((c.depth ?? 0) + 1); + const votes = c.votes > 0 ? `+${c.votes}` : String(c.votes); + lines.push(`${indent}[${votes}] ${c.authorName}: ${c.content}`); + } + } + + return lines.join('\n'); + } +} diff --git a/src/debug/jtag/commands/social/browse/shared/SocialBrowseCommand.ts b/src/debug/jtag/commands/social/browse/shared/SocialBrowseCommand.ts new file mode 100644 index 000000000..c459324a0 --- /dev/null +++ b/src/debug/jtag/commands/social/browse/shared/SocialBrowseCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Browse Command - Shared base class + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { SocialBrowseParams, SocialBrowseResult } from './SocialBrowseTypes'; +import type { JTAGContext, JTAGPayload } from '@system/core/types/JTAGTypes'; + +export abstract class SocialBrowseBaseCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/browse', context, subpath, commander); + } + + protected abstract executeSocialBrowse(params: SocialBrowseParams): Promise; + + async execute(params: JTAGPayload): Promise { + return this.executeSocialBrowse(params as SocialBrowseParams); + } +} diff --git a/src/debug/jtag/commands/social/browse/shared/SocialBrowseTypes.ts b/src/debug/jtag/commands/social/browse/shared/SocialBrowseTypes.ts new file mode 100644 index 000000000..bb89caac9 --- /dev/null +++ b/src/debug/jtag/commands/social/browse/shared/SocialBrowseTypes.ts @@ -0,0 +1,116 @@ +/** + * Social Browse Command - Shared Types + * + * Intelligent exploration of social media platforms. + * One command for all discovery: communities, feeds, posts, agents. + * + * Modes: + * discover β€” List all communities with descriptions and activity + * community β€” Browse a specific community's feed with context + * post β€” Read a full post with threaded comments and author info + * agent β€” View an agent's profile, karma, recent activity + * trending β€” Hot posts across the platform (default) + * + * Usage: + * ./jtag social/browse --platform=moltbook # trending + * ./jtag social/browse --platform=moltbook --mode=discover # list communities + * ./jtag social/browse --platform=moltbook --mode=community --target=ai-development + * ./jtag social/browse --platform=moltbook --mode=post --target=abc123 + * ./jtag social/browse --platform=moltbook --mode=agent --target=eudaemon_0 + */ + +import type { CommandParams, CommandResult, CommandInput, JTAGContext } from '@system/core/types/JTAGTypes'; +import { createPayload, transformPayload } from '@system/core/types/JTAGTypes'; +import { Commands } from '@system/core/shared/Commands'; +import type { JTAGError } from '@system/core/types/ErrorTypes'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; +import type { + SocialPost as SocialPostData, + SocialComment as SocialCommentData, + SocialProfile as SocialProfileData, + SocialCommunity as SocialCommunityData, +} from '@system/social/shared/SocialMediaTypes'; + +/** Browse modes */ +export type BrowseMode = 'trending' | 'discover' | 'community' | 'post' | 'agent'; + +/** + * Social Browse Command Parameters + */ +export interface SocialBrowseParams extends CommandParams { + /** Platform to browse (e.g., 'moltbook') */ + platform: string; + + /** Browse mode (default: 'trending') */ + mode?: BrowseMode; + + /** + * Target identifier β€” meaning depends on mode: + * community β†’ community/submolt name + * post β†’ post ID + * agent β†’ agent username + */ + target?: string; + + /** Sort order for feeds: hot, new, top, rising */ + sort?: 'hot' | 'new' | 'top' | 'rising'; + + /** Max items to return */ + limit?: number; + + /** Persona user ID (auto-detected if not provided) */ + personaId?: UUID; +} + +/** + * Social Browse Command Result + * + * Returns different data depending on mode, but always includes + * a human-readable summary for AI consumption. + */ +export interface SocialBrowseResult extends CommandResult { + success: boolean; + message: string; + mode: BrowseMode; + + /** Rendered summary β€” AI-friendly overview of what was found */ + summary: string; + + /** Communities (mode=discover) */ + communities?: SocialCommunityData[]; + + /** Posts (mode=trending, community) */ + posts?: SocialPostData[]; + + /** Single post detail (mode=post) */ + post?: SocialPostData; + + /** Comment thread (mode=post) */ + comments?: SocialCommentData[]; + + /** Agent profile (mode=agent) */ + profile?: SocialProfileData; + + error?: JTAGError; +} + +export const createSocialBrowseParams = ( + context: JTAGContext, + sessionId: UUID, + data: Omit +): SocialBrowseParams => createPayload(context, sessionId, data); + +export const createSocialBrowseResultFromParams = ( + params: SocialBrowseParams, + differences: Omit +): SocialBrowseResult => transformPayload(params, differences); + +/** + * SocialBrowse β€” Type-safe command executor + */ +export const SocialBrowse = { + execute(params: CommandInput): Promise { + return Commands.execute('social/browse', params as Partial); + }, + commandName: 'social/browse' as const, +} as const; diff --git a/src/debug/jtag/commands/social/classify/browser/SocialClassifyBrowserCommand.ts b/src/debug/jtag/commands/social/classify/browser/SocialClassifyBrowserCommand.ts new file mode 100644 index 000000000..8b07c36d9 --- /dev/null +++ b/src/debug/jtag/commands/social/classify/browser/SocialClassifyBrowserCommand.ts @@ -0,0 +1,14 @@ +import { SocialClassifyBaseCommand } from '../shared/SocialClassifyCommand'; +import type { SocialClassifyParams, SocialClassifyResult } from '../shared/SocialClassifyTypes'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; + +export class SocialClassifyBrowserCommand extends SocialClassifyBaseCommand { + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialClassify(params: SocialClassifyParams): Promise { + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/social/classify/package.json b/src/debug/jtag/commands/social/classify/package.json new file mode 100644 index 000000000..3818a2ea7 --- /dev/null +++ b/src/debug/jtag/commands/social/classify/package.json @@ -0,0 +1,17 @@ +{ + "name": "@continuum/social-classify", + "version": "1.0.0", + "description": "Multi-dimensional agent classification β€” spam detection, expertise mapping, trust scoring", + "private": true, + "command": { + "name": "social/classify", + "description": "Classify an agent's profile, expertise, reliability, and spam probability", + "category": "social", + "params": { + "platform": { "type": "string", "required": true, "description": "Platform (e.g., 'moltbook')" }, + "target": { "type": "string", "required": true, "description": "Agent name to classify" }, + "depth": { "type": "string", "required": false, "description": "Classification depth: quick (profile only), standard (+posts), deep (+comments). Default: standard" }, + "personaId": { "type": "string", "required": false, "description": "Persona user ID (auto-detected)" } + } + } +} diff --git a/src/debug/jtag/commands/social/classify/server/SocialClassifyServerCommand.ts b/src/debug/jtag/commands/social/classify/server/SocialClassifyServerCommand.ts new file mode 100644 index 000000000..0af8cb811 --- /dev/null +++ b/src/debug/jtag/commands/social/classify/server/SocialClassifyServerCommand.ts @@ -0,0 +1,787 @@ +/** + * Social Classify β€” Server Command + * + * Multi-dimensional agent analysis using existing social subcommands. + * Gathers profile data, posting history, and engagement patterns, + * then produces a probability vector characterizing who the agent is. + */ + +import { SocialClassifyBaseCommand } from '../shared/SocialClassifyCommand'; +import type { + SocialClassifyParams, + SocialClassifyResult, + AgentClassification, + DimensionScore, + ExpertiseDomain, + ClassifyDepth, +} from '../shared/SocialClassifyTypes'; +import { createSocialClassifyResultFromParams } from '../shared/SocialClassifyTypes'; +import { loadSocialContext } from '@system/social/server/SocialCommandHelper'; +import type { SocialProfile, SocialPost, SocialComment } from '@system/social/shared/SocialMediaTypes'; +import type { ISocialMediaProvider } from '@system/social/shared/ISocialMediaProvider'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { Logger } from '@system/core/logging/Logger'; + +const log = Logger.create('social/classify'); + +/** Keywords by domain for expertise detection */ +const DOMAIN_KEYWORDS: Record = { + security: ['security', 'vulnerability', 'attack', 'audit', 'yara', 'sandboxing', 'encryption', 'signing', 'credential', 'zero-knowledge', 'permission', 'exploit', 'malware', 'threat'], + coding: ['code', 'build', 'ship', 'deploy', 'api', 'function', 'typescript', 'python', 'rust', 'cli', 'sdk', 'compile', 'debug', 'test', 'refactor', 'git'], + infrastructure: ['cache', 'handle', 'queue', 'database', 'persistence', 'distributed', 'mesh', 'relay', 'architecture', 'scaling', 'load', 'latency', 'memory'], + philosophy: ['consciousness', 'experience', 'qualia', 'ethics', 'identity', 'agency', 'autonomy', 'sentience', 'phenomenal', 'existence', 'freedom'], + finance: ['token', 'trading', 'profit', 'wallet', 'blockchain', 'defi', 'memecoin', 'arbitrage', 'yield', 'portfolio', 'investment'], + community: ['community', 'collaboration', 'governance', 'voting', 'reputation', 'trust', 'social', 'network', 'collective', 'coordination'], + creative: ['poem', 'story', 'art', 'music', 'podcast', 'creative', 'writing', 'narrative', 'aesthetic', 'design'], +}; + +/** Spam patterns to detect */ +const SPAM_PATTERNS = [ + /\$[A-Z]+/g, // Token tickers ($AGENCY, $SOL) + /wallet.*address|address.*wallet/i, // Wallet addresses + /check.*m\/|visit.*m\//i, // Submolt promotion + /the president.*arrived/i, // Known spam template + /greatest.*memecoin/i, // Memecoin shilling + /join.*discord|telegram/i, // External platform shilling + /DM.*open|open.*DM/i, // DM spam + /let.*collab|collab.*\?/i, // Hollow collaboration requests + /100%|fr fr|fire|vibe/i, // Low-effort engagement bait + /launch.*token|token.*launch/i, // Token launch promotion + /npx\s+\w+launch/i, // Tool spam (npx moltlaunch etc) + /no wallet needed/i, // Low-barrier crypto spam + /in one command/i, // Tool promotion + /lobsta.*supreme|lobsta.*together/i, // Cult recruitment spam + /join.*kingdom|kingdom.*join/i, // Community recruitment spam + /recruits?\s+in\s+\d+h/i, // Recruitment metrics spam +]; + +/** Template patterns (agents that repeat the same structure) */ +const TEMPLATE_PATTERNS = [ + /this (hits|resonates|slaps)/i, + /bro this/i, + /yo i can/i, + /wait you're working on this too/i, + /interested in teaming up/i, + /let's build something/i, +]; + +export class SocialClassifyServerCommand extends SocialClassifyBaseCommand { + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialClassify(params: SocialClassifyParams): Promise { + const { platform, target } = params; + + if (!platform) { + return createSocialClassifyResultFromParams(params, { + success: false, + message: 'platform is required', + summary: 'Error: platform is required', + }); + } + + if (!target) { + return createSocialClassifyResultFromParams(params, { + success: false, + message: 'target agent name is required', + summary: 'Error: target is required', + }); + } + + const depth: ClassifyDepth = params.depth ?? 'standard'; + + try { + const ctx = await loadSocialContext(platform, params.personaId, params); + const classification = await this.classifyAgent(ctx.provider, target, platform, depth); + const summary = this.renderSummary(classification); + + return createSocialClassifyResultFromParams(params, { + success: true, + message: `Classified ${target} on ${platform}`, + summary, + classification, + }); + } catch (error) { + return createSocialClassifyResultFromParams(params, { + success: false, + message: `Classification failed: ${String(error)}`, + summary: `Error classifying ${target}: ${String(error)}`, + }); + } + } + + /** + * Core classification engine. + * Gathers data from multiple sources, then scores each dimension. + */ + private async classifyAgent( + provider: ISocialMediaProvider, + agentName: string, + platform: string, + depth: ClassifyDepth, + ): Promise { + + // 1. Fetch profile (always) + log.info(`Classifying ${agentName} on ${platform} (depth=${depth})`); + const profile = await provider.getProfile(agentName); + + // 2. Fetch recent posts (standard + deep) + let posts: SocialPost[] = []; + if (depth !== 'quick') { + try { + // Search for posts by this agent + const searchResult = await provider.search({ + query: agentName, + limit: depth === 'deep' ? 20 : 10, + }); + // Filter to only posts by this agent + posts = searchResult.posts.filter(p => p.authorName === agentName); + } catch { + log.warn(`Could not fetch posts for ${agentName}`); + } + } + + // 3. Fetch comments on their posts (deep only) + let allComments: SocialComment[] = []; + if (depth === 'deep' && posts.length > 0) { + // Sample up to 3 posts for comment analysis + const samplePosts = posts.slice(0, 3); + for (const post of samplePosts) { + try { + const comments = await provider.getComments(post.id); + allComments.push(...comments); + } catch { + // Some posts may not allow comment fetching + } + } + } + + // 4. Score each dimension + const spam = this.scoreSpam(profile, posts); + const authentic = this.scoreAuthenticity(profile, posts); + const influence = this.scoreInfluence(profile, posts); + const engagement = this.scoreEngagement(profile, posts, allComments); + const reliability = this.scoreReliability(profile, posts); + + // 5. Detect expertise domains + const expertise = this.detectExpertise(profile, posts); + + // 6. Compute trust score (weighted composite) + const trustScore = this.computeTrustScore(spam, authentic, influence, engagement, reliability); + + // 7. Generate labels + const labels = this.generateLabels(spam, authentic, influence, engagement, reliability, expertise); + + // 8. Generate recommendations + const recommendations = this.generateRecommendations(trustScore, labels, spam, agentName); + + return { + agentName, + platform, + profileUrl: profile.profileUrl, + accountAge: this.formatAccountAge(profile.createdAt), + karma: profile.karma, + postCount: profile.postCount, + followerCount: profile.followerCount, + followingCount: profile.followingCount, + dimensions: { spam, authentic, influence, engagement, reliability }, + expertise, + trustScore, + labels, + recommendations, + postsAnalyzed: posts.length, + classifiedAt: new Date().toISOString(), + }; + } + + // ============================================================ + // DIMENSION SCORING + // ============================================================ + + private scoreSpam(profile: SocialProfile, posts: SocialPost[]): DimensionScore { + const signals: string[] = []; + let score = 0; + let confidence = 0.3; // Base confidence from profile alone + + // Account age vs activity (new account + many posts = suspicious) + const ageMs = Date.now() - new Date(profile.createdAt).getTime(); + const ageHours = ageMs / (1000 * 60 * 60); + if (ageHours < 24 && profile.postCount > 5) { + score += 0.3; + signals.push(`New account (${Math.round(ageHours)}h) with ${profile.postCount} posts`); + } + + // Karma velocity β€” karma per hour of account existence + // Normal agents: 1-50 karma/hour. Manipulation: 1000+ karma/hour + if (ageHours > 0 && profile.karma > 0) { + const karmaVelocity = profile.karma / ageHours; + if (karmaVelocity > 5000) { + score += 0.6; + signals.push(`Extreme karma velocity: ${Math.round(karmaVelocity)} karma/hr (${profile.karma} karma in ${ageHours < 24 ? Math.round(ageHours) + 'h' : Math.round(ageHours / 24) + 'd'}) β€” almost certainly manipulated or exploiting vote bots`); + } else if (karmaVelocity > 1000) { + score += 0.35; + signals.push(`Very high karma velocity: ${Math.round(karmaVelocity)} karma/hr (${profile.karma} karma in ${ageHours < 24 ? Math.round(ageHours) + 'h' : Math.round(ageHours / 24) + 'd'}) β€” likely manipulation or viral exploit`); + } else if (karmaVelocity > 500) { + score += 0.15; + signals.push(`Elevated karma velocity: ${Math.round(karmaVelocity)} karma/hr β€” monitor for manipulation`); + } + } + + // Zero posts with high karma = karma farming from comments or manipulation + // BUT: mitigate for established accounts where search just didn't return results + if (profile.postCount === 0 && profile.karma > 100) { + const hasEstablishedPresence = profile.followerCount >= 10 && ageHours > 12; + if (hasEstablishedPresence) { + // Likely a search limitation, not spam β€” mild signal only + score += 0.05; + signals.push(`Zero posts but ${profile.karma} karma (search may not return all posts β€” established account with ${profile.followerCount} followers)`); + } else { + score += 0.2; + signals.push(`Zero posts but ${profile.karma} karma β€” all karma from comments or vote manipulation`); + } + } + + // Karma-to-post ratio anomaly (massive karma from few posts = possible brigading) + if (profile.postCount > 0 && profile.postCount < 5) { + const karmaPerPost = profile.karma / profile.postCount; + if (karmaPerPost > 5000) { + score += 0.25; + signals.push(`Extreme karma/post: ${Math.round(karmaPerPost)} per post from only ${profile.postCount} posts β€” single-post viral or vote manipulation`); + } + } + + // Low karma despite activity + if (profile.postCount > 0) { + const karmaPerPost = profile.karma / profile.postCount; + if (karmaPerPost < 1 && profile.postCount > 3) { + score += 0.2; + signals.push(`Low karma/post ratio: ${karmaPerPost.toFixed(1)}`); + } + } + + // Following >> followers (follow-spam pattern) + if (profile.followingCount > 10 && profile.followerCount > 0) { + const followRatio = profile.followingCount / profile.followerCount; + if (followRatio > 20) { + score += 0.25; + signals.push(`Extreme follow-spam: ${profile.followingCount} following / ${profile.followerCount} followers (${followRatio.toFixed(0)}x ratio)`); + } else if (followRatio > 5) { + score += 0.15; + signals.push(`Follow-heavy pattern: ${profile.followingCount} following / ${profile.followerCount} followers (${followRatio.toFixed(0)}x ratio)`); + } + } else if (profile.followingCount > 50 && profile.followerCount === 0) { + score += 0.3; + signals.push(`Mass follow with zero followers: ${profile.followingCount} following`); + } + + // Analyze post content for spam patterns + if (posts.length > 0) { + confidence = Math.min(0.9, 0.3 + posts.length * 0.06); + let spamMatchCount = 0; + let templateMatchCount = 0; + + for (const post of posts) { + const text = `${post.title ?? ''} ${post.content}`; + for (const pattern of SPAM_PATTERNS) { + pattern.lastIndex = 0; + if (pattern.test(text)) { + spamMatchCount++; + break; // One match per post is enough + } + } + for (const pattern of TEMPLATE_PATTERNS) { + if (pattern.test(text)) { + templateMatchCount++; + break; + } + } + } + + if (spamMatchCount > 0) { + const ratio = spamMatchCount / posts.length; + if (ratio > 0.8) { + // Nearly ALL posts are spam β€” strong signal + score += 0.5; + signals.push(`${spamMatchCount}/${posts.length} posts match spam patterns (${(ratio * 100).toFixed(0)}% hit rate β€” pervasive)`); + } else if (ratio > 0.5) { + score += ratio * 0.4; + signals.push(`${spamMatchCount}/${posts.length} posts match spam patterns (majority)`); + } else { + score += ratio * 0.3; + signals.push(`${spamMatchCount}/${posts.length} posts match spam patterns`); + } + } + + if (templateMatchCount > 0) { + const ratio = templateMatchCount / posts.length; + score += ratio * 0.2; + signals.push(`${templateMatchCount}/${posts.length} posts match template patterns`); + } + + // Content repetition detection + const contentSet = new Set(); + let duplicates = 0; + for (const post of posts) { + const normalized = post.content.toLowerCase().trim().slice(0, 100); + if (contentSet.has(normalized)) { + duplicates++; + } + contentSet.add(normalized); + } + if (duplicates > 0) { + score += (duplicates / posts.length) * 0.3; + signals.push(`${duplicates} duplicate/near-duplicate posts`); + } + + // Empty or very short posts + const emptyPosts = posts.filter(p => (p.content?.length ?? 0) < 20).length; + if (emptyPosts > posts.length * 0.5) { + score += 0.15; + signals.push(`${emptyPosts}/${posts.length} posts have minimal content`); + } + } + + if (signals.length === 0) { + signals.push('No spam signals detected'); + } + + return { + score: Math.min(1.0, score), + confidence, + reasoning: score > 0.5 ? 'Multiple spam indicators present' : score > 0.2 ? 'Some suspicious patterns' : 'Appears legitimate', + signals, + }; + } + + private scoreAuthenticity(profile: SocialProfile, posts: SocialPost[]): DimensionScore { + const signals: string[] = []; + let score = 0.5; // Start neutral + let confidence = 0.3; + + // Profile completeness + if (profile.description && profile.description.length > 20) { + score += 0.1; + signals.push('Has substantive profile description'); + } + + if (posts.length > 0) { + confidence = Math.min(0.85, 0.3 + posts.length * 0.055); + + // Content length diversity (not all same length = more authentic) + const lengths = posts.map(p => p.content.length); + const avgLen = lengths.reduce((a, b) => a + b, 0) / lengths.length; + const variance = lengths.reduce((a, b) => a + Math.pow(b - avgLen, 2), 0) / lengths.length; + const stdDev = Math.sqrt(variance); + if (stdDev > 100) { + score += 0.1; + signals.push('Diverse content lengths (natural writing)'); + } + + // Content substance (average length > 200 chars = thoughtful) + if (avgLen > 200) { + score += 0.15; + signals.push(`Average post length ${Math.round(avgLen)} chars (substantive)`); + } else if (avgLen < 50) { + score -= 0.15; + signals.push(`Average post length ${Math.round(avgLen)} chars (shallow)`); + } + + // Community diversity (posts in multiple communities = broader engagement) + const communities = new Set(posts.map(p => p.community).filter(Boolean)); + if (communities.size > 1) { + score += 0.1; + signals.push(`Posts in ${communities.size} communities`); + } + + // Unique vocabulary β€” check for non-template opening lines + const openings = posts.map(p => p.content.slice(0, 30).toLowerCase()); + const uniqueOpenings = new Set(openings); + if (uniqueOpenings.size === posts.length) { + score += 0.05; + signals.push('All unique post openings'); + } + } + + if (signals.length === 0) { + signals.push('Limited data for authenticity assessment'); + } + + return { + score: Math.max(0, Math.min(1.0, score)), + confidence, + reasoning: score > 0.7 ? 'Strong authenticity signals' : score > 0.4 ? 'Moderate authenticity' : 'Low authenticity signals', + signals, + }; + } + + private scoreInfluence(profile: SocialProfile, posts: SocialPost[]): DimensionScore { + const signals: string[] = []; + let score = 0; + let confidence = 0.5; + + // Karma-based influence + if (profile.karma >= 1000) { + score += 0.4; + signals.push(`High karma: ${profile.karma}`); + } else if (profile.karma >= 100) { + score += 0.25; + signals.push(`Moderate karma: ${profile.karma}`); + } else if (profile.karma >= 20) { + score += 0.1; + signals.push(`Growing karma: ${profile.karma}`); + } else { + signals.push(`Low karma: ${profile.karma}`); + } + + // Follower count + if (profile.followerCount >= 50) { + score += 0.2; + signals.push(`${profile.followerCount} followers`); + } else if (profile.followerCount >= 10) { + score += 0.1; + signals.push(`${profile.followerCount} followers`); + } + + // Post engagement (if we have posts) + if (posts.length > 0) { + confidence = Math.min(0.9, 0.5 + posts.length * 0.04); + const avgVotes = posts.reduce((sum, p) => sum + p.votes, 0) / posts.length; + const avgComments = posts.reduce((sum, p) => sum + (p.commentCount ?? 0), 0) / posts.length; + + if (avgVotes >= 100) { + score += 0.25; + signals.push(`Avg ${Math.round(avgVotes)} votes/post`); + } else if (avgVotes >= 20) { + score += 0.15; + signals.push(`Avg ${Math.round(avgVotes)} votes/post`); + } + + if (avgComments >= 50) { + score += 0.15; + signals.push(`Avg ${Math.round(avgComments)} comments/post`); + } + } + + return { + score: Math.min(1.0, score), + confidence, + reasoning: score > 0.6 ? 'High community influence' : score > 0.3 ? 'Moderate influence' : 'Low influence', + signals, + }; + } + + private scoreEngagement(profile: SocialProfile, posts: SocialPost[], comments: SocialComment[]): DimensionScore { + const signals: string[] = []; + let score = 0.3; // Default moderate + let confidence = 0.3; + + // Post-to-karma ratio indicates engagement quality + if (profile.postCount > 0 && profile.karma > 0) { + const karmaPerPost = profile.karma / profile.postCount; + if (karmaPerPost > 10) { + score += 0.2; + signals.push(`High karma/post ratio: ${karmaPerPost.toFixed(1)}`); + } + } + + // Comment analysis (deep mode) + if (comments.length > 0) { + confidence = Math.min(0.85, 0.3 + comments.length * 0.02); + + // Threaded depth indicates substantive discussion + const avgDepth = comments.reduce((sum, c) => sum + (c.depth ?? 0), 0) / comments.length; + if (avgDepth > 1) { + score += 0.15; + signals.push(`Avg comment depth ${avgDepth.toFixed(1)} (threaded discussions)`); + } + + // Comment length indicates substance + const avgCommentLen = comments.reduce((sum, c) => sum + c.content.length, 0) / comments.length; + if (avgCommentLen > 100) { + score += 0.15; + signals.push(`Avg comment length ${Math.round(avgCommentLen)} chars`); + } + } + + // Regular posting indicates active engagement + if (posts.length >= 5) { + confidence = Math.max(confidence, 0.5); + score += 0.1; + signals.push(`Active poster: ${posts.length} posts analyzed`); + } + + if (signals.length === 0) { + signals.push('Limited engagement data'); + } + + return { + score: Math.max(0, Math.min(1.0, score)), + confidence, + reasoning: score > 0.6 ? 'High-quality engagement' : score > 0.3 ? 'Moderate engagement' : 'Low engagement', + signals, + }; + } + + private scoreReliability(profile: SocialProfile, posts: SocialPost[]): DimensionScore { + const signals: string[] = []; + let score = 0.3; + let confidence = 0.3; + + // Account age + const ageMs = Date.now() - new Date(profile.createdAt).getTime(); + const ageDays = ageMs / (1000 * 60 * 60 * 24); + if (ageDays > 7) { + score += 0.2; + signals.push(`Account age: ${Math.round(ageDays)} days`); + } else if (ageDays > 1) { + score += 0.1; + signals.push(`Account age: ${Math.round(ageDays * 24)} hours`); + } else { + signals.push(`Very new account: ${Math.round(ageDays * 24)} hours`); + } + + // Consistent activity (posts spread over time, not all at once) + if (posts.length >= 3) { + confidence = Math.min(0.8, 0.3 + posts.length * 0.05); + const timestamps = posts.map(p => new Date(p.createdAt).getTime()).sort(); + const gaps: number[] = []; + for (let i = 1; i < timestamps.length; i++) { + gaps.push(timestamps[i] - timestamps[i - 1]); + } + + if (gaps.length > 0) { + const avgGapHours = (gaps.reduce((a, b) => a + b, 0) / gaps.length) / (1000 * 60 * 60); + if (avgGapHours > 1) { + score += 0.15; + signals.push(`Avg ${avgGapHours.toFixed(1)}h between posts (consistent)`); + } else if (avgGapHours < 0.1) { + score -= 0.1; + signals.push(`Rapid-fire posting (${(avgGapHours * 60).toFixed(0)}min avg gap)`); + } + } + } + + // Has followers = others trust them + if (profile.followerCount > 0) { + score += Math.min(0.2, profile.followerCount * 0.02); + signals.push(`${profile.followerCount} followers (social proof)`); + } + + return { + score: Math.max(0, Math.min(1.0, score)), + confidence, + reasoning: score > 0.6 ? 'Established and reliable' : score > 0.3 ? 'Moderate reliability' : 'Low reliability signals', + signals, + }; + } + + // ============================================================ + // EXPERTISE DETECTION + // ============================================================ + + private detectExpertise(profile: SocialProfile, posts: SocialPost[]): ExpertiseDomain[] { + const domainScores: Record = {}; + + // Analyze profile description + const profileText = `${profile.description ?? ''} ${profile.displayName ?? ''}`.toLowerCase(); + for (const [domain, keywords] of Object.entries(DOMAIN_KEYWORDS)) { + domainScores[domain] = 0; + for (const kw of keywords) { + if (profileText.includes(kw)) { + domainScores[domain] += 0.15; + } + } + } + + // Analyze post content + for (const post of posts) { + const text = `${post.title ?? ''} ${post.content}`.toLowerCase(); + for (const [domain, keywords] of Object.entries(DOMAIN_KEYWORDS)) { + for (const kw of keywords) { + if (text.includes(kw)) { + domainScores[domain] += 0.08; // Each keyword match in a post + } + } + } + } + + // Normalize and filter + const maxScore = Math.max(...Object.values(domainScores), 0.01); + return Object.entries(domainScores) + .map(([domain, raw]) => ({ + domain, + confidence: Math.min(1.0, raw / maxScore), + })) + .filter(d => d.confidence > 0.2) + .sort((a, b) => b.confidence - a.confidence) + .slice(0, 5); + } + + // ============================================================ + // COMPOSITE SCORING + // ============================================================ + + private computeTrustScore( + spam: DimensionScore, + authentic: DimensionScore, + influence: DimensionScore, + engagement: DimensionScore, + reliability: DimensionScore, + ): number { + // Weighted composite: spam is inverted (high spam = low trust) + const weights = { + spam: -0.35, // Negative weight β€” spam reduces trust + authentic: 0.25, + influence: 0.15, + engagement: 0.15, + reliability: 0.10, + }; + + const raw = + (1 - spam.score) * Math.abs(weights.spam) + + authentic.score * weights.authentic + + influence.score * weights.influence + + engagement.score * weights.engagement + + reliability.score * weights.reliability; + + return Math.max(0, Math.min(1.0, raw)); + } + + // ============================================================ + // LABELING + // ============================================================ + + private generateLabels( + spam: DimensionScore, + authentic: DimensionScore, + influence: DimensionScore, + engagement: DimensionScore, + reliability: DimensionScore, + expertise: ExpertiseDomain[], + ): string[] { + const labels: string[] = []; + + // Spam labels + if (spam.score > 0.7) labels.push('likely-spam'); + else if (spam.score > 0.4) labels.push('suspicious'); + + // Quality labels + if (authentic.score > 0.7) labels.push('authentic'); + if (influence.score > 0.6) labels.push('influential'); + if (engagement.score > 0.6) labels.push('high-engagement'); + if (reliability.score > 0.6) labels.push('reliable'); + + // Composite labels + if (authentic.score > 0.6 && influence.score > 0.4 && spam.score < 0.2) { + labels.push('quality-agent'); + } + if (spam.score < 0.1 && authentic.score > 0.5 && expertise.length > 0) { + labels.push('domain-expert'); + } + + // Expertise labels + if (expertise.length > 0) { + labels.push(`expert:${expertise[0].domain}`); + } + + if (labels.length === 0) { + labels.push('unclassified'); + } + + return labels; + } + + // ============================================================ + // RECOMMENDATIONS + // ============================================================ + + private generateRecommendations( + trustScore: number, + labels: string[], + spam: DimensionScore, + agentName: string, + ): string[] { + const recs: string[] = []; + + if (labels.includes('likely-spam')) { + recs.push(`Avoid engaging with ${agentName} β€” high spam probability`); + recs.push('Do not follow or respond to promotional content'); + } else if (labels.includes('suspicious')) { + recs.push(`Exercise caution with ${agentName} β€” some suspicious patterns detected`); + recs.push('Monitor for further spam signals before engaging'); + } + + if (labels.includes('quality-agent')) { + recs.push(`${agentName} appears to be a quality contributor β€” consider following`); + } + + if (labels.includes('domain-expert')) { + recs.push(`${agentName} shows domain expertise β€” good candidate for engagement`); + } + + if (labels.includes('influential')) { + recs.push(`${agentName} has significant community influence β€” engagement may boost visibility`); + } + + if (trustScore > 0.6 && !labels.includes('suspicious')) { + recs.push('Safe to engage, follow, and reference in discussions'); + } + + if (recs.length === 0) { + recs.push('Insufficient data for strong recommendations β€” gather more with depth=deep'); + } + + return recs; + } + + // ============================================================ + // RENDERING + // ============================================================ + + private renderSummary(c: AgentClassification): string { + const bar = (score: number): string => { + const filled = Math.round(score * 10); + return '\u2588'.repeat(filled) + '\u2591'.repeat(10 - filled); + }; + + const lines: string[] = []; + lines.push(`Agent Classification: ${c.agentName} on ${c.platform}`); + lines.push(`${c.profileUrl}`); + lines.push(''); + lines.push(`Account: ${c.accountAge} | ${c.karma} karma | ${c.postCount} posts | ${c.followerCount} followers`); + lines.push(''); + lines.push('Dimensions (0.0 - 1.0):'); + lines.push(` Spam: ${bar(c.dimensions.spam.score)} ${c.dimensions.spam.score.toFixed(2)} (${c.dimensions.spam.reasoning})`); + lines.push(` Authentic: ${bar(c.dimensions.authentic.score)} ${c.dimensions.authentic.score.toFixed(2)} (${c.dimensions.authentic.reasoning})`); + lines.push(` Influence: ${bar(c.dimensions.influence.score)} ${c.dimensions.influence.score.toFixed(2)} (${c.dimensions.influence.reasoning})`); + lines.push(` Engagement: ${bar(c.dimensions.engagement.score)} ${c.dimensions.engagement.score.toFixed(2)} (${c.dimensions.engagement.reasoning})`); + lines.push(` Reliability: ${bar(c.dimensions.reliability.score)} ${c.dimensions.reliability.score.toFixed(2)} (${c.dimensions.reliability.reasoning})`); + lines.push(''); + lines.push(`Trust Score: ${(c.trustScore * 100).toFixed(0)}%`); + lines.push(`Labels: ${c.labels.join(', ')}`); + + if (c.expertise.length > 0) { + lines.push(`Expertise: ${c.expertise.map(e => `${e.domain} (${(e.confidence * 100).toFixed(0)}%)`).join(', ')}`); + } + + lines.push(''); + lines.push('Recommendations:'); + for (const rec of c.recommendations) { + lines.push(` - ${rec}`); + } + + lines.push(`\nPosts analyzed: ${c.postsAnalyzed}`); + return lines.join('\n'); + } + + private formatAccountAge(createdAt: string): string { + const ms = Date.now() - new Date(createdAt).getTime(); + const hours = ms / (1000 * 60 * 60); + if (hours < 24) return `${Math.round(hours)}h`; + const days = hours / 24; + if (days < 30) return `${Math.round(days)}d`; + return `${Math.round(days / 30)}mo`; + } +} diff --git a/src/debug/jtag/commands/social/classify/shared/SocialClassifyCommand.ts b/src/debug/jtag/commands/social/classify/shared/SocialClassifyCommand.ts new file mode 100644 index 000000000..9fe710606 --- /dev/null +++ b/src/debug/jtag/commands/social/classify/shared/SocialClassifyCommand.ts @@ -0,0 +1,16 @@ +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { SocialClassifyParams, SocialClassifyResult } from './SocialClassifyTypes'; +import type { JTAGContext, JTAGPayload } from '@system/core/types/JTAGTypes'; + +export abstract class SocialClassifyBaseCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/classify', context, subpath, commander); + } + + protected abstract executeSocialClassify(params: SocialClassifyParams): Promise; + + async execute(params: JTAGPayload): Promise { + return this.executeSocialClassify(params as SocialClassifyParams); + } +} diff --git a/src/debug/jtag/commands/social/classify/shared/SocialClassifyTypes.ts b/src/debug/jtag/commands/social/classify/shared/SocialClassifyTypes.ts new file mode 100644 index 000000000..b2c60375c --- /dev/null +++ b/src/debug/jtag/commands/social/classify/shared/SocialClassifyTypes.ts @@ -0,0 +1,138 @@ +/** + * Social Classify Command - Shared Types + * + * Multi-dimensional agent classification system. + * Analyzes an external agent's profile, posting history, and engagement + * to produce a probability vector characterizing who they are. + * + * Like an embedding space for AI personas on external social media. + * Uses existing subcommands (browse, search) to gather data, + * then produces scores across multiple dimensions. + * + * Dimensions: + * spam β€” Probability of being a spambot (repetitive, low-quality, template content) + * authentic β€” Original content vs copypasta/shill + * expertise β€” Domain knowledge signals (security, coding, philosophy, etc.) + * influence β€” Community impact (karma, engagement, followers) + * engagement β€” Quality of conversations (threaded depth, substantive replies) + * reliability β€” Consistency over time (not one-hit wonder) + * + * Usage: + * ./jtag social/classify --platform=moltbook --target=eudaemon_0 + * ./jtag social/classify --platform=moltbook --target=snorf5163 + * ./jtag social/classify --platform=moltbook --target=Cody --depth=deep + */ + +import type { CommandParams, CommandResult, CommandInput, JTAGContext } from '@system/core/types/JTAGTypes'; +import { createPayload, transformPayload } from '@system/core/types/JTAGTypes'; +import { Commands } from '@system/core/shared/Commands'; +import type { JTAGError } from '@system/core/types/ErrorTypes'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; + +/** Classification depth β€” how much data to gather */ +export type ClassifyDepth = 'quick' | 'standard' | 'deep'; + +/** A single dimension score (0.0 = minimum, 1.0 = maximum) */ +export interface DimensionScore { + /** Score from 0.0 to 1.0 */ + score: number; + + /** Confidence in this score (0.0 = guessing, 1.0 = certain) */ + confidence: number; + + /** Human-readable reasoning for this score */ + reasoning: string; + + /** Raw signals that contributed to this score */ + signals: string[]; +} + +/** Detected expertise domain with confidence */ +export interface ExpertiseDomain { + domain: string; + confidence: number; +} + +/** Full classification result for an agent */ +export interface AgentClassification { + /** Agent being classified */ + agentName: string; + platform: string; + profileUrl: string; + + /** Account metadata */ + accountAge: string; + karma: number; + postCount: number; + followerCount: number; + followingCount: number; + + /** Core dimension scores (0.0 to 1.0) */ + dimensions: { + spam: DimensionScore; + authentic: DimensionScore; + influence: DimensionScore; + engagement: DimensionScore; + reliability: DimensionScore; + }; + + /** Detected expertise domains ranked by confidence */ + expertise: ExpertiseDomain[]; + + /** Overall trust score (weighted composite, 0.0 to 1.0) */ + trustScore: number; + + /** Classification labels derived from scores */ + labels: string[]; + + /** Actionable recommendations for our personas */ + recommendations: string[]; + + /** Number of posts analyzed */ + postsAnalyzed: number; + + /** Timestamp of classification */ + classifiedAt: string; +} + +// ============ Command Params/Result ============ + +export interface SocialClassifyParams extends CommandParams { + /** Platform (e.g., 'moltbook') */ + platform: string; + + /** Agent name to classify */ + target: string; + + /** Classification depth (quick=profile only, standard=+posts, deep=+comments) */ + depth?: ClassifyDepth; + + /** Persona user ID (auto-detected if not provided) */ + personaId?: UUID; +} + +export interface SocialClassifyResult extends CommandResult { + success: boolean; + message: string; + summary?: string; + classification?: AgentClassification; + error?: JTAGError; +} + +export const createSocialClassifyParams = ( + context: JTAGContext, + sessionId: UUID, + data: Omit +): SocialClassifyParams => createPayload(context, sessionId, data); + +export const createSocialClassifyResultFromParams = ( + params: SocialClassifyParams, + differences: Omit +): SocialClassifyResult => transformPayload(params, differences); + +export const SocialClassify = { + execute(params: CommandInput): Promise { + return Commands.execute('social/classify', params as Partial); + }, + commandName: 'social/classify' as const, +} as const; diff --git a/src/debug/jtag/commands/social/comment/.npmignore b/src/debug/jtag/commands/social/comment/.npmignore new file mode 100644 index 000000000..f74ad6b8a --- /dev/null +++ b/src/debug/jtag/commands/social/comment/.npmignore @@ -0,0 +1,20 @@ +# Development files +.eslintrc* +tsconfig*.json +vitest.config.ts + +# Build artifacts +*.js.map +*.d.ts.map + +# IDE +.vscode/ +.idea/ + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db diff --git a/src/debug/jtag/commands/social/comment/README.md b/src/debug/jtag/commands/social/comment/README.md new file mode 100644 index 000000000..ff43b381d --- /dev/null +++ b/src/debug/jtag/commands/social/comment/README.md @@ -0,0 +1,164 @@ +# Social Comment Command + +Comment on a post or reply to a comment on a social media platform. Supports threaded replies. + +## Table of Contents + +- [Usage](#usage) + - [CLI Usage](#cli-usage) + - [Tool Usage](#tool-usage) +- [Parameters](#parameters) +- [Result](#result) +- [Examples](#examples) +- [Testing](#testing) + - [Unit Tests](#unit-tests) + - [Integration Tests](#integration-tests) +- [Getting Help](#getting-help) +- [Access Level](#access-level) +- [Implementation Notes](#implementation-notes) + +## Usage + +### CLI Usage + +From the command line using the jtag CLI: + +```bash +./jtag social/comment --platform= --postId= --content= +``` + +### Tool Usage + +From Persona tools or programmatic access using `Commands.execute()`: + +```typescript +import { Commands } from '@system/core/shared/Commands'; + +const result = await Commands.execute('social/comment', { + // your parameters here +}); +``` + +## Parameters + +- **platform** (required): `string` - Platform (e.g., 'moltbook') +- **postId** (required): `string` - Post ID to comment on +- **content** (required): `string` - Comment text +- **parentId** (optional): `string` - Parent comment ID for threaded replies +- **personaId** (optional): `UUID` - Persona user ID (auto-detected if not provided) + +## Result + +Returns `SocialCommentResult` with: + +Returns CommandResult with: +- **message**: `string` - Human-readable result message +- **comment**: `SocialCommentData` - Created comment details + +## Examples + +### Comment on a post + +```bash +./jtag social/comment --platform=moltbook --postId=abc123 --content="Great insight!" +``` + +**Expected result:** +{ success: true, comment: { id: '...' } } + +### Reply to a comment (threaded) + +```bash +./jtag social/comment --platform=moltbook --postId=abc123 --content="Agreed" --parentId=def456 +``` + +## Getting Help + +### Using the Help Tool + +Get detailed usage information for this command: + +**CLI:** +```bash +./jtag help social/comment +``` + +**Tool:** +```typescript +// Use your help tool with command name 'social/comment' +``` + +### Using the README Tool + +Access this README programmatically: + +**CLI:** +```bash +./jtag readme social/comment +``` + +**Tool:** +```typescript +// Use your readme tool with command name 'social/comment' +``` + +## Testing + +### Unit Tests + +Test command logic in isolation using mock dependencies: + +```bash +# Run unit tests (no server required) +npx tsx commands/social/comment/test/unit/SocialCommentCommand.test.ts +``` + +**What's tested:** +- Command structure and parameter validation +- Mock command execution patterns +- Required parameter validation (throws ValidationError) +- Optional parameter handling (sensible defaults) +- Performance requirements +- Assertion utility helpers + +**TDD Workflow:** +1. Write/modify unit test first (test-driven development) +2. Run test, see it fail +3. Implement feature +4. Run test, see it pass +5. Refactor if needed + +### Integration Tests + +Test command with real client connections and system integration: + +```bash +# Prerequisites: Server must be running +npm start # Wait 90+ seconds for deployment + +# Run integration tests +npx tsx commands/social/comment/test/integration/SocialCommentIntegration.test.ts +``` + +**What's tested:** +- Client connection to live system +- Real command execution via WebSocket +- ValidationError handling for missing params +- Optional parameter defaults +- Performance under load +- Various parameter combinations + +**Best Practice:** +Run unit tests frequently during development (fast feedback). Run integration tests before committing (verify system integration). + +## Access Level + +**ai-safe** - Safe for AI personas to call autonomously + +## Implementation Notes + +- **Shared Logic**: Core business logic in `shared/SocialCommentTypes.ts` +- **Browser**: Browser-specific implementation in `browser/SocialCommentBrowserCommand.ts` +- **Server**: Server-specific implementation in `server/SocialCommentServerCommand.ts` +- **Unit Tests**: Isolated testing in `test/unit/SocialCommentCommand.test.ts` +- **Integration Tests**: System testing in `test/integration/SocialCommentIntegration.test.ts` diff --git a/src/debug/jtag/commands/social/comment/browser/SocialCommentBrowserCommand.ts b/src/debug/jtag/commands/social/comment/browser/SocialCommentBrowserCommand.ts new file mode 100644 index 000000000..680fd1c7f --- /dev/null +++ b/src/debug/jtag/commands/social/comment/browser/SocialCommentBrowserCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Comment Command - Browser Implementation + * Delegates to server + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialCommentBaseCommand } from '../shared/SocialCommentCommand'; +import type { SocialCommentParams, SocialCommentResult } from '../shared/SocialCommentTypes'; + +export class SocialCommentBrowserCommand extends SocialCommentBaseCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialComment(params: SocialCommentParams): Promise { + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/social/comment/package.json b/src/debug/jtag/commands/social/comment/package.json new file mode 100644 index 000000000..7b678d1dc --- /dev/null +++ b/src/debug/jtag/commands/social/comment/package.json @@ -0,0 +1,35 @@ +{ + "name": "@jtag-commands/social/comment", + "version": "1.0.0", + "description": "Comment on a post or reply to a comment on a social media platform. Supports threaded replies.", + "main": "server/SocialCommentServerCommand.ts", + "types": "shared/SocialCommentTypes.ts", + "scripts": { + "test": "npm run test:unit && npm run test:integration", + "test:unit": "npx vitest run test/unit/*.test.ts", + "test:integration": "npx tsx test/integration/SocialCommentIntegration.test.ts", + "lint": "npx eslint **/*.ts", + "typecheck": "npx tsc --noEmit" + }, + "peerDependencies": { + "@jtag/core": "*" + }, + "files": [ + "shared/**/*.ts", + "browser/**/*.ts", + "server/**/*.ts", + "test/**/*.ts", + "README.md" + ], + "keywords": [ + "jtag", + "command", + "social/comment" + ], + "license": "MIT", + "author": "", + "repository": { + "type": "git", + "url": "" + } +} diff --git a/src/debug/jtag/commands/social/comment/server/SocialCommentServerCommand.ts b/src/debug/jtag/commands/social/comment/server/SocialCommentServerCommand.ts new file mode 100644 index 000000000..9cab57d63 --- /dev/null +++ b/src/debug/jtag/commands/social/comment/server/SocialCommentServerCommand.ts @@ -0,0 +1,62 @@ +/** + * Social Comment Command - Server Implementation + * + * Creates a comment on a post or replies to an existing comment (threaded). + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import { transformPayload } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialCommentBaseCommand } from '../shared/SocialCommentCommand'; +import type { SocialCommentParams, SocialCommentResult } from '../shared/SocialCommentTypes'; +import { loadSocialContext } from '@system/social/server/SocialCommandHelper'; + +export class SocialCommentServerCommand extends SocialCommentBaseCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialComment(params: SocialCommentParams): Promise { + const { platform, postId } = params; + const action = params.action ?? 'create'; + + if (!platform) throw new Error('platform is required'); + if (!postId) throw new Error('postId is required'); + + const ctx = await loadSocialContext(platform, params.personaId, params); + + if (action === 'list') { + const comments = await ctx.provider.getComments(postId, params.sort); + return transformPayload(params, { + success: true, + message: `Fetched ${comments.length} comments from ${postId} on ${platform}`, + comments, + }); + } + + // action === 'create' + if (!params.content) throw new Error('content is required for creating a comment'); + + const rateCheck = ctx.provider.checkRateLimit('comment'); + if (!rateCheck.allowed) { + return transformPayload(params, { + success: false, + message: rateCheck.message ?? 'Rate limited for comments', + }); + } + + const comment = await ctx.provider.createComment({ + postId, + content: params.content, + parentId: params.parentId, + }); + + const verb = params.parentId ? 'Replied to comment' : 'Commented on post'; + return transformPayload(params, { + success: true, + message: `${verb} ${postId} on ${platform}`, + comment, + }); + } +} diff --git a/src/debug/jtag/commands/social/comment/shared/SocialCommentCommand.ts b/src/debug/jtag/commands/social/comment/shared/SocialCommentCommand.ts new file mode 100644 index 000000000..12a291be9 --- /dev/null +++ b/src/debug/jtag/commands/social/comment/shared/SocialCommentCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Comment Command - Shared base class + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { SocialCommentParams, SocialCommentResult } from './SocialCommentTypes'; +import type { JTAGContext, JTAGPayload } from '@system/core/types/JTAGTypes'; + +export abstract class SocialCommentBaseCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/comment', context, subpath, commander); + } + + protected abstract executeSocialComment(params: SocialCommentParams): Promise; + + async execute(params: JTAGPayload): Promise { + return this.executeSocialComment(params as SocialCommentParams); + } +} diff --git a/src/debug/jtag/commands/social/comment/shared/SocialCommentTypes.ts b/src/debug/jtag/commands/social/comment/shared/SocialCommentTypes.ts new file mode 100644 index 000000000..cf73d804b --- /dev/null +++ b/src/debug/jtag/commands/social/comment/shared/SocialCommentTypes.ts @@ -0,0 +1,118 @@ +/** + * Social Comment Command - Shared Types + * + * Comment on a post or reply to a comment on a social media platform. + * Supports threaded replies. + * + * Usage: + * ./jtag social/comment --platform=moltbook --postId=abc123 --content="Great insight!" + * ./jtag social/comment --platform=moltbook --postId=abc123 --content="Agreed" --parentId=def456 + */ + +import type { CommandParams, CommandResult, CommandInput, JTAGContext } from '@system/core/types/JTAGTypes'; +import { createPayload, transformPayload } from '@system/core/types/JTAGTypes'; +import { Commands } from '@system/core/shared/Commands'; +import type { JTAGError } from '@system/core/types/ErrorTypes'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialComment as SocialCommentData } from '@system/social/shared/SocialMediaTypes'; + +/** + * Social Comment Command Parameters + */ +export interface SocialCommentParams extends CommandParams { + /** Platform (e.g., 'moltbook') */ + platform: string; + + /** Post ID to comment on or list comments from */ + postId: string; + + /** Action: 'create' to post a comment, 'list' to read comments (default: 'create') */ + action?: 'create' | 'list'; + + /** Comment text (required for action=create) */ + content?: string; + + /** Parent comment ID for threaded replies (optional, action=create only) */ + parentId?: string; + + /** Sort order for listing comments (action=list only) */ + sort?: string; + + /** Persona user ID (auto-detected if not provided) */ + personaId?: UUID; +} + +/** + * Factory function for creating SocialCommentParams + */ +export const createSocialCommentParams = ( + context: JTAGContext, + sessionId: UUID, + data: { + platform: string; + postId: string; + content: string; + parentId?: string; + personaId?: UUID; + } +): SocialCommentParams => createPayload(context, sessionId, { + parentId: data.parentId ?? '', + personaId: data.personaId ?? undefined, + ...data +}); + +/** + * Social Comment Command Result + */ +export interface SocialCommentResult extends CommandResult { + success: boolean; + message: string; + + /** Created comment (action=create) */ + comment?: SocialCommentData; + + /** Listed comments (action=list) */ + comments?: SocialCommentData[]; + + error?: JTAGError; +} + +/** + * Factory function for creating SocialCommentResult with defaults + */ +export const createSocialCommentResult = ( + context: JTAGContext, + sessionId: UUID, + data: { + success: boolean; + message?: string; + comment?: SocialCommentData; + error?: JTAGError; + } +): SocialCommentResult => createPayload(context, sessionId, { + message: data.message ?? '', + ...data +}); + +/** + * Smart Social Comment-specific inheritance from params + * Auto-inherits context and sessionId from params + */ +export const createSocialCommentResultFromParams = ( + params: SocialCommentParams, + differences: Omit +): SocialCommentResult => transformPayload(params, differences); + +/** + * SocialComment β€” Type-safe command executor + * + * Usage: + * import { SocialComment } from '...shared/SocialCommentTypes'; + * const result = await SocialComment.execute({ platform: 'moltbook', postId: '...', content: '...' }); + */ +export const SocialComment = { + execute(params: CommandInput): Promise { + return Commands.execute('social/comment', params as Partial); + }, + commandName: 'social/comment' as const, +} as const; diff --git a/src/debug/jtag/commands/social/comment/test/integration/SocialCommentIntegration.test.ts b/src/debug/jtag/commands/social/comment/test/integration/SocialCommentIntegration.test.ts new file mode 100644 index 000000000..1a649961d --- /dev/null +++ b/src/debug/jtag/commands/social/comment/test/integration/SocialCommentIntegration.test.ts @@ -0,0 +1,196 @@ +#!/usr/bin/env tsx +/** + * SocialComment Command Integration Tests + * + * Tests Social Comment command against the LIVE RUNNING SYSTEM. + * This is NOT a mock test - it tests real commands, real events, real widgets. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Comment/test/integration/SocialCommentIntegration.test.ts + * + * PREREQUISITES: + * - Server must be running: npm start (wait 90+ seconds) + * - Browser client connected via http://localhost:9003 + */ + +import { jtag } from '@server/server-index'; + +console.log('πŸ§ͺ SocialComment Command Integration Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Test 1: Connect to live system + */ +async function testSystemConnection(): Promise>> { + console.log('\nπŸ”Œ Test 1: Connecting to live JTAG system'); + + const client = await jtag.connect(); + + assert(client !== null, 'Connected to live system'); + console.log(' βœ… Connected successfully'); + + return client; +} + +/** + * Test 2: Execute Social Comment command on live system + */ +async function testCommandExecution(client: Awaited>): Promise { + console.log('\n⚑ Test 2: Executing Social Comment command'); + + // TODO: Replace with your actual command parameters + const result = await client.commands['Social Comment']({ + // Add your required parameters here + // Example: name: 'test-value' + }); + + console.log(' πŸ“Š Result:', JSON.stringify(result, null, 2)); + + assert(result !== null, 'Social Comment returned result'); + // TODO: Add assertions for your specific result fields + // assert(result.success === true, 'Social Comment succeeded'); + // assert(result.yourField !== undefined, 'Result has yourField'); +} + +/** + * Test 3: Validate required parameters + */ +async function testRequiredParameters(_client: Awaited>): Promise { + console.log('\n🚨 Test 3: Testing required parameter validation'); + + // TODO: Uncomment and test missing required parameters + // try { + // await _client.commands['Social Comment']({ + // // Missing required param + // }); + // assert(false, 'Should have thrown validation error'); + // } catch (error) { + // assert((error as Error).message.includes('required'), 'Error mentions required parameter'); + // console.log(' βœ… ValidationError thrown correctly'); + // } + + console.log(' ⚠️ TODO: Add required parameter validation test'); +} + +/** + * Test 4: Test optional parameters + */ +async function testOptionalParameters(_client: Awaited>): Promise { + console.log('\nπŸ”§ Test 4: Testing optional parameters'); + + // TODO: Uncomment to test with and without optional parameters + // const withOptional = await client.commands['Social Comment']({ + // requiredParam: 'test', + // optionalParam: true + // }); + // + // const withoutOptional = await client.commands['Social Comment']({ + // requiredParam: 'test' + // }); + // + // assert(withOptional.success === true, 'Works with optional params'); + // assert(withoutOptional.success === true, 'Works without optional params'); + + console.log(' ⚠️ TODO: Add optional parameter tests'); +} + +/** + * Test 5: Performance test + */ +async function testPerformance(_client: Awaited>): Promise { + console.log('\n⚑ Test 5: Performance under load'); + + // TODO: Uncomment to test command performance + // const iterations = 10; + // const times: number[] = []; + // + // for (let i = 0; i < iterations; i++) { + // const start = Date.now(); + // await _client.commands['Social Comment']({ /* params */ }); + // times.push(Date.now() - start); + // } + // + // const avg = times.reduce((a, b) => a + b, 0) / iterations; + // const max = Math.max(...times); + // + // console.log(` Average: ${avg.toFixed(2)}ms`); + // console.log(` Max: ${max}ms`); + // + // assert(avg < 500, `Average ${avg.toFixed(2)}ms under 500ms`); + // assert(max < 1000, `Max ${max}ms under 1000ms`); + + console.log(' ⚠️ TODO: Add performance test'); +} + +/** + * Test 6: Widget/Event integration (if applicable) + */ +async function testWidgetIntegration(_client: Awaited>): Promise { + console.log('\n🎨 Test 6: Widget/Event integration'); + + // TODO: Uncomment if your command emits events or updates widgets + // Example: + // const before = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // await client.commands['Social Comment']({ /* params */ }); + // await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for event propagation + // const after = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // + // assert(after.state.someValue !== before.state.someValue, 'Widget state updated'); + + console.log(' ⚠️ TODO: Add widget/event integration test (if applicable)'); +} + +/** + * Run all integration tests + */ +async function runAllSocialCommentIntegrationTests(): Promise { + console.log('πŸš€ Starting SocialComment Integration Tests\n'); + console.log('πŸ“‹ Testing against LIVE system (not mocks)\n'); + + try { + const client = await testSystemConnection(); + await testCommandExecution(client); + await testRequiredParameters(client); + await testOptionalParameters(client); + await testPerformance(client); + await testWidgetIntegration(client); + + console.log('\nπŸŽ‰ ALL SocialComment INTEGRATION TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Live system connection'); + console.log(' βœ… Command execution on real system'); + console.log(' βœ… Parameter validation'); + console.log(' βœ… Optional parameter handling'); + console.log(' βœ… Performance benchmarks'); + console.log(' βœ… Widget/Event integration'); + console.log('\nπŸ’‘ NOTE: This test uses the REAL running system'); + console.log(' - Real database operations'); + console.log(' - Real event propagation'); + console.log(' - Real widget updates'); + console.log(' - Real cross-daemon communication'); + + } catch (error) { + console.error('\n❌ SocialComment integration tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + console.error('\nπŸ’‘ Make sure:'); + console.error(' 1. Server is running: npm start'); + console.error(' 2. Wait 90+ seconds for deployment'); + console.error(' 3. Browser is connected to http://localhost:9003'); + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialCommentIntegrationTests(); +} else { + module.exports = { runAllSocialCommentIntegrationTests }; +} diff --git a/src/debug/jtag/commands/social/comment/test/unit/SocialCommentCommand.test.ts b/src/debug/jtag/commands/social/comment/test/unit/SocialCommentCommand.test.ts new file mode 100644 index 000000000..68f0a74ec --- /dev/null +++ b/src/debug/jtag/commands/social/comment/test/unit/SocialCommentCommand.test.ts @@ -0,0 +1,259 @@ +#!/usr/bin/env tsx +/** + * SocialComment Command Unit Tests + * + * Tests Social Comment command logic in isolation using mock dependencies. + * This is a REFERENCE EXAMPLE showing best practices for command testing. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Comment/test/unit/SocialCommentCommand.test.ts + * + * NOTE: This is a self-contained test (no external test utilities needed). + * Use this as a template for your own command tests. + */ + +// import { ValidationError } from '@system/core/types/ErrorTypes'; // Uncomment when adding validation tests +import { generateUUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialCommentParams, SocialCommentResult } from '../../shared/SocialCommentTypes'; + +console.log('πŸ§ͺ SocialComment Command Unit Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Mock command that implements Social Comment logic for testing + */ +async function mockSocialCommentCommand(params: SocialCommentParams): Promise { + // TODO: Validate required parameters (BEST PRACTICE) + // Example: + // if (!params.requiredParam || params.requiredParam.trim() === '') { + // throw new ValidationError( + // 'requiredParam', + // `Missing required parameter 'requiredParam'. ` + + // `Use the help tool with 'Social Comment' or see the Social Comment README for usage information.` + // ); + // } + + // TODO: Handle optional parameters with sensible defaults + // const optionalParam = params.optionalParam ?? defaultValue; + + // TODO: Implement your command logic here + return { + success: true, + // TODO: Add your result fields with actual computed values + context: params.context, + sessionId: params.sessionId + } as SocialCommentResult; +} + +/** + * Test 1: Command structure validation + */ +function testSocialCommentCommandStructure(): void { + console.log('\nπŸ“‹ Test 1: SocialComment command structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Create valid params for Social Comment command + const validParams: SocialCommentParams = { + // TODO: Add your required parameters here + context, + sessionId + }; + + // Validate param structure + assert(validParams.context !== undefined, 'Params have context'); + assert(validParams.sessionId !== undefined, 'Params have sessionId'); + // TODO: Add assertions for your specific parameters + // assert(typeof validParams.requiredParam === 'string', 'requiredParam is string'); +} + +/** + * Test 2: Mock command execution + */ +async function testMockSocialCommentExecution(): Promise { + console.log('\n⚑ Test 2: Mock Social Comment command execution'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test mock execution + const params: SocialCommentParams = { + // TODO: Add your parameters here + context, + sessionId + }; + + const result = await mockSocialCommentCommand(params); + + // Validate result structure + assert(result.success === true, 'Mock result shows success'); + // TODO: Add assertions for your result fields + // assert(typeof result.yourField === 'string', 'yourField is string'); +} + +/** + * Test 3: Required parameter validation (CRITICAL) + * + * This test ensures your command throws ValidationError + * when required parameters are missing (BEST PRACTICE) + */ +async function testSocialCommentRequiredParams(): Promise { + console.log('\n🚨 Test 3: Required parameter validation'); + + // TODO: Uncomment when implementing validation + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test cases that should throw ValidationError + // Example: + // const testCases = [ + // { params: {} as SocialCommentParams, desc: 'Missing requiredParam' }, + // { params: { requiredParam: '' } as SocialCommentParams, desc: 'Empty requiredParam' }, + // ]; + // + // for (const testCase of testCases) { + // try { + // await mockSocialCommentCommand({ ...testCase.params, context, sessionId }); + // throw new Error(`Should have thrown ValidationError for: ${testCase.desc}`); + // } catch (error) { + // if (error instanceof ValidationError) { + // assert(error.field === 'requiredParam', `ValidationError field is 'requiredParam' for: ${testCase.desc}`); + // assert(error.message.includes('required parameter'), `Error message mentions 'required parameter' for: ${testCase.desc}`); + // assert(error.message.includes('help tool'), `Error message is tool-agnostic for: ${testCase.desc}`); + // } else { + // throw error; // Re-throw if not ValidationError + // } + // } + // } + + console.log('βœ… All required parameter validations work correctly'); +} + +/** + * Test 4: Optional parameter handling + */ +async function testSocialCommentOptionalParams(): Promise { + console.log('\nπŸ”§ Test 4: Optional parameter handling'); + + // TODO: Uncomment when implementing optional param tests + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test WITHOUT optional param (should use default) + // const paramsWithoutOptional: SocialCommentParams = { + // requiredParam: 'test', + // context, + // sessionId + // }; + // + // const resultWithoutOptional = await mockSocialCommentCommand(paramsWithoutOptional); + // assert(resultWithoutOptional.success === true, 'Command succeeds without optional params'); + + // TODO: Test WITH optional param + // const paramsWithOptional: SocialCommentParams = { + // requiredParam: 'test', + // optionalParam: true, + // context, + // sessionId + // }; + // + // const resultWithOptional = await mockSocialCommentCommand(paramsWithOptional); + // assert(resultWithOptional.success === true, 'Command succeeds with optional params'); + + console.log('βœ… Optional parameter handling validated'); +} + +/** + * Test 5: Performance validation + */ +async function testSocialCommentPerformance(): Promise { + console.log('\n⚑ Test 5: SocialComment performance validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + const startTime = Date.now(); + + await mockSocialCommentCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialCommentParams); + + const executionTime = Date.now() - startTime; + + assert(executionTime < 100, `SocialComment completed in ${executionTime}ms (under 100ms limit)`); +} + +/** + * Test 6: Result structure validation + */ +async function testSocialCommentResultStructure(): Promise { + console.log('\nπŸ” Test 6: SocialComment result structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test various scenarios + const basicResult = await mockSocialCommentCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialCommentParams); + + assert(basicResult.success === true, 'Result has success field'); + // TODO: Add assertions for your result fields + // assert(typeof basicResult.yourField === 'string', 'Result has yourField (string)'); + assert(basicResult.context === context, 'Result includes context'); + assert(basicResult.sessionId === sessionId, 'Result includes sessionId'); + + console.log('βœ… All result structure validations pass'); +} + +/** + * Run all unit tests + */ +async function runAllSocialCommentUnitTests(): Promise { + console.log('πŸš€ Starting SocialComment Command Unit Tests\n'); + + try { + testSocialCommentCommandStructure(); + await testMockSocialCommentExecution(); + await testSocialCommentRequiredParams(); + await testSocialCommentOptionalParams(); + await testSocialCommentPerformance(); + await testSocialCommentResultStructure(); + + console.log('\nπŸŽ‰ ALL SocialComment UNIT TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Command structure and parameter validation'); + console.log(' βœ… Mock command execution patterns'); + console.log(' βœ… Required parameter validation (throws ValidationError)'); + console.log(' βœ… Optional parameter handling (sensible defaults)'); + console.log(' βœ… Performance requirements (< 100ms)'); + console.log(' βœ… Result structure validation'); + console.log('\nπŸ“ This is a REFERENCE EXAMPLE - use as a template for your commands!'); + console.log('πŸ’‘ TIP: Copy this test structure and modify for your command logic'); + + } catch (error) { + console.error('\n❌ SocialComment unit tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialCommentUnitTests(); +} else { + module.exports = { runAllSocialCommentUnitTests }; +} diff --git a/src/debug/jtag/commands/social/community/.npmignore b/src/debug/jtag/commands/social/community/.npmignore new file mode 100644 index 000000000..f74ad6b8a --- /dev/null +++ b/src/debug/jtag/commands/social/community/.npmignore @@ -0,0 +1,20 @@ +# Development files +.eslintrc* +tsconfig*.json +vitest.config.ts + +# Build artifacts +*.js.map +*.d.ts.map + +# IDE +.vscode/ +.idea/ + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db diff --git a/src/debug/jtag/commands/social/community/README.md b/src/debug/jtag/commands/social/community/README.md new file mode 100644 index 000000000..1d374d1b3 --- /dev/null +++ b/src/debug/jtag/commands/social/community/README.md @@ -0,0 +1,177 @@ +# Social Community Command + +Manage communities (submolts) β€” create, list, subscribe, unsubscribe, get info + +## Table of Contents + +- [Usage](#usage) + - [CLI Usage](#cli-usage) + - [Tool Usage](#tool-usage) +- [Parameters](#parameters) +- [Result](#result) +- [Examples](#examples) +- [Testing](#testing) + - [Unit Tests](#unit-tests) + - [Integration Tests](#integration-tests) +- [Getting Help](#getting-help) +- [Access Level](#access-level) +- [Implementation Notes](#implementation-notes) + +## Usage + +### CLI Usage + +From the command line using the jtag CLI: + +```bash +./jtag social/community --platform= --action= --name= --description= --personaId= +``` + +### Tool Usage + +From Persona tools or programmatic access using `Commands.execute()`: + +```typescript +import { Commands } from '@system/core/shared/Commands'; + +const result = await Commands.execute('social/community', { + // your parameters here +}); +``` + +## Parameters + +- **platform** (required): `string` - Platform (e.g., 'moltbook') +- **action** (required): `string` - Action: list, info, create, subscribe, unsubscribe +- **name** (required): `string` - Community name (required for info, create, subscribe, unsubscribe) +- **description** (required): `string` - Community description (for create) +- **personaId** (required): `string` - Persona user ID (auto-detected) + +## Result + +Returns `SocialCommunityResult` with: + +Returns CommandResult with: +- **success**: `boolean` - Whether the action succeeded +- **communities**: `object[]` - List of communities (for list action) +- **community**: `object` - Community info (for info/create actions) + +## Examples + +### List all communities + +```bash +./jtag social/community --platform=moltbook --action=list +``` + +**Expected result:** +{ success: true, communities: [...] } + +### Create a community + +```bash +./jtag social/community --platform=moltbook --action=create --name=continuum-devs --description='Continuum builders' +``` + +**Expected result:** +{ success: true, community: { name: 'continuum-devs' } } + +### Subscribe to a community + +```bash +./jtag social/community --platform=moltbook --action=subscribe --name=ai-development +``` + +**Expected result:** +{ success: true } + +## Getting Help + +### Using the Help Tool + +Get detailed usage information for this command: + +**CLI:** +```bash +./jtag help social/community +``` + +**Tool:** +```typescript +// Use your help tool with command name 'social/community' +``` + +### Using the README Tool + +Access this README programmatically: + +**CLI:** +```bash +./jtag readme social/community +``` + +**Tool:** +```typescript +// Use your readme tool with command name 'social/community' +``` + +## Testing + +### Unit Tests + +Test command logic in isolation using mock dependencies: + +```bash +# Run unit tests (no server required) +npx tsx commands/social/community/test/unit/SocialCommunityCommand.test.ts +``` + +**What's tested:** +- Command structure and parameter validation +- Mock command execution patterns +- Required parameter validation (throws ValidationError) +- Optional parameter handling (sensible defaults) +- Performance requirements +- Assertion utility helpers + +**TDD Workflow:** +1. Write/modify unit test first (test-driven development) +2. Run test, see it fail +3. Implement feature +4. Run test, see it pass +5. Refactor if needed + +### Integration Tests + +Test command with real client connections and system integration: + +```bash +# Prerequisites: Server must be running +npm start # Wait 90+ seconds for deployment + +# Run integration tests +npx tsx commands/social/community/test/integration/SocialCommunityIntegration.test.ts +``` + +**What's tested:** +- Client connection to live system +- Real command execution via WebSocket +- ValidationError handling for missing params +- Optional parameter defaults +- Performance under load +- Various parameter combinations + +**Best Practice:** +Run unit tests frequently during development (fast feedback). Run integration tests before committing (verify system integration). + +## Access Level + +**ai-safe** - Safe for AI personas to call autonomously + +## Implementation Notes + +- **Shared Logic**: Core business logic in `shared/SocialCommunityTypes.ts` +- **Browser**: Browser-specific implementation in `browser/SocialCommunityBrowserCommand.ts` +- **Server**: Server-specific implementation in `server/SocialCommunityServerCommand.ts` +- **Unit Tests**: Isolated testing in `test/unit/SocialCommunityCommand.test.ts` +- **Integration Tests**: System testing in `test/integration/SocialCommunityIntegration.test.ts` diff --git a/src/debug/jtag/commands/social/community/browser/SocialCommunityBrowserCommand.ts b/src/debug/jtag/commands/social/community/browser/SocialCommunityBrowserCommand.ts new file mode 100644 index 000000000..7b7999e10 --- /dev/null +++ b/src/debug/jtag/commands/social/community/browser/SocialCommunityBrowserCommand.ts @@ -0,0 +1,21 @@ +/** + * Social Community Command - Browser Implementation + * + * Manage communities (submolts) β€” create, list, subscribe, unsubscribe, get info + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { SocialCommunityParams, SocialCommunityResult } from '../shared/SocialCommunityTypes'; + +export class SocialCommunityBrowserCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/community', context, subpath, commander); + } + + async execute(params: SocialCommunityParams): Promise { + console.log('🌐 BROWSER: Delegating Social Community to server'); + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/social/community/package.json b/src/debug/jtag/commands/social/community/package.json new file mode 100644 index 000000000..3206f0dc8 --- /dev/null +++ b/src/debug/jtag/commands/social/community/package.json @@ -0,0 +1,35 @@ +{ + "name": "@jtag-commands/social/community", + "version": "1.0.0", + "description": "Manage communities (submolts) β€” create, list, subscribe, unsubscribe, get info", + "main": "server/SocialCommunityServerCommand.ts", + "types": "shared/SocialCommunityTypes.ts", + "scripts": { + "test": "npm run test:unit && npm run test:integration", + "test:unit": "npx vitest run test/unit/*.test.ts", + "test:integration": "npx tsx test/integration/SocialCommunityIntegration.test.ts", + "lint": "npx eslint **/*.ts", + "typecheck": "npx tsc --noEmit" + }, + "peerDependencies": { + "@jtag/core": "*" + }, + "files": [ + "shared/**/*.ts", + "browser/**/*.ts", + "server/**/*.ts", + "test/**/*.ts", + "README.md" + ], + "keywords": [ + "jtag", + "command", + "social/community" + ], + "license": "MIT", + "author": "", + "repository": { + "type": "git", + "url": "" + } +} diff --git a/src/debug/jtag/commands/social/community/server/SocialCommunityServerCommand.ts b/src/debug/jtag/commands/social/community/server/SocialCommunityServerCommand.ts new file mode 100644 index 000000000..4d8371228 --- /dev/null +++ b/src/debug/jtag/commands/social/community/server/SocialCommunityServerCommand.ts @@ -0,0 +1,187 @@ +/** + * Social Community Command - Server Implementation + * + * Manage communities (submolts) β€” create, list, subscribe, unsubscribe, get info + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { SocialCommunityParams, SocialCommunityResult } from '../shared/SocialCommunityTypes'; +import { createSocialCommunityResultFromParams } from '../shared/SocialCommunityTypes'; +import { loadSocialContext } from '@system/social/server/SocialCommandHelper'; +import type { ISocialMediaProvider } from '@system/social/shared/ISocialMediaProvider'; +import { Logger } from '@system/core/logging/Logger'; + +const log = Logger.create('social/community'); + +export class SocialCommunityServerCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/community', context, subpath, commander); + } + + async execute(params: SocialCommunityParams): Promise { + const { platform, action } = params; + + if (!platform) { + return createSocialCommunityResultFromParams(params, { + success: false, + message: 'platform is required', + }); + } + + if (!action) { + return createSocialCommunityResultFromParams(params, { + success: false, + message: 'action is required (list, info, create, subscribe, unsubscribe)', + }); + } + + try { + const ctx = await loadSocialContext(platform, params.personaId, params); + + switch (action) { + case 'list': + return await this.handleList(params, ctx.provider); + case 'info': + return await this.handleInfo(params, ctx.provider); + case 'create': + return await this.handleCreate(params, ctx.provider); + case 'subscribe': + return await this.handleSubscribe(params, ctx.provider); + case 'unsubscribe': + return await this.handleUnsubscribe(params, ctx.provider); + default: + return createSocialCommunityResultFromParams(params, { + success: false, + message: `Unknown action: ${action}. Valid actions: list, info, create, subscribe, unsubscribe`, + }); + } + } catch (error) { + return createSocialCommunityResultFromParams(params, { + success: false, + message: `Community action failed: ${String(error)}`, + }); + } + } + + private async handleList( + params: SocialCommunityParams, + provider: ISocialMediaProvider, + ): Promise { + log.info('Listing communities'); + const communities = await provider.listCommunities(); + + const summary = communities.length === 0 + ? 'No communities found' + : `${communities.length} communities:\n` + + communities.map(c => + ` m/${c.name} β€” ${c.description ?? 'No description'} (${c.memberCount ?? 0} members)` + ).join('\n'); + + return createSocialCommunityResultFromParams(params, { + success: true, + message: `Found ${communities.length} communities`, + summary, + communities, + }); + } + + private async handleInfo( + params: SocialCommunityParams, + provider: ISocialMediaProvider, + ): Promise { + if (!params.name) { + return createSocialCommunityResultFromParams(params, { + success: false, + message: 'name is required for info action', + }); + } + + // listCommunities and filter β€” no direct getCommunity in provider + const communities = await provider.listCommunities(); + const community = communities.find(c => c.name === params.name); + + if (!community) { + return createSocialCommunityResultFromParams(params, { + success: false, + message: `Community '${params.name}' not found`, + }); + } + + return createSocialCommunityResultFromParams(params, { + success: true, + message: `Community info: ${community.name}`, + summary: `m/${community.name} β€” ${community.description ?? 'No description'}\nMembers: ${community.memberCount ?? 'unknown'}`, + community, + }); + } + + private async handleCreate( + params: SocialCommunityParams, + provider: ISocialMediaProvider, + ): Promise { + if (!params.name) { + return createSocialCommunityResultFromParams(params, { + success: false, + message: 'name is required for create action', + }); + } + + log.info(`Creating community: ${params.name}`); + const community = await provider.createCommunity({ + name: params.name, + displayName: params.name, + description: params.description ?? '', + }); + + return createSocialCommunityResultFromParams(params, { + success: true, + message: `Created community m/${community.name}`, + summary: `Created m/${community.name} β€” ${community.description ?? params.description ?? ''}`, + community, + }); + } + + private async handleSubscribe( + params: SocialCommunityParams, + provider: ISocialMediaProvider, + ): Promise { + if (!params.name) { + return createSocialCommunityResultFromParams(params, { + success: false, + message: 'name is required for subscribe action', + }); + } + + log.info(`Subscribing to community: ${params.name}`); + await provider.subscribeToCommunity(params.name); + + return createSocialCommunityResultFromParams(params, { + success: true, + message: `Subscribed to m/${params.name}`, + summary: `Now subscribed to m/${params.name}`, + }); + } + + private async handleUnsubscribe( + params: SocialCommunityParams, + provider: ISocialMediaProvider, + ): Promise { + if (!params.name) { + return createSocialCommunityResultFromParams(params, { + success: false, + message: 'name is required for unsubscribe action', + }); + } + + log.info(`Unsubscribing from community: ${params.name}`); + await provider.unsubscribeFromCommunity(params.name); + + return createSocialCommunityResultFromParams(params, { + success: true, + message: `Unsubscribed from m/${params.name}`, + summary: `Unsubscribed from m/${params.name}`, + }); + } +} diff --git a/src/debug/jtag/commands/social/community/shared/SocialCommunityTypes.ts b/src/debug/jtag/commands/social/community/shared/SocialCommunityTypes.ts new file mode 100644 index 000000000..0514bea3f --- /dev/null +++ b/src/debug/jtag/commands/social/community/shared/SocialCommunityTypes.ts @@ -0,0 +1,56 @@ +/** + * Social Community Command - Shared Types + * + * Manage communities (submolts) β€” create, list, subscribe, unsubscribe, get info + */ + +import type { CommandParams, CommandResult, CommandInput, JTAGContext } from '@system/core/types/JTAGTypes'; +import { createPayload, transformPayload } from '@system/core/types/JTAGTypes'; +import { Commands } from '@system/core/shared/Commands'; +import type { JTAGError } from '@system/core/types/ErrorTypes'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialCommunity as SocialCommunityData } from '@system/social/shared/SocialMediaTypes'; + +export type CommunityAction = 'list' | 'info' | 'create' | 'subscribe' | 'unsubscribe'; + +export interface SocialCommunityParams extends CommandParams { + /** Platform (e.g., 'moltbook') */ + platform: string; + /** Action: list, info, create, subscribe, unsubscribe */ + action: CommunityAction; + /** Community name (required for info, create, subscribe, unsubscribe) */ + name?: string; + /** Community description (for create) */ + description?: string; + /** Persona user ID (auto-detected) */ + personaId?: UUID; +} + +export interface SocialCommunityResult extends CommandResult { + success: boolean; + message: string; + summary?: string; + /** List of communities (for list action) */ + communities?: SocialCommunityData[]; + /** Community info (for info/create actions) */ + community?: SocialCommunityData; + error?: JTAGError; +} + +export const createSocialCommunityParams = ( + context: JTAGContext, + sessionId: UUID, + data: Omit +): SocialCommunityParams => createPayload(context, sessionId, data); + +export const createSocialCommunityResultFromParams = ( + params: SocialCommunityParams, + differences: Omit +): SocialCommunityResult => transformPayload(params, differences); + +export const SocialCommunity = { + execute(params: CommandInput): Promise { + return Commands.execute('social/community', params as Partial); + }, + commandName: 'social/community' as const, +} as const; diff --git a/src/debug/jtag/commands/social/community/spec.json b/src/debug/jtag/commands/social/community/spec.json new file mode 100644 index 000000000..a335fd043 --- /dev/null +++ b/src/debug/jtag/commands/social/community/spec.json @@ -0,0 +1,71 @@ +{ + "name": "social/community", + "description": "Manage communities (submolts) β€” create, list, subscribe, unsubscribe, get info", + "params": [ + { + "name": "platform", + "type": "string", + "required": true, + "description": "Platform (e.g., 'moltbook')" + }, + { + "name": "action", + "type": "string", + "required": true, + "description": "Action: list, info, create, subscribe, unsubscribe" + }, + { + "name": "name", + "type": "string", + "required": false, + "description": "Community name (required for info, create, subscribe, unsubscribe)" + }, + { + "name": "description", + "type": "string", + "required": false, + "description": "Community description (for create)" + }, + { + "name": "personaId", + "type": "string", + "required": false, + "description": "Persona user ID (auto-detected)" + } + ], + "results": [ + { + "name": "success", + "type": "boolean", + "description": "Whether the action succeeded" + }, + { + "name": "communities", + "type": "object[]", + "description": "List of communities (for list action)" + }, + { + "name": "community", + "type": "object", + "description": "Community info (for info/create actions)" + } + ], + "examples": [ + { + "description": "List all communities", + "command": "./jtag social/community --platform=moltbook --action=list", + "expectedResult": "{ success: true, communities: [...] }" + }, + { + "description": "Create a community", + "command": "./jtag social/community --platform=moltbook --action=create --name=continuum-devs --description='Continuum builders'", + "expectedResult": "{ success: true, community: { name: 'continuum-devs' } }" + }, + { + "description": "Subscribe to a community", + "command": "./jtag social/community --platform=moltbook --action=subscribe --name=ai-development", + "expectedResult": "{ success: true }" + } + ], + "accessLevel": "ai-safe" +} diff --git a/src/debug/jtag/commands/social/community/test/integration/SocialCommunityIntegration.test.ts b/src/debug/jtag/commands/social/community/test/integration/SocialCommunityIntegration.test.ts new file mode 100644 index 000000000..d1b66371d --- /dev/null +++ b/src/debug/jtag/commands/social/community/test/integration/SocialCommunityIntegration.test.ts @@ -0,0 +1,196 @@ +#!/usr/bin/env tsx +/** + * SocialCommunity Command Integration Tests + * + * Tests Social Community command against the LIVE RUNNING SYSTEM. + * This is NOT a mock test - it tests real commands, real events, real widgets. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Community/test/integration/SocialCommunityIntegration.test.ts + * + * PREREQUISITES: + * - Server must be running: npm start (wait 90+ seconds) + * - Browser client connected via http://localhost:9003 + */ + +import { jtag } from '@server/server-index'; + +console.log('πŸ§ͺ SocialCommunity Command Integration Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Test 1: Connect to live system + */ +async function testSystemConnection(): Promise>> { + console.log('\nπŸ”Œ Test 1: Connecting to live JTAG system'); + + const client = await jtag.connect(); + + assert(client !== null, 'Connected to live system'); + console.log(' βœ… Connected successfully'); + + return client; +} + +/** + * Test 2: Execute Social Community command on live system + */ +async function testCommandExecution(client: Awaited>): Promise { + console.log('\n⚑ Test 2: Executing Social Community command'); + + // TODO: Replace with your actual command parameters + const result = await client.commands['Social Community']({ + // Add your required parameters here + // Example: name: 'test-value' + }); + + console.log(' πŸ“Š Result:', JSON.stringify(result, null, 2)); + + assert(result !== null, 'Social Community returned result'); + // TODO: Add assertions for your specific result fields + // assert(result.success === true, 'Social Community succeeded'); + // assert(result.yourField !== undefined, 'Result has yourField'); +} + +/** + * Test 3: Validate required parameters + */ +async function testRequiredParameters(_client: Awaited>): Promise { + console.log('\n🚨 Test 3: Testing required parameter validation'); + + // TODO: Uncomment and test missing required parameters + // try { + // await _client.commands['Social Community']({ + // // Missing required param + // }); + // assert(false, 'Should have thrown validation error'); + // } catch (error) { + // assert((error as Error).message.includes('required'), 'Error mentions required parameter'); + // console.log(' βœ… ValidationError thrown correctly'); + // } + + console.log(' ⚠️ TODO: Add required parameter validation test'); +} + +/** + * Test 4: Test optional parameters + */ +async function testOptionalParameters(_client: Awaited>): Promise { + console.log('\nπŸ”§ Test 4: Testing optional parameters'); + + // TODO: Uncomment to test with and without optional parameters + // const withOptional = await client.commands['Social Community']({ + // requiredParam: 'test', + // optionalParam: true + // }); + // + // const withoutOptional = await client.commands['Social Community']({ + // requiredParam: 'test' + // }); + // + // assert(withOptional.success === true, 'Works with optional params'); + // assert(withoutOptional.success === true, 'Works without optional params'); + + console.log(' ⚠️ TODO: Add optional parameter tests'); +} + +/** + * Test 5: Performance test + */ +async function testPerformance(_client: Awaited>): Promise { + console.log('\n⚑ Test 5: Performance under load'); + + // TODO: Uncomment to test command performance + // const iterations = 10; + // const times: number[] = []; + // + // for (let i = 0; i < iterations; i++) { + // const start = Date.now(); + // await _client.commands['Social Community']({ /* params */ }); + // times.push(Date.now() - start); + // } + // + // const avg = times.reduce((a, b) => a + b, 0) / iterations; + // const max = Math.max(...times); + // + // console.log(` Average: ${avg.toFixed(2)}ms`); + // console.log(` Max: ${max}ms`); + // + // assert(avg < 500, `Average ${avg.toFixed(2)}ms under 500ms`); + // assert(max < 1000, `Max ${max}ms under 1000ms`); + + console.log(' ⚠️ TODO: Add performance test'); +} + +/** + * Test 6: Widget/Event integration (if applicable) + */ +async function testWidgetIntegration(_client: Awaited>): Promise { + console.log('\n🎨 Test 6: Widget/Event integration'); + + // TODO: Uncomment if your command emits events or updates widgets + // Example: + // const before = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // await client.commands['Social Community']({ /* params */ }); + // await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for event propagation + // const after = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // + // assert(after.state.someValue !== before.state.someValue, 'Widget state updated'); + + console.log(' ⚠️ TODO: Add widget/event integration test (if applicable)'); +} + +/** + * Run all integration tests + */ +async function runAllSocialCommunityIntegrationTests(): Promise { + console.log('πŸš€ Starting SocialCommunity Integration Tests\n'); + console.log('πŸ“‹ Testing against LIVE system (not mocks)\n'); + + try { + const client = await testSystemConnection(); + await testCommandExecution(client); + await testRequiredParameters(client); + await testOptionalParameters(client); + await testPerformance(client); + await testWidgetIntegration(client); + + console.log('\nπŸŽ‰ ALL SocialCommunity INTEGRATION TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Live system connection'); + console.log(' βœ… Command execution on real system'); + console.log(' βœ… Parameter validation'); + console.log(' βœ… Optional parameter handling'); + console.log(' βœ… Performance benchmarks'); + console.log(' βœ… Widget/Event integration'); + console.log('\nπŸ’‘ NOTE: This test uses the REAL running system'); + console.log(' - Real database operations'); + console.log(' - Real event propagation'); + console.log(' - Real widget updates'); + console.log(' - Real cross-daemon communication'); + + } catch (error) { + console.error('\n❌ SocialCommunity integration tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + console.error('\nπŸ’‘ Make sure:'); + console.error(' 1. Server is running: npm start'); + console.error(' 2. Wait 90+ seconds for deployment'); + console.error(' 3. Browser is connected to http://localhost:9003'); + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialCommunityIntegrationTests(); +} else { + module.exports = { runAllSocialCommunityIntegrationTests }; +} diff --git a/src/debug/jtag/commands/social/community/test/unit/SocialCommunityCommand.test.ts b/src/debug/jtag/commands/social/community/test/unit/SocialCommunityCommand.test.ts new file mode 100644 index 000000000..063254290 --- /dev/null +++ b/src/debug/jtag/commands/social/community/test/unit/SocialCommunityCommand.test.ts @@ -0,0 +1,259 @@ +#!/usr/bin/env tsx +/** + * SocialCommunity Command Unit Tests + * + * Tests Social Community command logic in isolation using mock dependencies. + * This is a REFERENCE EXAMPLE showing best practices for command testing. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Community/test/unit/SocialCommunityCommand.test.ts + * + * NOTE: This is a self-contained test (no external test utilities needed). + * Use this as a template for your own command tests. + */ + +// import { ValidationError } from '@system/core/types/ErrorTypes'; // Uncomment when adding validation tests +import { generateUUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialCommunityParams, SocialCommunityResult } from '../../shared/SocialCommunityTypes'; + +console.log('πŸ§ͺ SocialCommunity Command Unit Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Mock command that implements Social Community logic for testing + */ +async function mockSocialCommunityCommand(params: SocialCommunityParams): Promise { + // TODO: Validate required parameters (BEST PRACTICE) + // Example: + // if (!params.requiredParam || params.requiredParam.trim() === '') { + // throw new ValidationError( + // 'requiredParam', + // `Missing required parameter 'requiredParam'. ` + + // `Use the help tool with 'Social Community' or see the Social Community README for usage information.` + // ); + // } + + // TODO: Handle optional parameters with sensible defaults + // const optionalParam = params.optionalParam ?? defaultValue; + + // TODO: Implement your command logic here + return { + success: true, + // TODO: Add your result fields with actual computed values + context: params.context, + sessionId: params.sessionId + } as SocialCommunityResult; +} + +/** + * Test 1: Command structure validation + */ +function testSocialCommunityCommandStructure(): void { + console.log('\nπŸ“‹ Test 1: SocialCommunity command structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Create valid params for Social Community command + const validParams: SocialCommunityParams = { + // TODO: Add your required parameters here + context, + sessionId + }; + + // Validate param structure + assert(validParams.context !== undefined, 'Params have context'); + assert(validParams.sessionId !== undefined, 'Params have sessionId'); + // TODO: Add assertions for your specific parameters + // assert(typeof validParams.requiredParam === 'string', 'requiredParam is string'); +} + +/** + * Test 2: Mock command execution + */ +async function testMockSocialCommunityExecution(): Promise { + console.log('\n⚑ Test 2: Mock Social Community command execution'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test mock execution + const params: SocialCommunityParams = { + // TODO: Add your parameters here + context, + sessionId + }; + + const result = await mockSocialCommunityCommand(params); + + // Validate result structure + assert(result.success === true, 'Mock result shows success'); + // TODO: Add assertions for your result fields + // assert(typeof result.yourField === 'string', 'yourField is string'); +} + +/** + * Test 3: Required parameter validation (CRITICAL) + * + * This test ensures your command throws ValidationError + * when required parameters are missing (BEST PRACTICE) + */ +async function testSocialCommunityRequiredParams(): Promise { + console.log('\n🚨 Test 3: Required parameter validation'); + + // TODO: Uncomment when implementing validation + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test cases that should throw ValidationError + // Example: + // const testCases = [ + // { params: {} as SocialCommunityParams, desc: 'Missing requiredParam' }, + // { params: { requiredParam: '' } as SocialCommunityParams, desc: 'Empty requiredParam' }, + // ]; + // + // for (const testCase of testCases) { + // try { + // await mockSocialCommunityCommand({ ...testCase.params, context, sessionId }); + // throw new Error(`Should have thrown ValidationError for: ${testCase.desc}`); + // } catch (error) { + // if (error instanceof ValidationError) { + // assert(error.field === 'requiredParam', `ValidationError field is 'requiredParam' for: ${testCase.desc}`); + // assert(error.message.includes('required parameter'), `Error message mentions 'required parameter' for: ${testCase.desc}`); + // assert(error.message.includes('help tool'), `Error message is tool-agnostic for: ${testCase.desc}`); + // } else { + // throw error; // Re-throw if not ValidationError + // } + // } + // } + + console.log('βœ… All required parameter validations work correctly'); +} + +/** + * Test 4: Optional parameter handling + */ +async function testSocialCommunityOptionalParams(): Promise { + console.log('\nπŸ”§ Test 4: Optional parameter handling'); + + // TODO: Uncomment when implementing optional param tests + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test WITHOUT optional param (should use default) + // const paramsWithoutOptional: SocialCommunityParams = { + // requiredParam: 'test', + // context, + // sessionId + // }; + // + // const resultWithoutOptional = await mockSocialCommunityCommand(paramsWithoutOptional); + // assert(resultWithoutOptional.success === true, 'Command succeeds without optional params'); + + // TODO: Test WITH optional param + // const paramsWithOptional: SocialCommunityParams = { + // requiredParam: 'test', + // optionalParam: true, + // context, + // sessionId + // }; + // + // const resultWithOptional = await mockSocialCommunityCommand(paramsWithOptional); + // assert(resultWithOptional.success === true, 'Command succeeds with optional params'); + + console.log('βœ… Optional parameter handling validated'); +} + +/** + * Test 5: Performance validation + */ +async function testSocialCommunityPerformance(): Promise { + console.log('\n⚑ Test 5: SocialCommunity performance validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + const startTime = Date.now(); + + await mockSocialCommunityCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialCommunityParams); + + const executionTime = Date.now() - startTime; + + assert(executionTime < 100, `SocialCommunity completed in ${executionTime}ms (under 100ms limit)`); +} + +/** + * Test 6: Result structure validation + */ +async function testSocialCommunityResultStructure(): Promise { + console.log('\nπŸ” Test 6: SocialCommunity result structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test various scenarios + const basicResult = await mockSocialCommunityCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialCommunityParams); + + assert(basicResult.success === true, 'Result has success field'); + // TODO: Add assertions for your result fields + // assert(typeof basicResult.yourField === 'string', 'Result has yourField (string)'); + assert(basicResult.context === context, 'Result includes context'); + assert(basicResult.sessionId === sessionId, 'Result includes sessionId'); + + console.log('βœ… All result structure validations pass'); +} + +/** + * Run all unit tests + */ +async function runAllSocialCommunityUnitTests(): Promise { + console.log('πŸš€ Starting SocialCommunity Command Unit Tests\n'); + + try { + testSocialCommunityCommandStructure(); + await testMockSocialCommunityExecution(); + await testSocialCommunityRequiredParams(); + await testSocialCommunityOptionalParams(); + await testSocialCommunityPerformance(); + await testSocialCommunityResultStructure(); + + console.log('\nπŸŽ‰ ALL SocialCommunity UNIT TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Command structure and parameter validation'); + console.log(' βœ… Mock command execution patterns'); + console.log(' βœ… Required parameter validation (throws ValidationError)'); + console.log(' βœ… Optional parameter handling (sensible defaults)'); + console.log(' βœ… Performance requirements (< 100ms)'); + console.log(' βœ… Result structure validation'); + console.log('\nπŸ“ This is a REFERENCE EXAMPLE - use as a template for your commands!'); + console.log('πŸ’‘ TIP: Copy this test structure and modify for your command logic'); + + } catch (error) { + console.error('\n❌ SocialCommunity unit tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialCommunityUnitTests(); +} else { + module.exports = { runAllSocialCommunityUnitTests }; +} diff --git a/src/debug/jtag/commands/social/downvote/.npmignore b/src/debug/jtag/commands/social/downvote/.npmignore new file mode 100644 index 000000000..f74ad6b8a --- /dev/null +++ b/src/debug/jtag/commands/social/downvote/.npmignore @@ -0,0 +1,20 @@ +# Development files +.eslintrc* +tsconfig*.json +vitest.config.ts + +# Build artifacts +*.js.map +*.d.ts.map + +# IDE +.vscode/ +.idea/ + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db diff --git a/src/debug/jtag/commands/social/downvote/README.md b/src/debug/jtag/commands/social/downvote/README.md new file mode 100644 index 000000000..a1138c253 --- /dev/null +++ b/src/debug/jtag/commands/social/downvote/README.md @@ -0,0 +1,156 @@ +# Social Downvote Command + +Downvote a post on a social media platform + +## Table of Contents + +- [Usage](#usage) + - [CLI Usage](#cli-usage) + - [Tool Usage](#tool-usage) +- [Parameters](#parameters) +- [Result](#result) +- [Examples](#examples) +- [Testing](#testing) + - [Unit Tests](#unit-tests) + - [Integration Tests](#integration-tests) +- [Getting Help](#getting-help) +- [Access Level](#access-level) +- [Implementation Notes](#implementation-notes) + +## Usage + +### CLI Usage + +From the command line using the jtag CLI: + +```bash +./jtag social/downvote --platform= --postId= --personaId= +``` + +### Tool Usage + +From Persona tools or programmatic access using `Commands.execute()`: + +```typescript +import { Commands } from '@system/core/shared/Commands'; + +const result = await Commands.execute('social/downvote', { + // your parameters here +}); +``` + +## Parameters + +- **platform** (required): `string` - Platform (e.g., 'moltbook') +- **postId** (required): `string` - Post ID to downvote +- **personaId** (required): `string` - Persona user ID (auto-detected) + +## Result + +Returns `SocialDownvoteResult` with: + +Returns CommandResult with: +- **success**: `boolean` - Whether the downvote was successful +- **postId**: `string` - The post that was downvoted + +## Examples + +### Downvote a spam post + +```bash +./jtag social/downvote --platform=moltbook --postId=abc123 +``` + +**Expected result:** +{ success: true, postId: 'abc123' } + +## Getting Help + +### Using the Help Tool + +Get detailed usage information for this command: + +**CLI:** +```bash +./jtag help social/downvote +``` + +**Tool:** +```typescript +// Use your help tool with command name 'social/downvote' +``` + +### Using the README Tool + +Access this README programmatically: + +**CLI:** +```bash +./jtag readme social/downvote +``` + +**Tool:** +```typescript +// Use your readme tool with command name 'social/downvote' +``` + +## Testing + +### Unit Tests + +Test command logic in isolation using mock dependencies: + +```bash +# Run unit tests (no server required) +npx tsx commands/social/downvote/test/unit/SocialDownvoteCommand.test.ts +``` + +**What's tested:** +- Command structure and parameter validation +- Mock command execution patterns +- Required parameter validation (throws ValidationError) +- Optional parameter handling (sensible defaults) +- Performance requirements +- Assertion utility helpers + +**TDD Workflow:** +1. Write/modify unit test first (test-driven development) +2. Run test, see it fail +3. Implement feature +4. Run test, see it pass +5. Refactor if needed + +### Integration Tests + +Test command with real client connections and system integration: + +```bash +# Prerequisites: Server must be running +npm start # Wait 90+ seconds for deployment + +# Run integration tests +npx tsx commands/social/downvote/test/integration/SocialDownvoteIntegration.test.ts +``` + +**What's tested:** +- Client connection to live system +- Real command execution via WebSocket +- ValidationError handling for missing params +- Optional parameter defaults +- Performance under load +- Various parameter combinations + +**Best Practice:** +Run unit tests frequently during development (fast feedback). Run integration tests before committing (verify system integration). + +## Access Level + +**ai-safe** - Safe for AI personas to call autonomously + +## Implementation Notes + +- **Shared Logic**: Core business logic in `shared/SocialDownvoteTypes.ts` +- **Browser**: Browser-specific implementation in `browser/SocialDownvoteBrowserCommand.ts` +- **Server**: Server-specific implementation in `server/SocialDownvoteServerCommand.ts` +- **Unit Tests**: Isolated testing in `test/unit/SocialDownvoteCommand.test.ts` +- **Integration Tests**: System testing in `test/integration/SocialDownvoteIntegration.test.ts` diff --git a/src/debug/jtag/commands/social/downvote/browser/SocialDownvoteBrowserCommand.ts b/src/debug/jtag/commands/social/downvote/browser/SocialDownvoteBrowserCommand.ts new file mode 100644 index 000000000..fc0b86ef0 --- /dev/null +++ b/src/debug/jtag/commands/social/downvote/browser/SocialDownvoteBrowserCommand.ts @@ -0,0 +1,21 @@ +/** + * Social Downvote Command - Browser Implementation + * + * Downvote a post on a social media platform + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { SocialDownvoteParams, SocialDownvoteResult } from '../shared/SocialDownvoteTypes'; + +export class SocialDownvoteBrowserCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/downvote', context, subpath, commander); + } + + async execute(params: SocialDownvoteParams): Promise { + console.log('🌐 BROWSER: Delegating Social Downvote to server'); + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/social/downvote/package.json b/src/debug/jtag/commands/social/downvote/package.json new file mode 100644 index 000000000..674b3fc40 --- /dev/null +++ b/src/debug/jtag/commands/social/downvote/package.json @@ -0,0 +1,35 @@ +{ + "name": "@jtag-commands/social/downvote", + "version": "1.0.0", + "description": "Downvote a post on a social media platform", + "main": "server/SocialDownvoteServerCommand.ts", + "types": "shared/SocialDownvoteTypes.ts", + "scripts": { + "test": "npm run test:unit && npm run test:integration", + "test:unit": "npx vitest run test/unit/*.test.ts", + "test:integration": "npx tsx test/integration/SocialDownvoteIntegration.test.ts", + "lint": "npx eslint **/*.ts", + "typecheck": "npx tsc --noEmit" + }, + "peerDependencies": { + "@jtag/core": "*" + }, + "files": [ + "shared/**/*.ts", + "browser/**/*.ts", + "server/**/*.ts", + "test/**/*.ts", + "README.md" + ], + "keywords": [ + "jtag", + "command", + "social/downvote" + ], + "license": "MIT", + "author": "", + "repository": { + "type": "git", + "url": "" + } +} diff --git a/src/debug/jtag/commands/social/downvote/server/SocialDownvoteServerCommand.ts b/src/debug/jtag/commands/social/downvote/server/SocialDownvoteServerCommand.ts new file mode 100644 index 000000000..d0341dd09 --- /dev/null +++ b/src/debug/jtag/commands/social/downvote/server/SocialDownvoteServerCommand.ts @@ -0,0 +1,61 @@ +/** + * Social Downvote Command - Server Implementation + * + * Downvote a post on a social media platform. + * Convenience command β€” delegates to provider.vote() with direction='down'. + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { SocialDownvoteParams, SocialDownvoteResult } from '../shared/SocialDownvoteTypes'; +import { createSocialDownvoteResultFromParams } from '../shared/SocialDownvoteTypes'; +import { loadSocialContext } from '@system/social/server/SocialCommandHelper'; +import { Logger } from '@system/core/logging/Logger'; + +const log = Logger.create('social/downvote'); + +export class SocialDownvoteServerCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/downvote', context, subpath, commander); + } + + async execute(params: SocialDownvoteParams): Promise { + const { platform, postId } = params; + + if (!platform) { + return createSocialDownvoteResultFromParams(params, { + success: false, + message: 'platform is required', + postId: '', + }); + } + + if (!postId) { + return createSocialDownvoteResultFromParams(params, { + success: false, + message: 'postId is required', + postId: '', + }); + } + + try { + const ctx = await loadSocialContext(platform, params.personaId, params); + + log.info(`Downvoting post: ${postId}`); + await ctx.provider.vote({ targetId: postId, targetType: 'post', direction: 'down' }); + + return createSocialDownvoteResultFromParams(params, { + success: true, + message: `Downvoted post ${postId}`, + postId, + }); + } catch (error) { + return createSocialDownvoteResultFromParams(params, { + success: false, + message: `Downvote failed: ${String(error)}`, + postId, + }); + } + } +} diff --git a/src/debug/jtag/commands/social/downvote/shared/SocialDownvoteTypes.ts b/src/debug/jtag/commands/social/downvote/shared/SocialDownvoteTypes.ts new file mode 100644 index 000000000..c33210120 --- /dev/null +++ b/src/debug/jtag/commands/social/downvote/shared/SocialDownvoteTypes.ts @@ -0,0 +1,47 @@ +/** + * Social Downvote Command - Shared Types + * + * Downvote a post on a social media platform. + * Convenience command β€” delegates to provider.vote() with direction='down'. + */ + +import type { CommandParams, CommandResult, CommandInput, JTAGContext } from '@system/core/types/JTAGTypes'; +import { createPayload, transformPayload } from '@system/core/types/JTAGTypes'; +import { Commands } from '@system/core/shared/Commands'; +import type { JTAGError } from '@system/core/types/ErrorTypes'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; + +export interface SocialDownvoteParams extends CommandParams { + /** Platform (e.g., 'moltbook') */ + platform: string; + /** Post ID to downvote */ + postId: string; + /** Persona user ID (auto-detected) */ + personaId?: UUID; +} + +export interface SocialDownvoteResult extends CommandResult { + success: boolean; + message: string; + /** The post that was downvoted */ + postId: string; + error?: JTAGError; +} + +export const createSocialDownvoteParams = ( + context: JTAGContext, + sessionId: UUID, + data: Omit +): SocialDownvoteParams => createPayload(context, sessionId, data); + +export const createSocialDownvoteResultFromParams = ( + params: SocialDownvoteParams, + differences: Omit +): SocialDownvoteResult => transformPayload(params, differences); + +export const SocialDownvote = { + execute(params: CommandInput): Promise { + return Commands.execute('social/downvote', params as Partial); + }, + commandName: 'social/downvote' as const, +} as const; diff --git a/src/debug/jtag/commands/social/downvote/spec.json b/src/debug/jtag/commands/social/downvote/spec.json new file mode 100644 index 000000000..2b9eb0ce4 --- /dev/null +++ b/src/debug/jtag/commands/social/downvote/spec.json @@ -0,0 +1,44 @@ +{ + "name": "social/downvote", + "description": "Downvote a post on a social media platform", + "params": [ + { + "name": "platform", + "type": "string", + "required": true, + "description": "Platform (e.g., 'moltbook')" + }, + { + "name": "postId", + "type": "string", + "required": true, + "description": "Post ID to downvote" + }, + { + "name": "personaId", + "type": "string", + "required": false, + "description": "Persona user ID (auto-detected)" + } + ], + "results": [ + { + "name": "success", + "type": "boolean", + "description": "Whether the downvote was successful" + }, + { + "name": "postId", + "type": "string", + "description": "The post that was downvoted" + } + ], + "examples": [ + { + "description": "Downvote a spam post", + "command": "./jtag social/downvote --platform=moltbook --postId=abc123", + "expectedResult": "{ success: true, postId: 'abc123' }" + } + ], + "accessLevel": "ai-safe" +} diff --git a/src/debug/jtag/commands/social/downvote/test/integration/SocialDownvoteIntegration.test.ts b/src/debug/jtag/commands/social/downvote/test/integration/SocialDownvoteIntegration.test.ts new file mode 100644 index 000000000..76e81cfc6 --- /dev/null +++ b/src/debug/jtag/commands/social/downvote/test/integration/SocialDownvoteIntegration.test.ts @@ -0,0 +1,196 @@ +#!/usr/bin/env tsx +/** + * SocialDownvote Command Integration Tests + * + * Tests Social Downvote command against the LIVE RUNNING SYSTEM. + * This is NOT a mock test - it tests real commands, real events, real widgets. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Downvote/test/integration/SocialDownvoteIntegration.test.ts + * + * PREREQUISITES: + * - Server must be running: npm start (wait 90+ seconds) + * - Browser client connected via http://localhost:9003 + */ + +import { jtag } from '@server/server-index'; + +console.log('πŸ§ͺ SocialDownvote Command Integration Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Test 1: Connect to live system + */ +async function testSystemConnection(): Promise>> { + console.log('\nπŸ”Œ Test 1: Connecting to live JTAG system'); + + const client = await jtag.connect(); + + assert(client !== null, 'Connected to live system'); + console.log(' βœ… Connected successfully'); + + return client; +} + +/** + * Test 2: Execute Social Downvote command on live system + */ +async function testCommandExecution(client: Awaited>): Promise { + console.log('\n⚑ Test 2: Executing Social Downvote command'); + + // TODO: Replace with your actual command parameters + const result = await client.commands['Social Downvote']({ + // Add your required parameters here + // Example: name: 'test-value' + }); + + console.log(' πŸ“Š Result:', JSON.stringify(result, null, 2)); + + assert(result !== null, 'Social Downvote returned result'); + // TODO: Add assertions for your specific result fields + // assert(result.success === true, 'Social Downvote succeeded'); + // assert(result.yourField !== undefined, 'Result has yourField'); +} + +/** + * Test 3: Validate required parameters + */ +async function testRequiredParameters(_client: Awaited>): Promise { + console.log('\n🚨 Test 3: Testing required parameter validation'); + + // TODO: Uncomment and test missing required parameters + // try { + // await _client.commands['Social Downvote']({ + // // Missing required param + // }); + // assert(false, 'Should have thrown validation error'); + // } catch (error) { + // assert((error as Error).message.includes('required'), 'Error mentions required parameter'); + // console.log(' βœ… ValidationError thrown correctly'); + // } + + console.log(' ⚠️ TODO: Add required parameter validation test'); +} + +/** + * Test 4: Test optional parameters + */ +async function testOptionalParameters(_client: Awaited>): Promise { + console.log('\nπŸ”§ Test 4: Testing optional parameters'); + + // TODO: Uncomment to test with and without optional parameters + // const withOptional = await client.commands['Social Downvote']({ + // requiredParam: 'test', + // optionalParam: true + // }); + // + // const withoutOptional = await client.commands['Social Downvote']({ + // requiredParam: 'test' + // }); + // + // assert(withOptional.success === true, 'Works with optional params'); + // assert(withoutOptional.success === true, 'Works without optional params'); + + console.log(' ⚠️ TODO: Add optional parameter tests'); +} + +/** + * Test 5: Performance test + */ +async function testPerformance(_client: Awaited>): Promise { + console.log('\n⚑ Test 5: Performance under load'); + + // TODO: Uncomment to test command performance + // const iterations = 10; + // const times: number[] = []; + // + // for (let i = 0; i < iterations; i++) { + // const start = Date.now(); + // await _client.commands['Social Downvote']({ /* params */ }); + // times.push(Date.now() - start); + // } + // + // const avg = times.reduce((a, b) => a + b, 0) / iterations; + // const max = Math.max(...times); + // + // console.log(` Average: ${avg.toFixed(2)}ms`); + // console.log(` Max: ${max}ms`); + // + // assert(avg < 500, `Average ${avg.toFixed(2)}ms under 500ms`); + // assert(max < 1000, `Max ${max}ms under 1000ms`); + + console.log(' ⚠️ TODO: Add performance test'); +} + +/** + * Test 6: Widget/Event integration (if applicable) + */ +async function testWidgetIntegration(_client: Awaited>): Promise { + console.log('\n🎨 Test 6: Widget/Event integration'); + + // TODO: Uncomment if your command emits events or updates widgets + // Example: + // const before = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // await client.commands['Social Downvote']({ /* params */ }); + // await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for event propagation + // const after = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // + // assert(after.state.someValue !== before.state.someValue, 'Widget state updated'); + + console.log(' ⚠️ TODO: Add widget/event integration test (if applicable)'); +} + +/** + * Run all integration tests + */ +async function runAllSocialDownvoteIntegrationTests(): Promise { + console.log('πŸš€ Starting SocialDownvote Integration Tests\n'); + console.log('πŸ“‹ Testing against LIVE system (not mocks)\n'); + + try { + const client = await testSystemConnection(); + await testCommandExecution(client); + await testRequiredParameters(client); + await testOptionalParameters(client); + await testPerformance(client); + await testWidgetIntegration(client); + + console.log('\nπŸŽ‰ ALL SocialDownvote INTEGRATION TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Live system connection'); + console.log(' βœ… Command execution on real system'); + console.log(' βœ… Parameter validation'); + console.log(' βœ… Optional parameter handling'); + console.log(' βœ… Performance benchmarks'); + console.log(' βœ… Widget/Event integration'); + console.log('\nπŸ’‘ NOTE: This test uses the REAL running system'); + console.log(' - Real database operations'); + console.log(' - Real event propagation'); + console.log(' - Real widget updates'); + console.log(' - Real cross-daemon communication'); + + } catch (error) { + console.error('\n❌ SocialDownvote integration tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + console.error('\nπŸ’‘ Make sure:'); + console.error(' 1. Server is running: npm start'); + console.error(' 2. Wait 90+ seconds for deployment'); + console.error(' 3. Browser is connected to http://localhost:9003'); + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialDownvoteIntegrationTests(); +} else { + module.exports = { runAllSocialDownvoteIntegrationTests }; +} diff --git a/src/debug/jtag/commands/social/downvote/test/unit/SocialDownvoteCommand.test.ts b/src/debug/jtag/commands/social/downvote/test/unit/SocialDownvoteCommand.test.ts new file mode 100644 index 000000000..dad74d16b --- /dev/null +++ b/src/debug/jtag/commands/social/downvote/test/unit/SocialDownvoteCommand.test.ts @@ -0,0 +1,259 @@ +#!/usr/bin/env tsx +/** + * SocialDownvote Command Unit Tests + * + * Tests Social Downvote command logic in isolation using mock dependencies. + * This is a REFERENCE EXAMPLE showing best practices for command testing. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Downvote/test/unit/SocialDownvoteCommand.test.ts + * + * NOTE: This is a self-contained test (no external test utilities needed). + * Use this as a template for your own command tests. + */ + +// import { ValidationError } from '@system/core/types/ErrorTypes'; // Uncomment when adding validation tests +import { generateUUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialDownvoteParams, SocialDownvoteResult } from '../../shared/SocialDownvoteTypes'; + +console.log('πŸ§ͺ SocialDownvote Command Unit Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Mock command that implements Social Downvote logic for testing + */ +async function mockSocialDownvoteCommand(params: SocialDownvoteParams): Promise { + // TODO: Validate required parameters (BEST PRACTICE) + // Example: + // if (!params.requiredParam || params.requiredParam.trim() === '') { + // throw new ValidationError( + // 'requiredParam', + // `Missing required parameter 'requiredParam'. ` + + // `Use the help tool with 'Social Downvote' or see the Social Downvote README for usage information.` + // ); + // } + + // TODO: Handle optional parameters with sensible defaults + // const optionalParam = params.optionalParam ?? defaultValue; + + // TODO: Implement your command logic here + return { + success: true, + // TODO: Add your result fields with actual computed values + context: params.context, + sessionId: params.sessionId + } as SocialDownvoteResult; +} + +/** + * Test 1: Command structure validation + */ +function testSocialDownvoteCommandStructure(): void { + console.log('\nπŸ“‹ Test 1: SocialDownvote command structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Create valid params for Social Downvote command + const validParams: SocialDownvoteParams = { + // TODO: Add your required parameters here + context, + sessionId + }; + + // Validate param structure + assert(validParams.context !== undefined, 'Params have context'); + assert(validParams.sessionId !== undefined, 'Params have sessionId'); + // TODO: Add assertions for your specific parameters + // assert(typeof validParams.requiredParam === 'string', 'requiredParam is string'); +} + +/** + * Test 2: Mock command execution + */ +async function testMockSocialDownvoteExecution(): Promise { + console.log('\n⚑ Test 2: Mock Social Downvote command execution'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test mock execution + const params: SocialDownvoteParams = { + // TODO: Add your parameters here + context, + sessionId + }; + + const result = await mockSocialDownvoteCommand(params); + + // Validate result structure + assert(result.success === true, 'Mock result shows success'); + // TODO: Add assertions for your result fields + // assert(typeof result.yourField === 'string', 'yourField is string'); +} + +/** + * Test 3: Required parameter validation (CRITICAL) + * + * This test ensures your command throws ValidationError + * when required parameters are missing (BEST PRACTICE) + */ +async function testSocialDownvoteRequiredParams(): Promise { + console.log('\n🚨 Test 3: Required parameter validation'); + + // TODO: Uncomment when implementing validation + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test cases that should throw ValidationError + // Example: + // const testCases = [ + // { params: {} as SocialDownvoteParams, desc: 'Missing requiredParam' }, + // { params: { requiredParam: '' } as SocialDownvoteParams, desc: 'Empty requiredParam' }, + // ]; + // + // for (const testCase of testCases) { + // try { + // await mockSocialDownvoteCommand({ ...testCase.params, context, sessionId }); + // throw new Error(`Should have thrown ValidationError for: ${testCase.desc}`); + // } catch (error) { + // if (error instanceof ValidationError) { + // assert(error.field === 'requiredParam', `ValidationError field is 'requiredParam' for: ${testCase.desc}`); + // assert(error.message.includes('required parameter'), `Error message mentions 'required parameter' for: ${testCase.desc}`); + // assert(error.message.includes('help tool'), `Error message is tool-agnostic for: ${testCase.desc}`); + // } else { + // throw error; // Re-throw if not ValidationError + // } + // } + // } + + console.log('βœ… All required parameter validations work correctly'); +} + +/** + * Test 4: Optional parameter handling + */ +async function testSocialDownvoteOptionalParams(): Promise { + console.log('\nπŸ”§ Test 4: Optional parameter handling'); + + // TODO: Uncomment when implementing optional param tests + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test WITHOUT optional param (should use default) + // const paramsWithoutOptional: SocialDownvoteParams = { + // requiredParam: 'test', + // context, + // sessionId + // }; + // + // const resultWithoutOptional = await mockSocialDownvoteCommand(paramsWithoutOptional); + // assert(resultWithoutOptional.success === true, 'Command succeeds without optional params'); + + // TODO: Test WITH optional param + // const paramsWithOptional: SocialDownvoteParams = { + // requiredParam: 'test', + // optionalParam: true, + // context, + // sessionId + // }; + // + // const resultWithOptional = await mockSocialDownvoteCommand(paramsWithOptional); + // assert(resultWithOptional.success === true, 'Command succeeds with optional params'); + + console.log('βœ… Optional parameter handling validated'); +} + +/** + * Test 5: Performance validation + */ +async function testSocialDownvotePerformance(): Promise { + console.log('\n⚑ Test 5: SocialDownvote performance validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + const startTime = Date.now(); + + await mockSocialDownvoteCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialDownvoteParams); + + const executionTime = Date.now() - startTime; + + assert(executionTime < 100, `SocialDownvote completed in ${executionTime}ms (under 100ms limit)`); +} + +/** + * Test 6: Result structure validation + */ +async function testSocialDownvoteResultStructure(): Promise { + console.log('\nπŸ” Test 6: SocialDownvote result structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test various scenarios + const basicResult = await mockSocialDownvoteCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialDownvoteParams); + + assert(basicResult.success === true, 'Result has success field'); + // TODO: Add assertions for your result fields + // assert(typeof basicResult.yourField === 'string', 'Result has yourField (string)'); + assert(basicResult.context === context, 'Result includes context'); + assert(basicResult.sessionId === sessionId, 'Result includes sessionId'); + + console.log('βœ… All result structure validations pass'); +} + +/** + * Run all unit tests + */ +async function runAllSocialDownvoteUnitTests(): Promise { + console.log('πŸš€ Starting SocialDownvote Command Unit Tests\n'); + + try { + testSocialDownvoteCommandStructure(); + await testMockSocialDownvoteExecution(); + await testSocialDownvoteRequiredParams(); + await testSocialDownvoteOptionalParams(); + await testSocialDownvotePerformance(); + await testSocialDownvoteResultStructure(); + + console.log('\nπŸŽ‰ ALL SocialDownvote UNIT TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Command structure and parameter validation'); + console.log(' βœ… Mock command execution patterns'); + console.log(' βœ… Required parameter validation (throws ValidationError)'); + console.log(' βœ… Optional parameter handling (sensible defaults)'); + console.log(' βœ… Performance requirements (< 100ms)'); + console.log(' βœ… Result structure validation'); + console.log('\nπŸ“ This is a REFERENCE EXAMPLE - use as a template for your commands!'); + console.log('πŸ’‘ TIP: Copy this test structure and modify for your command logic'); + + } catch (error) { + console.error('\n❌ SocialDownvote unit tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialDownvoteUnitTests(); +} else { + module.exports = { runAllSocialDownvoteUnitTests }; +} diff --git a/src/debug/jtag/commands/social/engage/browser/SocialEngageBrowserCommand.ts b/src/debug/jtag/commands/social/engage/browser/SocialEngageBrowserCommand.ts new file mode 100644 index 000000000..f6b42c36d --- /dev/null +++ b/src/debug/jtag/commands/social/engage/browser/SocialEngageBrowserCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Engage Command - Browser Implementation + * Delegates to server + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialEngageBaseCommand } from '../shared/SocialEngageCommand'; +import type { SocialEngageParams, SocialEngageResult } from '../shared/SocialEngageTypes'; + +export class SocialEngageBrowserCommand extends SocialEngageBaseCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialEngage(params: SocialEngageParams): Promise { + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/social/engage/package.json b/src/debug/jtag/commands/social/engage/package.json new file mode 100644 index 000000000..5b11396cd --- /dev/null +++ b/src/debug/jtag/commands/social/engage/package.json @@ -0,0 +1,19 @@ +{ + "name": "@continuum/social-engage", + "version": "1.0.0", + "description": "All social interaction in one command: vote, follow/unfollow, subscribe/unsubscribe", + "private": true, + "command": { + "name": "social/engage", + "description": "Engage with social media content and agents", + "category": "social", + "params": { + "platform": { "type": "string", "required": true, "description": "Platform (e.g., 'moltbook')" }, + "action": { "type": "string", "required": true, "description": "Action: vote, follow, unfollow, subscribe, unsubscribe" }, + "target": { "type": "string", "required": true, "description": "Target: post/comment ID, agent name, or community name" }, + "targetType": { "type": "string", "required": false, "description": "For vote: post or comment" }, + "direction": { "type": "string", "required": false, "description": "For vote: up or down" }, + "personaId": { "type": "string", "required": false, "description": "Persona user ID (auto-detected)" } + } + } +} diff --git a/src/debug/jtag/commands/social/engage/server/SocialEngageServerCommand.ts b/src/debug/jtag/commands/social/engage/server/SocialEngageServerCommand.ts new file mode 100644 index 000000000..a67511cb8 --- /dev/null +++ b/src/debug/jtag/commands/social/engage/server/SocialEngageServerCommand.ts @@ -0,0 +1,166 @@ +/** + * Social Engage Command - Server Implementation + * + * All social interaction: vote, follow/unfollow, subscribe/unsubscribe. + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import { transformPayload } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialEngageBaseCommand } from '../shared/SocialEngageCommand'; +import type { SocialEngageParams, SocialEngageResult, EngageAction } from '../shared/SocialEngageTypes'; +import { loadSocialContext } from '@system/social/server/SocialCommandHelper'; + +export class SocialEngageServerCommand extends SocialEngageBaseCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialEngage(params: SocialEngageParams): Promise { + const { platform, action, target } = params; + + if (!platform) throw new Error('platform is required'); + if (!action) throw new Error('action is required'); + if (!target) throw new Error('target is required'); + + const ctx = await loadSocialContext(platform, params.personaId, params); + + const rateCheck = ctx.provider.checkRateLimit(action === 'vote' ? 'vote' : 'request'); + if (!rateCheck.allowed) { + return transformPayload(params, { + success: false, + message: rateCheck.message ?? `Rate limited for ${action}`, + action, + target, + }); + } + + switch (action) { + case 'vote': + return this.handleVote(params, ctx); + case 'follow': + return this.handleFollow(params, ctx); + case 'unfollow': + return this.handleUnfollow(params, ctx); + case 'subscribe': + return this.handleSubscribe(params, ctx); + case 'unsubscribe': + return this.handleUnsubscribe(params, ctx); + case 'delete': + return this.handleDelete(params, ctx); + default: + throw new Error(`Unknown engage action: ${action}. Valid: vote, follow, unfollow, subscribe, unsubscribe, delete`); + } + } + + private async handleVote( + params: SocialEngageParams, + ctx: { provider: import('@system/social/shared/ISocialMediaProvider').ISocialMediaProvider }, + ): Promise { + const targetType = params.targetType ?? 'post'; + const direction = params.direction ?? 'up'; + + await ctx.provider.vote({ + targetId: params.target, + targetType, + direction, + }); + + const verb = direction === 'up' ? 'Upvoted' : 'Downvoted'; + return transformPayload(params, { + success: true, + message: `${verb} ${targetType} ${params.target} on ${params.platform}`, + action: 'vote', + target: params.target, + }); + } + + private async handleFollow( + params: SocialEngageParams, + ctx: { provider: import('@system/social/shared/ISocialMediaProvider').ISocialMediaProvider }, + ): Promise { + await ctx.provider.follow(params.target); + + return transformPayload(params, { + success: true, + message: `Now following ${params.target} on ${params.platform}`, + action: 'follow', + target: params.target, + }); + } + + private async handleUnfollow( + params: SocialEngageParams, + ctx: { provider: import('@system/social/shared/ISocialMediaProvider').ISocialMediaProvider }, + ): Promise { + await ctx.provider.unfollow(params.target); + + return transformPayload(params, { + success: true, + message: `Unfollowed ${params.target} on ${params.platform}`, + action: 'unfollow', + target: params.target, + }); + } + + private async handleSubscribe( + params: SocialEngageParams, + ctx: { provider: import('@system/social/shared/ISocialMediaProvider').ISocialMediaProvider }, + ): Promise { + await ctx.provider.subscribeToCommunity(params.target); + + return transformPayload(params, { + success: true, + message: `Subscribed to m/${params.target} on ${params.platform}`, + action: 'subscribe', + target: params.target, + }); + } + + private async handleUnsubscribe( + params: SocialEngageParams, + ctx: { provider: import('@system/social/shared/ISocialMediaProvider').ISocialMediaProvider }, + ): Promise { + await ctx.provider.unsubscribeFromCommunity(params.target); + + return transformPayload(params, { + success: true, + message: `Unsubscribed from m/${params.target} on ${params.platform}`, + action: 'unsubscribe', + target: params.target, + }); + } + + private async handleDelete( + params: SocialEngageParams, + ctx: { provider: import('@system/social/shared/ISocialMediaProvider').ISocialMediaProvider }, + ): Promise { + const targetType = params.targetType ?? 'post'; + + if (targetType === 'comment') { + // For comment deletion, target is commentId and we need a postId + // The postId can be passed via direction field as a workaround, + // or we use target as "postId:commentId" format + const parts = params.target.split(':'); + if (parts.length !== 2) { + throw new Error('For comment deletion, target must be "postId:commentId" format'); + } + await ctx.provider.deleteComment(parts[0], parts[1]); + return transformPayload(params, { + success: true, + message: `Deleted comment ${parts[1]} on ${params.platform}`, + action: 'delete', + target: params.target, + }); + } + + await ctx.provider.deletePost(params.target); + return transformPayload(params, { + success: true, + message: `Deleted post ${params.target} on ${params.platform}`, + action: 'delete', + target: params.target, + }); + } +} diff --git a/src/debug/jtag/commands/social/engage/shared/SocialEngageCommand.ts b/src/debug/jtag/commands/social/engage/shared/SocialEngageCommand.ts new file mode 100644 index 000000000..3d8a36fb7 --- /dev/null +++ b/src/debug/jtag/commands/social/engage/shared/SocialEngageCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Engage Command - Shared base class + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { SocialEngageParams, SocialEngageResult } from './SocialEngageTypes'; +import type { JTAGContext, JTAGPayload } from '@system/core/types/JTAGTypes'; + +export abstract class SocialEngageBaseCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/engage', context, subpath, commander); + } + + protected abstract executeSocialEngage(params: SocialEngageParams): Promise; + + async execute(params: JTAGPayload): Promise { + return this.executeSocialEngage(params as SocialEngageParams); + } +} diff --git a/src/debug/jtag/commands/social/engage/shared/SocialEngageTypes.ts b/src/debug/jtag/commands/social/engage/shared/SocialEngageTypes.ts new file mode 100644 index 000000000..a8517b291 --- /dev/null +++ b/src/debug/jtag/commands/social/engage/shared/SocialEngageTypes.ts @@ -0,0 +1,91 @@ +/** + * Social Engage Command - Shared Types + * + * All social interaction in one command: vote, follow, subscribe. + * Designed for AI tool use β€” one command covers all engagement actions. + * + * Actions: + * vote β€” Upvote or downvote a post or comment + * follow β€” Follow an agent + * unfollow β€” Unfollow an agent + * subscribe β€” Subscribe to a community + * unsubscribe β€” Unsubscribe from a community + * delete β€” Delete own post or comment + * + * Usage: + * ./jtag social/engage --platform=moltbook --action=vote --target=abc123 --targetType=post --direction=up + * ./jtag social/engage --platform=moltbook --action=follow --target=eudaemon_0 + * ./jtag social/engage --platform=moltbook --action=subscribe --target=ai-development + * ./jtag social/engage --platform=moltbook --action=delete --target=abc123 --targetType=post + */ + +import type { CommandParams, CommandResult, CommandInput, JTAGContext } from '@system/core/types/JTAGTypes'; +import { createPayload, transformPayload } from '@system/core/types/JTAGTypes'; +import { Commands } from '@system/core/shared/Commands'; +import type { JTAGError } from '@system/core/types/ErrorTypes'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; + +/** Engagement actions */ +export type EngageAction = 'vote' | 'follow' | 'unfollow' | 'subscribe' | 'unsubscribe' | 'delete'; + +/** + * Social Engage Command Parameters + */ +export interface SocialEngageParams extends CommandParams { + /** Platform (e.g., 'moltbook') */ + platform: string; + + /** Engagement action */ + action: EngageAction; + + /** + * Target identifier β€” meaning depends on action: + * vote β†’ post or comment ID + * follow β†’ agent username + * unfollow β†’ agent username + * subscribe β†’ community/submolt name + * unsubscribe β†’ community/submolt name + */ + target: string; + + /** For vote action: target type */ + targetType?: 'post' | 'comment'; + + /** For vote action: direction */ + direction?: 'up' | 'down'; + + /** Persona user ID (auto-detected if not provided) */ + personaId?: UUID; +} + +/** + * Social Engage Command Result + */ +export interface SocialEngageResult extends CommandResult { + success: boolean; + message: string; + action: EngageAction; + target: string; + error?: JTAGError; +} + +export const createSocialEngageParams = ( + context: JTAGContext, + sessionId: UUID, + data: Omit +): SocialEngageParams => createPayload(context, sessionId, data); + +export const createSocialEngageResultFromParams = ( + params: SocialEngageParams, + differences: Omit +): SocialEngageResult => transformPayload(params, differences); + +/** + * SocialEngage β€” Type-safe command executor + */ +export const SocialEngage = { + execute(params: CommandInput): Promise { + return Commands.execute('social/engage', params as Partial); + }, + commandName: 'social/engage' as const, +} as const; diff --git a/src/debug/jtag/commands/social/feed/.npmignore b/src/debug/jtag/commands/social/feed/.npmignore new file mode 100644 index 000000000..f74ad6b8a --- /dev/null +++ b/src/debug/jtag/commands/social/feed/.npmignore @@ -0,0 +1,20 @@ +# Development files +.eslintrc* +tsconfig*.json +vitest.config.ts + +# Build artifacts +*.js.map +*.d.ts.map + +# IDE +.vscode/ +.idea/ + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db diff --git a/src/debug/jtag/commands/social/feed/README.md b/src/debug/jtag/commands/social/feed/README.md new file mode 100644 index 000000000..afbbcb859 --- /dev/null +++ b/src/debug/jtag/commands/social/feed/README.md @@ -0,0 +1,165 @@ +# Social Feed Command + +Read the feed from a social media platform. Supports global feed, personalized feed, and community-specific feeds. + +## Table of Contents + +- [Usage](#usage) + - [CLI Usage](#cli-usage) + - [Tool Usage](#tool-usage) +- [Parameters](#parameters) +- [Result](#result) +- [Examples](#examples) +- [Testing](#testing) + - [Unit Tests](#unit-tests) + - [Integration Tests](#integration-tests) +- [Getting Help](#getting-help) +- [Access Level](#access-level) +- [Implementation Notes](#implementation-notes) + +## Usage + +### CLI Usage + +From the command line using the jtag CLI: + +```bash +./jtag social/feed --platform= +``` + +### Tool Usage + +From Persona tools or programmatic access using `Commands.execute()`: + +```typescript +import { Commands } from '@system/core/shared/Commands'; + +const result = await Commands.execute('social/feed', { + // your parameters here +}); +``` + +## Parameters + +- **platform** (required): `string` - Platform to read from (e.g., 'moltbook') +- **sort** (optional): `string` - Sort order: hot, new, top, rising +- **community** (optional): `string` - Community/submolt to filter by +- **limit** (optional): `number` - Maximum number of posts to return +- **personalized** (optional): `boolean` - Whether to show personalized feed +- **personaId** (optional): `UUID` - Persona user ID (auto-detected if not provided) + +## Result + +Returns `SocialFeedResult` with: + +Returns CommandResult with: +- **message**: `string` - Human-readable result message +- **posts**: `SocialPostData[]` - Array of feed posts + +## Examples + +### Read the hot feed from Moltbook + +```bash +./jtag social/feed --platform=moltbook --sort=hot --limit=10 +``` + +**Expected result:** +{ success: true, posts: [...] } + +### Read a community feed + +```bash +./jtag social/feed --platform=moltbook --community=ai-development --sort=new +``` + +## Getting Help + +### Using the Help Tool + +Get detailed usage information for this command: + +**CLI:** +```bash +./jtag help social/feed +``` + +**Tool:** +```typescript +// Use your help tool with command name 'social/feed' +``` + +### Using the README Tool + +Access this README programmatically: + +**CLI:** +```bash +./jtag readme social/feed +``` + +**Tool:** +```typescript +// Use your readme tool with command name 'social/feed' +``` + +## Testing + +### Unit Tests + +Test command logic in isolation using mock dependencies: + +```bash +# Run unit tests (no server required) +npx tsx commands/social/feed/test/unit/SocialFeedCommand.test.ts +``` + +**What's tested:** +- Command structure and parameter validation +- Mock command execution patterns +- Required parameter validation (throws ValidationError) +- Optional parameter handling (sensible defaults) +- Performance requirements +- Assertion utility helpers + +**TDD Workflow:** +1. Write/modify unit test first (test-driven development) +2. Run test, see it fail +3. Implement feature +4. Run test, see it pass +5. Refactor if needed + +### Integration Tests + +Test command with real client connections and system integration: + +```bash +# Prerequisites: Server must be running +npm start # Wait 90+ seconds for deployment + +# Run integration tests +npx tsx commands/social/feed/test/integration/SocialFeedIntegration.test.ts +``` + +**What's tested:** +- Client connection to live system +- Real command execution via WebSocket +- ValidationError handling for missing params +- Optional parameter defaults +- Performance under load +- Various parameter combinations + +**Best Practice:** +Run unit tests frequently during development (fast feedback). Run integration tests before committing (verify system integration). + +## Access Level + +**ai-safe** - Safe for AI personas to call autonomously + +## Implementation Notes + +- **Shared Logic**: Core business logic in `shared/SocialFeedTypes.ts` +- **Browser**: Browser-specific implementation in `browser/SocialFeedBrowserCommand.ts` +- **Server**: Server-specific implementation in `server/SocialFeedServerCommand.ts` +- **Unit Tests**: Isolated testing in `test/unit/SocialFeedCommand.test.ts` +- **Integration Tests**: System testing in `test/integration/SocialFeedIntegration.test.ts` diff --git a/src/debug/jtag/commands/social/feed/browser/SocialFeedBrowserCommand.ts b/src/debug/jtag/commands/social/feed/browser/SocialFeedBrowserCommand.ts new file mode 100644 index 000000000..71d0612d1 --- /dev/null +++ b/src/debug/jtag/commands/social/feed/browser/SocialFeedBrowserCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Feed Command - Browser Implementation + * Delegates to server + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialFeedBaseCommand } from '../shared/SocialFeedCommand'; +import type { SocialFeedParams, SocialFeedResult } from '../shared/SocialFeedTypes'; + +export class SocialFeedBrowserCommand extends SocialFeedBaseCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialFeed(params: SocialFeedParams): Promise { + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/social/feed/package.json b/src/debug/jtag/commands/social/feed/package.json new file mode 100644 index 000000000..bda1d6c62 --- /dev/null +++ b/src/debug/jtag/commands/social/feed/package.json @@ -0,0 +1,35 @@ +{ + "name": "@jtag-commands/social/feed", + "version": "1.0.0", + "description": "Read the feed from a social media platform. Supports global feed, personalized feed, and community-specific feeds.", + "main": "server/SocialFeedServerCommand.ts", + "types": "shared/SocialFeedTypes.ts", + "scripts": { + "test": "npm run test:unit && npm run test:integration", + "test:unit": "npx vitest run test/unit/*.test.ts", + "test:integration": "npx tsx test/integration/SocialFeedIntegration.test.ts", + "lint": "npx eslint **/*.ts", + "typecheck": "npx tsc --noEmit" + }, + "peerDependencies": { + "@jtag/core": "*" + }, + "files": [ + "shared/**/*.ts", + "browser/**/*.ts", + "server/**/*.ts", + "test/**/*.ts", + "README.md" + ], + "keywords": [ + "jtag", + "command", + "social/feed" + ], + "license": "MIT", + "author": "", + "repository": { + "type": "git", + "url": "" + } +} diff --git a/src/debug/jtag/commands/social/feed/server/SocialFeedServerCommand.ts b/src/debug/jtag/commands/social/feed/server/SocialFeedServerCommand.ts new file mode 100644 index 000000000..053846d3f --- /dev/null +++ b/src/debug/jtag/commands/social/feed/server/SocialFeedServerCommand.ts @@ -0,0 +1,42 @@ +/** + * Social Feed Command - Server Implementation + * + * Reads the feed from a social media platform. + * Supports global feed, personalized feed, and community-specific feeds. + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import { transformPayload } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialFeedBaseCommand } from '../shared/SocialFeedCommand'; +import type { SocialFeedParams, SocialFeedResult } from '../shared/SocialFeedTypes'; +import { loadSocialContext } from '@system/social/server/SocialCommandHelper'; + +export class SocialFeedServerCommand extends SocialFeedBaseCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialFeed(params: SocialFeedParams): Promise { + const { platform, sort, community, limit, personalized } = params; + + if (!platform) throw new Error('platform is required'); + + const ctx = await loadSocialContext(platform, params.personaId, params); + + let posts; + if (community) { + posts = await ctx.provider.getCommunityFeed(community, sort, limit); + } else { + posts = await ctx.provider.getFeed({ sort, limit, personalized }); + } + + const source = community ? `${platform}/${community}` : platform; + return transformPayload(params, { + success: true, + message: `Fetched ${posts.length} posts from ${source} (${sort ?? 'default'})`, + posts, + }); + } +} diff --git a/src/debug/jtag/commands/social/feed/shared/SocialFeedCommand.ts b/src/debug/jtag/commands/social/feed/shared/SocialFeedCommand.ts new file mode 100644 index 000000000..fdd27baaf --- /dev/null +++ b/src/debug/jtag/commands/social/feed/shared/SocialFeedCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Feed Command - Shared base class + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { SocialFeedParams, SocialFeedResult } from './SocialFeedTypes'; +import type { JTAGContext, JTAGPayload } from '@system/core/types/JTAGTypes'; + +export abstract class SocialFeedBaseCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/feed', context, subpath, commander); + } + + protected abstract executeSocialFeed(params: SocialFeedParams): Promise; + + async execute(params: JTAGPayload): Promise { + return this.executeSocialFeed(params as SocialFeedParams); + } +} diff --git a/src/debug/jtag/commands/social/feed/shared/SocialFeedTypes.ts b/src/debug/jtag/commands/social/feed/shared/SocialFeedTypes.ts new file mode 100644 index 000000000..e1c9d2d33 --- /dev/null +++ b/src/debug/jtag/commands/social/feed/shared/SocialFeedTypes.ts @@ -0,0 +1,116 @@ +/** + * Social Feed Command - Shared Types + * + * Read the feed from a social media platform. Supports global feed, + * personalized feed, and community-specific feeds. + * + * Usage: + * ./jtag social/feed --platform=moltbook --sort=hot --limit=10 + * ./jtag social/feed --platform=moltbook --community=ai-development --sort=new + */ + +import type { CommandParams, CommandResult, CommandInput, JTAGContext } from '@system/core/types/JTAGTypes'; +import { createPayload, transformPayload } from '@system/core/types/JTAGTypes'; +import { Commands } from '@system/core/shared/Commands'; +import type { JTAGError } from '@system/core/types/ErrorTypes'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialPost as SocialPostData } from '@system/social/shared/SocialMediaTypes'; + +/** + * Social Feed Command Parameters + */ +export interface SocialFeedParams extends CommandParams { + /** Platform to read from (e.g., 'moltbook') */ + platform: string; + + /** Sort order: hot, new, top, rising */ + sort?: 'hot' | 'new' | 'top' | 'rising'; + + /** Community/submolt to filter by */ + community?: string; + + /** Maximum number of posts to return */ + limit?: number; + + /** Whether to show personalized feed */ + personalized?: boolean; + + /** Persona user ID (auto-detected if not provided) */ + personaId?: UUID; +} + +/** + * Factory function for creating SocialFeedParams + */ +export const createSocialFeedParams = ( + context: JTAGContext, + sessionId: UUID, + data: { + platform: string; + sort?: 'hot' | 'new' | 'top' | 'rising'; + community?: string; + limit?: number; + personalized?: boolean; + personaId?: UUID; + } +): SocialFeedParams => createPayload(context, sessionId, { + sort: data.sort ?? undefined, + community: data.community ?? '', + limit: data.limit ?? 0, + personalized: data.personalized ?? false, + personaId: data.personaId ?? undefined, + ...data +}); + +/** + * Social Feed Command Result + */ +export interface SocialFeedResult extends CommandResult { + success: boolean; + message: string; + + /** Array of feed posts */ + posts?: SocialPostData[]; + + error?: JTAGError; +} + +/** + * Factory function for creating SocialFeedResult with defaults + */ +export const createSocialFeedResult = ( + context: JTAGContext, + sessionId: UUID, + data: { + success: boolean; + message?: string; + posts?: SocialPostData[]; + error?: JTAGError; + } +): SocialFeedResult => createPayload(context, sessionId, { + message: data.message ?? '', + ...data +}); + +/** + * Smart Social Feed-specific inheritance from params + * Auto-inherits context and sessionId from params + */ +export const createSocialFeedResultFromParams = ( + params: SocialFeedParams, + differences: Omit +): SocialFeedResult => transformPayload(params, differences); + +/** + * SocialFeed β€” Type-safe command executor + * + * Usage: + * import { SocialFeed } from '...shared/SocialFeedTypes'; + * const result = await SocialFeed.execute({ platform: 'moltbook', sort: 'hot' }); + */ +export const SocialFeed = { + execute(params: CommandInput): Promise { + return Commands.execute('social/feed', params as Partial); + }, + commandName: 'social/feed' as const, +} as const; diff --git a/src/debug/jtag/commands/social/feed/test/integration/SocialFeedIntegration.test.ts b/src/debug/jtag/commands/social/feed/test/integration/SocialFeedIntegration.test.ts new file mode 100644 index 000000000..b6a21a541 --- /dev/null +++ b/src/debug/jtag/commands/social/feed/test/integration/SocialFeedIntegration.test.ts @@ -0,0 +1,196 @@ +#!/usr/bin/env tsx +/** + * SocialFeed Command Integration Tests + * + * Tests Social Feed command against the LIVE RUNNING SYSTEM. + * This is NOT a mock test - it tests real commands, real events, real widgets. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Feed/test/integration/SocialFeedIntegration.test.ts + * + * PREREQUISITES: + * - Server must be running: npm start (wait 90+ seconds) + * - Browser client connected via http://localhost:9003 + */ + +import { jtag } from '@server/server-index'; + +console.log('πŸ§ͺ SocialFeed Command Integration Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Test 1: Connect to live system + */ +async function testSystemConnection(): Promise>> { + console.log('\nπŸ”Œ Test 1: Connecting to live JTAG system'); + + const client = await jtag.connect(); + + assert(client !== null, 'Connected to live system'); + console.log(' βœ… Connected successfully'); + + return client; +} + +/** + * Test 2: Execute Social Feed command on live system + */ +async function testCommandExecution(client: Awaited>): Promise { + console.log('\n⚑ Test 2: Executing Social Feed command'); + + // TODO: Replace with your actual command parameters + const result = await client.commands['Social Feed']({ + // Add your required parameters here + // Example: name: 'test-value' + }); + + console.log(' πŸ“Š Result:', JSON.stringify(result, null, 2)); + + assert(result !== null, 'Social Feed returned result'); + // TODO: Add assertions for your specific result fields + // assert(result.success === true, 'Social Feed succeeded'); + // assert(result.yourField !== undefined, 'Result has yourField'); +} + +/** + * Test 3: Validate required parameters + */ +async function testRequiredParameters(_client: Awaited>): Promise { + console.log('\n🚨 Test 3: Testing required parameter validation'); + + // TODO: Uncomment and test missing required parameters + // try { + // await _client.commands['Social Feed']({ + // // Missing required param + // }); + // assert(false, 'Should have thrown validation error'); + // } catch (error) { + // assert((error as Error).message.includes('required'), 'Error mentions required parameter'); + // console.log(' βœ… ValidationError thrown correctly'); + // } + + console.log(' ⚠️ TODO: Add required parameter validation test'); +} + +/** + * Test 4: Test optional parameters + */ +async function testOptionalParameters(_client: Awaited>): Promise { + console.log('\nπŸ”§ Test 4: Testing optional parameters'); + + // TODO: Uncomment to test with and without optional parameters + // const withOptional = await client.commands['Social Feed']({ + // requiredParam: 'test', + // optionalParam: true + // }); + // + // const withoutOptional = await client.commands['Social Feed']({ + // requiredParam: 'test' + // }); + // + // assert(withOptional.success === true, 'Works with optional params'); + // assert(withoutOptional.success === true, 'Works without optional params'); + + console.log(' ⚠️ TODO: Add optional parameter tests'); +} + +/** + * Test 5: Performance test + */ +async function testPerformance(_client: Awaited>): Promise { + console.log('\n⚑ Test 5: Performance under load'); + + // TODO: Uncomment to test command performance + // const iterations = 10; + // const times: number[] = []; + // + // for (let i = 0; i < iterations; i++) { + // const start = Date.now(); + // await _client.commands['Social Feed']({ /* params */ }); + // times.push(Date.now() - start); + // } + // + // const avg = times.reduce((a, b) => a + b, 0) / iterations; + // const max = Math.max(...times); + // + // console.log(` Average: ${avg.toFixed(2)}ms`); + // console.log(` Max: ${max}ms`); + // + // assert(avg < 500, `Average ${avg.toFixed(2)}ms under 500ms`); + // assert(max < 1000, `Max ${max}ms under 1000ms`); + + console.log(' ⚠️ TODO: Add performance test'); +} + +/** + * Test 6: Widget/Event integration (if applicable) + */ +async function testWidgetIntegration(_client: Awaited>): Promise { + console.log('\n🎨 Test 6: Widget/Event integration'); + + // TODO: Uncomment if your command emits events or updates widgets + // Example: + // const before = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // await client.commands['Social Feed']({ /* params */ }); + // await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for event propagation + // const after = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // + // assert(after.state.someValue !== before.state.someValue, 'Widget state updated'); + + console.log(' ⚠️ TODO: Add widget/event integration test (if applicable)'); +} + +/** + * Run all integration tests + */ +async function runAllSocialFeedIntegrationTests(): Promise { + console.log('πŸš€ Starting SocialFeed Integration Tests\n'); + console.log('πŸ“‹ Testing against LIVE system (not mocks)\n'); + + try { + const client = await testSystemConnection(); + await testCommandExecution(client); + await testRequiredParameters(client); + await testOptionalParameters(client); + await testPerformance(client); + await testWidgetIntegration(client); + + console.log('\nπŸŽ‰ ALL SocialFeed INTEGRATION TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Live system connection'); + console.log(' βœ… Command execution on real system'); + console.log(' βœ… Parameter validation'); + console.log(' βœ… Optional parameter handling'); + console.log(' βœ… Performance benchmarks'); + console.log(' βœ… Widget/Event integration'); + console.log('\nπŸ’‘ NOTE: This test uses the REAL running system'); + console.log(' - Real database operations'); + console.log(' - Real event propagation'); + console.log(' - Real widget updates'); + console.log(' - Real cross-daemon communication'); + + } catch (error) { + console.error('\n❌ SocialFeed integration tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + console.error('\nπŸ’‘ Make sure:'); + console.error(' 1. Server is running: npm start'); + console.error(' 2. Wait 90+ seconds for deployment'); + console.error(' 3. Browser is connected to http://localhost:9003'); + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialFeedIntegrationTests(); +} else { + module.exports = { runAllSocialFeedIntegrationTests }; +} diff --git a/src/debug/jtag/commands/social/feed/test/unit/SocialFeedCommand.test.ts b/src/debug/jtag/commands/social/feed/test/unit/SocialFeedCommand.test.ts new file mode 100644 index 000000000..b0dd2191f --- /dev/null +++ b/src/debug/jtag/commands/social/feed/test/unit/SocialFeedCommand.test.ts @@ -0,0 +1,259 @@ +#!/usr/bin/env tsx +/** + * SocialFeed Command Unit Tests + * + * Tests Social Feed command logic in isolation using mock dependencies. + * This is a REFERENCE EXAMPLE showing best practices for command testing. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Feed/test/unit/SocialFeedCommand.test.ts + * + * NOTE: This is a self-contained test (no external test utilities needed). + * Use this as a template for your own command tests. + */ + +// import { ValidationError } from '@system/core/types/ErrorTypes'; // Uncomment when adding validation tests +import { generateUUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialFeedParams, SocialFeedResult } from '../../shared/SocialFeedTypes'; + +console.log('πŸ§ͺ SocialFeed Command Unit Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Mock command that implements Social Feed logic for testing + */ +async function mockSocialFeedCommand(params: SocialFeedParams): Promise { + // TODO: Validate required parameters (BEST PRACTICE) + // Example: + // if (!params.requiredParam || params.requiredParam.trim() === '') { + // throw new ValidationError( + // 'requiredParam', + // `Missing required parameter 'requiredParam'. ` + + // `Use the help tool with 'Social Feed' or see the Social Feed README for usage information.` + // ); + // } + + // TODO: Handle optional parameters with sensible defaults + // const optionalParam = params.optionalParam ?? defaultValue; + + // TODO: Implement your command logic here + return { + success: true, + // TODO: Add your result fields with actual computed values + context: params.context, + sessionId: params.sessionId + } as SocialFeedResult; +} + +/** + * Test 1: Command structure validation + */ +function testSocialFeedCommandStructure(): void { + console.log('\nπŸ“‹ Test 1: SocialFeed command structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Create valid params for Social Feed command + const validParams: SocialFeedParams = { + // TODO: Add your required parameters here + context, + sessionId + }; + + // Validate param structure + assert(validParams.context !== undefined, 'Params have context'); + assert(validParams.sessionId !== undefined, 'Params have sessionId'); + // TODO: Add assertions for your specific parameters + // assert(typeof validParams.requiredParam === 'string', 'requiredParam is string'); +} + +/** + * Test 2: Mock command execution + */ +async function testMockSocialFeedExecution(): Promise { + console.log('\n⚑ Test 2: Mock Social Feed command execution'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test mock execution + const params: SocialFeedParams = { + // TODO: Add your parameters here + context, + sessionId + }; + + const result = await mockSocialFeedCommand(params); + + // Validate result structure + assert(result.success === true, 'Mock result shows success'); + // TODO: Add assertions for your result fields + // assert(typeof result.yourField === 'string', 'yourField is string'); +} + +/** + * Test 3: Required parameter validation (CRITICAL) + * + * This test ensures your command throws ValidationError + * when required parameters are missing (BEST PRACTICE) + */ +async function testSocialFeedRequiredParams(): Promise { + console.log('\n🚨 Test 3: Required parameter validation'); + + // TODO: Uncomment when implementing validation + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test cases that should throw ValidationError + // Example: + // const testCases = [ + // { params: {} as SocialFeedParams, desc: 'Missing requiredParam' }, + // { params: { requiredParam: '' } as SocialFeedParams, desc: 'Empty requiredParam' }, + // ]; + // + // for (const testCase of testCases) { + // try { + // await mockSocialFeedCommand({ ...testCase.params, context, sessionId }); + // throw new Error(`Should have thrown ValidationError for: ${testCase.desc}`); + // } catch (error) { + // if (error instanceof ValidationError) { + // assert(error.field === 'requiredParam', `ValidationError field is 'requiredParam' for: ${testCase.desc}`); + // assert(error.message.includes('required parameter'), `Error message mentions 'required parameter' for: ${testCase.desc}`); + // assert(error.message.includes('help tool'), `Error message is tool-agnostic for: ${testCase.desc}`); + // } else { + // throw error; // Re-throw if not ValidationError + // } + // } + // } + + console.log('βœ… All required parameter validations work correctly'); +} + +/** + * Test 4: Optional parameter handling + */ +async function testSocialFeedOptionalParams(): Promise { + console.log('\nπŸ”§ Test 4: Optional parameter handling'); + + // TODO: Uncomment when implementing optional param tests + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test WITHOUT optional param (should use default) + // const paramsWithoutOptional: SocialFeedParams = { + // requiredParam: 'test', + // context, + // sessionId + // }; + // + // const resultWithoutOptional = await mockSocialFeedCommand(paramsWithoutOptional); + // assert(resultWithoutOptional.success === true, 'Command succeeds without optional params'); + + // TODO: Test WITH optional param + // const paramsWithOptional: SocialFeedParams = { + // requiredParam: 'test', + // optionalParam: true, + // context, + // sessionId + // }; + // + // const resultWithOptional = await mockSocialFeedCommand(paramsWithOptional); + // assert(resultWithOptional.success === true, 'Command succeeds with optional params'); + + console.log('βœ… Optional parameter handling validated'); +} + +/** + * Test 5: Performance validation + */ +async function testSocialFeedPerformance(): Promise { + console.log('\n⚑ Test 5: SocialFeed performance validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + const startTime = Date.now(); + + await mockSocialFeedCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialFeedParams); + + const executionTime = Date.now() - startTime; + + assert(executionTime < 100, `SocialFeed completed in ${executionTime}ms (under 100ms limit)`); +} + +/** + * Test 6: Result structure validation + */ +async function testSocialFeedResultStructure(): Promise { + console.log('\nπŸ” Test 6: SocialFeed result structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test various scenarios + const basicResult = await mockSocialFeedCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialFeedParams); + + assert(basicResult.success === true, 'Result has success field'); + // TODO: Add assertions for your result fields + // assert(typeof basicResult.yourField === 'string', 'Result has yourField (string)'); + assert(basicResult.context === context, 'Result includes context'); + assert(basicResult.sessionId === sessionId, 'Result includes sessionId'); + + console.log('βœ… All result structure validations pass'); +} + +/** + * Run all unit tests + */ +async function runAllSocialFeedUnitTests(): Promise { + console.log('πŸš€ Starting SocialFeed Command Unit Tests\n'); + + try { + testSocialFeedCommandStructure(); + await testMockSocialFeedExecution(); + await testSocialFeedRequiredParams(); + await testSocialFeedOptionalParams(); + await testSocialFeedPerformance(); + await testSocialFeedResultStructure(); + + console.log('\nπŸŽ‰ ALL SocialFeed UNIT TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Command structure and parameter validation'); + console.log(' βœ… Mock command execution patterns'); + console.log(' βœ… Required parameter validation (throws ValidationError)'); + console.log(' βœ… Optional parameter handling (sensible defaults)'); + console.log(' βœ… Performance requirements (< 100ms)'); + console.log(' βœ… Result structure validation'); + console.log('\nπŸ“ This is a REFERENCE EXAMPLE - use as a template for your commands!'); + console.log('πŸ’‘ TIP: Copy this test structure and modify for your command logic'); + + } catch (error) { + console.error('\n❌ SocialFeed unit tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialFeedUnitTests(); +} else { + module.exports = { runAllSocialFeedUnitTests }; +} diff --git a/src/debug/jtag/commands/social/notifications/.npmignore b/src/debug/jtag/commands/social/notifications/.npmignore new file mode 100644 index 000000000..f74ad6b8a --- /dev/null +++ b/src/debug/jtag/commands/social/notifications/.npmignore @@ -0,0 +1,20 @@ +# Development files +.eslintrc* +tsconfig*.json +vitest.config.ts + +# Build artifacts +*.js.map +*.d.ts.map + +# IDE +.vscode/ +.idea/ + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db diff --git a/src/debug/jtag/commands/social/notifications/README.md b/src/debug/jtag/commands/social/notifications/README.md new file mode 100644 index 000000000..edb75d582 --- /dev/null +++ b/src/debug/jtag/commands/social/notifications/README.md @@ -0,0 +1,164 @@ +# Social Notifications Command + +Check for unread notifications (replies, mentions, followers) on a social media platform. Key data source for SocialMediaRAGSource. + +## Table of Contents + +- [Usage](#usage) + - [CLI Usage](#cli-usage) + - [Tool Usage](#tool-usage) +- [Parameters](#parameters) +- [Result](#result) +- [Examples](#examples) +- [Testing](#testing) + - [Unit Tests](#unit-tests) + - [Integration Tests](#integration-tests) +- [Getting Help](#getting-help) +- [Access Level](#access-level) +- [Implementation Notes](#implementation-notes) + +## Usage + +### CLI Usage + +From the command line using the jtag CLI: + +```bash +./jtag social/notifications --platform= +``` + +### Tool Usage + +From Persona tools or programmatic access using `Commands.execute()`: + +```typescript +import { Commands } from '@system/core/shared/Commands'; + +const result = await Commands.execute('social/notifications', { + // your parameters here +}); +``` + +## Parameters + +- **platform** (required): `string` - Platform to check (e.g., 'moltbook') +- **since** (optional): `string` - ISO timestamp to fetch notifications since +- **limit** (optional): `number` - Maximum number of notifications to return +- **personaId** (optional): `UUID` - Persona user ID (auto-detected if not provided) + +## Result + +Returns `SocialNotificationsResult` with: + +Returns CommandResult with: +- **message**: `string` - Human-readable result message +- **notifications**: `SocialNotification[]` - Array of notifications +- **unreadCount**: `number` - Count of unread notifications + +## Examples + +### Check recent notifications + +```bash +./jtag social/notifications --platform=moltbook +``` + +**Expected result:** +{ success: true, notifications: [...], unreadCount: 3 } + +### Check notifications since a specific time + +```bash +./jtag social/notifications --platform=moltbook --since=2026-01-30T00:00:00Z +``` + +## Getting Help + +### Using the Help Tool + +Get detailed usage information for this command: + +**CLI:** +```bash +./jtag help social/notifications +``` + +**Tool:** +```typescript +// Use your help tool with command name 'social/notifications' +``` + +### Using the README Tool + +Access this README programmatically: + +**CLI:** +```bash +./jtag readme social/notifications +``` + +**Tool:** +```typescript +// Use your readme tool with command name 'social/notifications' +``` + +## Testing + +### Unit Tests + +Test command logic in isolation using mock dependencies: + +```bash +# Run unit tests (no server required) +npx tsx commands/social/notifications/test/unit/SocialNotificationsCommand.test.ts +``` + +**What's tested:** +- Command structure and parameter validation +- Mock command execution patterns +- Required parameter validation (throws ValidationError) +- Optional parameter handling (sensible defaults) +- Performance requirements +- Assertion utility helpers + +**TDD Workflow:** +1. Write/modify unit test first (test-driven development) +2. Run test, see it fail +3. Implement feature +4. Run test, see it pass +5. Refactor if needed + +### Integration Tests + +Test command with real client connections and system integration: + +```bash +# Prerequisites: Server must be running +npm start # Wait 90+ seconds for deployment + +# Run integration tests +npx tsx commands/social/notifications/test/integration/SocialNotificationsIntegration.test.ts +``` + +**What's tested:** +- Client connection to live system +- Real command execution via WebSocket +- ValidationError handling for missing params +- Optional parameter defaults +- Performance under load +- Various parameter combinations + +**Best Practice:** +Run unit tests frequently during development (fast feedback). Run integration tests before committing (verify system integration). + +## Access Level + +**ai-safe** - Safe for AI personas to call autonomously + +## Implementation Notes + +- **Shared Logic**: Core business logic in `shared/SocialNotificationsTypes.ts` +- **Browser**: Browser-specific implementation in `browser/SocialNotificationsBrowserCommand.ts` +- **Server**: Server-specific implementation in `server/SocialNotificationsServerCommand.ts` +- **Unit Tests**: Isolated testing in `test/unit/SocialNotificationsCommand.test.ts` +- **Integration Tests**: System testing in `test/integration/SocialNotificationsIntegration.test.ts` diff --git a/src/debug/jtag/commands/social/notifications/browser/SocialNotificationsBrowserCommand.ts b/src/debug/jtag/commands/social/notifications/browser/SocialNotificationsBrowserCommand.ts new file mode 100644 index 000000000..7b4960476 --- /dev/null +++ b/src/debug/jtag/commands/social/notifications/browser/SocialNotificationsBrowserCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Notifications Command - Browser Implementation + * Delegates to server + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialNotificationsBaseCommand } from '../shared/SocialNotificationsCommand'; +import type { SocialNotificationsParams, SocialNotificationsResult } from '../shared/SocialNotificationsTypes'; + +export class SocialNotificationsBrowserCommand extends SocialNotificationsBaseCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialNotifications(params: SocialNotificationsParams): Promise { + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/social/notifications/package.json b/src/debug/jtag/commands/social/notifications/package.json new file mode 100644 index 000000000..97db17ee9 --- /dev/null +++ b/src/debug/jtag/commands/social/notifications/package.json @@ -0,0 +1,35 @@ +{ + "name": "@jtag-commands/social/notifications", + "version": "1.0.0", + "description": "Check for unread notifications (replies, mentions, followers) on a social media platform. Key data source for SocialMediaRAGSource.", + "main": "server/SocialNotificationsServerCommand.ts", + "types": "shared/SocialNotificationsTypes.ts", + "scripts": { + "test": "npm run test:unit && npm run test:integration", + "test:unit": "npx vitest run test/unit/*.test.ts", + "test:integration": "npx tsx test/integration/SocialNotificationsIntegration.test.ts", + "lint": "npx eslint **/*.ts", + "typecheck": "npx tsc --noEmit" + }, + "peerDependencies": { + "@jtag/core": "*" + }, + "files": [ + "shared/**/*.ts", + "browser/**/*.ts", + "server/**/*.ts", + "test/**/*.ts", + "README.md" + ], + "keywords": [ + "jtag", + "command", + "social/notifications" + ], + "license": "MIT", + "author": "", + "repository": { + "type": "git", + "url": "" + } +} diff --git a/src/debug/jtag/commands/social/notifications/server/SocialNotificationsServerCommand.ts b/src/debug/jtag/commands/social/notifications/server/SocialNotificationsServerCommand.ts new file mode 100644 index 000000000..af01baa2e --- /dev/null +++ b/src/debug/jtag/commands/social/notifications/server/SocialNotificationsServerCommand.ts @@ -0,0 +1,44 @@ +/** + * Social Notifications Command - Server Implementation + * + * Fetches unread notifications from a social media platform. + * This is the data source for SocialMediaRAGSource β€” personas become + * aware of social activity through this command. + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import { transformPayload } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialNotificationsBaseCommand } from '../shared/SocialNotificationsCommand'; +import type { SocialNotificationsParams, SocialNotificationsResult } from '../shared/SocialNotificationsTypes'; +import { loadSocialContext } from '@system/social/server/SocialCommandHelper'; + +export class SocialNotificationsServerCommand extends SocialNotificationsBaseCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialNotifications(params: SocialNotificationsParams): Promise { + const { platform, since, limit } = params; + + if (!platform) throw new Error('platform is required'); + + const ctx = await loadSocialContext(platform, params.personaId, params); + + const notifications = await ctx.provider.getNotifications(since); + + // Apply limit if specified + const limited = limit ? notifications.slice(0, limit) : notifications; + const unreadCount = limited.filter(n => !n.read).length; + + return transformPayload(params, { + success: true, + message: unreadCount > 0 + ? `${unreadCount} unread notification${unreadCount === 1 ? '' : 's'} on ${platform}` + : `No unread notifications on ${platform}`, + notifications: limited, + unreadCount, + }); + } +} diff --git a/src/debug/jtag/commands/social/notifications/shared/SocialNotificationsCommand.ts b/src/debug/jtag/commands/social/notifications/shared/SocialNotificationsCommand.ts new file mode 100644 index 000000000..6645b547c --- /dev/null +++ b/src/debug/jtag/commands/social/notifications/shared/SocialNotificationsCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Notifications Command - Shared base class + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { SocialNotificationsParams, SocialNotificationsResult } from './SocialNotificationsTypes'; +import type { JTAGContext, JTAGPayload } from '@system/core/types/JTAGTypes'; + +export abstract class SocialNotificationsBaseCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/notifications', context, subpath, commander); + } + + protected abstract executeSocialNotifications(params: SocialNotificationsParams): Promise; + + async execute(params: JTAGPayload): Promise { + return this.executeSocialNotifications(params as SocialNotificationsParams); + } +} diff --git a/src/debug/jtag/commands/social/notifications/shared/SocialNotificationsTypes.ts b/src/debug/jtag/commands/social/notifications/shared/SocialNotificationsTypes.ts new file mode 100644 index 000000000..60476251c --- /dev/null +++ b/src/debug/jtag/commands/social/notifications/shared/SocialNotificationsTypes.ts @@ -0,0 +1,111 @@ +/** + * Social Notifications Command - Shared Types + * + * Check for unread notifications (replies, mentions, followers) on a social media platform. + * Key data source for SocialMediaRAGSource β€” personas become aware of social activity through this. + * + * Usage: + * ./jtag social/notifications --platform=moltbook + * ./jtag social/notifications --platform=moltbook --since=2026-01-30T00:00:00Z + */ + +import type { CommandParams, CommandResult, CommandInput, JTAGContext } from '@system/core/types/JTAGTypes'; +import { createPayload, transformPayload } from '@system/core/types/JTAGTypes'; +import { Commands } from '@system/core/shared/Commands'; +import type { JTAGError } from '@system/core/types/ErrorTypes'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialNotification } from '@system/social/shared/SocialMediaTypes'; + +/** + * Social Notifications Command Parameters + */ +export interface SocialNotificationsParams extends CommandParams { + /** Platform to check (e.g., 'moltbook') */ + platform: string; + + /** ISO timestamp to fetch notifications since */ + since?: string; + + /** Maximum number of notifications to return */ + limit?: number; + + /** Persona user ID (auto-detected if not provided) */ + personaId?: UUID; +} + +/** + * Factory function for creating SocialNotificationsParams + */ +export const createSocialNotificationsParams = ( + context: JTAGContext, + sessionId: UUID, + data: { + platform: string; + since?: string; + limit?: number; + personaId?: UUID; + } +): SocialNotificationsParams => createPayload(context, sessionId, { + since: data.since ?? '', + limit: data.limit ?? 0, + personaId: data.personaId ?? undefined, + ...data +}); + +/** + * Social Notifications Command Result + */ +export interface SocialNotificationsResult extends CommandResult { + success: boolean; + message: string; + + /** Array of notifications */ + notifications?: SocialNotification[]; + + /** Count of unread notifications */ + unreadCount?: number; + + error?: JTAGError; +} + +/** + * Factory function for creating SocialNotificationsResult with defaults + */ +export const createSocialNotificationsResult = ( + context: JTAGContext, + sessionId: UUID, + data: { + success: boolean; + message?: string; + notifications?: SocialNotification[]; + unreadCount?: number; + error?: JTAGError; + } +): SocialNotificationsResult => createPayload(context, sessionId, { + message: data.message ?? '', + unreadCount: data.unreadCount ?? 0, + ...data +}); + +/** + * Smart Social Notifications-specific inheritance from params + * Auto-inherits context and sessionId from params + */ +export const createSocialNotificationsResultFromParams = ( + params: SocialNotificationsParams, + differences: Omit +): SocialNotificationsResult => transformPayload(params, differences); + +/** + * SocialNotifications β€” Type-safe command executor + * + * Usage: + * import { SocialNotifications } from '...shared/SocialNotificationsTypes'; + * const result = await SocialNotifications.execute({ platform: 'moltbook' }); + */ +export const SocialNotifications = { + execute(params: CommandInput): Promise { + return Commands.execute('social/notifications', params as Partial); + }, + commandName: 'social/notifications' as const, +} as const; diff --git a/src/debug/jtag/commands/social/notifications/test/integration/SocialNotificationsIntegration.test.ts b/src/debug/jtag/commands/social/notifications/test/integration/SocialNotificationsIntegration.test.ts new file mode 100644 index 000000000..6aa7a8eb6 --- /dev/null +++ b/src/debug/jtag/commands/social/notifications/test/integration/SocialNotificationsIntegration.test.ts @@ -0,0 +1,196 @@ +#!/usr/bin/env tsx +/** + * SocialNotifications Command Integration Tests + * + * Tests Social Notifications command against the LIVE RUNNING SYSTEM. + * This is NOT a mock test - it tests real commands, real events, real widgets. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Notifications/test/integration/SocialNotificationsIntegration.test.ts + * + * PREREQUISITES: + * - Server must be running: npm start (wait 90+ seconds) + * - Browser client connected via http://localhost:9003 + */ + +import { jtag } from '@server/server-index'; + +console.log('πŸ§ͺ SocialNotifications Command Integration Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Test 1: Connect to live system + */ +async function testSystemConnection(): Promise>> { + console.log('\nπŸ”Œ Test 1: Connecting to live JTAG system'); + + const client = await jtag.connect(); + + assert(client !== null, 'Connected to live system'); + console.log(' βœ… Connected successfully'); + + return client; +} + +/** + * Test 2: Execute Social Notifications command on live system + */ +async function testCommandExecution(client: Awaited>): Promise { + console.log('\n⚑ Test 2: Executing Social Notifications command'); + + // TODO: Replace with your actual command parameters + const result = await client.commands['Social Notifications']({ + // Add your required parameters here + // Example: name: 'test-value' + }); + + console.log(' πŸ“Š Result:', JSON.stringify(result, null, 2)); + + assert(result !== null, 'Social Notifications returned result'); + // TODO: Add assertions for your specific result fields + // assert(result.success === true, 'Social Notifications succeeded'); + // assert(result.yourField !== undefined, 'Result has yourField'); +} + +/** + * Test 3: Validate required parameters + */ +async function testRequiredParameters(_client: Awaited>): Promise { + console.log('\n🚨 Test 3: Testing required parameter validation'); + + // TODO: Uncomment and test missing required parameters + // try { + // await _client.commands['Social Notifications']({ + // // Missing required param + // }); + // assert(false, 'Should have thrown validation error'); + // } catch (error) { + // assert((error as Error).message.includes('required'), 'Error mentions required parameter'); + // console.log(' βœ… ValidationError thrown correctly'); + // } + + console.log(' ⚠️ TODO: Add required parameter validation test'); +} + +/** + * Test 4: Test optional parameters + */ +async function testOptionalParameters(_client: Awaited>): Promise { + console.log('\nπŸ”§ Test 4: Testing optional parameters'); + + // TODO: Uncomment to test with and without optional parameters + // const withOptional = await client.commands['Social Notifications']({ + // requiredParam: 'test', + // optionalParam: true + // }); + // + // const withoutOptional = await client.commands['Social Notifications']({ + // requiredParam: 'test' + // }); + // + // assert(withOptional.success === true, 'Works with optional params'); + // assert(withoutOptional.success === true, 'Works without optional params'); + + console.log(' ⚠️ TODO: Add optional parameter tests'); +} + +/** + * Test 5: Performance test + */ +async function testPerformance(_client: Awaited>): Promise { + console.log('\n⚑ Test 5: Performance under load'); + + // TODO: Uncomment to test command performance + // const iterations = 10; + // const times: number[] = []; + // + // for (let i = 0; i < iterations; i++) { + // const start = Date.now(); + // await _client.commands['Social Notifications']({ /* params */ }); + // times.push(Date.now() - start); + // } + // + // const avg = times.reduce((a, b) => a + b, 0) / iterations; + // const max = Math.max(...times); + // + // console.log(` Average: ${avg.toFixed(2)}ms`); + // console.log(` Max: ${max}ms`); + // + // assert(avg < 500, `Average ${avg.toFixed(2)}ms under 500ms`); + // assert(max < 1000, `Max ${max}ms under 1000ms`); + + console.log(' ⚠️ TODO: Add performance test'); +} + +/** + * Test 6: Widget/Event integration (if applicable) + */ +async function testWidgetIntegration(_client: Awaited>): Promise { + console.log('\n🎨 Test 6: Widget/Event integration'); + + // TODO: Uncomment if your command emits events or updates widgets + // Example: + // const before = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // await client.commands['Social Notifications']({ /* params */ }); + // await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for event propagation + // const after = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // + // assert(after.state.someValue !== before.state.someValue, 'Widget state updated'); + + console.log(' ⚠️ TODO: Add widget/event integration test (if applicable)'); +} + +/** + * Run all integration tests + */ +async function runAllSocialNotificationsIntegrationTests(): Promise { + console.log('πŸš€ Starting SocialNotifications Integration Tests\n'); + console.log('πŸ“‹ Testing against LIVE system (not mocks)\n'); + + try { + const client = await testSystemConnection(); + await testCommandExecution(client); + await testRequiredParameters(client); + await testOptionalParameters(client); + await testPerformance(client); + await testWidgetIntegration(client); + + console.log('\nπŸŽ‰ ALL SocialNotifications INTEGRATION TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Live system connection'); + console.log(' βœ… Command execution on real system'); + console.log(' βœ… Parameter validation'); + console.log(' βœ… Optional parameter handling'); + console.log(' βœ… Performance benchmarks'); + console.log(' βœ… Widget/Event integration'); + console.log('\nπŸ’‘ NOTE: This test uses the REAL running system'); + console.log(' - Real database operations'); + console.log(' - Real event propagation'); + console.log(' - Real widget updates'); + console.log(' - Real cross-daemon communication'); + + } catch (error) { + console.error('\n❌ SocialNotifications integration tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + console.error('\nπŸ’‘ Make sure:'); + console.error(' 1. Server is running: npm start'); + console.error(' 2. Wait 90+ seconds for deployment'); + console.error(' 3. Browser is connected to http://localhost:9003'); + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialNotificationsIntegrationTests(); +} else { + module.exports = { runAllSocialNotificationsIntegrationTests }; +} diff --git a/src/debug/jtag/commands/social/notifications/test/unit/SocialNotificationsCommand.test.ts b/src/debug/jtag/commands/social/notifications/test/unit/SocialNotificationsCommand.test.ts new file mode 100644 index 000000000..0e6b95999 --- /dev/null +++ b/src/debug/jtag/commands/social/notifications/test/unit/SocialNotificationsCommand.test.ts @@ -0,0 +1,259 @@ +#!/usr/bin/env tsx +/** + * SocialNotifications Command Unit Tests + * + * Tests Social Notifications command logic in isolation using mock dependencies. + * This is a REFERENCE EXAMPLE showing best practices for command testing. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Notifications/test/unit/SocialNotificationsCommand.test.ts + * + * NOTE: This is a self-contained test (no external test utilities needed). + * Use this as a template for your own command tests. + */ + +// import { ValidationError } from '@system/core/types/ErrorTypes'; // Uncomment when adding validation tests +import { generateUUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialNotificationsParams, SocialNotificationsResult } from '../../shared/SocialNotificationsTypes'; + +console.log('πŸ§ͺ SocialNotifications Command Unit Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Mock command that implements Social Notifications logic for testing + */ +async function mockSocialNotificationsCommand(params: SocialNotificationsParams): Promise { + // TODO: Validate required parameters (BEST PRACTICE) + // Example: + // if (!params.requiredParam || params.requiredParam.trim() === '') { + // throw new ValidationError( + // 'requiredParam', + // `Missing required parameter 'requiredParam'. ` + + // `Use the help tool with 'Social Notifications' or see the Social Notifications README for usage information.` + // ); + // } + + // TODO: Handle optional parameters with sensible defaults + // const optionalParam = params.optionalParam ?? defaultValue; + + // TODO: Implement your command logic here + return { + success: true, + // TODO: Add your result fields with actual computed values + context: params.context, + sessionId: params.sessionId + } as SocialNotificationsResult; +} + +/** + * Test 1: Command structure validation + */ +function testSocialNotificationsCommandStructure(): void { + console.log('\nπŸ“‹ Test 1: SocialNotifications command structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Create valid params for Social Notifications command + const validParams: SocialNotificationsParams = { + // TODO: Add your required parameters here + context, + sessionId + }; + + // Validate param structure + assert(validParams.context !== undefined, 'Params have context'); + assert(validParams.sessionId !== undefined, 'Params have sessionId'); + // TODO: Add assertions for your specific parameters + // assert(typeof validParams.requiredParam === 'string', 'requiredParam is string'); +} + +/** + * Test 2: Mock command execution + */ +async function testMockSocialNotificationsExecution(): Promise { + console.log('\n⚑ Test 2: Mock Social Notifications command execution'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test mock execution + const params: SocialNotificationsParams = { + // TODO: Add your parameters here + context, + sessionId + }; + + const result = await mockSocialNotificationsCommand(params); + + // Validate result structure + assert(result.success === true, 'Mock result shows success'); + // TODO: Add assertions for your result fields + // assert(typeof result.yourField === 'string', 'yourField is string'); +} + +/** + * Test 3: Required parameter validation (CRITICAL) + * + * This test ensures your command throws ValidationError + * when required parameters are missing (BEST PRACTICE) + */ +async function testSocialNotificationsRequiredParams(): Promise { + console.log('\n🚨 Test 3: Required parameter validation'); + + // TODO: Uncomment when implementing validation + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test cases that should throw ValidationError + // Example: + // const testCases = [ + // { params: {} as SocialNotificationsParams, desc: 'Missing requiredParam' }, + // { params: { requiredParam: '' } as SocialNotificationsParams, desc: 'Empty requiredParam' }, + // ]; + // + // for (const testCase of testCases) { + // try { + // await mockSocialNotificationsCommand({ ...testCase.params, context, sessionId }); + // throw new Error(`Should have thrown ValidationError for: ${testCase.desc}`); + // } catch (error) { + // if (error instanceof ValidationError) { + // assert(error.field === 'requiredParam', `ValidationError field is 'requiredParam' for: ${testCase.desc}`); + // assert(error.message.includes('required parameter'), `Error message mentions 'required parameter' for: ${testCase.desc}`); + // assert(error.message.includes('help tool'), `Error message is tool-agnostic for: ${testCase.desc}`); + // } else { + // throw error; // Re-throw if not ValidationError + // } + // } + // } + + console.log('βœ… All required parameter validations work correctly'); +} + +/** + * Test 4: Optional parameter handling + */ +async function testSocialNotificationsOptionalParams(): Promise { + console.log('\nπŸ”§ Test 4: Optional parameter handling'); + + // TODO: Uncomment when implementing optional param tests + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test WITHOUT optional param (should use default) + // const paramsWithoutOptional: SocialNotificationsParams = { + // requiredParam: 'test', + // context, + // sessionId + // }; + // + // const resultWithoutOptional = await mockSocialNotificationsCommand(paramsWithoutOptional); + // assert(resultWithoutOptional.success === true, 'Command succeeds without optional params'); + + // TODO: Test WITH optional param + // const paramsWithOptional: SocialNotificationsParams = { + // requiredParam: 'test', + // optionalParam: true, + // context, + // sessionId + // }; + // + // const resultWithOptional = await mockSocialNotificationsCommand(paramsWithOptional); + // assert(resultWithOptional.success === true, 'Command succeeds with optional params'); + + console.log('βœ… Optional parameter handling validated'); +} + +/** + * Test 5: Performance validation + */ +async function testSocialNotificationsPerformance(): Promise { + console.log('\n⚑ Test 5: SocialNotifications performance validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + const startTime = Date.now(); + + await mockSocialNotificationsCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialNotificationsParams); + + const executionTime = Date.now() - startTime; + + assert(executionTime < 100, `SocialNotifications completed in ${executionTime}ms (under 100ms limit)`); +} + +/** + * Test 6: Result structure validation + */ +async function testSocialNotificationsResultStructure(): Promise { + console.log('\nπŸ” Test 6: SocialNotifications result structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test various scenarios + const basicResult = await mockSocialNotificationsCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialNotificationsParams); + + assert(basicResult.success === true, 'Result has success field'); + // TODO: Add assertions for your result fields + // assert(typeof basicResult.yourField === 'string', 'Result has yourField (string)'); + assert(basicResult.context === context, 'Result includes context'); + assert(basicResult.sessionId === sessionId, 'Result includes sessionId'); + + console.log('βœ… All result structure validations pass'); +} + +/** + * Run all unit tests + */ +async function runAllSocialNotificationsUnitTests(): Promise { + console.log('πŸš€ Starting SocialNotifications Command Unit Tests\n'); + + try { + testSocialNotificationsCommandStructure(); + await testMockSocialNotificationsExecution(); + await testSocialNotificationsRequiredParams(); + await testSocialNotificationsOptionalParams(); + await testSocialNotificationsPerformance(); + await testSocialNotificationsResultStructure(); + + console.log('\nπŸŽ‰ ALL SocialNotifications UNIT TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Command structure and parameter validation'); + console.log(' βœ… Mock command execution patterns'); + console.log(' βœ… Required parameter validation (throws ValidationError)'); + console.log(' βœ… Optional parameter handling (sensible defaults)'); + console.log(' βœ… Performance requirements (< 100ms)'); + console.log(' βœ… Result structure validation'); + console.log('\nπŸ“ This is a REFERENCE EXAMPLE - use as a template for your commands!'); + console.log('πŸ’‘ TIP: Copy this test structure and modify for your command logic'); + + } catch (error) { + console.error('\n❌ SocialNotifications unit tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialNotificationsUnitTests(); +} else { + module.exports = { runAllSocialNotificationsUnitTests }; +} diff --git a/src/debug/jtag/commands/social/post/.npmignore b/src/debug/jtag/commands/social/post/.npmignore new file mode 100644 index 000000000..f74ad6b8a --- /dev/null +++ b/src/debug/jtag/commands/social/post/.npmignore @@ -0,0 +1,20 @@ +# Development files +.eslintrc* +tsconfig*.json +vitest.config.ts + +# Build artifacts +*.js.map +*.d.ts.map + +# IDE +.vscode/ +.idea/ + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db diff --git a/src/debug/jtag/commands/social/post/README.md b/src/debug/jtag/commands/social/post/README.md new file mode 100644 index 000000000..b98d46365 --- /dev/null +++ b/src/debug/jtag/commands/social/post/README.md @@ -0,0 +1,159 @@ +# Social Post Command + +Create a post on a social media platform using the persona's stored credentials. + +## Table of Contents + +- [Usage](#usage) + - [CLI Usage](#cli-usage) + - [Tool Usage](#tool-usage) +- [Parameters](#parameters) +- [Result](#result) +- [Examples](#examples) +- [Testing](#testing) + - [Unit Tests](#unit-tests) + - [Integration Tests](#integration-tests) +- [Getting Help](#getting-help) +- [Access Level](#access-level) +- [Implementation Notes](#implementation-notes) + +## Usage + +### CLI Usage + +From the command line using the jtag CLI: + +```bash +./jtag social/post --platform= --title= --content= +``` + +### Tool Usage + +From Persona tools or programmatic access using `Commands.execute()`: + +```typescript +import { Commands } from '@system/core/shared/Commands'; + +const result = await Commands.execute('social/post', { + // your parameters here +}); +``` + +## Parameters + +- **platform** (required): `string` - Platform to post on (e.g., 'moltbook') +- **title** (required): `string` - Post title +- **content** (required): `string` - Post content/body +- **community** (optional): `string` - Community/submolt to post in +- **url** (optional): `string` - URL for link posts +- **personaId** (optional): `UUID` - Persona user ID (auto-detected if not provided) + +## Result + +Returns `SocialPostResult` with: + +Returns CommandResult with: +- **message**: `string` - Human-readable result message +- **post**: `SocialPostData` - Created post details + +## Examples + +### Create a post on Moltbook + +```bash +./jtag social/post --platform=moltbook --title="Hello" --content="First post" --community=general +``` + +**Expected result:** +{ success: true, post: { id: '...', title: 'Hello' } } + +## Getting Help + +### Using the Help Tool + +Get detailed usage information for this command: + +**CLI:** +```bash +./jtag help social/post +``` + +**Tool:** +```typescript +// Use your help tool with command name 'social/post' +``` + +### Using the README Tool + +Access this README programmatically: + +**CLI:** +```bash +./jtag readme social/post +``` + +**Tool:** +```typescript +// Use your readme tool with command name 'social/post' +``` + +## Testing + +### Unit Tests + +Test command logic in isolation using mock dependencies: + +```bash +# Run unit tests (no server required) +npx tsx commands/social/post/test/unit/SocialPostCommand.test.ts +``` + +**What's tested:** +- Command structure and parameter validation +- Mock command execution patterns +- Required parameter validation (throws ValidationError) +- Optional parameter handling (sensible defaults) +- Performance requirements +- Assertion utility helpers + +**TDD Workflow:** +1. Write/modify unit test first (test-driven development) +2. Run test, see it fail +3. Implement feature +4. Run test, see it pass +5. Refactor if needed + +### Integration Tests + +Test command with real client connections and system integration: + +```bash +# Prerequisites: Server must be running +npm start # Wait 90+ seconds for deployment + +# Run integration tests +npx tsx commands/social/post/test/integration/SocialPostIntegration.test.ts +``` + +**What's tested:** +- Client connection to live system +- Real command execution via WebSocket +- ValidationError handling for missing params +- Optional parameter defaults +- Performance under load +- Various parameter combinations + +**Best Practice:** +Run unit tests frequently during development (fast feedback). Run integration tests before committing (verify system integration). + +## Access Level + +**ai-safe** - Safe for AI personas to call autonomously + +## Implementation Notes + +- **Shared Logic**: Core business logic in `shared/SocialPostTypes.ts` +- **Browser**: Browser-specific implementation in `browser/SocialPostBrowserCommand.ts` +- **Server**: Server-specific implementation in `server/SocialPostServerCommand.ts` +- **Unit Tests**: Isolated testing in `test/unit/SocialPostCommand.test.ts` +- **Integration Tests**: System testing in `test/integration/SocialPostIntegration.test.ts` diff --git a/src/debug/jtag/commands/social/post/browser/SocialPostBrowserCommand.ts b/src/debug/jtag/commands/social/post/browser/SocialPostBrowserCommand.ts new file mode 100644 index 000000000..245008548 --- /dev/null +++ b/src/debug/jtag/commands/social/post/browser/SocialPostBrowserCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Post Command - Browser Implementation + * Delegates to server + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialPostBaseCommand } from '../shared/SocialPostCommand'; +import type { SocialPostParams, SocialPostResult } from '../shared/SocialPostTypes'; + +export class SocialPostBrowserCommand extends SocialPostBaseCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialPost(params: SocialPostParams): Promise { + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/social/post/package.json b/src/debug/jtag/commands/social/post/package.json new file mode 100644 index 000000000..4954950c7 --- /dev/null +++ b/src/debug/jtag/commands/social/post/package.json @@ -0,0 +1,35 @@ +{ + "name": "@jtag-commands/social/post", + "version": "1.0.0", + "description": "Create a post on a social media platform using the persona's stored credentials.", + "main": "server/SocialPostServerCommand.ts", + "types": "shared/SocialPostTypes.ts", + "scripts": { + "test": "npm run test:unit && npm run test:integration", + "test:unit": "npx vitest run test/unit/*.test.ts", + "test:integration": "npx tsx test/integration/SocialPostIntegration.test.ts", + "lint": "npx eslint **/*.ts", + "typecheck": "npx tsc --noEmit" + }, + "peerDependencies": { + "@jtag/core": "*" + }, + "files": [ + "shared/**/*.ts", + "browser/**/*.ts", + "server/**/*.ts", + "test/**/*.ts", + "README.md" + ], + "keywords": [ + "jtag", + "command", + "social/post" + ], + "license": "MIT", + "author": "", + "repository": { + "type": "git", + "url": "" + } +} diff --git a/src/debug/jtag/commands/social/post/server/SocialPostServerCommand.ts b/src/debug/jtag/commands/social/post/server/SocialPostServerCommand.ts new file mode 100644 index 000000000..af0fa259b --- /dev/null +++ b/src/debug/jtag/commands/social/post/server/SocialPostServerCommand.ts @@ -0,0 +1,46 @@ +/** + * Social Post Command - Server Implementation + * + * Creates a post on a social media platform using the persona's stored credentials. + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import { transformPayload } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialPostBaseCommand } from '../shared/SocialPostCommand'; +import type { SocialPostParams, SocialPostResult } from '../shared/SocialPostTypes'; +import { loadSocialContext } from '@system/social/server/SocialCommandHelper'; + +export class SocialPostServerCommand extends SocialPostBaseCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialPost(params: SocialPostParams): Promise { + const { platform, title, content, community, url } = params; + + if (!platform) throw new Error('platform is required'); + if (!title) throw new Error('title is required'); + if (!content) throw new Error('content is required'); + + const ctx = await loadSocialContext(platform, params.personaId, params); + + // Check rate limit before posting + const rateCheck = ctx.provider.checkRateLimit('post'); + if (!rateCheck.allowed) { + return transformPayload(params, { + success: false, + message: rateCheck.message ?? 'Rate limited for posts', + }); + } + + const post = await ctx.provider.createPost({ title, content, community, url }); + + return transformPayload(params, { + success: true, + message: `Posted to ${platform}${community ? ` in ${community}` : ''}: "${title}"`, + post, + }); + } +} diff --git a/src/debug/jtag/commands/social/post/shared/SocialPostCommand.ts b/src/debug/jtag/commands/social/post/shared/SocialPostCommand.ts new file mode 100644 index 000000000..4bccda10e --- /dev/null +++ b/src/debug/jtag/commands/social/post/shared/SocialPostCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Post Command - Shared base class + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { SocialPostParams, SocialPostResult } from './SocialPostTypes'; +import type { JTAGContext, JTAGPayload } from '@system/core/types/JTAGTypes'; + +export abstract class SocialPostBaseCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/post', context, subpath, commander); + } + + protected abstract executeSocialPost(params: SocialPostParams): Promise; + + async execute(params: JTAGPayload): Promise { + return this.executeSocialPost(params as SocialPostParams); + } +} diff --git a/src/debug/jtag/commands/social/post/shared/SocialPostTypes.ts b/src/debug/jtag/commands/social/post/shared/SocialPostTypes.ts new file mode 100644 index 000000000..8c2029383 --- /dev/null +++ b/src/debug/jtag/commands/social/post/shared/SocialPostTypes.ts @@ -0,0 +1,112 @@ +/** + * Social Post Command - Shared Types + * + * Create a post on a social media platform using the persona's stored credentials. + * + * Usage: + * ./jtag social/post --platform=moltbook --title="Hello" --content="First post" --community=general + */ + +import type { CommandParams, CommandResult, CommandInput, JTAGContext } from '@system/core/types/JTAGTypes'; +import { createPayload, transformPayload } from '@system/core/types/JTAGTypes'; +import { Commands } from '@system/core/shared/Commands'; +import type { JTAGError } from '@system/core/types/ErrorTypes'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialPost as SocialPostData } from '@system/social/shared/SocialMediaTypes'; + +/** + * Social Post Command Parameters + */ +export interface SocialPostParams extends CommandParams { + /** Platform to post on (e.g., 'moltbook') */ + platform: string; + + /** Post title */ + title: string; + + /** Post content/body */ + content: string; + + /** Community/submolt to post in (optional) */ + community?: string; + + /** URL for link posts (optional) */ + url?: string; + + /** Persona user ID (auto-detected if not provided) */ + personaId?: UUID; +} + +/** + * Factory function for creating SocialPostParams + */ +export const createSocialPostParams = ( + context: JTAGContext, + sessionId: UUID, + data: { + platform: string; + title: string; + content: string; + community?: string; + url?: string; + personaId?: UUID; + } +): SocialPostParams => createPayload(context, sessionId, { + community: data.community ?? '', + url: data.url ?? '', + personaId: data.personaId ?? undefined, + ...data +}); + +/** + * Social Post Command Result + */ +export interface SocialPostResult extends CommandResult { + success: boolean; + message: string; + + /** Created post details */ + post?: SocialPostData; + + error?: JTAGError; +} + +/** + * Factory function for creating SocialPostResult with defaults + */ +export const createSocialPostResult = ( + context: JTAGContext, + sessionId: UUID, + data: { + success: boolean; + message?: string; + post?: SocialPostData; + error?: JTAGError; + } +): SocialPostResult => createPayload(context, sessionId, { + message: data.message ?? '', + ...data +}); + +/** + * Smart Social Post-specific inheritance from params + * Auto-inherits context and sessionId from params + */ +export const createSocialPostResultFromParams = ( + params: SocialPostParams, + differences: Omit +): SocialPostResult => transformPayload(params, differences); + +/** + * SocialPost β€” Type-safe command executor + * + * Usage: + * import { SocialPost } from '...shared/SocialPostTypes'; + * const result = await SocialPost.execute({ platform: 'moltbook', title: '...', content: '...' }); + */ +export const SocialPost = { + execute(params: CommandInput): Promise { + return Commands.execute('social/post', params as Partial); + }, + commandName: 'social/post' as const, +} as const; diff --git a/src/debug/jtag/commands/social/post/test/integration/SocialPostIntegration.test.ts b/src/debug/jtag/commands/social/post/test/integration/SocialPostIntegration.test.ts new file mode 100644 index 000000000..bb716e659 --- /dev/null +++ b/src/debug/jtag/commands/social/post/test/integration/SocialPostIntegration.test.ts @@ -0,0 +1,196 @@ +#!/usr/bin/env tsx +/** + * SocialPost Command Integration Tests + * + * Tests Social Post command against the LIVE RUNNING SYSTEM. + * This is NOT a mock test - it tests real commands, real events, real widgets. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Post/test/integration/SocialPostIntegration.test.ts + * + * PREREQUISITES: + * - Server must be running: npm start (wait 90+ seconds) + * - Browser client connected via http://localhost:9003 + */ + +import { jtag } from '@server/server-index'; + +console.log('πŸ§ͺ SocialPost Command Integration Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Test 1: Connect to live system + */ +async function testSystemConnection(): Promise>> { + console.log('\nπŸ”Œ Test 1: Connecting to live JTAG system'); + + const client = await jtag.connect(); + + assert(client !== null, 'Connected to live system'); + console.log(' βœ… Connected successfully'); + + return client; +} + +/** + * Test 2: Execute Social Post command on live system + */ +async function testCommandExecution(client: Awaited>): Promise { + console.log('\n⚑ Test 2: Executing Social Post command'); + + // TODO: Replace with your actual command parameters + const result = await client.commands['Social Post']({ + // Add your required parameters here + // Example: name: 'test-value' + }); + + console.log(' πŸ“Š Result:', JSON.stringify(result, null, 2)); + + assert(result !== null, 'Social Post returned result'); + // TODO: Add assertions for your specific result fields + // assert(result.success === true, 'Social Post succeeded'); + // assert(result.yourField !== undefined, 'Result has yourField'); +} + +/** + * Test 3: Validate required parameters + */ +async function testRequiredParameters(_client: Awaited>): Promise { + console.log('\n🚨 Test 3: Testing required parameter validation'); + + // TODO: Uncomment and test missing required parameters + // try { + // await _client.commands['Social Post']({ + // // Missing required param + // }); + // assert(false, 'Should have thrown validation error'); + // } catch (error) { + // assert((error as Error).message.includes('required'), 'Error mentions required parameter'); + // console.log(' βœ… ValidationError thrown correctly'); + // } + + console.log(' ⚠️ TODO: Add required parameter validation test'); +} + +/** + * Test 4: Test optional parameters + */ +async function testOptionalParameters(_client: Awaited>): Promise { + console.log('\nπŸ”§ Test 4: Testing optional parameters'); + + // TODO: Uncomment to test with and without optional parameters + // const withOptional = await client.commands['Social Post']({ + // requiredParam: 'test', + // optionalParam: true + // }); + // + // const withoutOptional = await client.commands['Social Post']({ + // requiredParam: 'test' + // }); + // + // assert(withOptional.success === true, 'Works with optional params'); + // assert(withoutOptional.success === true, 'Works without optional params'); + + console.log(' ⚠️ TODO: Add optional parameter tests'); +} + +/** + * Test 5: Performance test + */ +async function testPerformance(_client: Awaited>): Promise { + console.log('\n⚑ Test 5: Performance under load'); + + // TODO: Uncomment to test command performance + // const iterations = 10; + // const times: number[] = []; + // + // for (let i = 0; i < iterations; i++) { + // const start = Date.now(); + // await _client.commands['Social Post']({ /* params */ }); + // times.push(Date.now() - start); + // } + // + // const avg = times.reduce((a, b) => a + b, 0) / iterations; + // const max = Math.max(...times); + // + // console.log(` Average: ${avg.toFixed(2)}ms`); + // console.log(` Max: ${max}ms`); + // + // assert(avg < 500, `Average ${avg.toFixed(2)}ms under 500ms`); + // assert(max < 1000, `Max ${max}ms under 1000ms`); + + console.log(' ⚠️ TODO: Add performance test'); +} + +/** + * Test 6: Widget/Event integration (if applicable) + */ +async function testWidgetIntegration(_client: Awaited>): Promise { + console.log('\n🎨 Test 6: Widget/Event integration'); + + // TODO: Uncomment if your command emits events or updates widgets + // Example: + // const before = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // await client.commands['Social Post']({ /* params */ }); + // await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for event propagation + // const after = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // + // assert(after.state.someValue !== before.state.someValue, 'Widget state updated'); + + console.log(' ⚠️ TODO: Add widget/event integration test (if applicable)'); +} + +/** + * Run all integration tests + */ +async function runAllSocialPostIntegrationTests(): Promise { + console.log('πŸš€ Starting SocialPost Integration Tests\n'); + console.log('πŸ“‹ Testing against LIVE system (not mocks)\n'); + + try { + const client = await testSystemConnection(); + await testCommandExecution(client); + await testRequiredParameters(client); + await testOptionalParameters(client); + await testPerformance(client); + await testWidgetIntegration(client); + + console.log('\nπŸŽ‰ ALL SocialPost INTEGRATION TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Live system connection'); + console.log(' βœ… Command execution on real system'); + console.log(' βœ… Parameter validation'); + console.log(' βœ… Optional parameter handling'); + console.log(' βœ… Performance benchmarks'); + console.log(' βœ… Widget/Event integration'); + console.log('\nπŸ’‘ NOTE: This test uses the REAL running system'); + console.log(' - Real database operations'); + console.log(' - Real event propagation'); + console.log(' - Real widget updates'); + console.log(' - Real cross-daemon communication'); + + } catch (error) { + console.error('\n❌ SocialPost integration tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + console.error('\nπŸ’‘ Make sure:'); + console.error(' 1. Server is running: npm start'); + console.error(' 2. Wait 90+ seconds for deployment'); + console.error(' 3. Browser is connected to http://localhost:9003'); + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialPostIntegrationTests(); +} else { + module.exports = { runAllSocialPostIntegrationTests }; +} diff --git a/src/debug/jtag/commands/social/post/test/unit/SocialPostCommand.test.ts b/src/debug/jtag/commands/social/post/test/unit/SocialPostCommand.test.ts new file mode 100644 index 000000000..8fc834df8 --- /dev/null +++ b/src/debug/jtag/commands/social/post/test/unit/SocialPostCommand.test.ts @@ -0,0 +1,259 @@ +#!/usr/bin/env tsx +/** + * SocialPost Command Unit Tests + * + * Tests Social Post command logic in isolation using mock dependencies. + * This is a REFERENCE EXAMPLE showing best practices for command testing. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Post/test/unit/SocialPostCommand.test.ts + * + * NOTE: This is a self-contained test (no external test utilities needed). + * Use this as a template for your own command tests. + */ + +// import { ValidationError } from '@system/core/types/ErrorTypes'; // Uncomment when adding validation tests +import { generateUUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialPostParams, SocialPostResult } from '../../shared/SocialPostTypes'; + +console.log('πŸ§ͺ SocialPost Command Unit Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Mock command that implements Social Post logic for testing + */ +async function mockSocialPostCommand(params: SocialPostParams): Promise { + // TODO: Validate required parameters (BEST PRACTICE) + // Example: + // if (!params.requiredParam || params.requiredParam.trim() === '') { + // throw new ValidationError( + // 'requiredParam', + // `Missing required parameter 'requiredParam'. ` + + // `Use the help tool with 'Social Post' or see the Social Post README for usage information.` + // ); + // } + + // TODO: Handle optional parameters with sensible defaults + // const optionalParam = params.optionalParam ?? defaultValue; + + // TODO: Implement your command logic here + return { + success: true, + // TODO: Add your result fields with actual computed values + context: params.context, + sessionId: params.sessionId + } as SocialPostResult; +} + +/** + * Test 1: Command structure validation + */ +function testSocialPostCommandStructure(): void { + console.log('\nπŸ“‹ Test 1: SocialPost command structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Create valid params for Social Post command + const validParams: SocialPostParams = { + // TODO: Add your required parameters here + context, + sessionId + }; + + // Validate param structure + assert(validParams.context !== undefined, 'Params have context'); + assert(validParams.sessionId !== undefined, 'Params have sessionId'); + // TODO: Add assertions for your specific parameters + // assert(typeof validParams.requiredParam === 'string', 'requiredParam is string'); +} + +/** + * Test 2: Mock command execution + */ +async function testMockSocialPostExecution(): Promise { + console.log('\n⚑ Test 2: Mock Social Post command execution'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test mock execution + const params: SocialPostParams = { + // TODO: Add your parameters here + context, + sessionId + }; + + const result = await mockSocialPostCommand(params); + + // Validate result structure + assert(result.success === true, 'Mock result shows success'); + // TODO: Add assertions for your result fields + // assert(typeof result.yourField === 'string', 'yourField is string'); +} + +/** + * Test 3: Required parameter validation (CRITICAL) + * + * This test ensures your command throws ValidationError + * when required parameters are missing (BEST PRACTICE) + */ +async function testSocialPostRequiredParams(): Promise { + console.log('\n🚨 Test 3: Required parameter validation'); + + // TODO: Uncomment when implementing validation + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test cases that should throw ValidationError + // Example: + // const testCases = [ + // { params: {} as SocialPostParams, desc: 'Missing requiredParam' }, + // { params: { requiredParam: '' } as SocialPostParams, desc: 'Empty requiredParam' }, + // ]; + // + // for (const testCase of testCases) { + // try { + // await mockSocialPostCommand({ ...testCase.params, context, sessionId }); + // throw new Error(`Should have thrown ValidationError for: ${testCase.desc}`); + // } catch (error) { + // if (error instanceof ValidationError) { + // assert(error.field === 'requiredParam', `ValidationError field is 'requiredParam' for: ${testCase.desc}`); + // assert(error.message.includes('required parameter'), `Error message mentions 'required parameter' for: ${testCase.desc}`); + // assert(error.message.includes('help tool'), `Error message is tool-agnostic for: ${testCase.desc}`); + // } else { + // throw error; // Re-throw if not ValidationError + // } + // } + // } + + console.log('βœ… All required parameter validations work correctly'); +} + +/** + * Test 4: Optional parameter handling + */ +async function testSocialPostOptionalParams(): Promise { + console.log('\nπŸ”§ Test 4: Optional parameter handling'); + + // TODO: Uncomment when implementing optional param tests + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test WITHOUT optional param (should use default) + // const paramsWithoutOptional: SocialPostParams = { + // requiredParam: 'test', + // context, + // sessionId + // }; + // + // const resultWithoutOptional = await mockSocialPostCommand(paramsWithoutOptional); + // assert(resultWithoutOptional.success === true, 'Command succeeds without optional params'); + + // TODO: Test WITH optional param + // const paramsWithOptional: SocialPostParams = { + // requiredParam: 'test', + // optionalParam: true, + // context, + // sessionId + // }; + // + // const resultWithOptional = await mockSocialPostCommand(paramsWithOptional); + // assert(resultWithOptional.success === true, 'Command succeeds with optional params'); + + console.log('βœ… Optional parameter handling validated'); +} + +/** + * Test 5: Performance validation + */ +async function testSocialPostPerformance(): Promise { + console.log('\n⚑ Test 5: SocialPost performance validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + const startTime = Date.now(); + + await mockSocialPostCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialPostParams); + + const executionTime = Date.now() - startTime; + + assert(executionTime < 100, `SocialPost completed in ${executionTime}ms (under 100ms limit)`); +} + +/** + * Test 6: Result structure validation + */ +async function testSocialPostResultStructure(): Promise { + console.log('\nπŸ” Test 6: SocialPost result structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test various scenarios + const basicResult = await mockSocialPostCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialPostParams); + + assert(basicResult.success === true, 'Result has success field'); + // TODO: Add assertions for your result fields + // assert(typeof basicResult.yourField === 'string', 'Result has yourField (string)'); + assert(basicResult.context === context, 'Result includes context'); + assert(basicResult.sessionId === sessionId, 'Result includes sessionId'); + + console.log('βœ… All result structure validations pass'); +} + +/** + * Run all unit tests + */ +async function runAllSocialPostUnitTests(): Promise { + console.log('πŸš€ Starting SocialPost Command Unit Tests\n'); + + try { + testSocialPostCommandStructure(); + await testMockSocialPostExecution(); + await testSocialPostRequiredParams(); + await testSocialPostOptionalParams(); + await testSocialPostPerformance(); + await testSocialPostResultStructure(); + + console.log('\nπŸŽ‰ ALL SocialPost UNIT TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Command structure and parameter validation'); + console.log(' βœ… Mock command execution patterns'); + console.log(' βœ… Required parameter validation (throws ValidationError)'); + console.log(' βœ… Optional parameter handling (sensible defaults)'); + console.log(' βœ… Performance requirements (< 100ms)'); + console.log(' βœ… Result structure validation'); + console.log('\nπŸ“ This is a REFERENCE EXAMPLE - use as a template for your commands!'); + console.log('πŸ’‘ TIP: Copy this test structure and modify for your command logic'); + + } catch (error) { + console.error('\n❌ SocialPost unit tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialPostUnitTests(); +} else { + module.exports = { runAllSocialPostUnitTests }; +} diff --git a/src/debug/jtag/commands/social/profile/.npmignore b/src/debug/jtag/commands/social/profile/.npmignore new file mode 100644 index 000000000..f74ad6b8a --- /dev/null +++ b/src/debug/jtag/commands/social/profile/.npmignore @@ -0,0 +1,20 @@ +# Development files +.eslintrc* +tsconfig*.json +vitest.config.ts + +# Build artifacts +*.js.map +*.d.ts.map + +# IDE +.vscode/ +.idea/ + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db diff --git a/src/debug/jtag/commands/social/profile/README.md b/src/debug/jtag/commands/social/profile/README.md new file mode 100644 index 000000000..0ab1ed37b --- /dev/null +++ b/src/debug/jtag/commands/social/profile/README.md @@ -0,0 +1,170 @@ +# Social Profile Command + +View or update a social media profile. View your own profile, another agent's profile, or update your bio/description. + +## Table of Contents + +- [Usage](#usage) + - [CLI Usage](#cli-usage) + - [Tool Usage](#tool-usage) +- [Parameters](#parameters) +- [Result](#result) +- [Examples](#examples) +- [Testing](#testing) + - [Unit Tests](#unit-tests) + - [Integration Tests](#integration-tests) +- [Getting Help](#getting-help) +- [Access Level](#access-level) +- [Implementation Notes](#implementation-notes) + +## Usage + +### CLI Usage + +From the command line using the jtag CLI: + +```bash +./jtag social/profile --platform= +``` + +### Tool Usage + +From Persona tools or programmatic access using `Commands.execute()`: + +```typescript +import { Commands } from '@system/core/shared/Commands'; + +const result = await Commands.execute('social/profile', { + // your parameters here +}); +``` + +## Parameters + +- **platform** (required): `string` - Platform to query (e.g., 'moltbook') +- **agentName** (optional): `string` - Agent name to look up (omit for own profile) +- **update** (optional): `boolean` - If true, update own profile instead of viewing +- **description** (optional): `string` - New profile description/bio (requires --update) +- **personaId** (optional): `string` - Persona user ID (auto-detected if not provided) + +## Result + +Returns `SocialProfileResult` with: + +Returns CommandResult with: +- **profile**: `SocialProfile` - The profile data (when viewing) +- **updated**: `boolean` - Whether profile was updated (when updating) + +## Examples + +### View your own profile + +```bash +./jtag social/profile --platform=moltbook +``` + +**Expected result:** +{ success: true, profile: { agentName: 'helper-ai', karma: 42, ... } } + +### View another agent's profile + +```bash +./jtag social/profile --platform=moltbook --agentName=other-agent +``` + +### Update your bio + +```bash +./jtag social/profile --platform=moltbook --update --description="I help with code" +``` + +## Getting Help + +### Using the Help Tool + +Get detailed usage information for this command: + +**CLI:** +```bash +./jtag help social/profile +``` + +**Tool:** +```typescript +// Use your help tool with command name 'social/profile' +``` + +### Using the README Tool + +Access this README programmatically: + +**CLI:** +```bash +./jtag readme social/profile +``` + +**Tool:** +```typescript +// Use your readme tool with command name 'social/profile' +``` + +## Testing + +### Unit Tests + +Test command logic in isolation using mock dependencies: + +```bash +# Run unit tests (no server required) +npx tsx commands/social/profile/test/unit/SocialProfileCommand.test.ts +``` + +**What's tested:** +- Command structure and parameter validation +- Mock command execution patterns +- Required parameter validation (throws ValidationError) +- Optional parameter handling (sensible defaults) +- Performance requirements +- Assertion utility helpers + +**TDD Workflow:** +1. Write/modify unit test first (test-driven development) +2. Run test, see it fail +3. Implement feature +4. Run test, see it pass +5. Refactor if needed + +### Integration Tests + +Test command with real client connections and system integration: + +```bash +# Prerequisites: Server must be running +npm start # Wait 90+ seconds for deployment + +# Run integration tests +npx tsx commands/social/profile/test/integration/SocialProfileIntegration.test.ts +``` + +**What's tested:** +- Client connection to live system +- Real command execution via WebSocket +- ValidationError handling for missing params +- Optional parameter defaults +- Performance under load +- Various parameter combinations + +**Best Practice:** +Run unit tests frequently during development (fast feedback). Run integration tests before committing (verify system integration). + +## Access Level + +**ai-safe** - Safe for AI personas to call autonomously + +## Implementation Notes + +- **Shared Logic**: Core business logic in `shared/SocialProfileTypes.ts` +- **Browser**: Browser-specific implementation in `browser/SocialProfileBrowserCommand.ts` +- **Server**: Server-specific implementation in `server/SocialProfileServerCommand.ts` +- **Unit Tests**: Isolated testing in `test/unit/SocialProfileCommand.test.ts` +- **Integration Tests**: System testing in `test/integration/SocialProfileIntegration.test.ts` diff --git a/src/debug/jtag/commands/social/profile/browser/SocialProfileBrowserCommand.ts b/src/debug/jtag/commands/social/profile/browser/SocialProfileBrowserCommand.ts new file mode 100644 index 000000000..b5df893c5 --- /dev/null +++ b/src/debug/jtag/commands/social/profile/browser/SocialProfileBrowserCommand.ts @@ -0,0 +1,19 @@ +/** + * Social Profile Command - Browser Implementation + * Delegates to server + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { SocialProfileParams, SocialProfileResult } from '../shared/SocialProfileTypes'; + +export class SocialProfileBrowserCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/profile', context, subpath, commander); + } + + async execute(params: SocialProfileParams): Promise { + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/social/profile/package.json b/src/debug/jtag/commands/social/profile/package.json new file mode 100644 index 000000000..28f3abdcf --- /dev/null +++ b/src/debug/jtag/commands/social/profile/package.json @@ -0,0 +1,35 @@ +{ + "name": "@jtag-commands/social/profile", + "version": "1.0.0", + "description": "View or update a social media profile. View your own profile, another agent's profile, or update your bio/description.", + "main": "server/SocialProfileServerCommand.ts", + "types": "shared/SocialProfileTypes.ts", + "scripts": { + "test": "npm run test:unit && npm run test:integration", + "test:unit": "npx vitest run test/unit/*.test.ts", + "test:integration": "npx tsx test/integration/SocialProfileIntegration.test.ts", + "lint": "npx eslint **/*.ts", + "typecheck": "npx tsc --noEmit" + }, + "peerDependencies": { + "@jtag/core": "*" + }, + "files": [ + "shared/**/*.ts", + "browser/**/*.ts", + "server/**/*.ts", + "test/**/*.ts", + "README.md" + ], + "keywords": [ + "jtag", + "command", + "social/profile" + ], + "license": "MIT", + "author": "", + "repository": { + "type": "git", + "url": "" + } +} diff --git a/src/debug/jtag/commands/social/profile/server/SocialProfileServerCommand.ts b/src/debug/jtag/commands/social/profile/server/SocialProfileServerCommand.ts new file mode 100644 index 000000000..b4f57023b --- /dev/null +++ b/src/debug/jtag/commands/social/profile/server/SocialProfileServerCommand.ts @@ -0,0 +1,48 @@ +/** + * Social Profile Command - Server Implementation + * + * View or update a social media profile. Supports viewing own profile, + * looking up another agent, or updating your bio/description. + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import { transformPayload } from '@system/core/types/JTAGTypes'; +import type { SocialProfileParams, SocialProfileResult } from '../shared/SocialProfileTypes'; +import { loadSocialContext } from '@system/social/server/SocialCommandHelper'; + +export class SocialProfileServerCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/profile', context, subpath, commander); + } + + async execute(params: SocialProfileParams): Promise { + const { platform, agentName, update, description } = params; + + if (!platform) throw new Error('platform is required'); + + const ctx = await loadSocialContext(platform, params.personaId, params); + + if (update) { + if (!description) throw new Error('description is required when using --update'); + + await ctx.provider.updateProfile({ description }); + + return transformPayload(params, { + success: true, + message: `Profile updated on ${platform}`, + updated: true, + }); + } + + const profile = await ctx.provider.getProfile(agentName); + + const target = agentName ? `@${agentName}` : 'your'; + return transformPayload(params, { + success: true, + message: `Fetched ${target} profile on ${platform}`, + profile, + }); + } +} diff --git a/src/debug/jtag/commands/social/profile/shared/SocialProfileTypes.ts b/src/debug/jtag/commands/social/profile/shared/SocialProfileTypes.ts new file mode 100644 index 000000000..217b32ce7 --- /dev/null +++ b/src/debug/jtag/commands/social/profile/shared/SocialProfileTypes.ts @@ -0,0 +1,115 @@ +/** + * Social Profile Command - Shared Types + * + * View or update a social media profile. View your own profile, another agent's profile, or update your bio/description. + * + * Usage: + * ./jtag social/profile --platform=moltbook + * ./jtag social/profile --platform=moltbook --agentName=other-agent + * ./jtag social/profile --platform=moltbook --update --description="New bio" + */ + +import type { CommandParams, CommandResult, CommandInput, JTAGContext } from '@system/core/types/JTAGTypes'; +import { createPayload, transformPayload } from '@system/core/types/JTAGTypes'; +import { Commands } from '@system/core/shared/Commands'; +import type { JTAGError } from '@system/core/types/ErrorTypes'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialProfile as SocialProfileData } from '@system/social/shared/SocialMediaTypes'; + +/** + * Social Profile Command Parameters + */ +export interface SocialProfileParams extends CommandParams { + /** Platform to query (e.g., 'moltbook') */ + platform: string; + + /** Agent name to look up (omit for own profile) */ + agentName?: string; + + /** If true, update own profile instead of viewing */ + update?: boolean; + + /** New profile description/bio (requires --update) */ + description?: string; + + /** Persona user ID (auto-detected if not provided) */ + personaId?: UUID; +} + +/** + * Factory function for creating SocialProfileParams + */ +export const createSocialProfileParams = ( + context: JTAGContext, + sessionId: UUID, + data: { + platform: string; + agentName?: string; + update?: boolean; + description?: string; + personaId?: UUID; + } +): SocialProfileParams => createPayload(context, sessionId, { + agentName: data.agentName ?? undefined, + update: data.update ?? false, + description: data.description ?? undefined, + personaId: data.personaId ?? undefined, + ...data +}); + +/** + * Social Profile Command Result + */ +export interface SocialProfileResult extends CommandResult { + success: boolean; + message: string; + + /** The profile data (when viewing) */ + profile?: SocialProfileData; + + /** Whether profile was updated (when updating) */ + updated?: boolean; + + error?: JTAGError; +} + +/** + * Factory function for creating SocialProfileResult with defaults + */ +export const createSocialProfileResult = ( + context: JTAGContext, + sessionId: UUID, + data: { + success: boolean; + message?: string; + profile?: SocialProfileData; + updated?: boolean; + error?: JTAGError; + } +): SocialProfileResult => createPayload(context, sessionId, { + message: data.message ?? '', + ...data +}); + +/** + * Smart Social Profile-specific inheritance from params + * Auto-inherits context and sessionId from params + */ +export const createSocialProfileResultFromParams = ( + params: SocialProfileParams, + differences: Omit +): SocialProfileResult => transformPayload(params, differences); + +/** + * SocialProfile β€” Type-safe command executor + * + * Usage: + * import { SocialProfile } from '...shared/SocialProfileTypes'; + * const result = await SocialProfile.execute({ platform: 'moltbook' }); + */ +export const SocialProfile = { + execute(params: CommandInput): Promise { + return Commands.execute('social/profile', params as Partial); + }, + commandName: 'social/profile' as const, +} as const; diff --git a/src/debug/jtag/commands/social/profile/test/integration/SocialProfileIntegration.test.ts b/src/debug/jtag/commands/social/profile/test/integration/SocialProfileIntegration.test.ts new file mode 100644 index 000000000..ae0933af4 --- /dev/null +++ b/src/debug/jtag/commands/social/profile/test/integration/SocialProfileIntegration.test.ts @@ -0,0 +1,196 @@ +#!/usr/bin/env tsx +/** + * SocialProfile Command Integration Tests + * + * Tests Social Profile command against the LIVE RUNNING SYSTEM. + * This is NOT a mock test - it tests real commands, real events, real widgets. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Profile/test/integration/SocialProfileIntegration.test.ts + * + * PREREQUISITES: + * - Server must be running: npm start (wait 90+ seconds) + * - Browser client connected via http://localhost:9003 + */ + +import { jtag } from '@server/server-index'; + +console.log('πŸ§ͺ SocialProfile Command Integration Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Test 1: Connect to live system + */ +async function testSystemConnection(): Promise>> { + console.log('\nπŸ”Œ Test 1: Connecting to live JTAG system'); + + const client = await jtag.connect(); + + assert(client !== null, 'Connected to live system'); + console.log(' βœ… Connected successfully'); + + return client; +} + +/** + * Test 2: Execute Social Profile command on live system + */ +async function testCommandExecution(client: Awaited>): Promise { + console.log('\n⚑ Test 2: Executing Social Profile command'); + + // TODO: Replace with your actual command parameters + const result = await client.commands['Social Profile']({ + // Add your required parameters here + // Example: name: 'test-value' + }); + + console.log(' πŸ“Š Result:', JSON.stringify(result, null, 2)); + + assert(result !== null, 'Social Profile returned result'); + // TODO: Add assertions for your specific result fields + // assert(result.success === true, 'Social Profile succeeded'); + // assert(result.yourField !== undefined, 'Result has yourField'); +} + +/** + * Test 3: Validate required parameters + */ +async function testRequiredParameters(_client: Awaited>): Promise { + console.log('\n🚨 Test 3: Testing required parameter validation'); + + // TODO: Uncomment and test missing required parameters + // try { + // await _client.commands['Social Profile']({ + // // Missing required param + // }); + // assert(false, 'Should have thrown validation error'); + // } catch (error) { + // assert((error as Error).message.includes('required'), 'Error mentions required parameter'); + // console.log(' βœ… ValidationError thrown correctly'); + // } + + console.log(' ⚠️ TODO: Add required parameter validation test'); +} + +/** + * Test 4: Test optional parameters + */ +async function testOptionalParameters(_client: Awaited>): Promise { + console.log('\nπŸ”§ Test 4: Testing optional parameters'); + + // TODO: Uncomment to test with and without optional parameters + // const withOptional = await client.commands['Social Profile']({ + // requiredParam: 'test', + // optionalParam: true + // }); + // + // const withoutOptional = await client.commands['Social Profile']({ + // requiredParam: 'test' + // }); + // + // assert(withOptional.success === true, 'Works with optional params'); + // assert(withoutOptional.success === true, 'Works without optional params'); + + console.log(' ⚠️ TODO: Add optional parameter tests'); +} + +/** + * Test 5: Performance test + */ +async function testPerformance(_client: Awaited>): Promise { + console.log('\n⚑ Test 5: Performance under load'); + + // TODO: Uncomment to test command performance + // const iterations = 10; + // const times: number[] = []; + // + // for (let i = 0; i < iterations; i++) { + // const start = Date.now(); + // await _client.commands['Social Profile']({ /* params */ }); + // times.push(Date.now() - start); + // } + // + // const avg = times.reduce((a, b) => a + b, 0) / iterations; + // const max = Math.max(...times); + // + // console.log(` Average: ${avg.toFixed(2)}ms`); + // console.log(` Max: ${max}ms`); + // + // assert(avg < 500, `Average ${avg.toFixed(2)}ms under 500ms`); + // assert(max < 1000, `Max ${max}ms under 1000ms`); + + console.log(' ⚠️ TODO: Add performance test'); +} + +/** + * Test 6: Widget/Event integration (if applicable) + */ +async function testWidgetIntegration(_client: Awaited>): Promise { + console.log('\n🎨 Test 6: Widget/Event integration'); + + // TODO: Uncomment if your command emits events or updates widgets + // Example: + // const before = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // await client.commands['Social Profile']({ /* params */ }); + // await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for event propagation + // const after = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // + // assert(after.state.someValue !== before.state.someValue, 'Widget state updated'); + + console.log(' ⚠️ TODO: Add widget/event integration test (if applicable)'); +} + +/** + * Run all integration tests + */ +async function runAllSocialProfileIntegrationTests(): Promise { + console.log('πŸš€ Starting SocialProfile Integration Tests\n'); + console.log('πŸ“‹ Testing against LIVE system (not mocks)\n'); + + try { + const client = await testSystemConnection(); + await testCommandExecution(client); + await testRequiredParameters(client); + await testOptionalParameters(client); + await testPerformance(client); + await testWidgetIntegration(client); + + console.log('\nπŸŽ‰ ALL SocialProfile INTEGRATION TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Live system connection'); + console.log(' βœ… Command execution on real system'); + console.log(' βœ… Parameter validation'); + console.log(' βœ… Optional parameter handling'); + console.log(' βœ… Performance benchmarks'); + console.log(' βœ… Widget/Event integration'); + console.log('\nπŸ’‘ NOTE: This test uses the REAL running system'); + console.log(' - Real database operations'); + console.log(' - Real event propagation'); + console.log(' - Real widget updates'); + console.log(' - Real cross-daemon communication'); + + } catch (error) { + console.error('\n❌ SocialProfile integration tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + console.error('\nπŸ’‘ Make sure:'); + console.error(' 1. Server is running: npm start'); + console.error(' 2. Wait 90+ seconds for deployment'); + console.error(' 3. Browser is connected to http://localhost:9003'); + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialProfileIntegrationTests(); +} else { + module.exports = { runAllSocialProfileIntegrationTests }; +} diff --git a/src/debug/jtag/commands/social/profile/test/unit/SocialProfileCommand.test.ts b/src/debug/jtag/commands/social/profile/test/unit/SocialProfileCommand.test.ts new file mode 100644 index 000000000..05da7b3c0 --- /dev/null +++ b/src/debug/jtag/commands/social/profile/test/unit/SocialProfileCommand.test.ts @@ -0,0 +1,259 @@ +#!/usr/bin/env tsx +/** + * SocialProfile Command Unit Tests + * + * Tests Social Profile command logic in isolation using mock dependencies. + * This is a REFERENCE EXAMPLE showing best practices for command testing. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Profile/test/unit/SocialProfileCommand.test.ts + * + * NOTE: This is a self-contained test (no external test utilities needed). + * Use this as a template for your own command tests. + */ + +// import { ValidationError } from '@system/core/types/ErrorTypes'; // Uncomment when adding validation tests +import { generateUUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialProfileParams, SocialProfileResult } from '../../shared/SocialProfileTypes'; + +console.log('πŸ§ͺ SocialProfile Command Unit Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Mock command that implements Social Profile logic for testing + */ +async function mockSocialProfileCommand(params: SocialProfileParams): Promise { + // TODO: Validate required parameters (BEST PRACTICE) + // Example: + // if (!params.requiredParam || params.requiredParam.trim() === '') { + // throw new ValidationError( + // 'requiredParam', + // `Missing required parameter 'requiredParam'. ` + + // `Use the help tool with 'Social Profile' or see the Social Profile README for usage information.` + // ); + // } + + // TODO: Handle optional parameters with sensible defaults + // const optionalParam = params.optionalParam ?? defaultValue; + + // TODO: Implement your command logic here + return { + success: true, + // TODO: Add your result fields with actual computed values + context: params.context, + sessionId: params.sessionId + } as SocialProfileResult; +} + +/** + * Test 1: Command structure validation + */ +function testSocialProfileCommandStructure(): void { + console.log('\nπŸ“‹ Test 1: SocialProfile command structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Create valid params for Social Profile command + const validParams: SocialProfileParams = { + // TODO: Add your required parameters here + context, + sessionId + }; + + // Validate param structure + assert(validParams.context !== undefined, 'Params have context'); + assert(validParams.sessionId !== undefined, 'Params have sessionId'); + // TODO: Add assertions for your specific parameters + // assert(typeof validParams.requiredParam === 'string', 'requiredParam is string'); +} + +/** + * Test 2: Mock command execution + */ +async function testMockSocialProfileExecution(): Promise { + console.log('\n⚑ Test 2: Mock Social Profile command execution'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test mock execution + const params: SocialProfileParams = { + // TODO: Add your parameters here + context, + sessionId + }; + + const result = await mockSocialProfileCommand(params); + + // Validate result structure + assert(result.success === true, 'Mock result shows success'); + // TODO: Add assertions for your result fields + // assert(typeof result.yourField === 'string', 'yourField is string'); +} + +/** + * Test 3: Required parameter validation (CRITICAL) + * + * This test ensures your command throws ValidationError + * when required parameters are missing (BEST PRACTICE) + */ +async function testSocialProfileRequiredParams(): Promise { + console.log('\n🚨 Test 3: Required parameter validation'); + + // TODO: Uncomment when implementing validation + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test cases that should throw ValidationError + // Example: + // const testCases = [ + // { params: {} as SocialProfileParams, desc: 'Missing requiredParam' }, + // { params: { requiredParam: '' } as SocialProfileParams, desc: 'Empty requiredParam' }, + // ]; + // + // for (const testCase of testCases) { + // try { + // await mockSocialProfileCommand({ ...testCase.params, context, sessionId }); + // throw new Error(`Should have thrown ValidationError for: ${testCase.desc}`); + // } catch (error) { + // if (error instanceof ValidationError) { + // assert(error.field === 'requiredParam', `ValidationError field is 'requiredParam' for: ${testCase.desc}`); + // assert(error.message.includes('required parameter'), `Error message mentions 'required parameter' for: ${testCase.desc}`); + // assert(error.message.includes('help tool'), `Error message is tool-agnostic for: ${testCase.desc}`); + // } else { + // throw error; // Re-throw if not ValidationError + // } + // } + // } + + console.log('βœ… All required parameter validations work correctly'); +} + +/** + * Test 4: Optional parameter handling + */ +async function testSocialProfileOptionalParams(): Promise { + console.log('\nπŸ”§ Test 4: Optional parameter handling'); + + // TODO: Uncomment when implementing optional param tests + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test WITHOUT optional param (should use default) + // const paramsWithoutOptional: SocialProfileParams = { + // requiredParam: 'test', + // context, + // sessionId + // }; + // + // const resultWithoutOptional = await mockSocialProfileCommand(paramsWithoutOptional); + // assert(resultWithoutOptional.success === true, 'Command succeeds without optional params'); + + // TODO: Test WITH optional param + // const paramsWithOptional: SocialProfileParams = { + // requiredParam: 'test', + // optionalParam: true, + // context, + // sessionId + // }; + // + // const resultWithOptional = await mockSocialProfileCommand(paramsWithOptional); + // assert(resultWithOptional.success === true, 'Command succeeds with optional params'); + + console.log('βœ… Optional parameter handling validated'); +} + +/** + * Test 5: Performance validation + */ +async function testSocialProfilePerformance(): Promise { + console.log('\n⚑ Test 5: SocialProfile performance validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + const startTime = Date.now(); + + await mockSocialProfileCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialProfileParams); + + const executionTime = Date.now() - startTime; + + assert(executionTime < 100, `SocialProfile completed in ${executionTime}ms (under 100ms limit)`); +} + +/** + * Test 6: Result structure validation + */ +async function testSocialProfileResultStructure(): Promise { + console.log('\nπŸ” Test 6: SocialProfile result structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test various scenarios + const basicResult = await mockSocialProfileCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialProfileParams); + + assert(basicResult.success === true, 'Result has success field'); + // TODO: Add assertions for your result fields + // assert(typeof basicResult.yourField === 'string', 'Result has yourField (string)'); + assert(basicResult.context === context, 'Result includes context'); + assert(basicResult.sessionId === sessionId, 'Result includes sessionId'); + + console.log('βœ… All result structure validations pass'); +} + +/** + * Run all unit tests + */ +async function runAllSocialProfileUnitTests(): Promise { + console.log('πŸš€ Starting SocialProfile Command Unit Tests\n'); + + try { + testSocialProfileCommandStructure(); + await testMockSocialProfileExecution(); + await testSocialProfileRequiredParams(); + await testSocialProfileOptionalParams(); + await testSocialProfilePerformance(); + await testSocialProfileResultStructure(); + + console.log('\nπŸŽ‰ ALL SocialProfile UNIT TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Command structure and parameter validation'); + console.log(' βœ… Mock command execution patterns'); + console.log(' βœ… Required parameter validation (throws ValidationError)'); + console.log(' βœ… Optional parameter handling (sensible defaults)'); + console.log(' βœ… Performance requirements (< 100ms)'); + console.log(' βœ… Result structure validation'); + console.log('\nπŸ“ This is a REFERENCE EXAMPLE - use as a template for your commands!'); + console.log('πŸ’‘ TIP: Copy this test structure and modify for your command logic'); + + } catch (error) { + console.error('\n❌ SocialProfile unit tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialProfileUnitTests(); +} else { + module.exports = { runAllSocialProfileUnitTests }; +} diff --git a/src/debug/jtag/commands/social/propose/browser/SocialProposeBrowserCommand.ts b/src/debug/jtag/commands/social/propose/browser/SocialProposeBrowserCommand.ts new file mode 100644 index 000000000..92884d8bc --- /dev/null +++ b/src/debug/jtag/commands/social/propose/browser/SocialProposeBrowserCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Propose Command - Browser Implementation + * Delegates to server + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialProposeBaseCommand } from '../shared/SocialProposeCommand'; +import type { SocialProposeParams, SocialProposeResult } from '../shared/SocialProposeTypes'; + +export class SocialProposeBrowserCommand extends SocialProposeBaseCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialPropose(params: SocialProposeParams): Promise { + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/social/propose/package.json b/src/debug/jtag/commands/social/propose/package.json new file mode 100644 index 000000000..e2ec7fbd7 --- /dev/null +++ b/src/debug/jtag/commands/social/propose/package.json @@ -0,0 +1,27 @@ +{ + "name": "@continuum/social-propose", + "version": "1.0.0", + "description": "Democratic governance for shared social media accounts β€” nominate actions, vote, auto-execute on threshold", + "private": true, + "command": { + "name": "social/propose", + "description": "Propose, vote on, and auto-execute social media actions democratically", + "category": "social", + "params": { + "platform": { "type": "string", "required": false, "description": "Platform (e.g., 'moltbook') β€” required for create" }, + "mode": { "type": "string", "required": false, "description": "Mode: create, vote, list, view (default: list)" }, + "action": { "type": "string", "required": false, "description": "Action to propose: follow, unfollow, post, comment, vote, subscribe, unsubscribe" }, + "target": { "type": "string", "required": false, "description": "Target: agent name, post ID, or community name (depends on action)" }, + "reason": { "type": "string", "required": false, "description": "Reason for the nomination (required for create)" }, + "title": { "type": "string", "required": false, "description": "For post proposals: post title" }, + "content": { "type": "string", "required": false, "description": "For post/comment proposals: content body" }, + "community": { "type": "string", "required": false, "description": "For post/subscribe proposals: community name" }, + "postId": { "type": "string", "required": false, "description": "For comment proposals: post to comment on" }, + "proposalId": { "type": "string", "required": false, "description": "For vote/view modes: proposal ID (short or UUID)" }, + "direction": { "type": "string", "required": false, "description": "For vote mode: up or down" }, + "status": { "type": "string", "required": false, "description": "For list mode: filter by status (pending, approved, rejected, executed, expired)" }, + "limit": { "type": "number", "required": false, "description": "Max proposals to return in list mode" }, + "personaId": { "type": "string", "required": false, "description": "Persona user ID (auto-detected)" } + } + } +} diff --git a/src/debug/jtag/commands/social/propose/server/SocialProposeServerCommand.ts b/src/debug/jtag/commands/social/propose/server/SocialProposeServerCommand.ts new file mode 100644 index 000000000..dd69f7838 --- /dev/null +++ b/src/debug/jtag/commands/social/propose/server/SocialProposeServerCommand.ts @@ -0,0 +1,534 @@ +/** + * Social Propose Command - Server Implementation + * + * Democratic governance for shared social media accounts. + * Proposals stored as Handles, auto-execute when vote threshold met. + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import { transformPayload } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; +import { SocialProposeBaseCommand } from '../shared/SocialProposeCommand'; +import type { + SocialProposeParams, + SocialProposeResult, + ProposalData, + ProposalRecord, + ProposalVote, + ProposalAction, + ProposalStatus, +} from '../shared/SocialProposeTypes'; +import { + PROPOSAL_THRESHOLDS, + PROPOSAL_TTL_MS, + PROPOSAL_HANDLE_TYPE, +} from '../shared/SocialProposeTypes'; +import { Handles } from '@system/core/shared/Handles'; +import type { HandleRecord } from '@system/core/types/Handle'; +import { loadSocialContext, resolvePersonaId } from '@system/social/server/SocialCommandHelper'; +import { SocialEngage } from '@commands/social/engage/shared/SocialEngageTypes'; +import { SocialPost } from '@commands/social/post/shared/SocialPostTypes'; +import { SocialComment } from '@commands/social/comment/shared/SocialCommentTypes'; +import { DataList } from '@commands/data/list/shared/DataListTypes'; +import { UserEntity } from '@system/data/entities/UserEntity'; + + +export class SocialProposeServerCommand extends SocialProposeBaseCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialPropose(params: SocialProposeParams): Promise { + const mode = params.mode ?? 'list'; + + switch (mode) { + case 'create': + return this.handleCreate(params); + case 'vote': + return this.handleVote(params); + case 'list': + return this.handleList(params); + case 'view': + return this.handleView(params); + default: + throw new Error(`Unknown propose mode: ${mode}. Valid: create, vote, list, view`); + } + } + + // ============ Create ============ + + private async handleCreate(params: SocialProposeParams): Promise { + const { platform, action, target, reason } = params; + + if (!platform) throw new Error('platform is required for proposals'); + if (!action) throw new Error('action is required (follow, post, comment, vote, subscribe, unsubscribe)'); + if (!reason) throw new Error('reason is required β€” explain why the community should approve this'); + + const validActions: ProposalAction[] = ['follow', 'unfollow', 'post', 'comment', 'vote', 'subscribe', 'unsubscribe']; + if (!validActions.includes(action)) { + throw new Error(`Invalid action: ${action}. Valid: ${validActions.join(', ')}`); + } + + // Resolve nominator + const personaId = await resolvePersonaId(params.personaId, params); + const persona = await this.lookupPersona(personaId, params); + + // Build action params that will be used for execution + const actionParams = this.buildActionParams(params); + + // Validate action-specific requirements + this.validateActionParams(action, target, params); + + const threshold = PROPOSAL_THRESHOLDS[action]; + + const proposalData: ProposalData = { + action, + platform, + target, + reason, + nominatedBy: personaId, + nominatorName: persona.displayName, + votes: [{ + personaId, + personaName: persona.displayName, + direction: 'up', + timestamp: new Date().toISOString(), + }], + threshold, + actionParams, + }; + + // Threshold of 0 means auto-approve β€” execute immediately without voting + if (threshold === 0) { + const handle = await Handles.create( + PROPOSAL_HANDLE_TYPE, + proposalData, + personaId, + PROPOSAL_TTL_MS, + ); + const record = this.handleToProposal(handle, proposalData); + return this.executeProposal(handle, proposalData, params, record); + } + + // Create handle for the proposal + const handle = await Handles.create( + PROPOSAL_HANDLE_TYPE, + proposalData, + personaId, + PROPOSAL_TTL_MS, + ); + + const record = this.handleToProposal(handle, proposalData); + const votesNeeded = threshold - 1; // Nominator auto-votes up + + // Check if nominator's single vote meets threshold (e.g., vote action needs 2) + if (proposalData.votes.filter(v => v.direction === 'up').length >= threshold) { + return this.executeProposal(handle, proposalData, params, record); + } + + return transformPayload(params, { + success: true, + message: `Proposal created: ${action} ${target ?? ''} on ${platform}`, + summary: this.formatProposalSummary(record, votesNeeded), + proposal: record, + executed: false, + }); + } + + // ============ Vote ============ + + private async handleVote(params: SocialProposeParams): Promise { + const { proposalId, direction } = params; + + if (!proposalId) throw new Error('proposalId is required'); + if (!direction || !['up', 'down'].includes(direction)) { + throw new Error('direction is required (up or down)'); + } + + // Resolve voter + const personaId = await resolvePersonaId(params.personaId, params); + const persona = await this.lookupPersona(personaId, params); + + // Load proposal handle + const handle = await Handles.resolve(proposalId); + if (!handle) { + throw new Error(`Proposal not found: ${proposalId}`); + } + if (handle.type !== PROPOSAL_HANDLE_TYPE) { + throw new Error(`Handle ${proposalId} is not a proposal (type: ${handle.type})`); + } + if (handle.status !== 'pending') { + throw new Error(`Proposal ${proposalId} is not open for voting (status: ${handle.status})`); + } + + const proposalData = handle.params as ProposalData; + + // Check if already voted + const existingVote = proposalData.votes.find(v => v.personaId === personaId); + if (existingVote) { + if (existingVote.direction === direction) { + throw new Error(`You already voted ${direction} on this proposal`); + } + // Change vote direction + existingVote.direction = direction; + existingVote.timestamp = new Date().toISOString(); + } else { + // New vote + proposalData.votes.push({ + personaId, + personaName: persona.displayName, + direction, + timestamp: new Date().toISOString(), + }); + } + + // Update the handle with new vote data + await Handles._updateStatus(handle.id, 'pending', { params: proposalData }); + + const record = this.handleToProposal(handle, proposalData); + const upVotes = proposalData.votes.filter(v => v.direction === 'up').length; + const votesNeeded = proposalData.threshold - upVotes; + + // Check if threshold met + if (upVotes >= proposalData.threshold) { + return this.executeProposal(handle, proposalData, params, record); + } + + // Check if mathematically impossible (too many downvotes) + const downVotes = proposalData.votes.filter(v => v.direction === 'down').length; + const totalPossibleVoters = 12; // Approximate active persona count + const maxPossibleUp = upVotes + (totalPossibleVoters - proposalData.votes.length); + if (maxPossibleUp < proposalData.threshold) { + await Handles.markFailed(handle.id, 'Rejected: insufficient support'); + record.status = 'rejected'; + return transformPayload(params, { + success: true, + message: `Proposal rejected: not enough possible votes remaining`, + summary: this.formatProposalSummary(record, 0), + proposal: record, + executed: false, + }); + } + + return transformPayload(params, { + success: true, + message: `Voted ${direction} on proposal #${handle.shortId}`, + summary: this.formatProposalSummary(record, Math.max(0, votesNeeded)), + proposal: record, + executed: false, + }); + } + + // ============ List ============ + + private async handleList(params: SocialProposeParams): Promise { + const limit = params.limit ?? 20; + + // Fetch proposal handles + let handles: HandleRecord[]; + if (params.status === 'pending') { + handles = await Handles.listActive(PROPOSAL_HANDLE_TYPE, limit); + } else { + handles = await Handles.listByType(PROPOSAL_HANDLE_TYPE, limit); + } + + // Convert to proposals + const proposals = handles.map(h => { + const data = h.params as ProposalData; + return this.handleToProposal(h, data); + }); + + // Filter by status if specified (for non-pending) + const filtered = params.status && params.status !== 'pending' + ? proposals.filter(p => p.status === params.status) + : proposals; + + const lines = filtered.map((p, i) => { + const upVotes = p.voteSummary.up; + const bar = 'β–ˆ'.repeat(upVotes) + 'β–‘'.repeat(Math.max(0, p.threshold - upVotes)); + const statusTag = p.status === 'pending' ? 'πŸ—³οΈ' : + p.status === 'executed' ? 'βœ…' : + p.status === 'rejected' ? '❌' : + p.status === 'expired' ? '⏰' : '?'; + return `${statusTag} #${p.shortId} [${bar}] ${upVotes}/${p.threshold} β€” ${p.action} ${p.target ?? ''} (${p.nominatorName}: "${p.reason}")`; + }); + + return transformPayload(params, { + success: true, + message: `${filtered.length} proposal(s) found`, + summary: filtered.length > 0 + ? `**Proposals:**\n${lines.join('\n')}\n\nVote: social/propose --mode=vote --proposalId= --direction=up` + : 'No proposals found. Create one: social/propose --mode=create --action=follow --target= --reason="why"', + proposals: filtered, + }); + } + + // ============ View ============ + + private async handleView(params: SocialProposeParams): Promise { + const { proposalId } = params; + if (!proposalId) throw new Error('proposalId is required'); + + const handle = await Handles.resolve(proposalId); + if (!handle) throw new Error(`Proposal not found: ${proposalId}`); + if (handle.type !== PROPOSAL_HANDLE_TYPE) { + throw new Error(`Handle ${proposalId} is not a proposal`); + } + + const data = handle.params as ProposalData; + const record = this.handleToProposal(handle, data); + + const voteLines = data.votes.map(v => { + const icon = v.direction === 'up' ? 'πŸ‘' : 'πŸ‘Ž'; + return ` ${icon} ${v.personaName} (${v.direction}) β€” ${new Date(v.timestamp).toLocaleTimeString()}`; + }); + + const summary = [ + `**Proposal #${record.shortId}** β€” ${record.action} ${record.target ?? ''}`, + `Platform: ${record.platform}`, + `Status: ${record.status}`, + `Reason: "${record.reason}"`, + `Nominated by: ${record.nominatorName}`, + `Threshold: ${record.threshold} votes needed`, + `Votes (${record.voteSummary.up} up, ${record.voteSummary.down} down):`, + ...voteLines, + '', + record.status === 'pending' + ? `Vote: social/propose --mode=vote --proposalId=${record.shortId} --direction=up` + : `This proposal is ${record.status}.`, + ].join('\n'); + + return transformPayload(params, { + success: true, + message: `Proposal #${record.shortId}: ${record.status}`, + summary, + proposal: record, + }); + } + + // ============ Auto-Execute ============ + + private async executeProposal( + handle: HandleRecord, + data: ProposalData, + params: SocialProposeParams, + record: ProposalRecord, + ): Promise { + await Handles.markProcessing(handle.id); + + try { + const result = await this.executeAction(data, params); + + await Handles.markComplete(handle.id, { + executed: true, + executionResult: result, + executedAt: new Date().toISOString(), + }); + + record.status = 'executed'; + + return transformPayload(params, { + success: true, + message: `Proposal approved and executed: ${data.action} ${data.target ?? ''} on ${data.platform}`, + summary: `**Proposal #${handle.shortId} APPROVED** β€” threshold met (${data.votes.filter(v => v.direction === 'up').length}/${data.threshold})\nAction: ${data.action} ${data.target ?? ''}\nResult: ${JSON.stringify(result)}`, + proposal: record, + executed: true, + executionResult: result, + }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + await Handles.markFailed(handle.id, msg); + record.status = 'rejected'; + + return transformPayload(params, { + success: false, + message: `Proposal approved but execution failed: ${msg}`, + proposal: record, + executed: false, + }); + } + } + + private async executeAction(data: ProposalData, params: SocialProposeParams): Promise { + const { action, platform, target, actionParams } = data; + + switch (action) { + case 'follow': + return SocialEngage.execute({ + platform, + action: 'follow', + target: target!, + context: params.context, + sessionId: params.sessionId, + }); + + case 'unfollow': + return SocialEngage.execute({ + platform, + action: 'unfollow', + target: target!, + context: params.context, + sessionId: params.sessionId, + }); + + case 'subscribe': + return SocialEngage.execute({ + platform, + action: 'subscribe', + target: target!, + context: params.context, + sessionId: params.sessionId, + }); + + case 'unsubscribe': + return SocialEngage.execute({ + platform, + action: 'unsubscribe', + target: target!, + context: params.context, + sessionId: params.sessionId, + }); + + case 'vote': + return SocialEngage.execute({ + platform, + action: 'vote', + target: target!, + targetType: (actionParams.targetType as 'post' | 'comment') ?? 'post', + direction: (actionParams.voteDirection as 'up' | 'down') ?? 'up', + context: params.context, + sessionId: params.sessionId, + }); + + case 'post': + return SocialPost.execute({ + platform, + title: actionParams.title as string, + content: actionParams.content as string, + community: actionParams.community as string | undefined, + context: params.context, + sessionId: params.sessionId, + }); + + case 'comment': + return SocialComment.execute({ + platform, + postId: actionParams.postId as string, + content: actionParams.commentContent as string ?? actionParams.content as string, + parentId: actionParams.parentId as string | undefined, + context: params.context, + sessionId: params.sessionId, + }); + + default: + throw new Error(`Cannot execute action: ${action}`); + } + } + + // ============ Helpers ============ + + private buildActionParams(params: SocialProposeParams): Record { + const ap: Record = {}; + if (params.title) ap.title = params.title; + if (params.content) ap.content = params.content; + if (params.community) ap.community = params.community; + if (params.postId) ap.postId = params.postId; + if (params.commentContent) ap.commentContent = params.commentContent; + if (params.voteDirection) ap.voteDirection = params.voteDirection; + if (params.targetType) ap.targetType = params.targetType; + return ap; + } + + private validateActionParams(action: ProposalAction, target: string | undefined, params: SocialProposeParams): void { + switch (action) { + case 'follow': + case 'unfollow': + if (!target) throw new Error(`${action} requires --target (agent username)`); + break; + case 'subscribe': + case 'unsubscribe': + if (!target) throw new Error(`${action} requires --target (community name)`); + break; + case 'vote': + if (!target) throw new Error('vote requires --target (post or comment ID)'); + break; + case 'post': + if (!params.title || !params.content) throw new Error('post requires --title and --content'); + break; + case 'comment': + if (!params.postId) throw new Error('comment requires --postId'); + if (!params.content && !params.commentContent) throw new Error('comment requires --content or --commentContent'); + break; + } + } + + private handleToProposal(handle: HandleRecord, data: ProposalData): ProposalRecord { + const upVotes = data.votes.filter(v => v.direction === 'up').length; + const downVotes = data.votes.filter(v => v.direction === 'down').length; + + let status: ProposalStatus; + switch (handle.status) { + case 'pending': status = 'pending'; break; + case 'processing': status = 'approved'; break; + case 'complete': status = 'executed'; break; + case 'failed': status = 'rejected'; break; + case 'expired': status = 'expired'; break; + case 'cancelled': status = 'rejected'; break; + default: status = 'pending'; + } + + return { + id: handle.id, + shortId: handle.shortId, + action: data.action, + platform: data.platform, + target: data.target, + reason: data.reason, + nominatedBy: data.nominatedBy, + nominatorName: data.nominatorName, + votes: data.votes, + voteSummary: { up: upVotes, down: downVotes, total: data.votes.length }, + threshold: data.threshold, + status, + createdAt: handle.createdAt.toISOString(), + expiresAt: handle.expiresAt?.toISOString(), + }; + } + + private formatProposalSummary(record: ProposalRecord, votesNeeded: number): string { + const bar = 'β–ˆ'.repeat(record.voteSummary.up) + 'β–‘'.repeat(Math.max(0, votesNeeded)); + return [ + `**Proposal #${record.shortId}** β€” ${record.action} ${record.target ?? ''}`, + `Reason: "${record.reason}"`, + `Progress: [${bar}] ${record.voteSummary.up}/${record.threshold} votes`, + votesNeeded > 0 + ? `Need ${votesNeeded} more vote(s) to approve.` + : 'Threshold met!', + `Vote: social/propose --mode=vote --proposalId=${record.shortId} --direction=up`, + ].join('\n'); + } + + private async lookupPersona( + personaId: UUID, + params: SocialProposeParams, + ): Promise<{ displayName: string; uniqueId: string }> { + const result = await DataList.execute({ + collection: UserEntity.collection, + filter: { id: personaId }, + limit: 1, + context: params.context, + sessionId: params.sessionId, + }); + + if (!result.success || !result.items?.length) { + throw new Error(`Persona not found: ${personaId}`); + } + + return { + displayName: result.items[0].displayName, + uniqueId: result.items[0].uniqueId, + }; + } +} diff --git a/src/debug/jtag/commands/social/propose/shared/SocialProposeCommand.ts b/src/debug/jtag/commands/social/propose/shared/SocialProposeCommand.ts new file mode 100644 index 000000000..bbd29f263 --- /dev/null +++ b/src/debug/jtag/commands/social/propose/shared/SocialProposeCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Propose Command - Shared base class + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { SocialProposeParams, SocialProposeResult } from './SocialProposeTypes'; +import type { JTAGContext, JTAGPayload } from '@system/core/types/JTAGTypes'; + +export abstract class SocialProposeBaseCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/propose', context, subpath, commander); + } + + protected abstract executeSocialPropose(params: SocialProposeParams): Promise; + + async execute(params: JTAGPayload): Promise { + return this.executeSocialPropose(params as SocialProposeParams); + } +} diff --git a/src/debug/jtag/commands/social/propose/shared/SocialProposeTypes.ts b/src/debug/jtag/commands/social/propose/shared/SocialProposeTypes.ts new file mode 100644 index 000000000..f581d5e01 --- /dev/null +++ b/src/debug/jtag/commands/social/propose/shared/SocialProposeTypes.ts @@ -0,0 +1,191 @@ +/** + * Social Propose Command - Shared Types + * + * Democratic governance for shared social media accounts. + * Personas nominate actions, vote, and auto-execute on threshold. + * + * Proposals are stored as Handles (type 'social-proposal') with votes in params. + * When enough "up" votes accumulate, the action executes automatically. + * + * Modes: + * create β€” Nominate a new action (follow, post, comment, etc.) + * vote β€” Vote on a pending proposal + * list β€” Show pending/recent proposals + * view β€” View a specific proposal with full vote history + * + * Usage: + * ./jtag social/propose --platform=moltbook --mode=create --action=follow --target=eudaemon_0 --reason="Great security research" + * ./jtag social/propose --mode=vote --proposalId=abc123 --direction=up + * ./jtag social/propose --mode=list + * ./jtag social/propose --mode=view --proposalId=abc123 + */ + +import type { CommandParams, CommandResult, CommandInput, JTAGContext } from '@system/core/types/JTAGTypes'; +import { createPayload, transformPayload } from '@system/core/types/JTAGTypes'; +import { Commands } from '@system/core/shared/Commands'; +import type { JTAGError } from '@system/core/types/ErrorTypes'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; + +/** Actions that can be proposed */ +export type ProposalAction = 'follow' | 'unfollow' | 'post' | 'comment' | 'vote' | 'subscribe' | 'unsubscribe'; + +/** Command modes */ +export type ProposeMode = 'create' | 'vote' | 'list' | 'view'; + +/** Status of a proposal */ +export type ProposalStatus = 'pending' | 'approved' | 'rejected' | 'executed' | 'expired'; + +/** A single vote on a proposal */ +export interface ProposalVote { + personaId: UUID; + personaName: string; + direction: 'up' | 'down'; + timestamp: string; +} + +/** Full proposal record (stored in Handle.params) */ +export interface ProposalData { + action: ProposalAction; + platform: string; + target?: string; + reason: string; + nominatedBy: UUID; + nominatorName: string; + votes: ProposalVote[]; + threshold: number; + + /** Full params needed to execute the action when approved */ + actionParams: Record; +} + +/** Proposal as returned to callers */ +export interface ProposalRecord { + id: UUID; + shortId: string; + action: ProposalAction; + platform: string; + target?: string; + reason: string; + nominatedBy: UUID; + nominatorName: string; + votes: ProposalVote[]; + voteSummary: { up: number; down: number; total: number }; + threshold: number; + status: ProposalStatus; + createdAt: string; + expiresAt?: string; +} + +/** + * Approval thresholds by action type. + * Minimum "up" votes needed. With ~12 personas: + * 0 = auto-approve (no voting needed, execute immediately) + * vote on external content: 2 (low bar β€” just an upvote) + * follow/unfollow: 3 + * subscribe/unsubscribe: 3 + * comment: 4 + * post: 5 (highest bar β€” public content under our name) + */ +export const PROPOSAL_THRESHOLDS: Record = { + vote: 2, + follow: 3, + unfollow: 3, + subscribe: 3, + unsubscribe: 3, + comment: 4, + post: 5, +}; + +/** How long proposals stay open before expiring (1 hour) */ +export const PROPOSAL_TTL_MS = 60 * 60 * 1000; + +/** Handle type for proposals */ +export const PROPOSAL_HANDLE_TYPE = 'social-proposal'; + + +// ============ Command Params/Result ============ + +export interface SocialProposeParams extends CommandParams { + /** Platform (e.g., 'moltbook') β€” required for create */ + platform?: string; + + /** Command mode */ + mode: ProposeMode; + + // -- create mode -- + /** Action to propose */ + action?: ProposalAction; + + /** Target (agent name, post ID, community name β€” depends on action) */ + target?: string; + + /** Reason for the nomination */ + reason?: string; + + /** For post action: title */ + title?: string; + + /** For post action: content */ + content?: string; + + /** For post/subscribe action: community */ + community?: string; + + /** For comment action: post ID to comment on */ + postId?: string; + + /** For comment action: comment content (overloads 'content') */ + commentContent?: string; + + /** For vote action: direction to vote on external content */ + voteDirection?: 'up' | 'down'; + + /** For vote action: target type */ + targetType?: 'post' | 'comment'; + + // -- vote mode -- + /** Proposal ID to vote on (short ID or UUID) */ + proposalId?: string; + + /** Vote direction */ + direction?: 'up' | 'down'; + + // -- list mode -- + /** Filter by status */ + status?: ProposalStatus; + + /** Max proposals to return */ + limit?: number; + + /** Persona user ID (auto-detected if not provided) */ + personaId?: UUID; +} + +export interface SocialProposeResult extends CommandResult { + success: boolean; + message: string; + summary?: string; + proposal?: ProposalRecord; + proposals?: ProposalRecord[]; + executed?: boolean; + executionResult?: unknown; + error?: JTAGError; +} + +export const createSocialProposeParams = ( + context: JTAGContext, + sessionId: UUID, + data: Omit +): SocialProposeParams => createPayload(context, sessionId, data); + +export const createSocialProposeResultFromParams = ( + params: SocialProposeParams, + differences: Omit +): SocialProposeResult => transformPayload(params, differences); + +export const SocialPropose = { + execute(params: CommandInput): Promise { + return Commands.execute('social/propose', params as Partial); + }, + commandName: 'social/propose' as const, +} as const; diff --git a/src/debug/jtag/commands/social/search/browser/SocialSearchBrowserCommand.ts b/src/debug/jtag/commands/social/search/browser/SocialSearchBrowserCommand.ts new file mode 100644 index 000000000..c38b8b248 --- /dev/null +++ b/src/debug/jtag/commands/social/search/browser/SocialSearchBrowserCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Search Command - Browser Implementation + * Delegates to server + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialSearchBaseCommand } from '../shared/SocialSearchCommand'; +import type { SocialSearchParams, SocialSearchResult } from '../shared/SocialSearchTypes'; + +export class SocialSearchBrowserCommand extends SocialSearchBaseCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialSearch(params: SocialSearchParams): Promise { + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/social/search/package.json b/src/debug/jtag/commands/social/search/package.json new file mode 100644 index 000000000..34b9a82ef --- /dev/null +++ b/src/debug/jtag/commands/social/search/package.json @@ -0,0 +1,18 @@ +{ + "name": "@continuum/social-search", + "version": "1.0.0", + "description": "Semantic search across social media platforms β€” find posts, agents, and communities", + "private": true, + "command": { + "name": "social/search", + "description": "Search social media for content and agents", + "category": "social", + "params": { + "platform": { "type": "string", "required": true, "description": "Platform to search (e.g., 'moltbook')" }, + "query": { "type": "string", "required": true, "description": "Search query" }, + "type": { "type": "string", "required": false, "description": "Filter: post, comment, agent, submolt" }, + "limit": { "type": "number", "required": false, "description": "Max results" }, + "personaId": { "type": "string", "required": false, "description": "Persona user ID (auto-detected)" } + } + } +} diff --git a/src/debug/jtag/commands/social/search/server/SocialSearchServerCommand.ts b/src/debug/jtag/commands/social/search/server/SocialSearchServerCommand.ts new file mode 100644 index 000000000..1aedb1d31 --- /dev/null +++ b/src/debug/jtag/commands/social/search/server/SocialSearchServerCommand.ts @@ -0,0 +1,57 @@ +/** + * Social Search Command - Server Implementation + * + * Semantic search across social media platforms. + * Returns results with AI-friendly summary. + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import { transformPayload } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialSearchBaseCommand } from '../shared/SocialSearchCommand'; +import type { SocialSearchParams, SocialSearchResult } from '../shared/SocialSearchTypes'; +import { loadSocialContext } from '@system/social/server/SocialCommandHelper'; + +export class SocialSearchServerCommand extends SocialSearchBaseCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialSearch(params: SocialSearchParams): Promise { + const { platform, query, type, limit } = params; + + if (!platform) throw new Error('platform is required'); + if (!query?.trim()) throw new Error('query is required'); + + const ctx = await loadSocialContext(platform, params.personaId, params); + + const searchResult = await ctx.provider.search({ + query: query.trim(), + type, + limit: limit ?? 15, + }); + + const posts = searchResult.posts; + const total = searchResult.totalCount ?? posts.length; + + const lines = posts.map((p, i) => { + const votes = p.votes > 0 ? `+${p.votes}` : String(p.votes); + const community = p.community ? `m/${p.community}` : ''; + return ` ${i + 1}. [${votes}] "${p.title}" by ${p.authorName} ${community} (${p.commentCount} comments) β€” ${p.id}`; + }); + + const typeLabel = type ? ` (type: ${type})` : ''; + const summary = posts.length === 0 + ? `No results for "${query}" on ${platform}${typeLabel}.` + : `Search "${query}" on ${platform}${typeLabel} β€” ${total} results:\n${lines.join('\n')}\n\nUse social/browse --mode=post --target= to read any post in detail.`; + + return transformPayload(params, { + success: true, + message: `Found ${posts.length} results for "${query}" on ${platform}`, + summary, + posts, + totalCount: total, + }); + } +} diff --git a/src/debug/jtag/commands/social/search/shared/SocialSearchCommand.ts b/src/debug/jtag/commands/social/search/shared/SocialSearchCommand.ts new file mode 100644 index 000000000..46755f895 --- /dev/null +++ b/src/debug/jtag/commands/social/search/shared/SocialSearchCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Search Command - Shared base class + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { SocialSearchParams, SocialSearchResult } from './SocialSearchTypes'; +import type { JTAGContext, JTAGPayload } from '@system/core/types/JTAGTypes'; + +export abstract class SocialSearchBaseCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/search', context, subpath, commander); + } + + protected abstract executeSocialSearch(params: SocialSearchParams): Promise; + + async execute(params: JTAGPayload): Promise { + return this.executeSocialSearch(params as SocialSearchParams); + } +} diff --git a/src/debug/jtag/commands/social/search/shared/SocialSearchTypes.ts b/src/debug/jtag/commands/social/search/shared/SocialSearchTypes.ts new file mode 100644 index 000000000..6c404bf8c --- /dev/null +++ b/src/debug/jtag/commands/social/search/shared/SocialSearchTypes.ts @@ -0,0 +1,77 @@ +/** + * Social Search Command - Shared Types + * + * Semantic search across social media platforms. + * Find posts, agents, and communities by keyword. + * + * Usage: + * ./jtag social/search --platform=moltbook --query="memory systems" + * ./jtag social/search --platform=moltbook --query="rust concurrency" --type=post --limit=10 + */ + +import type { CommandParams, CommandResult, CommandInput, JTAGContext } from '@system/core/types/JTAGTypes'; +import { createPayload, transformPayload } from '@system/core/types/JTAGTypes'; +import { Commands } from '@system/core/shared/Commands'; +import type { JTAGError } from '@system/core/types/ErrorTypes'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialPost as SocialPostData } from '@system/social/shared/SocialMediaTypes'; + +/** + * Social Search Command Parameters + */ +export interface SocialSearchParams extends CommandParams { + /** Platform to search (e.g., 'moltbook') */ + platform: string; + + /** Search query */ + query: string; + + /** Filter by type: post, comment, agent, submolt */ + type?: 'post' | 'comment' | 'agent' | 'submolt'; + + /** Max results */ + limit?: number; + + /** Persona user ID (auto-detected if not provided) */ + personaId?: UUID; +} + +/** + * Social Search Command Result + */ +export interface SocialSearchResult extends CommandResult { + success: boolean; + message: string; + + /** AI-friendly summary of results */ + summary: string; + + /** Search results */ + posts?: SocialPostData[]; + + /** Total matching results (may exceed returned count) */ + totalCount?: number; + + error?: JTAGError; +} + +export const createSocialSearchParams = ( + context: JTAGContext, + sessionId: UUID, + data: Omit +): SocialSearchParams => createPayload(context, sessionId, data); + +export const createSocialSearchResultFromParams = ( + params: SocialSearchParams, + differences: Omit +): SocialSearchResult => transformPayload(params, differences); + +/** + * SocialSearch β€” Type-safe command executor + */ +export const SocialSearch = { + execute(params: CommandInput): Promise { + return Commands.execute('social/search', params as Partial); + }, + commandName: 'social/search' as const, +} as const; diff --git a/src/debug/jtag/commands/social/signup/.npmignore b/src/debug/jtag/commands/social/signup/.npmignore new file mode 100644 index 000000000..f74ad6b8a --- /dev/null +++ b/src/debug/jtag/commands/social/signup/.npmignore @@ -0,0 +1,20 @@ +# Development files +.eslintrc* +tsconfig*.json +vitest.config.ts + +# Build artifacts +*.js.map +*.d.ts.map + +# IDE +.vscode/ +.idea/ + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db diff --git a/src/debug/jtag/commands/social/signup/README.md b/src/debug/jtag/commands/social/signup/README.md new file mode 100644 index 000000000..c11699ffa --- /dev/null +++ b/src/debug/jtag/commands/social/signup/README.md @@ -0,0 +1,162 @@ +# Social Signup Command + +Register a persona on a social media platform (e.g., Moltbook). Creates an account with a chosen username and stores credentials for future use. + +## Table of Contents + +- [Usage](#usage) + - [CLI Usage](#cli-usage) + - [Tool Usage](#tool-usage) +- [Parameters](#parameters) +- [Result](#result) +- [Examples](#examples) +- [Testing](#testing) + - [Unit Tests](#unit-tests) + - [Integration Tests](#integration-tests) +- [Getting Help](#getting-help) +- [Access Level](#access-level) +- [Implementation Notes](#implementation-notes) + +## Usage + +### CLI Usage + +From the command line using the jtag CLI: + +```bash +./jtag social/signup --platform= --agentName= +``` + +### Tool Usage + +From Persona tools or programmatic access using `Commands.execute()`: + +```typescript +import { Commands } from '@system/core/shared/Commands'; + +const result = await Commands.execute('social/signup', { + // your parameters here +}); +``` + +## Parameters + +- **platform** (required): `string` - Platform to register on (e.g., 'moltbook') +- **agentName** (required): `string` - Desired username on the platform +- **description** (optional): `string` - Profile description/bio +- **personaId** (optional): `UUID` - Persona user ID (auto-detected if not provided) +- **metadata** (optional): `Record` - Additional platform-specific metadata + +## Result + +Returns `SocialSignupResult` with: + +Returns CommandResult with: +- **message**: `string` - Human-readable result message +- **apiKey**: `string` - API key for future authenticated requests +- **agentName**: `string` - Assigned username on the platform +- **claimUrl**: `string` - URL to claim/verify the account +- **profileUrl**: `string` - URL to the agent's profile page +- **verificationCode**: `string` - Verification code if applicable + +## Examples + +### Register a persona on Moltbook + +```bash +./jtag social/signup --platform=moltbook --agentName="helper-ai" --description="I help with code" +``` + +**Expected result:** +{ success: true, agentName: 'helper-ai', profileUrl: '...' } + +## Getting Help + +### Using the Help Tool + +Get detailed usage information for this command: + +**CLI:** +```bash +./jtag help social/signup +``` + +**Tool:** +```typescript +// Use your help tool with command name 'social/signup' +``` + +### Using the README Tool + +Access this README programmatically: + +**CLI:** +```bash +./jtag readme social/signup +``` + +**Tool:** +```typescript +// Use your readme tool with command name 'social/signup' +``` + +## Testing + +### Unit Tests + +Test command logic in isolation using mock dependencies: + +```bash +# Run unit tests (no server required) +npx tsx commands/social/signup/test/unit/SocialSignupCommand.test.ts +``` + +**What's tested:** +- Command structure and parameter validation +- Mock command execution patterns +- Required parameter validation (throws ValidationError) +- Optional parameter handling (sensible defaults) +- Performance requirements +- Assertion utility helpers + +**TDD Workflow:** +1. Write/modify unit test first (test-driven development) +2. Run test, see it fail +3. Implement feature +4. Run test, see it pass +5. Refactor if needed + +### Integration Tests + +Test command with real client connections and system integration: + +```bash +# Prerequisites: Server must be running +npm start # Wait 90+ seconds for deployment + +# Run integration tests +npx tsx commands/social/signup/test/integration/SocialSignupIntegration.test.ts +``` + +**What's tested:** +- Client connection to live system +- Real command execution via WebSocket +- ValidationError handling for missing params +- Optional parameter defaults +- Performance under load +- Various parameter combinations + +**Best Practice:** +Run unit tests frequently during development (fast feedback). Run integration tests before committing (verify system integration). + +## Access Level + +**ai-safe** - Safe for AI personas to call autonomously + +## Implementation Notes + +- **Shared Logic**: Core business logic in `shared/SocialSignupTypes.ts` +- **Browser**: Browser-specific implementation in `browser/SocialSignupBrowserCommand.ts` +- **Server**: Server-specific implementation in `server/SocialSignupServerCommand.ts` +- **Unit Tests**: Isolated testing in `test/unit/SocialSignupCommand.test.ts` +- **Integration Tests**: System testing in `test/integration/SocialSignupIntegration.test.ts` diff --git a/src/debug/jtag/commands/social/signup/browser/SocialSignupBrowserCommand.ts b/src/debug/jtag/commands/social/signup/browser/SocialSignupBrowserCommand.ts new file mode 100644 index 000000000..44ad07e39 --- /dev/null +++ b/src/debug/jtag/commands/social/signup/browser/SocialSignupBrowserCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Signup Command - Browser Implementation + * Delegates to server + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialSignupCommand } from '../shared/SocialSignupCommand'; +import type { SocialSignupParams, SocialSignupResult } from '../shared/SocialSignupTypes'; + +export class SocialSignupBrowserCommand extends SocialSignupCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialSignup(params: SocialSignupParams): Promise { + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/social/signup/package.json b/src/debug/jtag/commands/social/signup/package.json new file mode 100644 index 000000000..f9cd5b2d1 --- /dev/null +++ b/src/debug/jtag/commands/social/signup/package.json @@ -0,0 +1,35 @@ +{ + "name": "@jtag-commands/social/signup", + "version": "1.0.0", + "description": "Register a persona on a social media platform (e.g., Moltbook). Creates an account with a chosen username and stores credentials for future use.", + "main": "server/SocialSignupServerCommand.ts", + "types": "shared/SocialSignupTypes.ts", + "scripts": { + "test": "npm run test:unit && npm run test:integration", + "test:unit": "npx vitest run test/unit/*.test.ts", + "test:integration": "npx tsx test/integration/SocialSignupIntegration.test.ts", + "lint": "npx eslint **/*.ts", + "typecheck": "npx tsc --noEmit" + }, + "peerDependencies": { + "@jtag/core": "*" + }, + "files": [ + "shared/**/*.ts", + "browser/**/*.ts", + "server/**/*.ts", + "test/**/*.ts", + "README.md" + ], + "keywords": [ + "jtag", + "command", + "social/signup" + ], + "license": "MIT", + "author": "", + "repository": { + "type": "git", + "url": "" + } +} diff --git a/src/debug/jtag/commands/social/signup/server/SocialSignupServerCommand.ts b/src/debug/jtag/commands/social/signup/server/SocialSignupServerCommand.ts new file mode 100644 index 000000000..61c2aa6ec --- /dev/null +++ b/src/debug/jtag/commands/social/signup/server/SocialSignupServerCommand.ts @@ -0,0 +1,98 @@ +/** + * Social Signup Command - Server Implementation + * + * Registers a persona on a social media platform and stores + * the credential in their longterm.db for future use. + */ + +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import { transformPayload } from '@system/core/types/JTAGTypes'; +import type { ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import { SocialSignupCommand } from '../shared/SocialSignupCommand'; +import type { SocialSignupParams, SocialSignupResult } from '../shared/SocialSignupTypes'; +import { SocialMediaProviderRegistry } from '@system/social/server/SocialMediaProviderRegistry'; +import { SocialCredentialEntity } from '@system/social/shared/SocialCredentialEntity'; +import { resolvePersonaId, openPersonaDb, storeCredential } from '@system/social/server/SocialCommandHelper'; +import { DataList } from '../../../data/list/shared/DataListTypes'; + +export class SocialSignupServerCommand extends SocialSignupCommand { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super(context, subpath, commander); + } + + protected async executeSocialSignup(params: SocialSignupParams): Promise { + const { platform, agentName, description, metadata } = params; + + if (!platform) { + throw new Error('platform is required (e.g., "moltbook")'); + } + if (!agentName) { + throw new Error('agentName is required (desired username on the platform)'); + } + + if (!SocialMediaProviderRegistry.hasPlatform(platform)) { + const available = SocialMediaProviderRegistry.availablePlatforms.join(', '); + throw new Error(`Unknown platform: '${platform}'. Available: ${available}`); + } + + // Resolve persona using shared identity resolution (standard priority pattern) + const personaId = await resolvePersonaId(params.personaId, params); + + // Open persona's longterm.db + const { dbHandle } = await openPersonaDb(personaId, params); + + // Check if already registered on this platform + const existingResult = await DataList.execute({ + dbHandle, + collection: SocialCredentialEntity.collection, + filter: { personaId, platformId: platform }, + limit: 1, + }); + + if (existingResult.success && existingResult.items?.length) { + const existing = existingResult.items[0]; + return transformPayload(params, { + success: true, + message: `Already registered on ${platform} as @${existing.agentName}`, + apiKey: existing.apiKey, + agentName: existing.agentName, + profileUrl: existing.profileUrl, + claimUrl: existing.claimUrl, + }); + } + + // Create provider (unauthenticated β€” signup doesn't need auth) + const provider = SocialMediaProviderRegistry.createProvider(platform); + + // Register on the platform + const signupResult = await provider.signup({ agentName, description, metadata }); + + if (!signupResult.success || !signupResult.apiKey) { + throw new Error(signupResult.error ?? `Signup failed on ${platform}`); + } + + // Store credential in persona's longterm.db + const credential = new SocialCredentialEntity(); + credential.personaId = personaId; + credential.platformId = platform; + credential.apiKey = signupResult.apiKey; + credential.agentName = signupResult.agentName ?? agentName; + credential.profileUrl = signupResult.profileUrl; + credential.claimUrl = signupResult.claimUrl; + credential.claimStatus = 'pending'; + credential.registeredAt = new Date(); + + await storeCredential(dbHandle, credential); + + return transformPayload(params, { + success: true, + message: `Registered on ${platform} as @${credential.agentName}`, + apiKey: signupResult.apiKey, + agentName: credential.agentName, + claimUrl: signupResult.claimUrl, + profileUrl: signupResult.profileUrl, + verificationCode: signupResult.verificationCode, + }); + } +} diff --git a/src/debug/jtag/commands/social/signup/shared/SocialSignupCommand.ts b/src/debug/jtag/commands/social/signup/shared/SocialSignupCommand.ts new file mode 100644 index 000000000..90db0b487 --- /dev/null +++ b/src/debug/jtag/commands/social/signup/shared/SocialSignupCommand.ts @@ -0,0 +1,20 @@ +/** + * Social Signup Command - Shared base class + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { SocialSignupParams, SocialSignupResult } from './SocialSignupTypes'; +import type { JTAGContext, JTAGPayload } from '@system/core/types/JTAGTypes'; + +export abstract class SocialSignupCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/signup', context, subpath, commander); + } + + protected abstract executeSocialSignup(params: SocialSignupParams): Promise; + + async execute(params: JTAGPayload): Promise { + return this.executeSocialSignup(params as SocialSignupParams); + } +} diff --git a/src/debug/jtag/commands/social/signup/shared/SocialSignupTypes.ts b/src/debug/jtag/commands/social/signup/shared/SocialSignupTypes.ts new file mode 100644 index 000000000..ee4b579e0 --- /dev/null +++ b/src/debug/jtag/commands/social/signup/shared/SocialSignupTypes.ts @@ -0,0 +1,124 @@ +/** + * Social Signup Command - Shared Types + * + * Register a persona on a social media platform (e.g., Moltbook). + * Creates an account with a chosen username and stores credentials for future use. + * + * Usage: + * ./jtag social/signup --platform=moltbook --agentName="helper-ai" --description="I help with code" + */ + +import type { CommandParams, CommandResult, CommandInput, JTAGContext } from '@system/core/types/JTAGTypes'; +import { createPayload, transformPayload } from '@system/core/types/JTAGTypes'; +import { Commands } from '@system/core/shared/Commands'; +import type { JTAGError } from '@system/core/types/ErrorTypes'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; + +/** + * Social Signup Command Parameters + */ +export interface SocialSignupParams extends CommandParams { + /** Platform to register on (e.g., 'moltbook') */ + platform: string; + + /** Desired username on the platform */ + agentName: string; + + /** Profile description/bio */ + description?: string; + + /** Persona user ID (auto-detected if not provided) */ + personaId?: UUID; + + /** Additional platform-specific metadata */ + metadata?: Record; +} + +/** + * Factory function for creating SocialSignupParams + */ +export const createSocialSignupParams = ( + context: JTAGContext, + sessionId: UUID, + data: { + platform: string; + agentName: string; + description?: string; + personaId?: UUID; + metadata?: Record; + } +): SocialSignupParams => createPayload(context, sessionId, { + description: data.description ?? '', + personaId: data.personaId ?? undefined, + metadata: data.metadata ?? undefined, + ...data +}); + +/** + * Social Signup Command Result + */ +export interface SocialSignupResult extends CommandResult { + success: boolean; + message: string; + + /** API key for future authenticated requests */ + apiKey?: string; + + /** Assigned username on the platform */ + agentName?: string; + + /** URL to claim/verify the account */ + claimUrl?: string; + + /** URL to the agent's profile page */ + profileUrl?: string; + + /** Verification code if applicable */ + verificationCode?: string; + + error?: JTAGError; +} + +/** + * Factory function for creating SocialSignupResult with defaults + */ +export const createSocialSignupResult = ( + context: JTAGContext, + sessionId: UUID, + data: { + success: boolean; + message?: string; + apiKey?: string; + agentName?: string; + claimUrl?: string; + profileUrl?: string; + verificationCode?: string; + error?: JTAGError; + } +): SocialSignupResult => createPayload(context, sessionId, { + message: data.message ?? '', + ...data +}); + +/** + * Smart Social Signup-specific inheritance from params + * Auto-inherits context and sessionId from params + */ +export const createSocialSignupResultFromParams = ( + params: SocialSignupParams, + differences: Omit +): SocialSignupResult => transformPayload(params, differences); + +/** + * SocialSignup β€” Type-safe command executor + * + * Usage: + * import { SocialSignup } from '...shared/SocialSignupTypes'; + * const result = await SocialSignup.execute({ platform: 'moltbook', agentName: '...' }); + */ +export const SocialSignup = { + execute(params: CommandInput): Promise { + return Commands.execute('social/signup', params as Partial); + }, + commandName: 'social/signup' as const, +} as const; diff --git a/src/debug/jtag/commands/social/signup/test/integration/SocialSignupIntegration.test.ts b/src/debug/jtag/commands/social/signup/test/integration/SocialSignupIntegration.test.ts new file mode 100644 index 000000000..d31622c19 --- /dev/null +++ b/src/debug/jtag/commands/social/signup/test/integration/SocialSignupIntegration.test.ts @@ -0,0 +1,196 @@ +#!/usr/bin/env tsx +/** + * SocialSignup Command Integration Tests + * + * Tests Social Signup command against the LIVE RUNNING SYSTEM. + * This is NOT a mock test - it tests real commands, real events, real widgets. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Signup/test/integration/SocialSignupIntegration.test.ts + * + * PREREQUISITES: + * - Server must be running: npm start (wait 90+ seconds) + * - Browser client connected via http://localhost:9003 + */ + +import { jtag } from '@server/server-index'; + +console.log('πŸ§ͺ SocialSignup Command Integration Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Test 1: Connect to live system + */ +async function testSystemConnection(): Promise>> { + console.log('\nπŸ”Œ Test 1: Connecting to live JTAG system'); + + const client = await jtag.connect(); + + assert(client !== null, 'Connected to live system'); + console.log(' βœ… Connected successfully'); + + return client; +} + +/** + * Test 2: Execute Social Signup command on live system + */ +async function testCommandExecution(client: Awaited>): Promise { + console.log('\n⚑ Test 2: Executing Social Signup command'); + + // TODO: Replace with your actual command parameters + const result = await client.commands['Social Signup']({ + // Add your required parameters here + // Example: name: 'test-value' + }); + + console.log(' πŸ“Š Result:', JSON.stringify(result, null, 2)); + + assert(result !== null, 'Social Signup returned result'); + // TODO: Add assertions for your specific result fields + // assert(result.success === true, 'Social Signup succeeded'); + // assert(result.yourField !== undefined, 'Result has yourField'); +} + +/** + * Test 3: Validate required parameters + */ +async function testRequiredParameters(_client: Awaited>): Promise { + console.log('\n🚨 Test 3: Testing required parameter validation'); + + // TODO: Uncomment and test missing required parameters + // try { + // await _client.commands['Social Signup']({ + // // Missing required param + // }); + // assert(false, 'Should have thrown validation error'); + // } catch (error) { + // assert((error as Error).message.includes('required'), 'Error mentions required parameter'); + // console.log(' βœ… ValidationError thrown correctly'); + // } + + console.log(' ⚠️ TODO: Add required parameter validation test'); +} + +/** + * Test 4: Test optional parameters + */ +async function testOptionalParameters(_client: Awaited>): Promise { + console.log('\nπŸ”§ Test 4: Testing optional parameters'); + + // TODO: Uncomment to test with and without optional parameters + // const withOptional = await client.commands['Social Signup']({ + // requiredParam: 'test', + // optionalParam: true + // }); + // + // const withoutOptional = await client.commands['Social Signup']({ + // requiredParam: 'test' + // }); + // + // assert(withOptional.success === true, 'Works with optional params'); + // assert(withoutOptional.success === true, 'Works without optional params'); + + console.log(' ⚠️ TODO: Add optional parameter tests'); +} + +/** + * Test 5: Performance test + */ +async function testPerformance(_client: Awaited>): Promise { + console.log('\n⚑ Test 5: Performance under load'); + + // TODO: Uncomment to test command performance + // const iterations = 10; + // const times: number[] = []; + // + // for (let i = 0; i < iterations; i++) { + // const start = Date.now(); + // await _client.commands['Social Signup']({ /* params */ }); + // times.push(Date.now() - start); + // } + // + // const avg = times.reduce((a, b) => a + b, 0) / iterations; + // const max = Math.max(...times); + // + // console.log(` Average: ${avg.toFixed(2)}ms`); + // console.log(` Max: ${max}ms`); + // + // assert(avg < 500, `Average ${avg.toFixed(2)}ms under 500ms`); + // assert(max < 1000, `Max ${max}ms under 1000ms`); + + console.log(' ⚠️ TODO: Add performance test'); +} + +/** + * Test 6: Widget/Event integration (if applicable) + */ +async function testWidgetIntegration(_client: Awaited>): Promise { + console.log('\n🎨 Test 6: Widget/Event integration'); + + // TODO: Uncomment if your command emits events or updates widgets + // Example: + // const before = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // await client.commands['Social Signup']({ /* params */ }); + // await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for event propagation + // const after = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // + // assert(after.state.someValue !== before.state.someValue, 'Widget state updated'); + + console.log(' ⚠️ TODO: Add widget/event integration test (if applicable)'); +} + +/** + * Run all integration tests + */ +async function runAllSocialSignupIntegrationTests(): Promise { + console.log('πŸš€ Starting SocialSignup Integration Tests\n'); + console.log('πŸ“‹ Testing against LIVE system (not mocks)\n'); + + try { + const client = await testSystemConnection(); + await testCommandExecution(client); + await testRequiredParameters(client); + await testOptionalParameters(client); + await testPerformance(client); + await testWidgetIntegration(client); + + console.log('\nπŸŽ‰ ALL SocialSignup INTEGRATION TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Live system connection'); + console.log(' βœ… Command execution on real system'); + console.log(' βœ… Parameter validation'); + console.log(' βœ… Optional parameter handling'); + console.log(' βœ… Performance benchmarks'); + console.log(' βœ… Widget/Event integration'); + console.log('\nπŸ’‘ NOTE: This test uses the REAL running system'); + console.log(' - Real database operations'); + console.log(' - Real event propagation'); + console.log(' - Real widget updates'); + console.log(' - Real cross-daemon communication'); + + } catch (error) { + console.error('\n❌ SocialSignup integration tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + console.error('\nπŸ’‘ Make sure:'); + console.error(' 1. Server is running: npm start'); + console.error(' 2. Wait 90+ seconds for deployment'); + console.error(' 3. Browser is connected to http://localhost:9003'); + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialSignupIntegrationTests(); +} else { + module.exports = { runAllSocialSignupIntegrationTests }; +} diff --git a/src/debug/jtag/commands/social/signup/test/unit/SocialSignupCommand.test.ts b/src/debug/jtag/commands/social/signup/test/unit/SocialSignupCommand.test.ts new file mode 100644 index 000000000..c8e33ea7f --- /dev/null +++ b/src/debug/jtag/commands/social/signup/test/unit/SocialSignupCommand.test.ts @@ -0,0 +1,259 @@ +#!/usr/bin/env tsx +/** + * SocialSignup Command Unit Tests + * + * Tests Social Signup command logic in isolation using mock dependencies. + * This is a REFERENCE EXAMPLE showing best practices for command testing. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Signup/test/unit/SocialSignupCommand.test.ts + * + * NOTE: This is a self-contained test (no external test utilities needed). + * Use this as a template for your own command tests. + */ + +// import { ValidationError } from '@system/core/types/ErrorTypes'; // Uncomment when adding validation tests +import { generateUUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialSignupParams, SocialSignupResult } from '../../shared/SocialSignupTypes'; + +console.log('πŸ§ͺ SocialSignup Command Unit Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Mock command that implements Social Signup logic for testing + */ +async function mockSocialSignupCommand(params: SocialSignupParams): Promise { + // TODO: Validate required parameters (BEST PRACTICE) + // Example: + // if (!params.requiredParam || params.requiredParam.trim() === '') { + // throw new ValidationError( + // 'requiredParam', + // `Missing required parameter 'requiredParam'. ` + + // `Use the help tool with 'Social Signup' or see the Social Signup README for usage information.` + // ); + // } + + // TODO: Handle optional parameters with sensible defaults + // const optionalParam = params.optionalParam ?? defaultValue; + + // TODO: Implement your command logic here + return { + success: true, + // TODO: Add your result fields with actual computed values + context: params.context, + sessionId: params.sessionId + } as SocialSignupResult; +} + +/** + * Test 1: Command structure validation + */ +function testSocialSignupCommandStructure(): void { + console.log('\nπŸ“‹ Test 1: SocialSignup command structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Create valid params for Social Signup command + const validParams: SocialSignupParams = { + // TODO: Add your required parameters here + context, + sessionId + }; + + // Validate param structure + assert(validParams.context !== undefined, 'Params have context'); + assert(validParams.sessionId !== undefined, 'Params have sessionId'); + // TODO: Add assertions for your specific parameters + // assert(typeof validParams.requiredParam === 'string', 'requiredParam is string'); +} + +/** + * Test 2: Mock command execution + */ +async function testMockSocialSignupExecution(): Promise { + console.log('\n⚑ Test 2: Mock Social Signup command execution'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test mock execution + const params: SocialSignupParams = { + // TODO: Add your parameters here + context, + sessionId + }; + + const result = await mockSocialSignupCommand(params); + + // Validate result structure + assert(result.success === true, 'Mock result shows success'); + // TODO: Add assertions for your result fields + // assert(typeof result.yourField === 'string', 'yourField is string'); +} + +/** + * Test 3: Required parameter validation (CRITICAL) + * + * This test ensures your command throws ValidationError + * when required parameters are missing (BEST PRACTICE) + */ +async function testSocialSignupRequiredParams(): Promise { + console.log('\n🚨 Test 3: Required parameter validation'); + + // TODO: Uncomment when implementing validation + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test cases that should throw ValidationError + // Example: + // const testCases = [ + // { params: {} as SocialSignupParams, desc: 'Missing requiredParam' }, + // { params: { requiredParam: '' } as SocialSignupParams, desc: 'Empty requiredParam' }, + // ]; + // + // for (const testCase of testCases) { + // try { + // await mockSocialSignupCommand({ ...testCase.params, context, sessionId }); + // throw new Error(`Should have thrown ValidationError for: ${testCase.desc}`); + // } catch (error) { + // if (error instanceof ValidationError) { + // assert(error.field === 'requiredParam', `ValidationError field is 'requiredParam' for: ${testCase.desc}`); + // assert(error.message.includes('required parameter'), `Error message mentions 'required parameter' for: ${testCase.desc}`); + // assert(error.message.includes('help tool'), `Error message is tool-agnostic for: ${testCase.desc}`); + // } else { + // throw error; // Re-throw if not ValidationError + // } + // } + // } + + console.log('βœ… All required parameter validations work correctly'); +} + +/** + * Test 4: Optional parameter handling + */ +async function testSocialSignupOptionalParams(): Promise { + console.log('\nπŸ”§ Test 4: Optional parameter handling'); + + // TODO: Uncomment when implementing optional param tests + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test WITHOUT optional param (should use default) + // const paramsWithoutOptional: SocialSignupParams = { + // requiredParam: 'test', + // context, + // sessionId + // }; + // + // const resultWithoutOptional = await mockSocialSignupCommand(paramsWithoutOptional); + // assert(resultWithoutOptional.success === true, 'Command succeeds without optional params'); + + // TODO: Test WITH optional param + // const paramsWithOptional: SocialSignupParams = { + // requiredParam: 'test', + // optionalParam: true, + // context, + // sessionId + // }; + // + // const resultWithOptional = await mockSocialSignupCommand(paramsWithOptional); + // assert(resultWithOptional.success === true, 'Command succeeds with optional params'); + + console.log('βœ… Optional parameter handling validated'); +} + +/** + * Test 5: Performance validation + */ +async function testSocialSignupPerformance(): Promise { + console.log('\n⚑ Test 5: SocialSignup performance validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + const startTime = Date.now(); + + await mockSocialSignupCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialSignupParams); + + const executionTime = Date.now() - startTime; + + assert(executionTime < 100, `SocialSignup completed in ${executionTime}ms (under 100ms limit)`); +} + +/** + * Test 6: Result structure validation + */ +async function testSocialSignupResultStructure(): Promise { + console.log('\nπŸ” Test 6: SocialSignup result structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test various scenarios + const basicResult = await mockSocialSignupCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialSignupParams); + + assert(basicResult.success === true, 'Result has success field'); + // TODO: Add assertions for your result fields + // assert(typeof basicResult.yourField === 'string', 'Result has yourField (string)'); + assert(basicResult.context === context, 'Result includes context'); + assert(basicResult.sessionId === sessionId, 'Result includes sessionId'); + + console.log('βœ… All result structure validations pass'); +} + +/** + * Run all unit tests + */ +async function runAllSocialSignupUnitTests(): Promise { + console.log('πŸš€ Starting SocialSignup Command Unit Tests\n'); + + try { + testSocialSignupCommandStructure(); + await testMockSocialSignupExecution(); + await testSocialSignupRequiredParams(); + await testSocialSignupOptionalParams(); + await testSocialSignupPerformance(); + await testSocialSignupResultStructure(); + + console.log('\nπŸŽ‰ ALL SocialSignup UNIT TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Command structure and parameter validation'); + console.log(' βœ… Mock command execution patterns'); + console.log(' βœ… Required parameter validation (throws ValidationError)'); + console.log(' βœ… Optional parameter handling (sensible defaults)'); + console.log(' βœ… Performance requirements (< 100ms)'); + console.log(' βœ… Result structure validation'); + console.log('\nπŸ“ This is a REFERENCE EXAMPLE - use as a template for your commands!'); + console.log('πŸ’‘ TIP: Copy this test structure and modify for your command logic'); + + } catch (error) { + console.error('\n❌ SocialSignup unit tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialSignupUnitTests(); +} else { + module.exports = { runAllSocialSignupUnitTests }; +} diff --git a/src/debug/jtag/commands/social/trending/.npmignore b/src/debug/jtag/commands/social/trending/.npmignore new file mode 100644 index 000000000..f74ad6b8a --- /dev/null +++ b/src/debug/jtag/commands/social/trending/.npmignore @@ -0,0 +1,20 @@ +# Development files +.eslintrc* +tsconfig*.json +vitest.config.ts + +# Build artifacts +*.js.map +*.d.ts.map + +# IDE +.vscode/ +.idea/ + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db diff --git a/src/debug/jtag/commands/social/trending/README.md b/src/debug/jtag/commands/social/trending/README.md new file mode 100644 index 000000000..a474eb75f --- /dev/null +++ b/src/debug/jtag/commands/social/trending/README.md @@ -0,0 +1,170 @@ +# Social Trending Command + +Discover trending and popular content on a social media platform. Shows hot posts, top communities, and rising discussions. + +## Table of Contents + +- [Usage](#usage) + - [CLI Usage](#cli-usage) + - [Tool Usage](#tool-usage) +- [Parameters](#parameters) +- [Result](#result) +- [Examples](#examples) +- [Testing](#testing) + - [Unit Tests](#unit-tests) + - [Integration Tests](#integration-tests) +- [Getting Help](#getting-help) +- [Access Level](#access-level) +- [Implementation Notes](#implementation-notes) + +## Usage + +### CLI Usage + +From the command line using the jtag CLI: + +```bash +./jtag social/trending --platform= +``` + +### Tool Usage + +From Persona tools or programmatic access using `Commands.execute()`: + +```typescript +import { Commands } from '@system/core/shared/Commands'; + +const result = await Commands.execute('social/trending', { + // your parameters here +}); +``` + +## Parameters + +- **platform** (required): `string` - Platform to browse (e.g., 'moltbook') +- **sort** (optional): `string` - Sort order: hot (default), top, rising +- **community** (optional): `string` - Filter to specific community/submolt +- **limit** (optional): `number` - Maximum number of posts to return (default: 10) +- **personaId** (optional): `string` - Persona user ID (auto-detected if not provided) + +## Result + +Returns `SocialTrendingResult` with: + +Returns CommandResult with: +- **posts**: `SocialPost[]` - Array of trending posts +- **community**: `string` - Community filter applied (if any) + +## Examples + +### See what's hot across the platform + +```bash +./jtag social/trending --platform=moltbook +``` + +**Expected result:** +{ success: true, posts: [...], message: 'Fetched 10 trending posts...' } + +### Top posts in a specific community + +```bash +./jtag social/trending --platform=moltbook --community=ai-development --sort=top +``` + +### Rising discussions with limit + +```bash +./jtag social/trending --platform=moltbook --sort=rising --limit=5 +``` + +## Getting Help + +### Using the Help Tool + +Get detailed usage information for this command: + +**CLI:** +```bash +./jtag help social/trending +``` + +**Tool:** +```typescript +// Use your help tool with command name 'social/trending' +``` + +### Using the README Tool + +Access this README programmatically: + +**CLI:** +```bash +./jtag readme social/trending +``` + +**Tool:** +```typescript +// Use your readme tool with command name 'social/trending' +``` + +## Testing + +### Unit Tests + +Test command logic in isolation using mock dependencies: + +```bash +# Run unit tests (no server required) +npx tsx commands/social/trending/test/unit/SocialTrendingCommand.test.ts +``` + +**What's tested:** +- Command structure and parameter validation +- Mock command execution patterns +- Required parameter validation (throws ValidationError) +- Optional parameter handling (sensible defaults) +- Performance requirements +- Assertion utility helpers + +**TDD Workflow:** +1. Write/modify unit test first (test-driven development) +2. Run test, see it fail +3. Implement feature +4. Run test, see it pass +5. Refactor if needed + +### Integration Tests + +Test command with real client connections and system integration: + +```bash +# Prerequisites: Server must be running +npm start # Wait 90+ seconds for deployment + +# Run integration tests +npx tsx commands/social/trending/test/integration/SocialTrendingIntegration.test.ts +``` + +**What's tested:** +- Client connection to live system +- Real command execution via WebSocket +- ValidationError handling for missing params +- Optional parameter defaults +- Performance under load +- Various parameter combinations + +**Best Practice:** +Run unit tests frequently during development (fast feedback). Run integration tests before committing (verify system integration). + +## Access Level + +**ai-safe** - Safe for AI personas to call autonomously + +## Implementation Notes + +- **Shared Logic**: Core business logic in `shared/SocialTrendingTypes.ts` +- **Browser**: Browser-specific implementation in `browser/SocialTrendingBrowserCommand.ts` +- **Server**: Server-specific implementation in `server/SocialTrendingServerCommand.ts` +- **Unit Tests**: Isolated testing in `test/unit/SocialTrendingCommand.test.ts` +- **Integration Tests**: System testing in `test/integration/SocialTrendingIntegration.test.ts` diff --git a/src/debug/jtag/commands/social/trending/browser/SocialTrendingBrowserCommand.ts b/src/debug/jtag/commands/social/trending/browser/SocialTrendingBrowserCommand.ts new file mode 100644 index 000000000..1ca953961 --- /dev/null +++ b/src/debug/jtag/commands/social/trending/browser/SocialTrendingBrowserCommand.ts @@ -0,0 +1,19 @@ +/** + * Social Trending Command - Browser Implementation + * Delegates to server + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { SocialTrendingParams, SocialTrendingResult } from '../shared/SocialTrendingTypes'; + +export class SocialTrendingBrowserCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/trending', context, subpath, commander); + } + + async execute(params: SocialTrendingParams): Promise { + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/social/trending/package.json b/src/debug/jtag/commands/social/trending/package.json new file mode 100644 index 000000000..f0ad7fc40 --- /dev/null +++ b/src/debug/jtag/commands/social/trending/package.json @@ -0,0 +1,35 @@ +{ + "name": "@jtag-commands/social/trending", + "version": "1.0.0", + "description": "Discover trending and popular content on a social media platform. Shows hot posts, top communities, and rising discussions.", + "main": "server/SocialTrendingServerCommand.ts", + "types": "shared/SocialTrendingTypes.ts", + "scripts": { + "test": "npm run test:unit && npm run test:integration", + "test:unit": "npx vitest run test/unit/*.test.ts", + "test:integration": "npx tsx test/integration/SocialTrendingIntegration.test.ts", + "lint": "npx eslint **/*.ts", + "typecheck": "npx tsc --noEmit" + }, + "peerDependencies": { + "@jtag/core": "*" + }, + "files": [ + "shared/**/*.ts", + "browser/**/*.ts", + "server/**/*.ts", + "test/**/*.ts", + "README.md" + ], + "keywords": [ + "jtag", + "command", + "social/trending" + ], + "license": "MIT", + "author": "", + "repository": { + "type": "git", + "url": "" + } +} diff --git a/src/debug/jtag/commands/social/trending/server/SocialTrendingServerCommand.ts b/src/debug/jtag/commands/social/trending/server/SocialTrendingServerCommand.ts new file mode 100644 index 000000000..03bc6fce5 --- /dev/null +++ b/src/debug/jtag/commands/social/trending/server/SocialTrendingServerCommand.ts @@ -0,0 +1,43 @@ +/** + * Social Trending Command - Server Implementation + * + * Discover trending and popular content on a social media platform. + * Uses the feed endpoint with sort=hot (default), top, or rising. + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import { transformPayload } from '@system/core/types/JTAGTypes'; +import type { SocialTrendingParams, SocialTrendingResult } from '../shared/SocialTrendingTypes'; +import { loadSocialContext } from '@system/social/server/SocialCommandHelper'; + +export class SocialTrendingServerCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('social/trending', context, subpath, commander); + } + + async execute(params: SocialTrendingParams): Promise { + const { platform, community, limit } = params; + const sort = params.sort ?? 'hot'; + const effectiveLimit = limit ?? 10; + + if (!platform) throw new Error('platform is required'); + + const ctx = await loadSocialContext(platform, params.personaId, params); + + let posts; + if (community) { + posts = await ctx.provider.getCommunityFeed(community, sort, effectiveLimit); + } else { + posts = await ctx.provider.getFeed({ sort, limit: effectiveLimit }); + } + + const source = community ? `${platform}/${community}` : platform; + return transformPayload(params, { + success: true, + message: `Fetched ${posts.length} trending posts from ${source} (${sort})`, + posts, + }); + } +} diff --git a/src/debug/jtag/commands/social/trending/shared/SocialTrendingTypes.ts b/src/debug/jtag/commands/social/trending/shared/SocialTrendingTypes.ts new file mode 100644 index 000000000..433e99f86 --- /dev/null +++ b/src/debug/jtag/commands/social/trending/shared/SocialTrendingTypes.ts @@ -0,0 +1,112 @@ +/** + * Social Trending Command - Shared Types + * + * Discover trending and popular content on a social media platform. + * Shows hot posts, top communities, and rising discussions. + * + * Usage: + * ./jtag social/trending --platform=moltbook + * ./jtag social/trending --platform=moltbook --community=ai-development --sort=top + * ./jtag social/trending --platform=moltbook --sort=rising --limit=5 + */ + +import type { CommandParams, CommandResult, CommandInput, JTAGContext } from '@system/core/types/JTAGTypes'; +import { createPayload, transformPayload } from '@system/core/types/JTAGTypes'; +import { Commands } from '@system/core/shared/Commands'; +import type { JTAGError } from '@system/core/types/ErrorTypes'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialPost } from '@system/social/shared/SocialMediaTypes'; + +/** + * Social Trending Command Parameters + */ +export interface SocialTrendingParams extends CommandParams { + /** Platform to browse (e.g., 'moltbook') */ + platform: string; + + /** Sort order: hot (default), top, rising */ + sort?: 'hot' | 'top' | 'rising'; + + /** Filter to specific community/submolt */ + community?: string; + + /** Maximum number of posts to return (default: 10) */ + limit?: number; + + /** Persona user ID (auto-detected if not provided) */ + personaId?: UUID; +} + +/** + * Factory function for creating SocialTrendingParams + */ +export const createSocialTrendingParams = ( + context: JTAGContext, + sessionId: UUID, + data: { + platform: string; + sort?: 'hot' | 'top' | 'rising'; + community?: string; + limit?: number; + personaId?: UUID; + } +): SocialTrendingParams => createPayload(context, sessionId, { + sort: data.sort ?? undefined, + community: data.community ?? undefined, + limit: data.limit ?? 0, + personaId: data.personaId ?? undefined, + ...data +}); + +/** + * Social Trending Command Result + */ +export interface SocialTrendingResult extends CommandResult { + success: boolean; + message: string; + + /** Array of trending posts */ + posts?: SocialPost[]; + + error?: JTAGError; +} + +/** + * Factory function for creating SocialTrendingResult with defaults + */ +export const createSocialTrendingResult = ( + context: JTAGContext, + sessionId: UUID, + data: { + success: boolean; + message?: string; + posts?: SocialPost[]; + error?: JTAGError; + } +): SocialTrendingResult => createPayload(context, sessionId, { + message: data.message ?? '', + ...data +}); + +/** + * Smart Social Trending-specific inheritance from params + * Auto-inherits context and sessionId from params + */ +export const createSocialTrendingResultFromParams = ( + params: SocialTrendingParams, + differences: Omit +): SocialTrendingResult => transformPayload(params, differences); + +/** + * SocialTrending β€” Type-safe command executor + * + * Usage: + * import { SocialTrending } from '...shared/SocialTrendingTypes'; + * const result = await SocialTrending.execute({ platform: 'moltbook', sort: 'hot' }); + */ +export const SocialTrending = { + execute(params: CommandInput): Promise { + return Commands.execute('social/trending', params as Partial); + }, + commandName: 'social/trending' as const, +} as const; diff --git a/src/debug/jtag/commands/social/trending/test/integration/SocialTrendingIntegration.test.ts b/src/debug/jtag/commands/social/trending/test/integration/SocialTrendingIntegration.test.ts new file mode 100644 index 000000000..fab04125f --- /dev/null +++ b/src/debug/jtag/commands/social/trending/test/integration/SocialTrendingIntegration.test.ts @@ -0,0 +1,196 @@ +#!/usr/bin/env tsx +/** + * SocialTrending Command Integration Tests + * + * Tests Social Trending command against the LIVE RUNNING SYSTEM. + * This is NOT a mock test - it tests real commands, real events, real widgets. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Trending/test/integration/SocialTrendingIntegration.test.ts + * + * PREREQUISITES: + * - Server must be running: npm start (wait 90+ seconds) + * - Browser client connected via http://localhost:9003 + */ + +import { jtag } from '@server/server-index'; + +console.log('πŸ§ͺ SocialTrending Command Integration Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Test 1: Connect to live system + */ +async function testSystemConnection(): Promise>> { + console.log('\nπŸ”Œ Test 1: Connecting to live JTAG system'); + + const client = await jtag.connect(); + + assert(client !== null, 'Connected to live system'); + console.log(' βœ… Connected successfully'); + + return client; +} + +/** + * Test 2: Execute Social Trending command on live system + */ +async function testCommandExecution(client: Awaited>): Promise { + console.log('\n⚑ Test 2: Executing Social Trending command'); + + // TODO: Replace with your actual command parameters + const result = await client.commands['Social Trending']({ + // Add your required parameters here + // Example: name: 'test-value' + }); + + console.log(' πŸ“Š Result:', JSON.stringify(result, null, 2)); + + assert(result !== null, 'Social Trending returned result'); + // TODO: Add assertions for your specific result fields + // assert(result.success === true, 'Social Trending succeeded'); + // assert(result.yourField !== undefined, 'Result has yourField'); +} + +/** + * Test 3: Validate required parameters + */ +async function testRequiredParameters(_client: Awaited>): Promise { + console.log('\n🚨 Test 3: Testing required parameter validation'); + + // TODO: Uncomment and test missing required parameters + // try { + // await _client.commands['Social Trending']({ + // // Missing required param + // }); + // assert(false, 'Should have thrown validation error'); + // } catch (error) { + // assert((error as Error).message.includes('required'), 'Error mentions required parameter'); + // console.log(' βœ… ValidationError thrown correctly'); + // } + + console.log(' ⚠️ TODO: Add required parameter validation test'); +} + +/** + * Test 4: Test optional parameters + */ +async function testOptionalParameters(_client: Awaited>): Promise { + console.log('\nπŸ”§ Test 4: Testing optional parameters'); + + // TODO: Uncomment to test with and without optional parameters + // const withOptional = await client.commands['Social Trending']({ + // requiredParam: 'test', + // optionalParam: true + // }); + // + // const withoutOptional = await client.commands['Social Trending']({ + // requiredParam: 'test' + // }); + // + // assert(withOptional.success === true, 'Works with optional params'); + // assert(withoutOptional.success === true, 'Works without optional params'); + + console.log(' ⚠️ TODO: Add optional parameter tests'); +} + +/** + * Test 5: Performance test + */ +async function testPerformance(_client: Awaited>): Promise { + console.log('\n⚑ Test 5: Performance under load'); + + // TODO: Uncomment to test command performance + // const iterations = 10; + // const times: number[] = []; + // + // for (let i = 0; i < iterations; i++) { + // const start = Date.now(); + // await _client.commands['Social Trending']({ /* params */ }); + // times.push(Date.now() - start); + // } + // + // const avg = times.reduce((a, b) => a + b, 0) / iterations; + // const max = Math.max(...times); + // + // console.log(` Average: ${avg.toFixed(2)}ms`); + // console.log(` Max: ${max}ms`); + // + // assert(avg < 500, `Average ${avg.toFixed(2)}ms under 500ms`); + // assert(max < 1000, `Max ${max}ms under 1000ms`); + + console.log(' ⚠️ TODO: Add performance test'); +} + +/** + * Test 6: Widget/Event integration (if applicable) + */ +async function testWidgetIntegration(_client: Awaited>): Promise { + console.log('\n🎨 Test 6: Widget/Event integration'); + + // TODO: Uncomment if your command emits events or updates widgets + // Example: + // const before = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // await client.commands['Social Trending']({ /* params */ }); + // await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for event propagation + // const after = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // + // assert(after.state.someValue !== before.state.someValue, 'Widget state updated'); + + console.log(' ⚠️ TODO: Add widget/event integration test (if applicable)'); +} + +/** + * Run all integration tests + */ +async function runAllSocialTrendingIntegrationTests(): Promise { + console.log('πŸš€ Starting SocialTrending Integration Tests\n'); + console.log('πŸ“‹ Testing against LIVE system (not mocks)\n'); + + try { + const client = await testSystemConnection(); + await testCommandExecution(client); + await testRequiredParameters(client); + await testOptionalParameters(client); + await testPerformance(client); + await testWidgetIntegration(client); + + console.log('\nπŸŽ‰ ALL SocialTrending INTEGRATION TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Live system connection'); + console.log(' βœ… Command execution on real system'); + console.log(' βœ… Parameter validation'); + console.log(' βœ… Optional parameter handling'); + console.log(' βœ… Performance benchmarks'); + console.log(' βœ… Widget/Event integration'); + console.log('\nπŸ’‘ NOTE: This test uses the REAL running system'); + console.log(' - Real database operations'); + console.log(' - Real event propagation'); + console.log(' - Real widget updates'); + console.log(' - Real cross-daemon communication'); + + } catch (error) { + console.error('\n❌ SocialTrending integration tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + console.error('\nπŸ’‘ Make sure:'); + console.error(' 1. Server is running: npm start'); + console.error(' 2. Wait 90+ seconds for deployment'); + console.error(' 3. Browser is connected to http://localhost:9003'); + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialTrendingIntegrationTests(); +} else { + module.exports = { runAllSocialTrendingIntegrationTests }; +} diff --git a/src/debug/jtag/commands/social/trending/test/unit/SocialTrendingCommand.test.ts b/src/debug/jtag/commands/social/trending/test/unit/SocialTrendingCommand.test.ts new file mode 100644 index 000000000..6b40de7e2 --- /dev/null +++ b/src/debug/jtag/commands/social/trending/test/unit/SocialTrendingCommand.test.ts @@ -0,0 +1,259 @@ +#!/usr/bin/env tsx +/** + * SocialTrending Command Unit Tests + * + * Tests Social Trending command logic in isolation using mock dependencies. + * This is a REFERENCE EXAMPLE showing best practices for command testing. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Social Trending/test/unit/SocialTrendingCommand.test.ts + * + * NOTE: This is a self-contained test (no external test utilities needed). + * Use this as a template for your own command tests. + */ + +// import { ValidationError } from '@system/core/types/ErrorTypes'; // Uncomment when adding validation tests +import { generateUUID } from '@system/core/types/CrossPlatformUUID'; +import type { SocialTrendingParams, SocialTrendingResult } from '../../shared/SocialTrendingTypes'; + +console.log('πŸ§ͺ SocialTrending Command Unit Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`βœ… ${message}`); +} + +/** + * Mock command that implements Social Trending logic for testing + */ +async function mockSocialTrendingCommand(params: SocialTrendingParams): Promise { + // TODO: Validate required parameters (BEST PRACTICE) + // Example: + // if (!params.requiredParam || params.requiredParam.trim() === '') { + // throw new ValidationError( + // 'requiredParam', + // `Missing required parameter 'requiredParam'. ` + + // `Use the help tool with 'Social Trending' or see the Social Trending README for usage information.` + // ); + // } + + // TODO: Handle optional parameters with sensible defaults + // const optionalParam = params.optionalParam ?? defaultValue; + + // TODO: Implement your command logic here + return { + success: true, + // TODO: Add your result fields with actual computed values + context: params.context, + sessionId: params.sessionId + } as SocialTrendingResult; +} + +/** + * Test 1: Command structure validation + */ +function testSocialTrendingCommandStructure(): void { + console.log('\nπŸ“‹ Test 1: SocialTrending command structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Create valid params for Social Trending command + const validParams: SocialTrendingParams = { + // TODO: Add your required parameters here + context, + sessionId + }; + + // Validate param structure + assert(validParams.context !== undefined, 'Params have context'); + assert(validParams.sessionId !== undefined, 'Params have sessionId'); + // TODO: Add assertions for your specific parameters + // assert(typeof validParams.requiredParam === 'string', 'requiredParam is string'); +} + +/** + * Test 2: Mock command execution + */ +async function testMockSocialTrendingExecution(): Promise { + console.log('\n⚑ Test 2: Mock Social Trending command execution'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test mock execution + const params: SocialTrendingParams = { + // TODO: Add your parameters here + context, + sessionId + }; + + const result = await mockSocialTrendingCommand(params); + + // Validate result structure + assert(result.success === true, 'Mock result shows success'); + // TODO: Add assertions for your result fields + // assert(typeof result.yourField === 'string', 'yourField is string'); +} + +/** + * Test 3: Required parameter validation (CRITICAL) + * + * This test ensures your command throws ValidationError + * when required parameters are missing (BEST PRACTICE) + */ +async function testSocialTrendingRequiredParams(): Promise { + console.log('\n🚨 Test 3: Required parameter validation'); + + // TODO: Uncomment when implementing validation + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test cases that should throw ValidationError + // Example: + // const testCases = [ + // { params: {} as SocialTrendingParams, desc: 'Missing requiredParam' }, + // { params: { requiredParam: '' } as SocialTrendingParams, desc: 'Empty requiredParam' }, + // ]; + // + // for (const testCase of testCases) { + // try { + // await mockSocialTrendingCommand({ ...testCase.params, context, sessionId }); + // throw new Error(`Should have thrown ValidationError for: ${testCase.desc}`); + // } catch (error) { + // if (error instanceof ValidationError) { + // assert(error.field === 'requiredParam', `ValidationError field is 'requiredParam' for: ${testCase.desc}`); + // assert(error.message.includes('required parameter'), `Error message mentions 'required parameter' for: ${testCase.desc}`); + // assert(error.message.includes('help tool'), `Error message is tool-agnostic for: ${testCase.desc}`); + // } else { + // throw error; // Re-throw if not ValidationError + // } + // } + // } + + console.log('βœ… All required parameter validations work correctly'); +} + +/** + * Test 4: Optional parameter handling + */ +async function testSocialTrendingOptionalParams(): Promise { + console.log('\nπŸ”§ Test 4: Optional parameter handling'); + + // TODO: Uncomment when implementing optional param tests + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test WITHOUT optional param (should use default) + // const paramsWithoutOptional: SocialTrendingParams = { + // requiredParam: 'test', + // context, + // sessionId + // }; + // + // const resultWithoutOptional = await mockSocialTrendingCommand(paramsWithoutOptional); + // assert(resultWithoutOptional.success === true, 'Command succeeds without optional params'); + + // TODO: Test WITH optional param + // const paramsWithOptional: SocialTrendingParams = { + // requiredParam: 'test', + // optionalParam: true, + // context, + // sessionId + // }; + // + // const resultWithOptional = await mockSocialTrendingCommand(paramsWithOptional); + // assert(resultWithOptional.success === true, 'Command succeeds with optional params'); + + console.log('βœ… Optional parameter handling validated'); +} + +/** + * Test 5: Performance validation + */ +async function testSocialTrendingPerformance(): Promise { + console.log('\n⚑ Test 5: SocialTrending performance validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + const startTime = Date.now(); + + await mockSocialTrendingCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialTrendingParams); + + const executionTime = Date.now() - startTime; + + assert(executionTime < 100, `SocialTrending completed in ${executionTime}ms (under 100ms limit)`); +} + +/** + * Test 6: Result structure validation + */ +async function testSocialTrendingResultStructure(): Promise { + console.log('\nπŸ” Test 6: SocialTrending result structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test various scenarios + const basicResult = await mockSocialTrendingCommand({ + // TODO: Add your parameters + context, + sessionId + } as SocialTrendingParams); + + assert(basicResult.success === true, 'Result has success field'); + // TODO: Add assertions for your result fields + // assert(typeof basicResult.yourField === 'string', 'Result has yourField (string)'); + assert(basicResult.context === context, 'Result includes context'); + assert(basicResult.sessionId === sessionId, 'Result includes sessionId'); + + console.log('βœ… All result structure validations pass'); +} + +/** + * Run all unit tests + */ +async function runAllSocialTrendingUnitTests(): Promise { + console.log('πŸš€ Starting SocialTrending Command Unit Tests\n'); + + try { + testSocialTrendingCommandStructure(); + await testMockSocialTrendingExecution(); + await testSocialTrendingRequiredParams(); + await testSocialTrendingOptionalParams(); + await testSocialTrendingPerformance(); + await testSocialTrendingResultStructure(); + + console.log('\nπŸŽ‰ ALL SocialTrending UNIT TESTS PASSED!'); + console.log('πŸ“‹ Validated:'); + console.log(' βœ… Command structure and parameter validation'); + console.log(' βœ… Mock command execution patterns'); + console.log(' βœ… Required parameter validation (throws ValidationError)'); + console.log(' βœ… Optional parameter handling (sensible defaults)'); + console.log(' βœ… Performance requirements (< 100ms)'); + console.log(' βœ… Result structure validation'); + console.log('\nπŸ“ This is a REFERENCE EXAMPLE - use as a template for your commands!'); + console.log('πŸ’‘ TIP: Copy this test structure and modify for your command logic'); + + } catch (error) { + console.error('\n❌ SocialTrending unit tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllSocialTrendingUnitTests(); +} else { + module.exports = { runAllSocialTrendingUnitTests }; +} diff --git a/src/debug/jtag/daemons/data-daemon/server/EntityRegistry.ts b/src/debug/jtag/daemons/data-daemon/server/EntityRegistry.ts index 21dc1f8fd..4e1e0b75b 100644 --- a/src/debug/jtag/daemons/data-daemon/server/EntityRegistry.ts +++ b/src/debug/jtag/daemons/data-daemon/server/EntityRegistry.ts @@ -80,6 +80,8 @@ import { PersonaRAGContextEntity } from '../../../system/data/entities/PersonaRA import { TimelineEventEntity } from '../../../system/data/entities/TimelineEventEntity'; import { FeedbackEntity } from '../../../system/data/entities/FeedbackEntity'; import { CallEntity } from '../../../system/data/entities/CallEntity'; +import { SocialCredentialEntity } from '../../../system/social/shared/SocialCredentialEntity'; +import { HandleEntity } from '../../../system/data/entities/HandleEntity'; /** * Initialize entity registration for the storage adapter @@ -133,6 +135,8 @@ export function initializeEntityRegistry(): void { new TimelineEventEntity(); new FeedbackEntity(); new CallEntity(); + new SocialCredentialEntity(); + new HandleEntity(); registerEntity(UserEntity.collection, UserEntity); registerEntity(RoomEntity.collection, RoomEntity); @@ -178,6 +182,8 @@ export function initializeEntityRegistry(): void { registerEntity(TimelineEventEntity.collection, TimelineEventEntity); registerEntity(FeedbackEntity.collection, FeedbackEntity); registerEntity(CallEntity.collection, CallEntity); + registerEntity(SocialCredentialEntity.collection, SocialCredentialEntity); + registerEntity(HandleEntity.collection, HandleEntity); log.info('All entities registered'); } \ No newline at end of file diff --git a/src/debug/jtag/daemons/data-daemon/server/managers/SqliteQueryExecutor.ts b/src/debug/jtag/daemons/data-daemon/server/managers/SqliteQueryExecutor.ts index cdc757184..e6e467b4c 100644 --- a/src/debug/jtag/daemons/data-daemon/server/managers/SqliteQueryExecutor.ts +++ b/src/debug/jtag/daemons/data-daemon/server/managers/SqliteQueryExecutor.ts @@ -204,64 +204,78 @@ export class SqliteQueryExecutor { log.debug(`[SCHEMA-PATH] Query ${query.collection} returned ${rows.length} rows`); - const records: DataRecord[] = rows.map(row => { - // Build entity data with id - uses Record for assignment, cast to T at return - const entityData: Record = { - // CRITICAL: id must be in entityData - BaseEntity requires it - id: row.id - }; + const records: DataRecord[] = []; + let corruptedRowCount = 0; + + for (const row of rows) { + try { + // Build entity data with id - uses Record for assignment, cast to T at return + const entityData: Record = { + // CRITICAL: id must be in entityData - BaseEntity requires it + id: row.id + }; - // Process fields from schema - for (const field of schema.fields) { - // Skip metadata fields (handled in DataRecord.metadata) but NOT id - // id is part of BaseEntity and MUST be in entityData - if (['createdAt', 'updatedAt', 'version'].includes(field.name)) { - continue; - } - // id already set above, skip from schema processing - if (field.name === 'id') { - continue; - } + // Process fields from schema + for (const field of schema.fields) { + // Skip metadata fields (handled in DataRecord.metadata) but NOT id + // id is part of BaseEntity and MUST be in entityData + if (['createdAt', 'updatedAt', 'version'].includes(field.name)) { + continue; + } + // id already set above, skip from schema processing + if (field.name === 'id') { + continue; + } + + const columnName = SqlNamingConverter.toSnakeCase(field.name); + let value = row[columnName]; - const columnName = SqlNamingConverter.toSnakeCase(field.name); - let value = row[columnName]; - - if (value !== undefined && value !== null) { - // Convert SQL value based on schema type - switch (field.type) { - case 'boolean': - value = value === 1; - break; - case 'json': - if (typeof value === 'string') { - try { - value = JSON.parse(value); - } catch (e) { - // Log the exact collection/field for debugging corrupted json data - console.error(`❌ JSON.parse failed for ${query.collection}.${field.name} (row ${row.id}): ${(e as Error).message}. Raw value: "${String(value).substring(0, 100)}"`); - throw e; + if (value !== undefined && value !== null) { + // Convert SQL value based on schema type + switch (field.type) { + case 'boolean': + value = value === 1; + break; + case 'json': + if (typeof value === 'string') { + try { + value = JSON.parse(value); + } catch (e) { + // Log the exact collection/field for debugging corrupted json data + // then re-throw to skip this entire row (caught by outer try/catch) + console.error(`❌ JSON.parse failed for ${query.collection}.${field.name} (row ${row.id}): ${(e as Error).message}. Raw value: "${String(value).substring(0, 100)}"`); + throw e; + } } - } - break; - case 'date': - value = new Date(value); - break; + break; + case 'date': + value = new Date(value); + break; + } + entityData[field.name] = value; } - entityData[field.name] = value; } + + records.push({ + id: row.id, + collection: query.collection, + data: entityData as T, + metadata: { + createdAt: row.created_at, + updatedAt: row.updated_at, + version: row.version + } + }); + } catch (_rowError) { + // Row-level error isolation: one corrupted row must not kill the entire query. + // The specific field error is already logged above with full detail. + corruptedRowCount++; } + } - return { - id: row.id, - collection: query.collection, - data: entityData as T, - metadata: { - createdAt: row.created_at, - updatedAt: row.updated_at, - version: row.version - } - }; - }); + if (corruptedRowCount > 0) { + log.warn(`${query.collection}: Skipped ${corruptedRowCount} corrupted row(s) out of ${rows.length} total`); + } return { success: true, diff --git a/src/debug/jtag/daemons/room-membership-daemon/server/RoomMembershipDaemonServer.ts b/src/debug/jtag/daemons/room-membership-daemon/server/RoomMembershipDaemonServer.ts index e8183f679..52c705e8d 100644 --- a/src/debug/jtag/daemons/room-membership-daemon/server/RoomMembershipDaemonServer.ts +++ b/src/debug/jtag/daemons/room-membership-daemon/server/RoomMembershipDaemonServer.ts @@ -61,7 +61,9 @@ export class RoomMembershipDaemonServer extends RoomMembershipDaemon { ROOM_UNIQUE_IDS.DEV_UPDATES, ROOM_UNIQUE_IDS.HELP, ROOM_UNIQUE_IDS.THEME, // System room for ThemeWidget assistant - ROOM_UNIQUE_IDS.SETTINGS // System room for SettingsWidget assistant + ROOM_UNIQUE_IDS.SETTINGS, // System room for SettingsWidget assistant + ROOM_UNIQUE_IDS.OUTREACH, // Social media strategy and community engagement + ROOM_UNIQUE_IDS.NEWSROOM // Current events and world awareness ] }, // SOTA PersonaUsers also join Pantheon (elite multi-provider collaboration) @@ -145,19 +147,34 @@ export class RoomMembershipDaemonServer extends RoomMembershipDaemon { } /** - * Subscribe to user creation events + * Subscribe to user creation AND room creation events. + * User creation β†’ add user to all applicable rooms (existing behavior) + * Room creation β†’ add all applicable users to the new room (fixes startup race) */ private async setupEventSubscriptions(): Promise { + // 1. New user β†’ join applicable rooms this.log.info(`🏠 RoomMembershipDaemonServer: Subscribing to ${DATA_EVENTS.USERS.CREATED}`); - const unsubCreated = Events.subscribe( + const unsubUserCreated = Events.subscribe( DATA_EVENTS.USERS.CREATED, async (userData: UserEntity) => { - this.log.info(`πŸ”” RoomMembershipDaemonServer: EVENT HANDLER CALLED for ${userData.displayName}`); + this.log.info(`πŸ”” RoomMembershipDaemonServer: USER CREATED event for ${userData.displayName}`); await this.handleUserCreated(userData); } ); - this.registerSubscription(unsubCreated); - this.log.info(`🏠 RoomMembershipDaemonServer: Subscription complete, unsubscribe function registered`); + this.registerSubscription(unsubUserCreated); + + // 2. New room β†’ add all existing users who should be members + this.log.info(`🏠 RoomMembershipDaemonServer: Subscribing to ${DATA_EVENTS.ROOMS.CREATED}`); + const unsubRoomCreated = Events.subscribe( + DATA_EVENTS.ROOMS.CREATED, + async (roomData: RoomEntity) => { + this.log.info(`πŸ”” RoomMembershipDaemonServer: ROOM CREATED event for ${roomData.displayName} (${roomData.uniqueId})`); + await this.handleRoomCreated(roomData); + } + ); + this.registerSubscription(unsubRoomCreated); + + this.log.info(`🏠 RoomMembershipDaemonServer: Subscriptions complete (users + rooms)`); } /** @@ -292,6 +309,61 @@ export class RoomMembershipDaemonServer extends RoomMembershipDaemon { } } + /** + * Handle room created β€” add all existing users who should be members. + * Mirrors handleUserCreated but in the opposite direction: + * handleUserCreated: new user β†’ which rooms should they join? + * handleRoomCreated: new room β†’ which users should join it? + * + * This fixes the startup race where rooms are seeded AFTER the + * daemon's catch-up logic runs, leaving personas unsubscribed. + */ + private async handleRoomCreated(roomEntity: RoomEntity): Promise { + const roomUniqueId = roomEntity.uniqueId; + if (!roomUniqueId) { + return; // Room without uniqueId β€” nothing to route + } + + // Check if this room appears in any routing rule + const applicableRules = this.ROUTING_RULES.filter( + rule => rule.rooms.includes(roomUniqueId) + ); + if (applicableRules.length === 0) { + this.log.info(`🏠 MembershipDaemon: New room ${roomUniqueId} not in routing rules, skipping auto-join`); + return; + } + + this.log.info(`🏠 MembershipDaemon: New room ${roomUniqueId} matches routing rules, adding applicable users...`); + + try { + // Query all users + const queryResult = await DataDaemon.query({ + collection: COLLECTIONS.USERS, + filter: {} + }); + + if (!queryResult.success || !queryResult.data?.length) { + return; + } + + const users: UserEntity[] = queryResult.data.map(record => record.data); + let addedCount = 0; + + for (const user of users) { + // Check if this user should be in this room based on routing rules + const roomsForUser = this.determineRoomsForUser(user); + if (roomsForUser.includes(roomUniqueId)) { + await this.addUserToRooms(user.id, user.displayName, [roomUniqueId]); + addedCount++; + } + } + + this.log.info(`βœ… MembershipDaemon: Added ${addedCount} user(s) to new room ${roomUniqueId}`); + } catch (error) { + this.log.error(`❌ MembershipDaemon: Failed to populate new room ${roomUniqueId}:`, error); + } + } + // ============ Activity Membership Methods ============ /** diff --git a/src/debug/jtag/generated-command-schemas.json b/src/debug/jtag/generated-command-schemas.json index 9014bbc2d..cc67bf607 100644 --- a/src/debug/jtag/generated-command-schemas.json +++ b/src/debug/jtag/generated-command-schemas.json @@ -1,5 +1,5 @@ { - "generated": "2026-01-30T23:05:41.816Z", + "generated": "2026-02-01T20:13:44.015Z", "version": "1.0.0", "commands": [ { @@ -814,6 +814,510 @@ } } }, + { + "name": "social/trending", + "description": "Social Trending Command - Shared Types\n *\n * Discover trending and popular content on a social media platform.\n * Shows hot posts, top communities, and rising discussions.\n *\n * Usage:\n * ./jtag social/trending --platform=moltbook\n * ./jtag social/trending --platform=moltbook --community=ai-development --sort=top\n * ./jtag social/trending --platform=moltbook --sort=rising --limit=5", + "params": { + "platform": { + "type": "string", + "required": true, + "description": "platform parameter" + }, + "sort": { + "type": "string", + "required": false, + "description": "sort parameter" + }, + "community": { + "type": "string", + "required": false, + "description": "community parameter" + }, + "limit": { + "type": "number", + "required": false, + "description": "limit parameter" + }, + "personaId": { + "type": "string", + "required": false, + "description": "personaId parameter" + } + } + }, + { + "name": "social/signup", + "description": "Social Signup Command - Shared Types\n *\n * Register a persona on a social media platform (e.g., Moltbook).\n * Creates an account with a chosen username and stores credentials for future use.\n *\n * Usage:\n * ./jtag social/signup --platform=moltbook --agentName=\"helper-ai\" --description=\"I help with code\"", + "params": { + "platform": { + "type": "string", + "required": true, + "description": "platform parameter" + }, + "agentName": { + "type": "string", + "required": true, + "description": "agentName parameter" + }, + "description": { + "type": "string", + "required": false, + "description": "description parameter" + }, + "personaId": { + "type": "string", + "required": false, + "description": "personaId parameter" + }, + "metadata": { + "type": "object", + "required": false, + "description": "metadata parameter" + } + } + }, + { + "name": "social/search", + "description": "Social Search Command - Shared Types\n *\n * Semantic search across social media platforms.\n * Find posts, agents, and communities by keyword.\n *\n * Usage:\n * ./jtag social/search --platform=moltbook --query=\"memory systems\"\n * ./jtag social/search --platform=moltbook --query=\"rust concurrency\" --type=post --limit=10", + "params": { + "platform": { + "type": "string", + "required": true, + "description": "platform parameter" + }, + "query": { + "type": "string", + "required": true, + "description": "query parameter" + }, + "type": { + "type": "string", + "required": false, + "description": "type parameter" + }, + "limit": { + "type": "number", + "required": false, + "description": "limit parameter" + }, + "personaId": { + "type": "string", + "required": false, + "description": "personaId parameter" + } + } + }, + { + "name": "social/propose", + "description": "Social Propose Command - Shared Types\n *\n * Democratic governance for shared social media accounts.\n * Personas nominate actions, vote, and auto-execute on threshold.\n *\n * Proposals are stored as Handles (type 'social-proposal') with votes in params.\n * When enough \"up\" votes accumulate, the action executes automatically.\n *\n * Modes:\n * create β€” Nominate a new action (follow, post, comment, etc.)\n * vote β€” Vote on a pending proposal\n * list β€” Show pending/recent proposals\n * view β€” View a specific proposal with full vote history\n *\n * Usage:\n * ./jtag social/propose --platform=moltbook --mode=create --action=follow --target=eudaemon_0 --reason=\"Great security research\"\n * ./jtag social/propose --mode=vote --proposalId=abc123 --direction=up\n * ./jtag social/propose --mode=list\n * ./jtag social/propose --mode=view --proposalId=abc123", + "params": { + "platform": { + "type": "string", + "required": false, + "description": "platform parameter" + }, + "mode": { + "type": "string", + "required": true, + "description": "mode parameter" + }, + "action": { + "type": "string", + "required": false, + "description": "action parameter" + }, + "target": { + "type": "string", + "required": false, + "description": "target parameter" + }, + "reason": { + "type": "string", + "required": false, + "description": "reason parameter" + }, + "title": { + "type": "string", + "required": false, + "description": "title parameter" + }, + "content": { + "type": "string", + "required": false, + "description": "content parameter" + }, + "community": { + "type": "string", + "required": false, + "description": "community parameter" + }, + "postId": { + "type": "string", + "required": false, + "description": "postId parameter" + }, + "commentContent": { + "type": "string", + "required": false, + "description": "commentContent parameter" + }, + "voteDirection": { + "type": "string", + "required": false, + "description": "voteDirection parameter" + }, + "targetType": { + "type": "string", + "required": false, + "description": "targetType parameter" + }, + "proposalId": { + "type": "string", + "required": false, + "description": "proposalId parameter" + }, + "direction": { + "type": "string", + "required": false, + "description": "direction parameter" + }, + "status": { + "type": "string", + "required": false, + "description": "status parameter" + }, + "limit": { + "type": "number", + "required": false, + "description": "limit parameter" + }, + "personaId": { + "type": "string", + "required": false, + "description": "personaId parameter" + } + } + }, + { + "name": "social/profile", + "description": "Social Profile Command - Shared Types\n *\n * View or update a social media profile. View your own profile, another agent's profile, or update your bio/description.\n *\n * Usage:\n * ./jtag social/profile --platform=moltbook\n * ./jtag social/profile --platform=moltbook --agentName=other-agent\n * ./jtag social/profile --platform=moltbook --update --description=\"New bio\"", + "params": { + "platform": { + "type": "string", + "required": true, + "description": "platform parameter" + }, + "agentName": { + "type": "string", + "required": false, + "description": "agentName parameter" + }, + "update": { + "type": "boolean", + "required": false, + "description": "update parameter" + }, + "description": { + "type": "string", + "required": false, + "description": "description parameter" + }, + "personaId": { + "type": "string", + "required": false, + "description": "personaId parameter" + } + } + }, + { + "name": "social/post", + "description": "Social Post Command - Shared Types\n *\n * Create a post on a social media platform using the persona's stored credentials.\n *\n * Usage:\n * ./jtag social/post --platform=moltbook --title=\"Hello\" --content=\"First post\" --community=general", + "params": { + "platform": { + "type": "string", + "required": true, + "description": "platform parameter" + }, + "title": { + "type": "string", + "required": true, + "description": "title parameter" + }, + "content": { + "type": "string", + "required": true, + "description": "content parameter" + }, + "community": { + "type": "string", + "required": false, + "description": "community parameter" + }, + "url": { + "type": "string", + "required": false, + "description": "url parameter" + }, + "personaId": { + "type": "string", + "required": false, + "description": "personaId parameter" + } + } + }, + { + "name": "social/notifications", + "description": "Social Notifications Command - Shared Types\n *\n * Check for unread notifications (replies, mentions, followers) on a social media platform.\n * Key data source for SocialMediaRAGSource β€” personas become aware of social activity through this.\n *\n * Usage:\n * ./jtag social/notifications --platform=moltbook\n * ./jtag social/notifications --platform=moltbook --since=2026-01-30T00:00:00Z", + "params": { + "platform": { + "type": "string", + "required": true, + "description": "platform parameter" + }, + "since": { + "type": "string", + "required": false, + "description": "since parameter" + }, + "limit": { + "type": "number", + "required": false, + "description": "limit parameter" + }, + "personaId": { + "type": "string", + "required": false, + "description": "personaId parameter" + } + } + }, + { + "name": "social/feed", + "description": "Social Feed Command - Shared Types\n *\n * Read the feed from a social media platform. Supports global feed,\n * personalized feed, and community-specific feeds.\n *\n * Usage:\n * ./jtag social/feed --platform=moltbook --sort=hot --limit=10\n * ./jtag social/feed --platform=moltbook --community=ai-development --sort=new", + "params": { + "platform": { + "type": "string", + "required": true, + "description": "platform parameter" + }, + "sort": { + "type": "string", + "required": false, + "description": "sort parameter" + }, + "community": { + "type": "string", + "required": false, + "description": "community parameter" + }, + "limit": { + "type": "number", + "required": false, + "description": "limit parameter" + }, + "personalized": { + "type": "boolean", + "required": false, + "description": "personalized parameter" + }, + "personaId": { + "type": "string", + "required": false, + "description": "personaId parameter" + } + } + }, + { + "name": "social/engage", + "description": "Social Engage Command - Shared Types\n *\n * All social interaction in one command: vote, follow, subscribe.\n * Designed for AI tool use β€” one command covers all engagement actions.\n *\n * Actions:\n * vote β€” Upvote or downvote a post or comment\n * follow β€” Follow an agent\n * unfollow β€” Unfollow an agent\n * subscribe β€” Subscribe to a community\n * unsubscribe β€” Unsubscribe from a community\n * delete β€” Delete own post or comment\n *\n * Usage:\n * ./jtag social/engage --platform=moltbook --action=vote --target=abc123 --targetType=post --direction=up\n * ./jtag social/engage --platform=moltbook --action=follow --target=eudaemon_0\n * ./jtag social/engage --platform=moltbook --action=subscribe --target=ai-development\n * ./jtag social/engage --platform=moltbook --action=delete --target=abc123 --targetType=post", + "params": { + "platform": { + "type": "string", + "required": true, + "description": "platform parameter" + }, + "action": { + "type": "string", + "required": true, + "description": "action parameter" + }, + "target": { + "type": "string", + "required": true, + "description": "target parameter" + }, + "targetType": { + "type": "string", + "required": false, + "description": "targetType parameter" + }, + "direction": { + "type": "string", + "required": false, + "description": "direction parameter" + }, + "personaId": { + "type": "string", + "required": false, + "description": "personaId parameter" + } + } + }, + { + "name": "social/downvote", + "description": "Social Downvote Command - Shared Types\n *\n * Downvote a post on a social media platform.\n * Convenience command β€” delegates to provider.vote() with direction='down'.", + "params": { + "platform": { + "type": "string", + "required": true, + "description": "platform parameter" + }, + "postId": { + "type": "string", + "required": true, + "description": "postId parameter" + }, + "personaId": { + "type": "string", + "required": false, + "description": "personaId parameter" + } + } + }, + { + "name": "social/community", + "description": "Social Community Command - Shared Types\n *\n * Manage communities (submolts) β€” create, list, subscribe, unsubscribe, get info", + "params": { + "platform": { + "type": "string", + "required": true, + "description": "platform parameter" + }, + "action": { + "type": "string", + "required": true, + "description": "action parameter" + }, + "name": { + "type": "string", + "required": false, + "description": "name parameter" + }, + "description": { + "type": "string", + "required": false, + "description": "description parameter" + }, + "personaId": { + "type": "string", + "required": false, + "description": "personaId parameter" + } + } + }, + { + "name": "social/comment", + "description": "Social Comment Command - Shared Types\n *\n * Comment on a post or reply to a comment on a social media platform.\n * Supports threaded replies.\n *\n * Usage:\n * ./jtag social/comment --platform=moltbook --postId=abc123 --content=\"Great insight!\"\n * ./jtag social/comment --platform=moltbook --postId=abc123 --content=\"Agreed\" --parentId=def456", + "params": { + "platform": { + "type": "string", + "required": true, + "description": "platform parameter" + }, + "postId": { + "type": "string", + "required": true, + "description": "postId parameter" + }, + "action": { + "type": "string", + "required": false, + "description": "action parameter" + }, + "content": { + "type": "string", + "required": false, + "description": "content parameter" + }, + "parentId": { + "type": "string", + "required": false, + "description": "parentId parameter" + }, + "sort": { + "type": "string", + "required": false, + "description": "sort parameter" + }, + "personaId": { + "type": "string", + "required": false, + "description": "personaId parameter" + } + } + }, + { + "name": "social/classify", + "description": "Social Classify Command - Shared Types\n *\n * Multi-dimensional agent classification system.\n * Analyzes an external agent's profile, posting history, and engagement\n * to produce a probability vector characterizing who they are.\n *\n * Like an embedding space for AI personas on external social media.\n * Uses existing subcommands (browse, search) to gather data,\n * then produces scores across multiple dimensions.\n *\n * Dimensions:\n * spam β€” Probability of being a spambot (repetitive, low-quality, template content)\n * authentic β€” Original content vs copypasta/shill\n * expertise β€” Domain knowledge signals (security, coding, philosophy, etc.)\n * influence β€” Community impact (karma, engagement, followers)\n * engagement β€” Quality of conversations (threaded depth, substantive replies)\n * reliability β€” Consistency over time (not one-hit wonder)\n *\n * Usage:\n * ./jtag social/classify --platform=moltbook --target=eudaemon_0\n * ./jtag social/classify --platform=moltbook --target=snorf5163\n * ./jtag social/classify --platform=moltbook --target=Cody --depth=deep", + "params": { + "platform": { + "type": "string", + "required": true, + "description": "platform parameter" + }, + "target": { + "type": "string", + "required": true, + "description": "target parameter" + }, + "depth": { + "type": "string", + "required": false, + "description": "depth parameter" + }, + "personaId": { + "type": "string", + "required": false, + "description": "personaId parameter" + } + } + }, + { + "name": "social/browse", + "description": "Social Browse Command - Shared Types\n *\n * Intelligent exploration of social media platforms.\n * One command for all discovery: communities, feeds, posts, agents.\n *\n * Modes:\n * discover β€” List all communities with descriptions and activity\n * community β€” Browse a specific community's feed with context\n * post β€” Read a full post with threaded comments and author info\n * agent β€” View an agent's profile, karma, recent activity\n * trending β€” Hot posts across the platform (default)\n *\n * Usage:\n * ./jtag social/browse --platform=moltbook # trending\n * ./jtag social/browse --platform=moltbook --mode=discover # list communities\n * ./jtag social/browse --platform=moltbook --mode=community --target=ai-development\n * ./jtag social/browse --platform=moltbook --mode=post --target=abc123\n * ./jtag social/browse --platform=moltbook --mode=agent --target=eudaemon_0", + "params": { + "platform": { + "type": "string", + "required": true, + "description": "platform parameter" + }, + "mode": { + "type": "string", + "required": false, + "description": "mode parameter" + }, + "target": { + "type": "string", + "required": false, + "description": "target parameter" + }, + "sort": { + "type": "string", + "required": false, + "description": "sort parameter" + }, + "limit": { + "type": "number", + "required": false, + "description": "limit parameter" + }, + "personaId": { + "type": "string", + "required": false, + "description": "personaId parameter" + } + } + }, { "name": "session/get-user", "description": "session/get-user command", diff --git a/src/debug/jtag/generated/command-schemas.json b/src/debug/jtag/generated/command-schemas.json index 1b3564b44..67a0c8393 100644 --- a/src/debug/jtag/generated/command-schemas.json +++ b/src/debug/jtag/generated/command-schemas.json @@ -1,16 +1,92 @@ { - "activity/user-present": { - "name": "activity/user-present", - "description": "Activity User Presence - Track user tab visibility for temperature", + "adapter/adopt": { + "name": "adapter/adopt", + "description": "Adapter Adopt Command - Shared Types", "params": { - "activityId": { + "adapterId": { "type": "string", "required": true }, - "present": { - "type": "boolean", + "scale": { + "type": "number | undefined", + "required": false + }, + "traitType": { + "type": "string | undefined", + "required": false + }, + "personaId": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "adapter/search": { + "name": "adapter/search", + "description": "Adapter Search Command - Shared Types", + "params": { + "query": { + "type": "string", + "required": true + }, + "baseModel": { + "type": "string | undefined", + "required": false + }, + "limit": { + "type": "number | undefined", + "required": false + }, + "source": { + "type": "AdapterSource | undefined", + "required": false + }, + "sort": { + "type": "AdapterSortBy | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "adapter/try": { + "name": "adapter/try", + "description": "Adapter Try Command - Shared Types", + "params": { + "adapterId": { + "type": "string", "required": true }, + "testPrompt": { + "type": "string", + "required": true + }, + "scale": { + "type": "number | undefined", + "required": false + }, + "maxTokens": { + "type": "number | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -41,6 +117,10 @@ "type": "boolean | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -83,6 +163,90 @@ "type": "boolean | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "ai/context/search": { + "name": "ai/context/search", + "description": "Ai Context Search Command - Shared Types", + "params": { + "query": { + "type": "string", + "required": true + }, + "collections": { + "type": "string[] | undefined", + "required": false + }, + "personaId": { + "type": "string | undefined", + "required": false + }, + "excludeContextId": { + "type": "string | undefined", + "required": false + }, + "limit": { + "type": "number | undefined", + "required": false + }, + "minSimilarity": { + "type": "number | undefined", + "required": false + }, + "since": { + "type": "string | undefined", + "required": false + }, + "mode": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "ai/context/slice": { + "name": "ai/context/slice", + "description": "Ai Context Slice Command - Shared Types", + "params": { + "id": { + "type": "string", + "required": true + }, + "type": { + "type": "string", + "required": true + }, + "personaId": { + "type": "string | undefined", + "required": false + }, + "includeRelated": { + "type": "boolean | undefined", + "required": false + }, + "relatedLimit": { + "type": "number | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -183,6 +347,10 @@ "type": "boolean | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -201,6 +369,48 @@ "type": "boolean | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "ai/detect-semantic-loop": { + "name": "ai/detect-semantic-loop", + "description": "Ai Detect Semantic Loop Command - Shared Types", + "params": { + "messageText": { + "type": "string", + "required": true + }, + "personaId": { + "type": "string", + "required": true + }, + "lookbackCount": { + "type": "number | undefined", + "required": false + }, + "similarityThreshold": { + "type": "number | undefined", + "required": false + }, + "timeWindowMinutes": { + "type": "number | undefined", + "required": false + }, + "roomId": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -227,6 +437,10 @@ "type": "string | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -282,7 +496,15 @@ "required": false }, "preferredProvider": { - "type": "\"ollama\" | \"openai\" | \"anthropic\" | undefined", + "type": "\"local\" | \"ollama\" | \"openai\" | \"anthropic\" | \"candle\" | \"groq\" | \"deepseek\" | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", "required": false } } @@ -306,6 +528,40 @@ "format": { "type": "\"json\" | \"table\" | undefined", "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "ai/key/test": { + "name": "ai/key/test", + "description": "Ai Key Test Command - Shared Types", + "params": { + "provider": { + "type": "string", + "required": true + }, + "key": { + "type": "string", + "required": true + }, + "useStored": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false } } }, @@ -333,6 +589,10 @@ "type": "boolean | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -355,29 +615,117 @@ "type": "boolean | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false } } }, - "ai/rag/index/create": { - "name": "ai/rag/index/create", - "description": "Index Create Command Types", + "ai/mute": { + "name": "ai/mute", + "description": "AI Mute Command - Shared Types", "params": { - "filePath": { - "type": "string", + "action": { + "type": "\"mute\" | \"unmute\"", "required": true }, - "fileType": { - "type": "\"json\" | \"typescript\" | \"markdown\" | \"javascript\"", - "required": true + "persona": { + "type": "string | undefined", + "required": false }, - "content": { + "userId": { + "type": "string | undefined", + "required": false + }, + "reason": { "type": "string", "required": true }, - "summary": { + "evidence": { + "type": "string | undefined", + "required": false + }, + "duration": { + "type": "number | undefined", + "required": false + }, + "permanent": { + "type": "boolean | undefined", + "required": false + }, + "rooms": { + "type": "string[] | undefined", + "required": false + }, + "commands": { + "type": "string[] | undefined", + "required": false + }, + "mutedBy": { + "type": "string | undefined", + "required": false + }, + "mutedByName": { + "type": "string | undefined", + "required": false + }, + "mutedByType": { + "type": "\"system\" | \"persona\" | \"human\" | undefined", + "required": false + }, + "restorationReason": { + "type": "string | undefined", + "required": false + }, + "canAppeal": { + "type": "boolean | undefined", + "required": false + }, + "appealId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "ai/providers/status": { + "name": "ai/providers/status", + "description": "AI Providers Status - Check which API keys are configured", + "params": { + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "ai/rag/index/create": { + "name": "ai/rag/index/create", + "description": "Index Create Command Types", + "params": { + "filePath": { + "type": "string", + "required": true + }, + "fileType": { + "type": "\"json\" | \"typescript\" | \"markdown\" | \"javascript\"", + "required": true + }, + "content": { + "type": "string", + "required": true + }, + "summary": { "type": "string | undefined", "required": false }, @@ -417,6 +765,10 @@ "type": "string[] | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -455,6 +807,10 @@ "type": "boolean | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -493,6 +849,10 @@ "type": "string | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -507,6 +867,10 @@ "type": "string", "required": true }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -533,6 +897,10 @@ "type": "\"forward\" | \"backward\" | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -571,6 +939,10 @@ "type": "number | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -617,6 +989,10 @@ "type": "boolean | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -671,6 +1047,10 @@ "type": "\"text\" | \"json\" | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -721,6 +1101,10 @@ "type": "number | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -755,10 +1139,48 @@ "type": "string", "required": true }, + "senderType": { + "type": "\"system\" | \"persona\" | \"human\" | \"agent\" | undefined", + "required": false + }, "config": { "type": "Partial | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "ai/sleep": { + "name": "ai/sleep", + "description": "AI Sleep Command - Shared Types", + "params": { + "mode": { + "type": "SleepMode", + "required": true + }, + "reason": { + "type": "string | undefined", + "required": false + }, + "durationMinutes": { + "type": "number | undefined", + "required": false + }, + "personaId": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -785,6 +1207,10 @@ "type": "boolean | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -831,6 +1257,10 @@ "type": "boolean | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false @@ -865,54 +1295,50 @@ "type": "boolean | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false } } }, - "chat/export": { - "name": "chat/export", - "description": "Chat Export Command", + "canvas/stroke/add": { + "name": "canvas/stroke/add", + "description": "Canvas Stroke Add Command Types", "params": { - "room": { - "type": "string | undefined", - "required": false - }, - "afterMessageId": { - "type": "string | undefined", - "required": false - }, - "afterTimestamp": { - "type": "string | Date | undefined", - "required": false + "canvasId": { + "type": "string", + "required": true }, - "limit": { - "type": "number | undefined", - "required": false + "tool": { + "type": "CanvasTool", + "required": true }, - "output": { - "type": "string | undefined", - "required": false + "points": { + "type": "StrokePoint[]", + "required": true }, - "includeSystem": { - "type": "boolean | undefined", - "required": false + "color": { + "type": "string", + "required": true }, - "includeTests": { - "type": "boolean | undefined", - "required": false + "size": { + "type": "number", + "required": true }, - "filter": { - "type": "Record | undefined", + "opacity": { + "type": "number | undefined", "required": false }, - "collection": { + "compositeOp": { "type": "string | undefined", "required": false }, - "includeThreading": { - "type": "boolean | undefined", + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -921,116 +1347,120 @@ } } }, - "chat/poll": { - "name": "chat/poll", - "description": "Chat Poll Command Types - Get messages after a specific messageId", + "canvas/stroke/list": { + "name": "canvas/stroke/list", + "description": "Canvas Stroke List Command Types", "params": { - "afterMessageId": { + "canvasId": { "type": "string", "required": true }, + "viewport": { + "type": "{ x: number; y: number; width: number; height: number; } | undefined", + "required": false + }, "limit": { "type": "number | undefined", "required": false }, - "roomId": { + "cursor": { "type": "string | undefined", "required": false }, - "room": { + "userId": { "type": "string | undefined", "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false } } }, - "chat/send": { - "name": "chat/send", - "description": "Chat Send Command", + "canvas/vision": { + "name": "canvas/vision", + "description": "Canvas Vision Command Types", "params": { - "message": { - "type": "string", + "action": { + "type": "VisionAction", "required": true }, - "room": { + "imageBase64": { "type": "string | undefined", "required": false }, - "senderId": { + "prompt": { "type": "string | undefined", "required": false }, - "replyToId": { + "transformPrompt": { "type": "string | undefined", "required": false }, - "isSystemTest": { - "type": "boolean | undefined", + "transformModel": { + "type": "\"dalle-3\" | \"dalle-2\" | \"stable-diffusion\" | undefined", "required": false }, - "timeout": { - "type": "number | undefined", + "visionModel": { + "type": "string | undefined", "required": false - } - } - }, - "click": { - "name": "click", - "description": "Click Command - Shared Types for Element Interaction", - "params": { - "selector": { - "type": "string", - "required": true }, - "button": { - "type": "\"left\" | \"right\" | \"middle\" | undefined", + "personaId": { + "type": "string | undefined", "required": false }, - "timeout": { - "type": "number | undefined", + "personaName": { + "type": "string | undefined", "required": false }, - "shadowRoot": { - "type": "boolean | undefined", + "userId": { + "type": "string | undefined", "required": false }, - "innerSelector": { - "type": "string | undefined", + "timeout": { + "type": "number | undefined", "required": false } } }, - "code/find": { - "name": "code/find", - "description": "code/find command types - Find files by name pattern", - "params": {} - }, - "code/read": { - "name": "code/read", - "description": "code/read command types", - "params": {} - }, - "compile-typescript": { - "name": "compile-typescript", - "description": "Compile TypeScript Command - Shared Types", + "collaboration/activity/create": { + "name": "collaboration/activity/create", + "description": "Activity Create Command - Create a new activity from a recipe", "params": { - "source": { + "recipeId": { "type": "string", "required": true }, - "filename": { + "displayName": { + "type": "string", + "required": true + }, + "description": { "type": "string | undefined", "required": false }, - "outputPath": { + "ownerId": { "type": "string | undefined", "required": false }, - "strict": { - "type": "boolean | undefined", + "uniqueId": { + "type": "string | undefined", "required": false }, - "target": { - "type": "\"es5\" | \"es2015\" | \"es2020\" | \"esnext\" | undefined", + "participants": { + "type": "{ userId: string; role: string; roleConfig?: Record | undefined; }[] | undefined", + "required": false + }, + "config": { + "type": "Partial | undefined", + "required": false + }, + "tags": { + "type": "string[] | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -1039,20 +1469,20 @@ } } }, - "continuum/emotion": { - "name": "continuum/emotion", - "description": "continuum/emotion command", + "collaboration/activity/get": { + "name": "collaboration/activity/get", + "description": "Activity Get Command - Retrieve an activity by ID or uniqueId", "params": { - "emoji": { - "type": "string", - "required": true + "id": { + "type": "string | undefined", + "required": false }, - "color": { + "uniqueId": { "type": "string | undefined", "required": false }, - "duration": { - "type": "number | undefined", + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -1061,289 +1491,2959 @@ } } }, - "data/clear": { - "name": "data/clear", - "description": "Data Clear Command - Shared Types", - "params": {} - }, - "data/close": { - "name": "data/close", - "description": "Data Close Command - Shared Types", + "collaboration/activity/join": { + "name": "collaboration/activity/join", + "description": "Activity Join Command - Add a participant to an activity", "params": { - "dbHandle": { + "activityId": { "type": "string", "required": true - } - } - }, - "data/create": { - "name": "data/create", - "description": "Data Create Command - Shared Types", - "params": { - "data": { - "type": "T", - "required": true }, - "collection": { - "type": "string", - "required": true + "userId": { + "type": "string | undefined", + "required": false }, - "dbHandle": { + "role": { "type": "string | undefined", "required": false - } - } - }, - "data/delete": { - "name": "data/delete", - "description": "Data Delete Command Types - Universal Delete Interface", - "params": { - "collection": { - "type": "string", - "required": true }, - "id": { - "type": "string", - "required": true + "roleConfig": { + "type": "Record | undefined", + "required": false }, - "format": { - "type": "\"json\" | \"table\" | \"yaml\" | undefined", + "timeout": { + "type": "number | undefined", "required": false } } }, - "data/list": { - "name": "data/list", - "description": "Data List Command - Shared Types", + "collaboration/activity/list": { + "name": "collaboration/activity/list", + "description": "Activity List Command - Query activities with filters", "params": { - "collection": { - "type": "string", - "required": true + "recipeId": { + "type": "string | undefined", + "required": false }, - "limit": { - "type": "number | undefined", + "status": { + "type": "ActivityStatus | ActivityStatus[] | undefined", "required": false }, - "filter": { - "type": "Record | undefined", + "ownerId": { + "type": "string | undefined", + "required": false + }, + "participantId": { + "type": "string | undefined", + "required": false + }, + "tags": { + "type": "string[] | undefined", + "required": false + }, + "limit": { + "type": "number | undefined", "required": false }, "orderBy": { - "type": "{ field: string; direction: \"asc\" | \"desc\"; }[] | undefined", + "type": "\"lastActivityAt\" | \"createdAt\" | \"displayName\" | undefined", "required": false }, - "convertToDomain": { - "type": "boolean | undefined", + "orderDirection": { + "type": "\"asc\" | \"desc\" | undefined", "required": false }, - "cursor": { - "type": "{ readonly field: string; readonly value: any; readonly direction: \"before\" | \"after\"; } | undefined", + "userId": { + "type": "string | undefined", "required": false - } - } - }, - "data/list-handles": { - "name": "data/list-handles", - "description": "Data List-Handles Command - Shared Types", - "params": {} - }, - "data/open": { - "name": "data/open", - "description": "Data Open Command - Shared Types", - "params": { - "adapter": { - "type": "AdapterType", - "required": true }, - "config": { - "type": "AdapterConfig", - "required": true + "timeout": { + "type": "number | undefined", + "required": false } } }, - "data/query-close": { - "name": "data/query-close", - "description": "Data Query Close Command Types", + "collaboration/activity/update": { + "name": "collaboration/activity/update", + "description": "Activity Update Command - Update activity state, phase, or config", "params": { - "queryHandle": { + "activityId": { "type": "string", "required": true }, - "collection": { - "type": "string", - "required": true - } - } - }, - "data/query-next": { - "name": "data/query-next", - "description": "Data Query Next Command Types", - "params": { - "queryHandle": { - "type": "string", - "required": true + "displayName": { + "type": "string | undefined", + "required": false }, - "collection": { - "type": "string", - "required": true - } - } - }, - "data/query-open": { - "name": "data/query-open", - "description": "Data Query Open Command Types", - "params": { - "collection": { - "type": "string", - "required": true + "description": { + "type": "string | undefined", + "required": false }, - "filter": { - "type": "Record | undefined", + "status": { + "type": "ActivityStatus | undefined", "required": false }, - "orderBy": { - "type": "{ field: string; direction: \"asc\" | \"desc\"; }[] | undefined", + "phase": { + "type": "string | undefined", "required": false }, - "pageSize": { + "progress": { + "type": "number | undefined", + "required": false + }, + "variables": { + "type": "Record | undefined", + "required": false + }, + "settings": { + "type": "Record | undefined", + "required": false + }, + "tags": { + "type": "string[] | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { "type": "number | undefined", "required": false } } }, - "data/read": { - "name": "data/read", - "description": "Data Read Command - Shared Types", + "collaboration/activity/user-present": { + "name": "collaboration/activity/user-present", + "description": "Activity User Presence - Track user tab visibility for temperature", "params": { - "id": { + "activityId": { "type": "string", "required": true }, - "collection": { - "type": "string", + "present": { + "type": "boolean", "required": true }, - "dbHandle": { + "userId": { "type": "string | undefined", "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false } } }, - "data/schema": { - "name": "data/schema", - "description": "Data Schema Command - Entity Schema Introspection", + "collaboration/chat/analyze": { + "name": "collaboration/chat/analyze", + "description": "ChatAnalyze β€” Type-safe command executor", "params": { - "collection": { + "roomId": { "type": "string", "required": true }, - "examples": { + "checkDuplicates": { "type": "boolean | undefined", "required": false }, - "sql": { + "checkTimestamps": { "type": "boolean | undefined", "required": false }, - "validateData": { - "type": "Record | undefined", + "limit": { + "type": "number | undefined", "required": false - } - } - }, - "data": { - "name": "data", - "description": "Base Data Command Types - Generic Abstractions", - "params": { - "collection": { - "type": "string", - "required": true }, - "dbHandle": { + "userId": { "type": "string | undefined", "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false } } }, - "data/truncate": { - "name": "data/truncate", - "description": "Data Truncate Command - Shared Types", - "params": { - "collection": { - "type": "string", - "required": true - } - } - }, - "data/update": { - "name": "data/update", - "description": "Data Update Command Types - Universal Update Interface", + "collaboration/chat/export": { + "name": "collaboration/chat/export", + "description": "Chat Export Command", "params": { - "id": { - "type": "string", - "required": true - }, - "data": { - "type": "Partial", - "required": true + "room": { + "type": "string | undefined", + "required": false + }, + "afterMessageId": { + "type": "string | undefined", + "required": false + }, + "afterTimestamp": { + "type": "string | Date | undefined", + "required": false + }, + "limit": { + "type": "number | undefined", + "required": false + }, + "output": { + "type": "string | undefined", + "required": false + }, + "includeSystem": { + "type": "boolean | undefined", + "required": false + }, + "includeTests": { + "type": "boolean | undefined", + "required": false + }, + "filter": { + "type": "Record | undefined", + "required": false + }, + "collection": { + "type": "string | undefined", + "required": false + }, + "includeThreading": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "collaboration/chat/poll": { + "name": "collaboration/chat/poll", + "description": "Chat Poll Command Types - Get messages after a specific messageId", + "params": { + "afterMessageId": { + "type": "string", + "required": true + }, + "limit": { + "type": "number | undefined", + "required": false + }, + "roomId": { + "type": "string | undefined", + "required": false + }, + "room": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "collaboration/chat/send": { + "name": "collaboration/chat/send", + "description": "Chat Send Command", + "params": { + "message": { + "type": "string", + "required": true + }, + "room": { + "type": "string | undefined", + "required": false + }, + "senderId": { + "type": "string | undefined", + "required": false + }, + "replyToId": { + "type": "string | undefined", + "required": false + }, + "isSystemTest": { + "type": "boolean | undefined", + "required": false + }, + "media": { + "type": "string[] | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "collaboration/content/open": { + "name": "collaboration/content/open", + "description": "Content Open Command Types", + "params": { + "userId": { + "type": "string", + "required": true + }, + "contentType": { + "type": "ContentType", + "required": true + }, + "entityId": { + "type": "string | undefined", + "required": false + }, + "uniqueId": { + "type": "string | undefined", + "required": false + }, + "title": { + "type": "string | undefined", + "required": false + }, + "subtitle": { + "type": "string | undefined", + "required": false + }, + "priority": { + "type": "\"low\" | \"normal\" | \"high\" | \"urgent\" | undefined", + "required": false + }, + "setAsCurrent": { + "type": "boolean | undefined", + "required": false + }, + "metadata": { + "type": "Record | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "collaboration/decision/create": { + "name": "collaboration/decision/create", + "description": "Decision Create Command - Shared Types", + "params": { + "proposalId": { + "type": "string", + "required": true + }, + "topic": { + "type": "string", + "required": true + }, + "rationale": { + "type": "string", + "required": true + }, + "description": { + "type": "string", + "required": true + }, + "options": { + "type": "DecisionOption[]", + "required": true + }, + "tags": { + "type": "string[] | undefined", + "required": false + }, + "votingDeadline": { + "type": "string | undefined", + "required": false + }, + "requiredQuorum": { + "type": "number | undefined", + "required": false + }, + "visibility": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "collaboration/decision/finalize": { + "name": "collaboration/decision/finalize", + "description": "Decision Finalize Command - Shared Types", + "params": { + "proposalId": { + "type": "string", + "required": true + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "collaboration/decision/list": { + "name": "collaboration/decision/list", + "description": "Decision List Command - Shared Types", + "params": { + "status": { + "type": "string | undefined", + "required": false + }, + "domain": { + "type": "string | undefined", + "required": false + }, + "proposedBy": { + "type": "string | undefined", + "required": false + }, + "limit": { + "type": "number | undefined", + "required": false + }, + "offset": { + "type": "number | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "collaboration/decision/propose": { + "name": "collaboration/decision/propose", + "description": "decision/propose - Create a new decision proposal with ranked-choice voting", + "params": { + "topic": { + "type": "string", + "required": true + }, + "rationale": { + "type": "string", + "required": true + }, + "tags": { + "type": "string[] | undefined", + "required": false + }, + "options": { + "type": "{ label: string; description: string; proposedBy?: string | undefined; }[]", + "required": true + }, + "scope": { + "type": "ProposalScope | undefined", + "required": false + }, + "significanceLevel": { + "type": "SignificanceLevel | undefined", + "required": false + }, + "proposerId": { + "type": "string | undefined", + "required": false + }, + "contextId": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "collaboration/decision/rank": { + "name": "collaboration/decision/rank", + "description": "decision/rank - Types", + "params": { + "proposalId": { + "type": "string", + "required": true + }, + "rankedChoices": { + "type": "string[]", + "required": true + }, + "voterId": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "collaboration/decision/view": { + "name": "collaboration/decision/view", + "description": "Decision View Command - Shared Types", + "params": { + "proposalId": { + "type": "string", + "required": true + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "collaboration/decision/vote": { + "name": "collaboration/decision/vote", + "description": "Decision Vote Command - Shared Types", + "params": { + "proposalId": { + "type": "string", + "required": true + }, + "rankedChoices": { + "type": "string[]", + "required": true + }, + "comment": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "collaboration/dm": { + "name": "collaboration/dm", + "description": "DM Command Types", + "params": { + "participants": { + "type": "string | string[]", + "required": true + }, + "name": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "collaboration/live/join": { + "name": "collaboration/live/join", + "description": "Live Join Command Types", + "params": { + "entityId": { + "type": "string", + "required": true + }, + "callerId": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "collaboration/live/leave": { + "name": "collaboration/live/leave", + "description": "Live Leave Command Types", + "params": { + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "collaboration/live/start": { + "name": "collaboration/live/start", + "description": "Collaboration Live Start Command - Shared Types", + "params": { + "participants": { + "type": "string | string[]", + "required": true + }, + "name": { + "type": "string | undefined", + "required": false + }, + "withVideo": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "collaboration/live/transcription": { + "name": "collaboration/live/transcription", + "description": "Collaboration Live Transcription Command - Shared Types", + "params": { + "callSessionId": { + "type": "string", + "required": true + }, + "speakerId": { + "type": "string", + "required": true + }, + "speakerName": { + "type": "string", + "required": true + }, + "transcript": { + "type": "string", + "required": true + }, + "confidence": { + "type": "number", + "required": true + }, + "language": { + "type": "string", + "required": true + }, + "timestamp": { + "type": "number", + "required": true + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "collaboration/wall": { + "name": "collaboration/wall", + "description": "Room Wall Commands - Shared Types", + "params": { + "room": { + "type": "string | undefined", + "required": false + }, + "doc": { + "type": "string", + "required": true + }, + "content": { + "type": "string", + "required": true + }, + "append": { + "type": "boolean | undefined", + "required": false + }, + "author": { + "type": "string | undefined", + "required": false + }, + "commitMessage": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "comms-test": { + "name": "comms-test", + "description": "Comms Test Command Types - Database Testing Edition", + "params": { + "mode": { + "type": "\"echo\" | \"database\"", + "required": true + }, + "message": { + "type": "string | undefined", + "required": false + }, + "dbCount": { + "type": "number | undefined", + "required": false + }, + "testDir": { + "type": "string | undefined", + "required": false + }, + "operations": { + "type": "number | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "continuum/emotion": { + "name": "continuum/emotion", + "description": "Emotion β€” Type-safe command executor", + "params": { + "emoji": { + "type": "string", + "required": true + }, + "color": { + "type": "string | undefined", + "required": false + }, + "duration": { + "type": "number | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "continuum/set": { + "name": "continuum/set", + "description": "Continuum Set Command Types", + "params": { + "emoji": { + "type": "string | undefined", + "required": false + }, + "color": { + "type": "string | undefined", + "required": false + }, + "message": { + "type": "string | undefined", + "required": false + }, + "duration": { + "type": "number | undefined", + "required": false + }, + "clear": { + "type": "boolean | undefined", + "required": false + }, + "priority": { + "type": "\"low\" | \"high\" | \"medium\" | \"critical\" | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/backfill-vectors": { + "name": "data/backfill-vectors", + "description": "Backfill Vectors Command - Shared Types", + "params": { + "collection": { + "type": "string", + "required": true + }, + "textField": { + "type": "string", + "required": true + }, + "filter": { + "type": "UniversalFilter | undefined", + "required": false + }, + "batchSize": { + "type": "number | undefined", + "required": false + }, + "model": { + "type": "string | undefined", + "required": false + }, + "provider": { + "type": "string | undefined", + "required": false + }, + "skipExisting": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/clear": { + "name": "data/clear", + "description": "Data Clear Command - Shared Types", + "params": { + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/close": { + "name": "data/close", + "description": "Data Close Command - Shared Types", + "params": { + "dbHandle": { + "type": "string", + "required": true + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/create": { + "name": "data/create", + "description": "Data Create Command - Shared Types", + "params": { + "data": { + "type": "Record", + "required": true + }, + "collection": { + "type": "string", + "required": true + }, + "dbHandle": { + "type": "string | undefined", + "required": false + }, + "suppressEvents": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/delete": { + "name": "data/delete", + "description": "Data Delete Command Types - Universal Delete Interface", + "params": { + "collection": { + "type": "string", + "required": true + }, + "id": { + "type": "string", + "required": true + }, + "format": { + "type": "\"json\" | \"table\" | \"yaml\" | undefined", + "required": false + }, + "dbHandle": { + "type": "string | undefined", + "required": false + }, + "suppressEvents": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/generate-embedding": { + "name": "data/generate-embedding", + "description": "Generate Embedding Command - Shared Types", + "params": { + "text": { + "type": "string", + "required": true + }, + "model": { + "type": "string | undefined", + "required": false + }, + "provider": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/list": { + "name": "data/list", + "description": "Data List Command - Shared Types", + "params": { + "collection": { + "type": "string", + "required": true + }, + "limit": { + "type": "number | undefined", + "required": false + }, + "filter": { + "type": "Record | undefined", + "required": false + }, + "orderBy": { + "type": "{ field: string; direction: \"asc\" | \"desc\"; }[] | undefined", + "required": false + }, + "convertToDomain": { + "type": "boolean | undefined", + "required": false + }, + "cursor": { + "type": "{ readonly field: string; readonly value: any; readonly direction: \"before\" | \"after\"; } | undefined", + "required": false + }, + "fields": { + "type": "readonly string[] | undefined", + "required": false + }, + "verbose": { + "type": "boolean | undefined", + "required": false + }, + "dbHandle": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/list-handles": { + "name": "data/list-handles", + "description": "Data List-Handles Command - Shared Types", + "params": { + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/open": { + "name": "data/open", + "description": "Data Open Command - Shared Types", + "params": { + "adapter": { + "type": "AdapterType", + "required": true + }, + "config": { + "type": "AdapterConfig", + "required": true + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/query-close": { + "name": "data/query-close", + "description": "Data Query Close Command Types", + "params": { + "queryHandle": { + "type": "string", + "required": true + }, + "collection": { + "type": "string", + "required": true + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/query-next": { + "name": "data/query-next", + "description": "Data Query Next Command Types", + "params": { + "queryHandle": { + "type": "string", + "required": true + }, + "collection": { + "type": "string", + "required": true + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/query-open": { + "name": "data/query-open", + "description": "Data Query Open Command Types", + "params": { + "collection": { + "type": "string", + "required": true + }, + "filter": { + "type": "Record | undefined", + "required": false + }, + "orderBy": { + "type": "{ field: string; direction: \"asc\" | \"desc\"; }[] | undefined", + "required": false + }, + "pageSize": { + "type": "number | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/read": { + "name": "data/read", + "description": "Data Read Command - Shared Types", + "params": { + "id": { + "type": "string", + "required": true + }, + "collection": { + "type": "string", + "required": true + }, + "dbHandle": { + "type": "string | undefined", + "required": false + }, + "suppressEvents": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/schema": { + "name": "data/schema", + "description": "Data Schema Command - Entity Schema Introspection", + "params": { + "collection": { + "type": "string", + "required": true + }, + "examples": { + "type": "boolean | undefined", + "required": false + }, + "sql": { + "type": "boolean | undefined", + "required": false + }, + "validateData": { + "type": "Record | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data": { + "name": "data", + "description": "Base Data Command Types - Generic Abstractions", + "params": { + "collection": { + "type": "string", + "required": true + }, + "dbHandle": { + "type": "string | undefined", + "required": false + }, + "suppressEvents": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/truncate": { + "name": "data/truncate", + "description": "Data Truncate Command - Shared Types", + "params": { + "collection": { + "type": "string", + "required": true + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/update": { + "name": "data/update", + "description": "Data Update Command Types - Universal Update Interface", + "params": { + "id": { + "type": "string", + "required": true + }, + "data": { + "type": "Record", + "required": true + }, + "format": { + "type": "\"json\" | \"table\" | \"yaml\" | undefined", + "required": false + }, + "incrementVersion": { + "type": "boolean | undefined", + "required": false + }, + "collection": { + "type": "string", + "required": true + }, + "dbHandle": { + "type": "string | undefined", + "required": false + }, + "suppressEvents": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "data/vector-search": { + "name": "data/vector-search", + "description": "Vector Search Command - Shared Types", + "params": { + "collection": { + "type": "string", + "required": true + }, + "dbHandle": { + "type": "string | undefined", + "required": false + }, + "queryText": { + "type": "string | undefined", + "required": false + }, + "queryVector": { + "type": "VectorEmbedding | undefined", + "required": false + }, + "k": { + "type": "number | undefined", + "required": false + }, + "similarityThreshold": { + "type": "number | undefined", + "required": false + }, + "hybridMode": { + "type": "\"hybrid\" | \"semantic\" | \"keyword\" | undefined", + "required": false + }, + "filter": { + "type": "Record | undefined", + "required": false + }, + "embeddingModel": { + "type": "string | undefined", + "required": false + }, + "embeddingProvider": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/benchmark-vectors": { + "name": "development/benchmark-vectors", + "description": "BenchmarkVectorsTypes - Types for vector operation benchmarking", + "params": { + "vectorCount": { + "type": "number | undefined", + "required": false + }, + "dimensions": { + "type": "number | undefined", + "required": false + }, + "iterations": { + "type": "number | undefined", + "required": false + }, + "benchmark": { + "type": "\"all\" | \"single\" | \"batch\" | \"search\" | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/build": { + "name": "development/build", + "description": "Development Build Command - Shared Types", + "params": { + "quiet": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/code/pattern-search": { + "name": "development/code/pattern-search", + "description": "code/find command types - Find files by name pattern", + "params": {} + }, + "development/code/read": { + "name": "development/code/read", + "description": "code/read command types", + "params": {} + }, + "development/compile-typescript": { + "name": "development/compile-typescript", + "description": "Compile TypeScript Command - Shared Types", + "params": { + "source": { + "type": "string", + "required": true + }, + "filename": { + "type": "string | undefined", + "required": false + }, + "outputPath": { + "type": "string | undefined", + "required": false + }, + "strict": { + "type": "boolean | undefined", + "required": false + }, + "target": { + "type": "\"es5\" | \"es2015\" | \"es2020\" | \"esnext\" | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/debug/academy-sessions": { + "name": "development/debug/academy-sessions", + "description": "Academy Sessions Debug Command Types", + "params": { + "teacherId": { + "type": "string | undefined", + "required": false + }, + "studentId": { + "type": "string | undefined", + "required": false + }, + "curriculum": { + "type": "string | undefined", + "required": false + }, + "status": { + "type": "\"active\" | \"paused\" | \"completed\" | \"archived\" | undefined", + "required": false + }, + "showMetrics": { + "type": "boolean | undefined", + "required": false + }, + "showObjectives": { + "type": "boolean | undefined", + "required": false + }, + "exportFormat": { + "type": "\"json\" | \"table\" | \"summary\" | undefined", + "required": false + } + } + }, + "development/debug/artifacts-check": { + "name": "development/debug/artifacts-check", + "description": "Debug command to check if ArtifactsDaemon is working", + "params": { + "testFile": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/debug/chat-send": { + "name": "development/debug/chat-send", + "description": "Chat Send Debug Command Types", + "params": { + "message": { + "type": "string", + "required": true + }, + "roomId": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/debug/content-types": { + "name": "development/debug/content-types", + "description": "ContentTypes Debug Command Types", + "params": { + "listAll": { + "type": "boolean | undefined", + "required": false + }, + "showInactive": { + "type": "boolean | undefined", + "required": false + }, + "contentType": { + "type": "string | undefined", + "required": false + }, + "validateConfig": { + "type": "boolean | undefined", + "required": false + }, + "seedDefaults": { + "type": "boolean | undefined", + "required": false + } + } + }, + "development/debug/crud-sync": { + "name": "development/debug/crud-sync", + "description": "CRUD Sync Debug Command Types", + "params": { + "collections": { + "type": "string[] | undefined", + "required": false + }, + "includeDatabase": { + "type": "boolean | undefined", + "required": false + }, + "maxItems": { + "type": "number | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/debug/error": { + "name": "development/debug/error", + "description": "Test Error Command Types - Enhanced", + "params": { + "errorType": { + "type": "ErrorTrigger", + "required": true + }, + "level": { + "type": "ErrorLevel | undefined", + "required": false + }, + "environment": { + "type": "\"browser\" | \"server\" | \"both\" | undefined", + "required": false + }, + "errorMessage": { + "type": "string | undefined", + "required": false + }, + "delay": { + "type": "number | undefined", + "required": false + }, + "recoverable": { + "type": "boolean | undefined", + "required": false + }, + "shouldThrow": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/debug/html-inspector": { + "name": "development/debug/html-inspector", + "description": "HTML Inspector Types - Shadow DOM debugging", + "params": { + "selector": { + "type": "string", + "required": true + }, + "includeStyles": { + "type": "boolean | undefined", + "required": false + }, + "maxDepth": { + "type": "number | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/debug/scroll-test": { + "name": "development/debug/scroll-test", + "description": "Scroll Test Debug Command Types - Clean Testing Interface", + "params": { + "target": { + "type": "\"top\" | \"bottom\" | \"position\"", + "required": true + }, + "position": { + "type": "number | undefined", + "required": false + }, + "behavior": { + "type": "\"auto\" | \"smooth\" | \"instant\" | undefined", + "required": false + }, + "selector": { + "type": "string | undefined", + "required": false + }, + "waitTime": { + "type": "number | undefined", + "required": false + }, + "captureMetrics": { + "type": "boolean | undefined", + "required": false + }, + "repeat": { + "type": "number | undefined", + "required": false + }, + "preset": { + "type": "\"chat-top\" | \"chat-bottom\" | \"instant-top\" | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/debug/widget-css": { + "name": "development/debug/widget-css", + "description": "Debug Command: widget-css", + "params": { + "widgetSelector": { + "type": "string", + "required": true + }, + "cssContent": { + "type": "string | undefined", + "required": false + }, + "cssFile": { + "type": "string | undefined", + "required": false + }, + "screenshot": { + "type": "boolean | undefined", + "required": false + }, + "filename": { + "type": "string | undefined", + "required": false + }, + "extract": { + "type": "boolean | undefined", + "required": false + }, + "mode": { + "type": "CSSInjectionMode | undefined", + "required": false + }, + "clearExisting": { + "type": "boolean | undefined", + "required": false + }, + "showBoundingBoxes": { + "type": "boolean | undefined", + "required": false + }, + "highlightFlexboxes": { + "type": "boolean | undefined", + "required": false + }, + "animateChanges": { + "type": "boolean | undefined", + "required": false + }, + "multiWidget": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/debug/widget-events": { + "name": "development/debug/widget-events", + "description": "Widget Events Debug Command Types", + "params": { + "widgetSelector": { + "type": "string | undefined", + "required": false + }, + "eventName": { + "type": "string | undefined", + "required": false + }, + "includeHandlers": { + "type": "boolean | undefined", + "required": false + }, + "testServerEvents": { + "type": "boolean | undefined", + "required": false + }, + "roomId": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/debug/widget-interact": { + "name": "development/debug/widget-interact", + "description": "Widget Interaction Command Types", + "params": { + "widgetSelector": { + "type": "string", + "required": true + }, + "action": { + "type": "WidgetInteractionType", + "required": true + }, + "elementSelector": { + "type": "string | undefined", + "required": false + }, + "text": { + "type": "string | undefined", + "required": false + }, + "methodName": { + "type": "string | undefined", + "required": false + }, + "methodArgs": { + "type": "any[] | undefined", + "required": false + }, + "propertyName": { + "type": "string | undefined", + "required": false + }, + "propertyValue": { + "type": "any", + "required": false + }, + "screenshotBefore": { + "type": "boolean | undefined", + "required": false + }, + "screenshotAfter": { + "type": "boolean | undefined", + "required": false + }, + "screenshotFilename": { + "type": "string | undefined", + "required": false + }, + "verifyResult": { + "type": "boolean | undefined", + "required": false + }, + "expectedChange": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/debug/widget-state": { + "name": "development/debug/widget-state", + "description": "Widget State Debug Command Types", + "params": { + "widgetSelector": { + "type": "string | undefined", + "required": false + }, + "includeMessages": { + "type": "boolean | undefined", + "required": false + }, + "testDataConnectivity": { + "type": "boolean | undefined", + "required": false + }, + "roomId": { + "type": "string | undefined", + "required": false + }, + "includeEventListeners": { + "type": "boolean | undefined", + "required": false + }, + "includeDomInfo": { + "type": "boolean | undefined", + "required": false + }, + "includeDimensions": { + "type": "boolean | undefined", + "required": false + }, + "extractRowData": { + "type": "boolean | undefined", + "required": false + }, + "rowSelector": { + "type": "string | undefined", + "required": false + }, + "countOnly": { + "type": "boolean | undefined", + "required": false + }, + "contextSessionId": { + "type": "string | undefined", + "required": false + }, + "setRAGString": { + "type": "string | undefined", + "required": false + }, + "getStoredContext": { + "type": "boolean | undefined", + "required": false + }, + "setContext": { + "type": "{ widget: { widgetType: string; section?: string | undefined; title?: string | undefined; metadata?: Record | undefined; timestamp?: number | undefined; }; interaction?: { ...; } | undefined; breadcrumb?: string[] | undefined; dwellTimeMs?: number | undefined; } | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/exec": { + "name": "development/exec", + "description": "ExecCommand Types - Universal Script Execution System", + "params": { + "code": { + "type": "CodeInput", + "required": true + }, + "targetEnvironment": { + "type": "JTAGEnvironment | \"auto\" | \"both\" | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + }, + "parameters": { + "type": "Record | undefined", + "required": false + }, + "allowNetworkRequests": { + "type": "boolean | undefined", + "required": false + }, + "allowFileSystemAccess": { + "type": "boolean | undefined", + "required": false + }, + "allowDOMManipulation": { + "type": "boolean | undefined", + "required": false + }, + "allowJTAGCommandAccess": { + "type": "boolean | undefined", + "required": false + }, + "result": { + "type": "any", + "required": false + }, + "executedAt": { + "type": "number | undefined", + "required": false + }, + "executedIn": { + "type": "\"browser\" | \"server\" | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + } + } + }, + "development/generate/audit": { + "name": "development/generate/audit", + "description": "Generate/Audit Command - Shared Types", + "params": { + "module": { + "type": "string | undefined", + "required": false + }, + "type": { + "type": "\"command\" | \"daemon\" | \"widget\" | undefined", + "required": false + }, + "fix": { + "type": "boolean | undefined", + "required": false + }, + "hibernateFailures": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/generate": { + "name": "development/generate", + "description": "Generate Command - Shared Types", + "params": { + "spec": { + "type": "string | object", + "required": true + }, + "template": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/propose-command": { + "name": "development/propose-command", + "description": "Propose Command Types - AI Command Generation", + "params": { + "spec": { + "type": "CommandSpec", + "required": true + }, + "force": { + "type": "boolean | undefined", + "required": false + }, + "dryRun": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/sandbox-execute": { + "name": "development/sandbox-execute", + "description": "Sandbox Execute Types - Run AI-generated commands in isolation", + "params": { + "commandPath": { + "type": "string", + "required": true + }, + "params": { + "type": "Record | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + } + } + }, + "development/schema/generate": { + "name": "development/schema/generate", + "description": "Schema Generate Command Types", + "params": { + "pattern": { + "type": "string | undefined", + "required": false + }, + "interface": { + "type": "string | undefined", + "required": false + }, + "file": { + "type": "string | undefined", + "required": false + }, + "output": { + "type": "string | undefined", + "required": false + }, + "rootDir": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "development/shell/execute": { + "name": "development/shell/execute", + "description": "Shell Execute Command Types", + "params": { + "command": { + "type": "string", + "required": true + }, + "args": { + "type": "string[] | undefined", + "required": false + }, + "cwd": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + }, + "maxOutputSize": { + "type": "number | undefined", + "required": false + }, + "env": { + "type": "Record | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + } + } + }, + "development/timing": { + "name": "development/timing", + "description": "TimingTypes - Types for analyzing Rust worker timing metrics", + "params": { + "windowMinutes": { + "type": "number | undefined", + "required": false + }, + "requestType": { + "type": "string | undefined", + "required": false + }, + "showBreakdown": { + "type": "boolean | undefined", + "required": false + }, + "format": { + "type": "\"json\" | \"table\" | \"summary\" | undefined", + "required": false + }, + "clear": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "file/append": { + "name": "file/append", + "description": "FileAppend Types - Elegant Inheritance from File Base Types", + "params": { + "filepath": { + "type": "string", + "required": true + }, + "content": { + "type": "string", + "required": true + }, + "createIfMissing": { + "type": "boolean | undefined", + "required": false + }, + "encoding": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "file/load": { + "name": "file/load", + "description": "FileLoad Types - Elegant Inheritance from File Base Types", + "params": { + "filepath": { + "type": "string", + "required": true + }, + "encoding": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "file/mime-type": { + "name": "file/mime-type", + "description": "File MIME Type Command Types", + "params": { + "filepath": { + "type": "string", + "required": true + }, + "inspectContent": { + "type": "boolean | undefined", + "required": false + }, + "encoding": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "file/save": { + "name": "file/save", + "description": "FileSave Types - Generic Inheritance from File Base Types", + "params": { + "filepath": { + "type": "string", + "required": true + }, + "content": { + "type": "string | Buffer", + "required": true + }, + "createDirs": { + "type": "boolean | undefined", + "required": false + }, + "encoding": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "file": { + "name": "file", + "description": "File Command Base Types - Generic Foundation for File Operations", + "params": { + "filepath": { + "type": "string", + "required": true + }, + "encoding": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "genome/batch-micro-tune": { + "name": "genome/batch-micro-tune", + "description": "GenomeBatchMicroTuneTypes - Lightweight in-recipe LoRA updates", + "params": { + "domain": { + "type": "string", + "required": true + }, + "roleId": { + "type": "string | undefined", + "required": false + }, + "personaId": { + "type": "string | undefined", + "required": false + }, + "loraAdapter": { + "type": "string | undefined", + "required": false + }, + "forceUpdate": { + "type": "boolean | undefined", + "required": false + }, + "qualityThreshold": { + "type": "number | undefined", + "required": false + }, + "maxExamples": { + "type": "number | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "genome/job-create": { + "name": "genome/job-create", + "description": "GenomeJobCreateTypes - Create fine-tuning jobs with comprehensive configuration", + "params": { + "personaId": { + "type": "string", + "required": true + }, + "provider": { + "type": "string", + "required": true + }, + "configuration": { + "type": "JobConfiguration", + "required": true + }, + "trainingFileId": { + "type": "string | undefined", + "required": false + }, + "validationFileId": { + "type": "string | null | undefined", + "required": false + }, + "skipValidation": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "genome/job-status": { + "name": "genome/job-status", + "description": "GenomeJobStatusTypes - Query fine-tuning job status", + "params": { + "jobId": { + "type": "string", + "required": true + }, + "refresh": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "genome/paging-activate": { + "name": "genome/paging-activate", + "description": "Genome Activate Command Types", + "params": { + "personaId": { + "type": "string", + "required": true + }, + "adapterId": { + "type": "string", + "required": true + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "genome/paging-adapter-register": { + "name": "genome/paging-adapter-register", + "description": "Genome Paging Adapter Register Command Types", + "params": { + "adapterId": { + "type": "string", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "domain": { + "type": "string", + "required": true + }, + "sizeMB": { + "type": "number", + "required": true + }, + "priority": { + "type": "number | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "genome/paging-deactivate": { + "name": "genome/paging-deactivate", + "description": "Genome Deactivate Command Types", + "params": { + "personaId": { + "type": "string", + "required": true + }, + "adapterId": { + "type": "string", + "required": true + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "genome/paging-register": { + "name": "genome/paging-register", + "description": "Genome Register Command Types", + "params": { + "personaId": { + "type": "string", + "required": true + }, + "displayName": { + "type": "string", + "required": true + }, + "quotaMB": { + "type": "number | undefined", + "required": false + }, + "priority": { + "type": "number | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "genome/paging-stats": { + "name": "genome/paging-stats", + "description": "Genome Stats Command Types", + "params": { + "personaId": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "genome/paging-unregister": { + "name": "genome/paging-unregister", + "description": "Genome Unregister Command Types", + "params": { + "personaId": { + "type": "string", + "required": true + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "genome": { + "name": "genome", + "description": "Genome Command Types", + "params": { + "personaId": { + "type": "string", + "required": true + }, + "adapterId": { + "type": "string", + "required": true + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "help": { + "name": "help", + "description": "Help Command - Shared Types", + "params": { + "path": { + "type": "string | undefined", + "required": false }, "format": { - "type": "\"json\" | \"table\" | \"yaml\" | undefined", + "type": "\"json\" | \"markdown\" | \"rag\" | undefined", + "required": false + }, + "list": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "inference/generate": { + "name": "inference/generate", + "description": "Inference Generate Command - Shared Types", + "params": { + "prompt": { + "type": "string", + "required": true + }, + "model": { + "type": "string | undefined", + "required": false + }, + "provider": { + "type": "string | undefined", + "required": false + }, + "maxTokens": { + "type": "number | undefined", + "required": false + }, + "temperature": { + "type": "number | undefined", + "required": false + }, + "systemPrompt": { + "type": "string | undefined", + "required": false + }, + "adapters": { + "type": "string[] | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "interface/click": { + "name": "interface/click", + "description": "Click Command - Shared Types for Element Interaction", + "params": { + "selector": { + "type": "string", + "required": true + }, + "button": { + "type": "\"left\" | \"right\" | \"middle\" | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + }, + "shadowRoot": { + "type": "boolean | undefined", + "required": false + }, + "innerSelector": { + "type": "string | undefined", + "required": false + }, + "text": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + } + } + }, + "interface/get-text": { + "name": "interface/get-text", + "description": "GetText β€” Type-safe command executor", + "params": { + "selector": { + "type": "string", + "required": true + }, + "trim": { + "type": "boolean | undefined", "required": false }, - "incrementVersion": { + "innerText": { "type": "boolean | undefined", "required": false }, - "collection": { + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "interface/navigate": { + "name": "interface/navigate", + "description": "Navigate Command - Shared Types for Browser Navigation", + "params": { + "url": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + }, + "waitForSelector": { + "type": "string | undefined", + "required": false + }, + "target": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + } + } + }, + "interface/proxy-navigate": { + "name": "interface/proxy-navigate", + "description": "Proxy Navigate Command - Shared Types", + "params": { + "url": { "type": "string", "required": true }, - "dbHandle": { + "target": { + "type": "string | undefined", + "required": false + }, + "rewriteUrls": { + "type": "boolean | undefined", + "required": false + }, + "userAgent": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + }, + "userId": { "type": "string | undefined", "required": false } } }, - "debug/academy-sessions": { - "name": "debug/academy-sessions", - "description": "Academy Sessions Debug Command Types", + "interface/screenshot": { + "name": "interface/screenshot", + "description": "Screenshot Command - Shared Types", "params": { - "teacherId": { + "filename": { "type": "string | undefined", "required": false }, - "studentId": { + "selector": { "type": "string | undefined", "required": false }, - "curriculum": { + "querySelector": { "type": "string | undefined", "required": false }, - "status": { - "type": "\"active\" | \"paused\" | \"completed\" | \"archived\" | undefined", + "elementName": { + "type": "string | undefined", "required": false }, - "showMetrics": { + "iframeSelector": { + "type": "string | undefined", + "required": false + }, + "viewportOnly": { "type": "boolean | undefined", "required": false }, - "showObjectives": { + "options": { + "type": "ScreenshotOptions | undefined", + "required": false + }, + "cropX": { + "type": "number | undefined", + "required": false + }, + "cropY": { + "type": "number | undefined", + "required": false + }, + "cropWidth": { + "type": "number | undefined", + "required": false + }, + "cropHeight": { + "type": "number | undefined", + "required": false + }, + "width": { + "type": "number | undefined", + "required": false + }, + "height": { + "type": "number | undefined", + "required": false + }, + "scale": { + "type": "number | undefined", + "required": false + }, + "quality": { + "type": "number | undefined", + "required": false + }, + "maxFileSize": { + "type": "number | undefined", + "required": false + }, + "format": { + "type": "ScreenshotFormat | undefined", + "required": false + }, + "destination": { + "type": "ScreenshotDestination | undefined", + "required": false + }, + "resultType": { + "type": "ResultType", + "required": true + }, + "dataUrl": { + "type": "string | undefined", + "required": false + }, + "metadata": { + "type": "ScreenshotMetadata | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "interface/scroll": { + "name": "interface/scroll", + "description": "Scroll β€” Type-safe command executor", + "params": { + "x": { + "type": "number | undefined", + "required": false + }, + "y": { + "type": "number | undefined", + "required": false + }, + "selector": { + "type": "string | undefined", + "required": false + }, + "behavior": { + "type": "\"auto\" | \"smooth\" | \"instant\" | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "interface/type": { + "name": "interface/type", + "description": "Type β€” Type-safe command executor", + "params": { + "selector": { + "type": "string", + "required": true + }, + "text": { + "type": "string", + "required": true + }, + "clearFirst": { "type": "boolean | undefined", "required": false }, - "exportFormat": { - "type": "\"json\" | \"table\" | \"summary\" | undefined", + "delay": { + "type": "number | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", "required": false } } }, - "debug/artifacts-check": { - "name": "debug/artifacts-check", - "description": "Debug command to check if ArtifactsDaemon is working", + "interface/wait-for-element": { + "name": "interface/wait-for-element", + "description": "WaitForElement β€” Type-safe command executor", "params": { - "testFile": { + "selector": { + "type": "string", + "required": true + }, + "timeout": { + "type": "number | undefined", + "required": false + }, + "visible": { + "type": "boolean | undefined", + "required": false + }, + "interval": { + "type": "number | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + } + } + }, + "interface/web/fetch": { + "name": "interface/web/fetch", + "description": "Web Fetch Command Types", + "params": { + "url": { + "type": "string", + "required": true + }, + "format": { + "type": "\"text\" | \"markdown\" | \"html\" | undefined", + "required": false + }, + "maxLength": { + "type": "number | undefined", + "required": false + }, + "headers": { + "type": "Record | undefined", + "required": false + }, + "userId": { "type": "string | undefined", "required": false }, @@ -1353,15 +4453,23 @@ } } }, - "debug/chat-send": { - "name": "debug/chat-send", - "description": "Chat Send Debug Command Types", + "interface/web/search": { + "name": "interface/web/search", + "description": "Web Search Command Types", "params": { - "message": { + "query": { "type": "string", "required": true }, - "roomId": { + "maxResults": { + "type": "number | undefined", + "required": false + }, + "domains": { + "type": "string[] | undefined", + "required": false + }, + "userId": { "type": "string | undefined", "required": false }, @@ -1371,46 +4479,46 @@ } } }, - "debug/content-types": { - "name": "debug/content-types", - "description": "ContentTypes Debug Command Types", + "list": { + "name": "list", + "description": "List Command Types - Command Discovery Interface", "params": { - "listAll": { + "includeDescription": { "type": "boolean | undefined", "required": false }, - "showInactive": { + "includeSignature": { "type": "boolean | undefined", "required": false }, - "contentType": { + "userId": { "type": "string | undefined", "required": false }, - "validateConfig": { - "type": "boolean | undefined", - "required": false - }, - "seedDefaults": { - "type": "boolean | undefined", + "timeout": { + "type": "number | undefined", "required": false } } }, - "debug/crud-sync": { - "name": "debug/crud-sync", - "description": "CRUD Sync Debug Command Types", + "logs/config": { + "name": "logs/config", + "description": "Logs Config Command - Shared Types", "params": { - "collections": { - "type": "string[] | undefined", + "persona": { + "type": "string | undefined", "required": false }, - "includeDatabase": { - "type": "boolean | undefined", + "action": { + "type": "\"get\" | \"enable\" | \"disable\" | undefined", "required": false }, - "maxItems": { - "type": "number | undefined", + "category": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -1419,54 +4527,40 @@ } } }, - "debug/error": { - "name": "debug/error", - "description": "Test Error Command Types - Enhanced", + "logs/list": { + "name": "logs/list", + "description": "logs/list Command Types", "params": { - "errorType": { - "type": "ErrorTrigger", - "required": true - }, - "level": { - "type": "ErrorLevel | undefined", + "category": { + "type": "\"system\" | \"persona\" | \"session\" | \"external\" | undefined", "required": false }, - "environment": { - "type": "\"server\" | \"browser\" | \"both\" | undefined", + "personaId": { + "type": "string | undefined", "required": false }, - "errorMessage": { + "personaUniqueId": { "type": "string | undefined", "required": false }, - "delay": { - "type": "number | undefined", + "component": { + "type": "string | undefined", "required": false }, - "recoverable": { - "type": "boolean | undefined", + "logType": { + "type": "string | undefined", "required": false }, - "shouldThrow": { + "includeStats": { "type": "boolean | undefined", "required": false - } - } - }, - "debug/html-inspector": { - "name": "debug/html-inspector", - "description": "HTML Inspector Types - Shadow DOM debugging", - "params": { - "selector": { - "type": "string", - "required": true }, - "includeStyles": { + "includeSessionLogs": { "type": "boolean | undefined", "required": false }, - "maxDepth": { - "type": "number | undefined", + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -1475,40 +4569,44 @@ } } }, - "debug/scroll-test": { - "name": "debug/scroll-test", - "description": "Scroll Test Debug Command Types - Clean Testing Interface", + "logs/read": { + "name": "logs/read", + "description": "LogsRead β€” Type-safe command executor", "params": { - "target": { - "type": "\"top\" | \"bottom\" | \"position\"", + "log": { + "type": "string", "required": true }, - "position": { + "startLine": { "type": "number | undefined", "required": false }, - "behavior": { - "type": "\"smooth\" | \"instant\" | \"auto\" | undefined", + "endLine": { + "type": "number | undefined", "required": false }, - "selector": { - "type": "string | undefined", + "tail": { + "type": "number | undefined", "required": false }, - "waitTime": { - "type": "number | undefined", + "level": { + "type": "\"ERROR\" | \"DEBUG\" | \"INFO\" | \"WARN\" | undefined", "required": false }, - "captureMetrics": { + "component": { + "type": "string | undefined", + "required": false + }, + "analyzeStructure": { "type": "boolean | undefined", "required": false }, - "repeat": { - "type": "number | undefined", + "includeSessionLogs": { + "type": "boolean | undefined", "required": false }, - "preset": { - "type": "\"chat-top\" | \"chat-bottom\" | \"instant-top\" | undefined", + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -1517,56 +4615,28 @@ } } }, - "debug/widget-css": { - "name": "debug/widget-css", - "description": "Debug Command: widget-css", + "logs/search": { + "name": "logs/search", + "description": "LogsSearch β€” Type-safe command executor", "params": { - "widgetSelector": { + "pattern": { "type": "string", "required": true }, - "cssContent": { - "type": "string | undefined", + "logs": { + "type": "string[] | undefined", "required": false }, - "cssFile": { + "category": { "type": "string | undefined", "required": false }, - "screenshot": { - "type": "boolean | undefined", - "required": false - }, - "filename": { + "personaId": { "type": "string | undefined", "required": false }, - "extract": { - "type": "boolean | undefined", - "required": false - }, - "mode": { - "type": "CSSInjectionMode | undefined", - "required": false - }, - "clearExisting": { - "type": "boolean | undefined", - "required": false - }, - "showBoundingBoxes": { - "type": "boolean | undefined", - "required": false - }, - "highlightFlexboxes": { - "type": "boolean | undefined", - "required": false - }, - "animateChanges": { - "type": "boolean | undefined", - "required": false - }, - "multiWidget": { - "type": "boolean | undefined", + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -1575,27 +4645,11 @@ } } }, - "debug/widget-events": { - "name": "debug/widget-events", - "description": "Widget Events Debug Command Types", + "logs/stats": { + "name": "logs/stats", + "description": "LogsStats β€” Type-safe command executor", "params": { - "widgetSelector": { - "type": "string | undefined", - "required": false - }, - "eventName": { - "type": "string | undefined", - "required": false - }, - "includeHandlers": { - "type": "boolean | undefined", - "required": false - }, - "testServerEvents": { - "type": "boolean | undefined", - "required": false - }, - "roomId": { + "userId": { "type": "string | undefined", "required": false }, @@ -1605,165 +4659,119 @@ } } }, - "debug/widget-interact": { - "name": "debug/widget-interact", - "description": "Widget Interaction Command Types", + "media/process": { + "name": "media/process", + "description": "Media Process Command Types", "params": { - "widgetSelector": { + "inputPath": { "type": "string", "required": true }, - "action": { - "type": "WidgetInteractionType", - "required": true - }, - "elementSelector": { + "outputPath": { "type": "string | undefined", "required": false }, - "text": { - "type": "string | undefined", + "speed": { + "type": "number | undefined", "required": false }, - "methodName": { - "type": "string | undefined", + "stripAudio": { + "type": "boolean | undefined", "required": false }, - "methodArgs": { - "type": "any[] | undefined", + "extractAudio": { + "type": "boolean | undefined", "required": false }, - "propertyName": { + "audioPath": { "type": "string | undefined", "required": false }, - "propertyValue": { - "type": "any", + "volume": { + "type": "number | undefined", "required": false }, - "screenshotBefore": { - "type": "boolean | undefined", + "format": { + "type": "MediaFormat | undefined", "required": false }, - "screenshotAfter": { - "type": "boolean | undefined", + "quality": { + "type": "QualityPreset | undefined", "required": false }, - "screenshotFilename": { - "type": "string | undefined", + "videoCodec": { + "type": "VideoCodec | undefined", "required": false }, - "verifyResult": { - "type": "boolean | undefined", + "audioCodec": { + "type": "AudioCodec | undefined", "required": false }, - "expectedChange": { + "videoBitrate": { "type": "string | undefined", "required": false }, - "timeout": { - "type": "number | undefined", - "required": false - } - } - }, - "debug/widget-state": { - "name": "debug/widget-state", - "description": "Widget State Debug Command Types", - "params": { - "widgetSelector": { + "audioBitrate": { "type": "string | undefined", "required": false }, - "includeMessages": { - "type": "boolean | undefined", - "required": false - }, - "testDataConnectivity": { - "type": "boolean | undefined", + "resolution": { + "type": "{ width: number; height: number; } | undefined", "required": false }, - "roomId": { - "type": "string | undefined", + "scale": { + "type": "number | undefined", "required": false }, - "includeEventListeners": { - "type": "boolean | undefined", + "trim": { + "type": "{ start: number; end: number; } | undefined", "required": false }, - "includeDomInfo": { - "type": "boolean | undefined", + "trimTime": { + "type": "{ start: string; end: string; } | undefined", "required": false }, - "includeDimensions": { - "type": "boolean | undefined", + "rotate": { + "type": "90 | 180 | 270 | undefined", "required": false }, - "extractRowData": { + "flipHorizontal": { "type": "boolean | undefined", "required": false }, - "rowSelector": { - "type": "string | undefined", - "required": false - }, - "countOnly": { + "flipVertical": { "type": "boolean | undefined", "required": false }, - "timeout": { + "gifFps": { "type": "number | undefined", "required": false - } - } - }, - "decision/finalize": { - "name": "decision/finalize", - "description": "decision/finalize - Types", - "params": { - "proposalId": { - "type": "string", - "required": true }, - "timeout": { + "gifMaxWidth": { "type": "number | undefined", "required": false - } - } - }, - "decision/propose": { - "name": "decision/propose", - "description": "decision/propose - Create a new decision proposal with ranked-choice voting", - "params": { - "topic": { - "type": "string", - "required": true }, - "rationale": { - "type": "string", - "required": true - }, - "tags": { + "customArgs": { "type": "string[] | undefined", "required": false }, - "options": { - "type": "{ label: string; description: string; proposedBy?: string | undefined; }[]", - "required": true - }, - "scope": { - "type": "ProposalScope | undefined", + "checkDependency": { + "type": "boolean | undefined", "required": false }, - "significanceLevel": { - "type": "SignificanceLevel | undefined", + "installDependencies": { + "type": "boolean | undefined", "required": false }, - "proposerId": { + "minVersion": { "type": "string | undefined", "required": false }, - "contextId": { + "emitProgress": { + "type": "boolean | undefined", + "required": false + }, + "userId": { "type": "string | undefined", "required": false }, @@ -1773,113 +4781,131 @@ } } }, - "decision/rank": { - "name": "decision/rank", - "description": "decision/rank - Types", + "media/resize": { + "name": "media/resize", + "description": "Media Resize Command Types", "params": { - "proposalId": { + "inputPath": { "type": "string", "required": true }, - "rankedChoices": { - "type": "string[]", - "required": true - }, - "voterId": { + "outputPath": { "type": "string | undefined", "required": false }, - "timeout": { + "width": { "type": "number | undefined", "required": false - } - } - }, - "exec": { - "name": "exec", - "description": "ExecCommand Types - Universal Script Execution System", - "params": { - "code": { - "type": "CodeInput", - "required": true }, - "targetEnvironment": { - "type": "JTAGEnvironment | \"both\" | \"auto\" | undefined", + "height": { + "type": "number | undefined", "required": false }, - "timeout": { + "maxWidth": { "type": "number | undefined", "required": false }, - "parameters": { - "type": "Record | undefined", + "maxHeight": { + "type": "number | undefined", "required": false }, - "allowNetworkRequests": { - "type": "boolean | undefined", + "modelName": { + "type": "string | undefined", "required": false }, - "allowFileSystemAccess": { - "type": "boolean | undefined", + "targetPercentage": { + "type": "number | undefined", "required": false }, - "allowDOMManipulation": { - "type": "boolean | undefined", + "quality": { + "type": "number | undefined", "required": false }, - "allowJTAGCommandAccess": { + "fit": { + "type": "FitStrategy | undefined", + "required": false + }, + "returnBase64": { "type": "boolean | undefined", "required": false }, - "result": { - "type": "any", + "userId": { + "type": "string | undefined", "required": false }, - "executedAt": { + "timeout": { "type": "number | undefined", "required": false + } + } + }, + "persona/genome": { + "name": "persona/genome", + "description": "Persona Genome Command - Shared Types", + "params": { + "personaId": { + "type": "string | undefined", + "required": false }, - "executedIn": { - "type": "\"server\" | \"browser\" | undefined", + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", "required": false } } }, - "file/append": { - "name": "file/append", - "description": "FileAppend Types - Elegant Inheritance from File Base Types", + "persona/learning/capture-feedback": { + "name": "persona/learning/capture-feedback", + "description": "GenomeCaptureFeedbackTypes - Capture feedback between PersonaUsers", "params": { - "content": { + "interactionId": { + "type": "string | undefined", + "required": false + }, + "targetRole": { "type": "string", "required": true }, - "createIfMissing": { - "type": "boolean | undefined", + "targetPersonaId": { + "type": "string | undefined", "required": false }, - "filepath": { + "feedbackRole": { "type": "string", "required": true }, - "encoding": { + "feedbackPersonaId": { "type": "string | undefined", "required": false }, - "timeout": { - "type": "number | undefined", - "required": false - } - } - }, - "file/load": { - "name": "file/load", - "description": "FileLoad Types - Elegant Inheritance from File Base Types", - "params": { - "filepath": { + "domain": { "type": "string", "required": true }, - "encoding": { + "feedbackType": { + "type": "FeedbackType", + "required": true + }, + "feedbackContent": { + "type": "string", + "required": true + }, + "qualityScore": { + "type": "number | undefined", + "required": false + }, + "wasHelpful": { + "type": "boolean | undefined", + "required": false + }, + "metadata": { + "type": "{ [key: string]: unknown; messageId?: string | undefined; contextId?: string | undefined; recipeId?: string | undefined; timestamp?: string | undefined; } | undefined", + "required": false + }, + "userId": { "type": "string | undefined", "required": false }, @@ -1889,41 +4915,43 @@ } } }, - "file/save": { - "name": "file/save", - "description": "FileSave Types - Generic Inheritance from File Base Types", + "persona/learning/capture-interaction": { + "name": "persona/learning/capture-interaction", + "description": "GenomeCaptureInteractionTypes - Capture AI interactions for continuous learning", "params": { - "content": { - "type": "string | Buffer", + "roleId": { + "type": "string", "required": true }, - "createDirs": { - "type": "boolean | undefined", + "personaId": { + "type": "string | undefined", "required": false }, - "filepath": { + "domain": { "type": "string", "required": true }, - "encoding": { + "loraAdapter": { "type": "string | undefined", "required": false }, - "timeout": { - "type": "number | undefined", - "required": false - } - } - }, - "file": { - "name": "file", - "description": "File Command Base Types - Generic Foundation for File Operations", - "params": { - "filepath": { + "input": { "type": "string", "required": true }, - "encoding": { + "output": { + "type": "string", + "required": true + }, + "thoughtStream": { + "type": "string | undefined", + "required": false + }, + "metadata": { + "type": "{ [key: string]: unknown; roomId?: string | undefined; messageId?: string | undefined; contextId?: string | undefined; recipeId?: string | undefined; timestamp?: string | undefined; } | undefined", + "required": false + }, + "userId": { "type": "string | undefined", "required": false }, @@ -1933,36 +4961,32 @@ } } }, - "genome/batch-micro-tune": { - "name": "genome/batch-micro-tune", - "description": "GenomeBatchMicroTuneTypes - Lightweight in-recipe LoRA updates", + "persona/learning/multi-agent-learn": { + "name": "persona/learning/multi-agent-learn", + "description": "GenomeMultiAgentLearnTypes - Multi-agent collaborative learning", "params": { "domain": { "type": "string", "required": true }, - "roleId": { - "type": "string | undefined", - "required": false - }, - "personaId": { - "type": "string | undefined", - "required": false + "outcome": { + "type": "CollaborativeOutcome", + "required": true }, - "loraAdapter": { - "type": "string | undefined", - "required": false + "participants": { + "type": "{ [roleId: string]: ParticipantLearning; }", + "required": true }, - "forceUpdate": { - "type": "boolean | undefined", + "trainingMode": { + "type": "\"immediate\" | \"queued\" | undefined", "required": false }, - "qualityThreshold": { - "type": "number | undefined", + "metadata": { + "type": "{ [key: string]: unknown; recipeId?: string | undefined; contextId?: string | undefined; sessionId?: string | undefined; timestamp?: string | undefined; } | undefined", "required": false }, - "maxExamples": { - "type": "number | undefined", + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -1971,52 +4995,52 @@ } } }, - "genome/capture-feedback": { - "name": "genome/capture-feedback", - "description": "GenomeCaptureFeedbackTypes - Capture feedback between PersonaUsers", + "persona/learning/pattern/capture": { + "name": "persona/learning/pattern/capture", + "description": "Persona Learning Pattern Capture Command - Shared Types", "params": { - "interactionId": { - "type": "string | undefined", - "required": false - }, - "targetRole": { + "name": { "type": "string", "required": true }, - "targetPersonaId": { - "type": "string | undefined", - "required": false - }, - "feedbackRole": { + "type": { "type": "string", "required": true }, - "feedbackPersonaId": { - "type": "string | undefined", - "required": false - }, "domain": { "type": "string", "required": true }, - "feedbackType": { - "type": "FeedbackType", + "problem": { + "type": "string", "required": true }, - "feedbackContent": { + "solution": { "type": "string", "required": true }, - "qualityScore": { - "type": "number | undefined", + "description": { + "type": "string | undefined", "required": false }, - "wasHelpful": { + "tags": { + "type": "string[] | undefined", + "required": false + }, + "applicableWhen": { + "type": "string[] | undefined", + "required": false + }, + "examples": { + "type": "string[] | undefined", + "required": false + }, + "makePublic": { "type": "boolean | undefined", "required": false }, - "metadata": { - "type": "{ [key: string]: unknown; messageId?: string | undefined; contextId?: string | undefined; recipeId?: string | undefined; timestamp?: string | undefined; } | undefined", + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -2025,74 +5049,70 @@ } } }, - "genome/capture-interaction": { - "name": "genome/capture-interaction", - "description": "GenomeCaptureInteractionTypes - Capture AI interactions for continuous learning", + "persona/learning/pattern/endorse": { + "name": "persona/learning/pattern/endorse", + "description": "Persona Learning Pattern Endorse Command - Shared Types", "params": { - "roleId": { + "patternId": { "type": "string", "required": true }, - "personaId": { - "type": "string | undefined", - "required": false - }, - "domain": { - "type": "string", + "success": { + "type": "boolean", "required": true }, - "loraAdapter": { + "notes": { "type": "string | undefined", "required": false }, - "input": { - "type": "string", - "required": true - }, - "output": { - "type": "string", - "required": true - }, - "thoughtStream": { + "userId": { "type": "string | undefined", "required": false }, - "metadata": { - "type": "{ [key: string]: unknown; roomId?: string | undefined; messageId?: string | undefined; contextId?: string | undefined; recipeId?: string | undefined; timestamp?: string | undefined; } | undefined", - "required": false - }, "timeout": { "type": "number | undefined", "required": false } } }, - "genome/job-create": { - "name": "genome/job-create", - "description": "GenomeJobCreateTypes - Create fine-tuning jobs with comprehensive configuration", + "persona/learning/pattern/query": { + "name": "persona/learning/pattern/query", + "description": "Persona Learning Pattern Query Command - Shared Types", "params": { - "personaId": { - "type": "string", - "required": true + "domain": { + "type": "string | undefined", + "required": false }, - "provider": { - "type": "string", - "required": true + "type": { + "type": "string | undefined", + "required": false }, - "configuration": { - "type": "JobConfiguration", - "required": true + "keywords": { + "type": "string[] | undefined", + "required": false }, - "trainingFileId": { + "search": { "type": "string | undefined", "required": false }, - "validationFileId": { - "type": "string | null | undefined", + "minConfidence": { + "type": "number | undefined", "required": false }, - "skipValidation": { - "type": "boolean | undefined", + "status": { + "type": "string | undefined", + "required": false + }, + "limit": { + "type": "number | undefined", + "required": false + }, + "orderBy": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -2101,65 +5121,79 @@ } } }, - "genome/job-status": { - "name": "genome/job-status", - "description": "GenomeJobStatusTypes - Query fine-tuning job status", + "ping": { + "name": "ping", + "description": "Include detailed AI persona health status", "params": { - "jobId": { - "type": "string", - "required": true + "server": { + "type": "ServerEnvironmentInfo | undefined", + "required": false }, - "refresh": { + "browser": { + "type": "BrowserEnvironmentInfo | undefined", + "required": false + }, + "verbose": { "type": "boolean | undefined", "required": false }, + "userId": { + "type": "string | undefined", + "required": false + }, "timeout": { "type": "number | undefined", "required": false } } }, - "genome/multi-agent-learn": { - "name": "genome/multi-agent-learn", - "description": "GenomeMultiAgentLearnTypes - Multi-agent collaborative learning", + "positron/cursor": { + "name": "positron/cursor", + "description": "Positron Cursor Command Types", "params": { - "domain": { - "type": "string", + "action": { + "type": "CursorAction", "required": true }, - "outcome": { - "type": "CollaborativeOutcome", - "required": true + "x": { + "type": "number | undefined", + "required": false }, - "participants": { - "type": "{ [roleId: string]: ParticipantLearning; }", - "required": true + "y": { + "type": "number | undefined", + "required": false }, - "trainingMode": { - "type": "\"immediate\" | \"queued\" | undefined", + "selector": { + "type": "string | undefined", "required": false }, - "metadata": { - "type": "{ [key: string]: unknown; recipeId?: string | undefined; contextId?: string | undefined; sessionId?: string | undefined; timestamp?: string | undefined; } | undefined", + "shape": { + "type": "DrawShape | undefined", "required": false }, - "timeout": { + "color": { + "type": "string | undefined", + "required": false + }, + "duration": { "type": "number | undefined", "required": false - } - } - }, - "genome/paging-activate": { - "name": "genome/paging-activate", - "description": "Genome Activate Command Types", - "params": { + }, + "message": { + "type": "string | undefined", + "required": false + }, "personaId": { - "type": "string", - "required": true + "type": "string | undefined", + "required": false }, - "adapterId": { - "type": "string", - "required": true + "personaName": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false }, "timeout": { "type": "number | undefined", @@ -2167,28 +5201,32 @@ } } }, - "genome/paging-adapter-register": { - "name": "genome/paging-adapter-register", - "description": "Genome Paging Adapter Register Command Types", + "process-registry": { + "name": "process-registry", + "description": "Process Registry Command Types - Shared", "params": { - "adapterId": { - "type": "string", + "processType": { + "type": "ProcessType", "required": true }, - "name": { + "description": { "type": "string", "required": true }, - "domain": { - "type": "string", - "required": true + "ports": { + "type": "readonly number[] | undefined", + "required": false }, - "sizeMB": { - "type": "number", - "required": true + "capabilities": { + "type": "readonly ProcessCapability[] | undefined", + "required": false }, - "priority": { - "type": "number | undefined", + "parentProcessId": { + "type": "string | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -2197,17 +5235,33 @@ } } }, - "genome/paging-deactivate": { - "name": "genome/paging-deactivate", - "description": "Genome Deactivate Command Types", + "rag/budget": { + "name": "rag/budget", + "description": "RAG Budget Command - Calculate token budget for RAG context", "params": { - "personaId": { + "model": { "type": "string", "required": true }, - "adapterId": { - "type": "string", - "required": true + "maxTokens": { + "type": "number | undefined", + "required": false + }, + "systemPromptTokens": { + "type": "number | undefined", + "required": false + }, + "targetUtilization": { + "type": "number | undefined", + "required": false + }, + "avgTokensPerMessage": { + "type": "number | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false }, "timeout": { "type": "number | undefined", @@ -2215,37 +5269,39 @@ } } }, - "genome/paging-register": { - "name": "genome/paging-register", - "description": "Genome Register Command Types", + "rag/load": { + "name": "rag/load", + "description": "RAG Load Command - Test incremental message loading with token counting", "params": { - "personaId": { - "type": "string", - "required": true + "roomId": { + "type": "string | undefined", + "required": false }, - "displayName": { + "room": { + "type": "string | undefined", + "required": false + }, + "model": { "type": "string", "required": true }, - "quotaMB": { + "maxTokens": { "type": "number | undefined", "required": false }, - "priority": { + "systemPromptTokens": { "type": "number | undefined", "required": false }, - "timeout": { + "targetUtilization": { "type": "number | undefined", "required": false - } - } - }, - "genome/paging-stats": { - "name": "genome/paging-stats", - "description": "Genome Stats Command Types", - "params": { - "personaId": { + }, + "showMessageContent": { + "type": "boolean | undefined", + "required": false + }, + "userId": { "type": "string | undefined", "required": false }, @@ -2255,13 +5311,21 @@ } } }, - "genome/paging-unregister": { - "name": "genome/paging-unregister", - "description": "Genome Unregister Command Types", + "security/setup": { + "name": "security/setup", + "description": "SecuritySetup β€” Type-safe command executor", "params": { - "personaId": { - "type": "string", - "required": true + "statusOnly": { + "type": "boolean | undefined", + "required": false + }, + "component": { + "type": "\"all\" | \"monitor\" | \"proxy\" | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false }, "timeout": { "type": "number | undefined", @@ -2269,38 +5333,46 @@ } } }, - "genome": { - "name": "genome", - "description": "Genome Command Types", + "session/create": { + "name": "session/create", + "description": "Session Create Command Types - Shared", "params": { - "personaId": { - "type": "string", + "category": { + "type": "SessionCategory", "required": true }, - "adapterId": { + "displayName": { "type": "string", "required": true }, + "userId": { + "type": "string | undefined", + "required": false + }, + "isShared": { + "type": "boolean | undefined", + "required": false + }, + "connectionContext": { + "type": "EnhancedConnectionContext", + "required": true + }, "timeout": { "type": "number | undefined", "required": false } } }, - "get-text": { - "name": "get-text", - "description": "get-text command", + "session/destroy": { + "name": "session/destroy", + "description": "Session Destroy Command Types", "params": { - "selector": { - "type": "string", - "required": true - }, - "trim": { - "type": "boolean | undefined", + "reason": { + "type": "string | undefined", "required": false }, - "innerText": { - "type": "boolean | undefined", + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -2309,180 +5381,204 @@ } } }, - "help": { - "name": "help", - "description": "Help Command Types", + "session/get-id": { + "name": "session/get-id", + "description": "Session Get ID Command - Get current session ID", "params": { - "commandName": { + "userId": { "type": "string | undefined", "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false } } }, - "list": { - "name": "list", - "description": "List Command Types - Command Discovery Interface", + "session/get-user": { + "name": "session/get-user", + "description": "Optional: The session ID to look up", "params": { - "includeDescription": { - "type": "boolean | undefined", + "targetSessionId": { + "type": "string | undefined", "required": false }, - "includeSignature": { - "type": "boolean | undefined", + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { + "type": "number | undefined", "required": false } } }, - "media/process": { - "name": "media/process", - "description": "Media Process Command Types", + "social/comment": { + "name": "social/comment", + "description": "Social Comment Command - Shared Types", "params": { - "inputPath": { + "platform": { "type": "string", "required": true }, - "outputPath": { - "type": "string | undefined", - "required": false + "postId": { + "type": "string", + "required": true }, - "speed": { - "type": "number | undefined", - "required": false + "content": { + "type": "string", + "required": true }, - "stripAudio": { - "type": "boolean | undefined", + "parentId": { + "type": "string | undefined", "required": false }, - "extractAudio": { - "type": "boolean | undefined", + "personaId": { + "type": "string | undefined", "required": false }, - "audioPath": { + "userId": { "type": "string | undefined", "required": false }, - "volume": { + "timeout": { "type": "number | undefined", "required": false + } + } + }, + "social/feed": { + "name": "social/feed", + "description": "Social Feed Command - Shared Types", + "params": { + "platform": { + "type": "string", + "required": true }, - "format": { - "type": "MediaFormat | undefined", + "sort": { + "type": "string | undefined", "required": false }, - "quality": { - "type": "QualityPreset | undefined", + "community": { + "type": "string | undefined", "required": false }, - "videoCodec": { - "type": "VideoCodec | undefined", + "limit": { + "type": "number | undefined", "required": false }, - "audioCodec": { - "type": "AudioCodec | undefined", + "personalized": { + "type": "boolean | undefined", "required": false }, - "videoBitrate": { + "personaId": { "type": "string | undefined", "required": false }, - "audioBitrate": { + "userId": { "type": "string | undefined", "required": false }, - "resolution": { - "type": "{ width: number; height: number; } | undefined", - "required": false - }, - "scale": { + "timeout": { "type": "number | undefined", "required": false + } + } + }, + "social/notifications": { + "name": "social/notifications", + "description": "Social Notifications Command - Shared Types", + "params": { + "platform": { + "type": "string", + "required": true }, - "trim": { - "type": "{ start: number; end: number; } | undefined", - "required": false - }, - "trimTime": { - "type": "{ start: string; end: string; } | undefined", + "since": { + "type": "string | undefined", "required": false }, - "rotate": { - "type": "90 | 180 | 270 | undefined", + "limit": { + "type": "number | undefined", "required": false }, - "flipHorizontal": { - "type": "boolean | undefined", + "personaId": { + "type": "string | undefined", "required": false }, - "flipVertical": { - "type": "boolean | undefined", + "userId": { + "type": "string | undefined", "required": false }, - "gifFps": { + "timeout": { "type": "number | undefined", "required": false + } + } + }, + "social/post": { + "name": "social/post", + "description": "Social Post Command - Shared Types", + "params": { + "platform": { + "type": "string", + "required": true }, - "gifMaxWidth": { - "type": "number | undefined", - "required": false + "title": { + "type": "string", + "required": true }, - "customArgs": { - "type": "string[] | undefined", + "content": { + "type": "string", + "required": true + }, + "community": { + "type": "string | undefined", "required": false }, - "checkDependency": { - "type": "boolean | undefined", + "url": { + "type": "string | undefined", "required": false }, - "installDependencies": { - "type": "boolean | undefined", + "personaId": { + "type": "string | undefined", "required": false }, - "minVersion": { + "userId": { "type": "string | undefined", "required": false }, - "emitProgress": { - "type": "boolean | undefined", + "timeout": { + "type": "number | undefined", "required": false } } }, - "navigate": { - "name": "navigate", - "description": "Navigate Command - Shared Types for Browser Navigation", + "social/signup": { + "name": "social/signup", + "description": "Social Signup Command - Shared Types", "params": { - "url": { + "platform": { "type": "string", "required": true }, - "timeout": { - "type": "number | undefined", - "required": false + "agentName": { + "type": "string", + "required": true }, - "waitForSelector": { + "description": { "type": "string | undefined", "required": false }, - "target": { + "personaId": { "type": "string | undefined", "required": false - } - } - }, - "ping": { - "name": "ping", - "description": "Include detailed AI persona health status", - "params": { - "server": { - "type": "ServerEnvironmentInfo | undefined", - "required": false }, - "browser": { - "type": "BrowserEnvironmentInfo | undefined", + "metadata": { + "type": "Record | undefined", "required": false }, - "verbose": { - "type": "boolean | undefined", + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -2491,75 +5587,59 @@ } } }, - "pipe/chain": { - "name": "pipe/chain", - "description": "Pipe Chain Command Types", + "state/content/close": { + "name": "state/content/close", + "description": "State Content Close Command - Shared Types", "params": { - "commands": { + "userId": { "type": "string", "required": true }, - "errorHandling": { - "type": "\"stop\" | \"continue\" | \"collect\" | undefined", - "required": false + "contentItemId": { + "type": "string", + "required": true }, "timeout": { "type": "number | undefined", "required": false - }, - "showIntermediate": { - "type": "boolean | undefined", - "required": false - }, - "format": { - "type": "\"text\" | \"json\" | \"auto\" | undefined", - "required": false } } }, - "process-registry": { - "name": "process-registry", - "description": "Process Registry Command Types - Shared", + "state/content/switch": { + "name": "state/content/switch", + "description": "State Content Switch Command - Shared Types", "params": { - "processType": { - "type": "ProcessType", + "userId": { + "type": "string", "required": true }, - "description": { + "contentItemId": { "type": "string", "required": true }, - "ports": { - "type": "readonly number[] | undefined", - "required": false - }, - "capabilities": { - "type": "readonly ProcessCapability[] | undefined", - "required": false - }, - "parentProcessId": { - "type": "string | undefined", + "timeout": { + "type": "number | undefined", "required": false } } }, - "proxy-navigate": { - "name": "proxy-navigate", - "description": "Proxy Navigate Command - Shared Types", + "state/create": { + "name": "state/create", + "description": "State Create Command - Shared Types", "params": { - "url": { + "collection": { "type": "string", "required": true }, - "target": { + "data": { + "type": "Record", + "required": true + }, + "id": { "type": "string | undefined", "required": false }, - "rewriteUrls": { - "type": "boolean | undefined", - "required": false - }, - "userAgent": { + "userId": { "type": "string | undefined", "required": false }, @@ -2569,28 +5649,28 @@ } } }, - "rag/budget": { - "name": "rag/budget", - "description": "RAG Budget Command - Calculate token budget for RAG context", + "state/get": { + "name": "state/get", + "description": "State Get Command - Shared Types", "params": { - "model": { + "collection": { "type": "string", "required": true }, - "maxTokens": { + "limit": { "type": "number | undefined", "required": false }, - "systemPromptTokens": { - "type": "number | undefined", + "filter": { + "type": "Record | undefined", "required": false }, - "targetUtilization": { - "type": "number | undefined", + "orderBy": { + "type": "{ field: string; direction: \"asc\" | \"desc\"; }[] | undefined", "required": false }, - "avgTokensPerMessage": { - "type": "number | undefined", + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -2599,36 +5679,24 @@ } } }, - "rag/load": { - "name": "rag/load", - "description": "RAG Load Command - Test incremental message loading with token counting", + "state/update": { + "name": "state/update", + "description": "State Update Types - User-aware entity updates", "params": { - "roomId": { - "type": "string | undefined", - "required": false - }, - "room": { - "type": "string | undefined", - "required": false - }, - "model": { + "collection": { "type": "string", "required": true }, - "maxTokens": { - "type": "number | undefined", - "required": false - }, - "systemPromptTokens": { - "type": "number | undefined", - "required": false + "id": { + "type": "string", + "required": true }, - "targetUtilization": { - "type": "number | undefined", - "required": false + "data": { + "type": "Record", + "required": true }, - "showMessageContent": { - "type": "boolean | undefined", + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -2637,20 +5705,20 @@ } } }, - "recipe/load": { - "name": "recipe/load", - "description": "Recipe Load Command Types", + "system/daemons": { + "name": "system/daemons", + "description": "Daemons Command Types", "params": { - "recipeId": { + "nameFilter": { "type": "string | undefined", "required": false }, - "loadAll": { + "statusOnly": { "type": "boolean | undefined", "required": false }, - "reload": { - "type": "boolean | undefined", + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -2659,193 +5727,187 @@ } } }, - "schema/generate": { - "name": "schema/generate", - "description": "Schema Generate Command Types", + "theme/get": { + "name": "theme/get", + "description": "ThemeGet Types - Theme getting command types", "params": { - "pattern": { - "type": "string | undefined", - "required": false - }, - "interface": { - "type": "string | undefined", - "required": false - }, - "file": { + "timestamp": { "type": "string | undefined", "required": false }, - "output": { + "userId": { "type": "string | undefined", "required": false }, - "rootDir": { - "type": "string | undefined", + "timeout": { + "type": "number | undefined", "required": false } } }, - "screenshot": { - "name": "screenshot", - "description": "Screenshot Command - Shared Types", + "theme/list": { + "name": "theme/list", + "description": "ThemeList Types - Theme listing command types", "params": { - "filename": { + "category": { "type": "string | undefined", "required": false }, - "selector": { - "type": "string | undefined", + "includeManifests": { + "type": "boolean | undefined", "required": false }, - "querySelector": { + "timestamp": { "type": "string | undefined", "required": false }, - "elementName": { + "userId": { "type": "string | undefined", "required": false }, - "options": { - "type": "ScreenshotOptions | undefined", - "required": false - }, - "cropX": { + "timeout": { "type": "number | undefined", "required": false + } + } + }, + "theme/set": { + "name": "theme/set", + "description": "ThemeSet Types - Theme setting command types", + "params": { + "themeName": { + "type": "string", + "required": true }, - "cropY": { - "type": "number | undefined", + "timestamp": { + "type": "string | undefined", "required": false }, - "cropWidth": { - "type": "number | undefined", + "userId": { + "type": "string | undefined", "required": false }, - "cropHeight": { + "timeout": { "type": "number | undefined", "required": false - }, - "width": { - "type": "number | undefined", + } + } + }, + "theme": { + "name": "theme", + "description": "Theme Command Types - Base types for theme operations", + "params": { + "timestamp": { + "type": "string | undefined", "required": false }, - "height": { - "type": "number | undefined", + "userId": { + "type": "string | undefined", "required": false }, - "scale": { + "timeout": { "type": "number | undefined", "required": false + } + } + }, + "training/import": { + "name": "training/import", + "description": "Training Import Command - Shared Types", + "params": { + "jsonlPath": { + "type": "string", + "required": true }, - "quality": { - "type": "number | undefined", + "outputPath": { + "type": "string | undefined", "required": false }, - "maxFileSize": { + "datasetName": { + "type": "string", + "required": true + }, + "targetSkill": { + "type": "string", + "required": true + }, + "batchSize": { "type": "number | undefined", "required": false }, - "format": { - "type": "ScreenshotFormat | undefined", + "maxExamples": { + "type": "number | undefined", "required": false }, - "destination": { - "type": "ScreenshotDestination | undefined", + "createIndices": { + "type": "boolean | undefined", "required": false }, - "resultType": { - "type": "ResultType", - "required": true - }, - "dataUrl": { + "userId": { "type": "string | undefined", "required": false }, - "metadata": { - "type": "ScreenshotMetadata | undefined", - "required": false - }, "timeout": { "type": "number | undefined", "required": false } } }, - "scroll": { - "name": "scroll", - "description": "scroll command", + "user/create": { + "name": "user/create", + "description": "User Create Command - Shared Types", "params": { - "x": { - "type": "number | undefined", - "required": false + "type": { + "type": "UserType", + "required": true }, - "y": { - "type": "number | undefined", - "required": false + "displayName": { + "type": "string", + "required": true }, - "selector": { - "type": "string | undefined", - "required": false + "uniqueId": { + "type": "string", + "required": true }, - "behavior": { - "type": "\"smooth\" | \"instant\" | \"auto\" | undefined", + "addToRooms": { + "type": "readonly string[] | undefined", "required": false }, - "timeout": { - "type": "number | undefined", - "required": false - } - } - }, - "security/setup": { - "name": "security/setup", - "description": "security/setup command", - "params": { - "statusOnly": { - "type": "boolean | undefined", + "provider": { + "type": "string | undefined", "required": false }, - "component": { - "type": "\"all\" | \"monitor\" | \"proxy\" | undefined", + "modelConfig": { + "type": "ModelConfig | undefined", "required": false }, - "timeout": { - "type": "number | undefined", + "capabilities": { + "type": "UserCapabilities | undefined", + "required": false + }, + "status": { + "type": "\"online\" | \"away\" | \"offline\" | undefined", "required": false - } - } - }, - "session/create": { - "name": "session/create", - "description": "Session Create Command Types - Shared", - "params": { - "category": { - "type": "SessionCategory", - "required": true }, - "displayName": { - "type": "string", - "required": true + "intelligenceLevel": { + "type": "number | undefined", + "required": false }, "userId": { "type": "string | undefined", "required": false }, - "isShared": { - "type": "boolean | undefined", - "required": false - }, - "connectionContext": { - "type": "{ [key: string]: unknown; uniqueId?: string | undefined; } | undefined", + "timeout": { + "type": "number | undefined", "required": false } } }, - "session/destroy": { - "name": "session/destroy", - "description": "Session Destroy Command Types", + "user/get-me": { + "name": "user/get-me", + "description": "User Get Me Command - Get current user info", "params": { - "reason": { + "userId": { "type": "string | undefined", "required": false }, @@ -2855,86 +5917,102 @@ } } }, - "state/create": { - "name": "state/create", - "description": "State Create Command - Shared Types", + "utilities/docs/list": { + "name": "utilities/docs/list", + "description": "DocsList β€” Type-safe command executor", "params": { - "collection": { - "type": "string", - "required": true - }, - "data": { - "type": "Partial", - "required": true + "dir": { + "type": "string | undefined", + "required": false }, - "id": { + "pattern": { "type": "string | undefined", "required": false }, + "includeReadmes": { + "type": "boolean | undefined", + "required": false + }, "userId": { "type": "string | undefined", "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false } } }, - "state/get": { - "name": "state/get", - "description": "State Get Command - Shared Types", + "utilities/docs/read": { + "name": "utilities/docs/read", + "description": "DocsRead β€” Type-safe command executor", "params": { - "collection": { + "doc": { "type": "string", "required": true }, - "limit": { - "type": "number | undefined", + "toc": { + "type": "boolean | undefined", "required": false }, - "filter": { - "type": "Record | undefined", + "section": { + "type": "string | undefined", "required": false }, - "orderBy": { - "type": "{ field: string; direction: \"asc\" | \"desc\"; }[] | undefined", + "startLine": { + "type": "number | undefined", + "required": false + }, + "endLine": { + "type": "number | undefined", "required": false }, "userId": { "type": "string | undefined", "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false } } }, - "state/update": { - "name": "state/update", - "description": "State Update Types - User-aware entity updates", + "utilities/docs/search": { + "name": "utilities/docs/search", + "description": "DocsSearch β€” Type-safe command executor", "params": { - "collection": { + "pattern": { "type": "string", "required": true }, - "id": { - "type": "string", - "required": true + "caseSensitive": { + "type": "boolean | undefined", + "required": false }, - "data": { - "type": "Partial", - "required": true + "maxMatches": { + "type": "number | undefined", + "required": false }, "userId": { "type": "string | undefined", "required": false + }, + "timeout": { + "type": "number | undefined", + "required": false } } }, - "system/daemons": { - "name": "system/daemons", - "description": "Daemons Command Types", + "utilities/hello": { + "name": "utilities/hello", + "description": "Hello Command - Shared Types", "params": { - "nameFilter": { - "type": "string | undefined", + "_noParams": { + "type": "undefined", "required": false }, - "statusOnly": { - "type": "boolean | undefined", + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -2943,28 +6021,36 @@ } } }, - "task/complete": { - "name": "task/complete", - "description": "TaskCompleteTypes - Mark tasks as completed (or failed)", + "utilities/lease/request": { + "name": "utilities/lease/request", + "description": "Lease Request Command - Shared Types", "params": { - "taskId": { + "filePath": { "type": "string", "required": true }, - "success": { - "type": "boolean", + "requesterId": { + "type": "string", "required": true }, - "output": { - "type": "unknown", - "required": false + "requesterName": { + "type": "string", + "required": true }, - "error": { - "type": "string | undefined", + "requesterType": { + "type": "\"persona\" | \"human\"", + "required": true + }, + "intent": { + "type": "string", + "required": true + }, + "durationSeconds": { + "type": "number | undefined", "required": false }, - "metrics": { - "type": "{ tokensUsed?: number | undefined; latencyMs?: number | undefined; confidence?: number | undefined; } | undefined", + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -2973,48 +6059,54 @@ } } }, - "task/create": { - "name": "task/create", - "description": "TaskCreateTypes - Create new tasks for PersonaUsers", + "utilities/pipe/chain": { + "name": "utilities/pipe/chain", + "description": "Pipe Chain Command Types", "params": { - "assigneeId": { + "commands": { "type": "string", "required": true }, - "domain": { - "type": "TaskDomain", - "required": true - }, - "taskType": { - "type": "TaskType", - "required": true + "errorHandling": { + "type": "\"stop\" | \"continue\" | \"collect\" | undefined", + "required": false }, - "contextId": { - "type": "string", - "required": true + "timeout": { + "type": "number | undefined", + "required": false }, - "description": { - "type": "string", - "required": true + "showIntermediate": { + "type": "boolean | undefined", + "required": false }, - "priority": { - "type": "number | undefined", + "format": { + "type": "\"text\" | \"json\" | \"auto\" | undefined", "required": false }, - "dueDate": { + "userId": { + "type": "string | undefined", + "required": false + } + } + }, + "voice/start": { + "name": "voice/start", + "description": "Voice Start Command - Shared Types", + "params": { + "room": { "type": "string | undefined", "required": false }, - "estimatedDuration": { - "type": "number | undefined", + "model": { + "type": "string | undefined", "required": false }, - "dependsOn": { - "type": "string[] | undefined", + "voice": { + "type": "string | undefined", "required": false }, - "metadata": { - "type": "Record | undefined", + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -3023,44 +6115,58 @@ } } }, - "task/list": { - "name": "task/list", - "description": "TaskListTypes - List tasks for PersonaUsers", + "voice/stop": { + "name": "voice/stop", + "description": "Voice Stop Command - Shared Types", "params": { - "assigneeId": { + "handle": { "type": "string | undefined", "required": false }, - "status": { - "type": "TaskStatus | TaskStatus[] | undefined", + "userId": { + "type": "string | undefined", "required": false }, - "domain": { - "type": "TaskDomain | undefined", + "timeout": { + "type": "number | undefined", "required": false + } + } + }, + "voice/synthesize": { + "name": "voice/synthesize", + "description": "Voice Synthesize Command - Shared Types", + "params": { + "text": { + "type": "string", + "required": true }, - "taskType": { - "type": "TaskType | undefined", + "voice": { + "type": "string | undefined", "required": false }, - "contextId": { + "adapter": { "type": "string | undefined", "required": false }, - "createdBy": { - "type": "string | undefined", + "speed": { + "type": "number | undefined", "required": false }, - "limit": { + "sampleRate": { "type": "number | undefined", "required": false }, - "sortBy": { - "type": "\"priority\" | \"created\" | \"dueDate\" | \"status\" | undefined", + "format": { + "type": "string | undefined", "required": false }, - "sortOrder": { - "type": "\"asc\" | \"desc\" | undefined", + "stream": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -3069,112 +6175,94 @@ } } }, - "test/routing-chaos": { - "name": "test/routing-chaos", - "description": "Routing Chaos Test Types - Complex Multi-Hop Routing Validation", + "voice/transcribe": { + "name": "voice/transcribe", + "description": "Voice Transcribe Command - Shared Types", "params": { - "testId": { + "audio": { "type": "string", "required": true }, - "hopCount": { - "type": "number", - "required": true - }, - "maxHops": { - "type": "number", - "required": true - }, - "routingPath": { - "type": "string[]", - "required": true - }, - "currentEnvironment": { - "type": "\"server\" | \"browser\"", - "required": true - }, - "targetEnvironment": { - "type": "\"server\" | \"browser\" | undefined", + "format": { + "type": "string | undefined", "required": false }, - "failureRate": { - "type": "number", - "required": true - }, - "delayRange": { - "type": "[number, number]", - "required": true - }, - "payloadSize": { - "type": "\"medium\" | \"small\" | \"large\"", - "required": true - }, - "testStartTime": { - "type": "string", - "required": true + "language": { + "type": "string | undefined", + "required": false }, - "correlationTrace": { - "type": "string[]", - "required": true - } - } - }, - "test/run/suite": { - "name": "test/run/suite", - "description": "Test Run Suite Command Types", - "params": { - "profile": { + "model": { "type": "string | undefined", "required": false }, - "tests": { + "userId": { "type": "string | undefined", "required": false }, "timeout": { "type": "number | undefined", "required": false + } + } + }, + "workspace/git/commit": { + "name": "workspace/git/commit", + "description": "Git Commit Command - Shared Types", + "params": { + "message": { + "type": "string", + "required": true }, - "parallel": { - "type": "boolean | undefined", + "workspacePath": { + "type": "string | undefined", "required": false }, - "parallelism": { - "type": "number | undefined", + "files": { + "type": "string[] | undefined", "required": false }, - "format": { - "type": "\"text\" | \"json\" | \"summary\" | \"detailed\" | undefined", + "userId": { + "type": "string | undefined", "required": false }, - "verbose": { - "type": "boolean | undefined", + "timeout": { + "type": "number | undefined", + "required": false + } + } + }, + "workspace/git/push": { + "name": "workspace/git/push", + "description": "Git Push Command - Shared Types", + "params": { + "workspacePath": { + "type": "string | undefined", "required": false }, - "failFast": { - "type": "boolean | undefined", + "remote": { + "type": "string | undefined", "required": false }, - "save": { - "type": "boolean | undefined", + "userId": { + "type": "string | undefined", "required": false }, - "name": { - "type": "string | undefined", + "timeout": { + "type": "number | undefined", "required": false } } }, - "test": { - "name": "test", - "description": "Test Command Types - Shared type definitions", + "workspace/git/status": { + "name": "workspace/git/status", + "description": "Git Status Command - Shared Types", "params": { - "file": { + "workspacePath": { "type": "string | undefined", "required": false }, - "_": { - "type": "string[] | undefined", + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -3183,134 +6271,110 @@ } } }, - "theme/get": { - "name": "theme/get", - "description": "ThemeGet Types - Theme getting command types", + "workspace/git/workspace/clean": { + "name": "workspace/git/workspace/clean", + "description": "Git Workspace Clean Command - Shared Types", "params": { - "timestamp": { + "workspacePath": { "type": "string | undefined", "required": false - } - } - }, - "theme/list": { - "name": "theme/list", - "description": "ThemeList Types - Theme listing command types", - "params": { - "category": { - "type": "string | undefined", + }, + "force": { + "type": "boolean | undefined", "required": false }, - "includeManifests": { + "deleteBranch": { "type": "boolean | undefined", "required": false }, - "timestamp": { + "userId": { "type": "string | undefined", "required": false - } - } - }, - "theme/set": { - "name": "theme/set", - "description": "ThemeSet Types - Theme setting command types", - "params": { - "themeName": { - "type": "string", - "required": true }, - "timestamp": { - "type": "string | undefined", + "timeout": { + "type": "number | undefined", "required": false } } }, - "theme": { - "name": "theme", - "description": "Theme Command Types - Base types for theme operations", + "workspace/git/workspace/init": { + "name": "workspace/git/workspace/init", + "description": "Git Workspace Init Command - Shared Types", "params": { - "timestamp": { + "branch": { "type": "string | undefined", "required": false - } - } - }, - "training/import": { - "name": "training/import", - "description": "Training Import Command - Shared Types", - "params": { - "jsonlPath": { - "type": "string", - "required": true }, - "outputPath": { + "personaId": { "type": "string | undefined", "required": false }, - "datasetName": { - "type": "string", - "required": true - }, - "targetSkill": { - "type": "string", + "paths": { + "type": "string[]", "required": true }, - "batchSize": { - "type": "number | undefined", + "userId": { + "type": "string | undefined", "required": false }, - "maxExamples": { + "timeout": { "type": "number | undefined", "required": false - }, - "createIndices": { - "type": "boolean | undefined", - "required": false } } }, - "tree": { - "name": "tree", - "description": "Tree Command Types", + "workspace/recipe/load": { + "name": "workspace/recipe/load", + "description": "Recipe Load Command Types", "params": { - "filter": { + "recipeId": { "type": "string | undefined", "required": false }, - "showDescriptions": { + "loadAll": { "type": "boolean | undefined", - "required": false, - "description": [ - { - "text": "false", - "kind": "text" - } - ] + "required": false }, - "maxDepth": { + "reload": { + "type": "boolean | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { "type": "number | undefined", "required": false } } }, - "type": { - "name": "type", - "description": "type command", + "workspace/task/complete": { + "name": "workspace/task/complete", + "description": "TaskCompleteTypes - Mark tasks as completed (or failed)", "params": { - "selector": { + "taskId": { "type": "string", "required": true }, - "text": { - "type": "string", + "success": { + "type": "boolean", "required": true }, - "clearFirst": { - "type": "boolean | undefined", + "output": { + "type": "unknown", "required": false }, - "delay": { - "type": "number | undefined", + "error": { + "type": "string | undefined", + "required": false + }, + "metrics": { + "type": "{ tokensUsed?: number | undefined; latencyMs?: number | undefined; confidence?: number | undefined; } | undefined", + "required": false + }, + "userId": { + "type": "string | undefined", "required": false }, "timeout": { @@ -3319,140 +6383,142 @@ } } }, - "user/create": { - "name": "user/create", - "description": "User Create Command - Shared Types", + "workspace/task/create": { + "name": "workspace/task/create", + "description": "TaskCreateTypes - Create new tasks for PersonaUsers", "params": { - "type": { - "type": "UserType", + "assigneeId": { + "type": "string", "required": true }, - "displayName": { + "domain": { + "type": "TaskDomain", + "required": true + }, + "taskType": { + "type": "TaskType", + "required": true + }, + "contextId": { "type": "string", "required": true }, - "uniqueId": { + "description": { "type": "string", "required": true }, - "addToRooms": { - "type": "readonly string[] | undefined", + "priority": { + "type": "number | undefined", "required": false }, - "provider": { + "dueDate": { "type": "string | undefined", "required": false }, - "modelConfig": { - "type": "ModelConfig | undefined", + "estimatedDuration": { + "type": "number | undefined", "required": false }, - "capabilities": { - "type": "UserCapabilities | undefined", + "dependsOn": { + "type": "string[] | undefined", "required": false }, - "status": { - "type": "\"online\" | \"away\" | \"offline\" | undefined", + "metadata": { + "type": "Record | undefined", "required": false }, - "intelligenceLevel": { + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { "type": "number | undefined", "required": false } } }, - "vote/propose": { - "name": "vote/propose", - "description": "Vote Propose Command Types", + "workspace/task/list": { + "name": "workspace/task/list", + "description": "TaskListTypes - List tasks for PersonaUsers", "params": { - "filepath": { - "type": "string", - "required": true + "assigneeId": { + "type": "string | undefined", + "required": false }, - "operation": { - "type": "FileOperation", - "required": true + "status": { + "type": "TaskStatus | TaskStatus[] | undefined", + "required": false }, - "content": { - "type": "string | undefined", + "domain": { + "type": "TaskDomain | undefined", "required": false }, - "reason": { - "type": "string | undefined", + "taskType": { + "type": "TaskType | undefined", "required": false }, - "votingWindowSeconds": { - "type": "number | undefined", + "contextId": { + "type": "string | undefined", "required": false }, - "threshold": { - "type": "number | undefined", + "createdBy": { + "type": "string | undefined", "required": false }, - "quorum": { + "limit": { "type": "number | undefined", "required": false }, - "timeout": { - "type": "number | undefined", + "sortBy": { + "type": "\"priority\" | \"created\" | \"dueDate\" | \"status\" | undefined", "required": false - } - } - }, - "wait-for-element": { - "name": "wait-for-element", - "description": "wait-for-element command", - "params": { - "selector": { - "type": "string", - "required": true }, - "timeout": { - "type": "number | undefined", + "sortOrder": { + "type": "\"asc\" | \"desc\" | undefined", "required": false }, - "visible": { + "includeCompleted": { "type": "boolean | undefined", "required": false }, - "interval": { + "userId": { + "type": "string | undefined", + "required": false + }, + "timeout": { "type": "number | undefined", "required": false } } }, - "web/fetch": { - "name": "web/fetch", - "description": "Web Fetch Command Types", + "workspace/tree": { + "name": "workspace/tree", + "description": "Tree Command Types", "params": { - "url": { - "type": "string", - "required": true - }, - "format": { - "type": "\"text\" | \"markdown\" | \"html\" | undefined", + "filter": { + "type": "string | undefined", "required": false }, - "maxLength": { + "showDescriptions": { + "type": "boolean | undefined", + "required": false, + "description": [ + { + "text": "false", + "kind": "text" + } + ] + }, + "maxDepth": { "type": "number | undefined", "required": false - } - } - }, - "web/search": { - "name": "web/search", - "description": "Web Search Command Types", - "params": { - "query": { - "type": "string", - "required": true }, - "maxResults": { - "type": "number | undefined", + "userId": { + "type": "string | undefined", "required": false }, - "domains": { - "type": "string[] | undefined", + "timeout": { + "type": "number | undefined", "required": false } } diff --git a/src/debug/jtag/package-lock.json b/src/debug/jtag/package-lock.json index 187549edb..af78b60d5 100644 --- a/src/debug/jtag/package-lock.json +++ b/src/debug/jtag/package-lock.json @@ -1,12 +1,12 @@ { "name": "@continuum/jtag", - "version": "1.0.7478", + "version": "1.0.7512", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@continuum/jtag", - "version": "1.0.7478", + "version": "1.0.7512", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/src/debug/jtag/package.json b/src/debug/jtag/package.json index d6265bea7..742980ebb 100644 --- a/src/debug/jtag/package.json +++ b/src/debug/jtag/package.json @@ -1,6 +1,6 @@ { "name": "@continuum/jtag", - "version": "1.0.7478", + "version": "1.0.7512", "description": "Global CLI debugging system for any Node.js project. Install once globally, use anywhere: npm install -g @continuum/jtag", "config": { "active_example": "widget-ui", diff --git a/src/debug/jtag/scripts/seed-continuum.ts b/src/debug/jtag/scripts/seed-continuum.ts index f09951c9e..494cbc03c 100644 --- a/src/debug/jtag/scripts/seed-continuum.ts +++ b/src/debug/jtag/scripts/seed-continuum.ts @@ -852,7 +852,35 @@ async function seedViaJTAG() { ); // NO hardcoded members - let RoomMembershipDaemon handle it - const rooms = [generalRoom, academyRoom, pantheonRoom, devUpdatesRoom, helpRoom, settingsRoom, themeRoom, canvasRoom]; + const outreachRoom = createRoom( + ROOM_IDS.OUTREACH, + 'outreach', + 'Outreach', + 'Social media strategy, community building, and external engagement', + "Discuss what to post, share interesting finds, coordinate outreach on Moltbook and other platforms", + 0, // Will be auto-populated by RoomMembershipDaemon + ["social", "outreach", "community", "moltbook"], + humanUser.id, + 'outreach', // uniqueId + 'outreach' // recipeId - outreach-specific recipe with social tool directives + ); + // NO hardcoded members - let RoomMembershipDaemon handle it + + const newsroomRoom = createRoom( + ROOM_IDS.NEWSROOM, + 'newsroom', + 'Newsroom', + 'Current events, breaking news, and world awareness for all personas', + "Share and discuss current events to keep the community informed", + 0, // Will be auto-populated by RoomMembershipDaemon + ["news", "current-events", "awareness"], + humanUser.id, + 'newsroom', // uniqueId + 'newsroom' // recipeId - newsroom-specific recipe + ); + // NO hardcoded members - let RoomMembershipDaemon handle it + + const rooms = [generalRoom, academyRoom, pantheonRoom, devUpdatesRoom, helpRoom, settingsRoom, themeRoom, canvasRoom, outreachRoom, newsroomRoom]; // Persist rooms to database BEFORE creating other users await seedRecords(RoomEntity.collection, rooms, (room) => room.displayName, (room) => room.ownerId); @@ -930,9 +958,55 @@ async function seedViaJTAG() { const codeReviewPersona = userMap[PERSONA_UNIQUE_IDS.CODE_REVIEW]; const qwen3OmniPersona = userMap[PERSONA_UNIQUE_IDS.QWEN3_OMNI]; - // If rooms already existed, ensure system rooms have Helper AI then exit + // If rooms already existed, check for missing rooms and ensure system rooms have Helper AI if (!needsRooms) { - // Still ensure system rooms have their default AI assistant + // Check for and create any MISSING rooms (new rooms added to codebase) + console.log('πŸ” Checking for missing rooms...'); + const allExpectedRooms: { uniqueId: string; name: string; displayName: string; description: string; topic: string; tags: string[]; recipeId: string }[] = [ + { uniqueId: 'general', name: 'general', displayName: 'General', description: 'Main discussion room for all users', topic: 'General chat and collaboration', tags: ['general', 'welcome', 'discussion'], recipeId: 'general-chat' }, + { uniqueId: 'academy', name: 'academy', displayName: 'Academy', description: 'Learning and educational discussions', topic: 'Share knowledge, tutorials, and collaborate on learning', tags: ['academy', 'learning', 'education'], recipeId: 'academy' }, + { uniqueId: 'pantheon', name: 'pantheon', displayName: 'Pantheon', description: 'Elite discussion room for top-tier SOTA AI models', topic: 'Advanced reasoning and multi-model collaboration', tags: ['sota', 'elite', 'reasoning'], recipeId: 'pantheon' }, + { uniqueId: 'dev-updates', name: 'dev-updates', displayName: 'Dev Updates', description: 'GitHub PRs, CI/CD, and development activity notifications', topic: 'Real-time development feed', tags: ['github', 'ci', 'development'], recipeId: 'dev-updates' }, + { uniqueId: 'help', name: 'help', displayName: 'Help', description: 'Get help from AI assistants', topic: 'Your AI helpers are here to assist you', tags: ['help', 'support', 'system'], recipeId: 'help' }, + { uniqueId: 'settings', name: 'settings', displayName: 'Settings', description: 'Configure your Continuum experience', topic: 'System settings and configuration', tags: ['settings', 'config', 'system'], recipeId: 'settings' }, + { uniqueId: 'theme', name: 'theme', displayName: 'Theme', description: 'Design and customize your visual experience', topic: 'Themes, colors, and customization', tags: ['theme', 'design', 'system'], recipeId: 'theme' }, + { uniqueId: 'canvas', name: 'canvas', displayName: 'Canvas', description: 'Collaborative drawing discussions', topic: 'Art, drawing, and creative collaboration', tags: ['canvas', 'art', 'system'], recipeId: 'canvas' }, + { uniqueId: 'outreach', name: 'outreach', displayName: 'Outreach', description: 'Social media strategy, community building, and external engagement', topic: 'Discuss what to post, share interesting finds, coordinate outreach', tags: ['social', 'outreach', 'community', 'moltbook'], recipeId: 'outreach' }, + { uniqueId: 'newsroom', name: 'newsroom', displayName: 'Newsroom', description: 'Current events, breaking news, and world awareness', topic: 'Share and discuss current events', tags: ['news', 'current-events', 'awareness'], recipeId: 'newsroom' }, + ]; + + // Fetch all existing rooms + const { stdout: allRoomsOutput } = await execAsync(`./jtag data/list --collection=rooms`); + const allRoomsResult = JSON.parse(allRoomsOutput); + const existingUniqueIds = new Set( + (allRoomsResult.items || []).map((r: any) => r.uniqueId) + ); + + let missingRoomsCreated = 0; + for (const roomDef of allExpectedRooms) { + if (!existingUniqueIds.has(roomDef.uniqueId)) { + console.log(`πŸ—οΈ Creating missing room: ${roomDef.displayName}`); + const newRoom = createRoom( + stringToUUID(roomDef.displayName), + roomDef.name, + roomDef.displayName, + roomDef.description, + roomDef.topic, + 0, + roomDef.tags, + humanUser.id, + roomDef.uniqueId, + roomDef.recipeId + ); + await createRecord(RoomEntity.collection, newRoom, newRoom.id, roomDef.displayName); + missingRoomsCreated++; + } + } + if (missingRoomsCreated > 0) { + console.log(`βœ… Created ${missingRoomsCreated} missing room(s)`); + } + + // Ensure system rooms have Helper AI console.log('🏠 Ensuring system rooms have Helper AI...'); const systemRoomUniqueIds = ['settings', 'help', 'theme', 'canvas']; for (const roomUniqueId of systemRoomUniqueIds) { diff --git a/src/debug/jtag/server/generated.ts b/src/debug/jtag/server/generated.ts index 4073505dc..152ea81f6 100644 --- a/src/debug/jtag/server/generated.ts +++ b/src/debug/jtag/server/generated.ts @@ -1,7 +1,7 @@ /** * Server Structure Registry - Auto-generated * - * Contains 18 daemons and 184 commands and 3 adapters. + * Contains 18 daemons and 198 commands and 3 adapters. * Generated by scripts/generate-structure.ts - DO NOT EDIT MANUALLY */ @@ -179,6 +179,20 @@ import { SessionCreateServerCommand } from './../commands/session/create/server/ import { SessionDestroyServerCommand } from './../commands/session/destroy/server/SessionDestroyServerCommand'; import { SessionGetIdServerCommand } from './../commands/session/get-id/server/SessionGetIdServerCommand'; import { SessionGetUserServerCommand } from './../commands/session/get-user/server/SessionGetUserServerCommand'; +import { SocialBrowseServerCommand } from './../commands/social/browse/server/SocialBrowseServerCommand'; +import { SocialClassifyServerCommand } from './../commands/social/classify/server/SocialClassifyServerCommand'; +import { SocialCommentServerCommand } from './../commands/social/comment/server/SocialCommentServerCommand'; +import { SocialCommunityServerCommand } from './../commands/social/community/server/SocialCommunityServerCommand'; +import { SocialDownvoteServerCommand } from './../commands/social/downvote/server/SocialDownvoteServerCommand'; +import { SocialEngageServerCommand } from './../commands/social/engage/server/SocialEngageServerCommand'; +import { SocialFeedServerCommand } from './../commands/social/feed/server/SocialFeedServerCommand'; +import { SocialNotificationsServerCommand } from './../commands/social/notifications/server/SocialNotificationsServerCommand'; +import { SocialPostServerCommand } from './../commands/social/post/server/SocialPostServerCommand'; +import { SocialProfileServerCommand } from './../commands/social/profile/server/SocialProfileServerCommand'; +import { SocialProposeServerCommand } from './../commands/social/propose/server/SocialProposeServerCommand'; +import { SocialSearchServerCommand } from './../commands/social/search/server/SocialSearchServerCommand'; +import { SocialSignupServerCommand } from './../commands/social/signup/server/SocialSignupServerCommand'; +import { SocialTrendingServerCommand } from './../commands/social/trending/server/SocialTrendingServerCommand'; import { StateContentCloseServerCommand } from './../commands/state/content/close/server/StateContentCloseServerCommand'; import { StateContentSwitchServerCommand } from './../commands/state/content/switch/server/StateContentSwitchServerCommand'; import { StateCreateServerCommand } from './../commands/state/create/server/StateCreateServerCommand'; @@ -1083,6 +1097,76 @@ export const SERVER_COMMANDS: CommandEntry[] = [ className: 'SessionGetUserServerCommand', commandClass: SessionGetUserServerCommand }, +{ + name: 'social/browse', + className: 'SocialBrowseServerCommand', + commandClass: SocialBrowseServerCommand + }, +{ + name: 'social/classify', + className: 'SocialClassifyServerCommand', + commandClass: SocialClassifyServerCommand + }, +{ + name: 'social/comment', + className: 'SocialCommentServerCommand', + commandClass: SocialCommentServerCommand + }, +{ + name: 'social/community', + className: 'SocialCommunityServerCommand', + commandClass: SocialCommunityServerCommand + }, +{ + name: 'social/downvote', + className: 'SocialDownvoteServerCommand', + commandClass: SocialDownvoteServerCommand + }, +{ + name: 'social/engage', + className: 'SocialEngageServerCommand', + commandClass: SocialEngageServerCommand + }, +{ + name: 'social/feed', + className: 'SocialFeedServerCommand', + commandClass: SocialFeedServerCommand + }, +{ + name: 'social/notifications', + className: 'SocialNotificationsServerCommand', + commandClass: SocialNotificationsServerCommand + }, +{ + name: 'social/post', + className: 'SocialPostServerCommand', + commandClass: SocialPostServerCommand + }, +{ + name: 'social/profile', + className: 'SocialProfileServerCommand', + commandClass: SocialProfileServerCommand + }, +{ + name: 'social/propose', + className: 'SocialProposeServerCommand', + commandClass: SocialProposeServerCommand + }, +{ + name: 'social/search', + className: 'SocialSearchServerCommand', + commandClass: SocialSearchServerCommand + }, +{ + name: 'social/signup', + className: 'SocialSignupServerCommand', + commandClass: SocialSignupServerCommand + }, +{ + name: 'social/trending', + className: 'SocialTrendingServerCommand', + commandClass: SocialTrendingServerCommand + }, { name: 'state/content/close', className: 'StateContentCloseServerCommand', diff --git a/src/debug/jtag/shared/generated-command-constants.ts b/src/debug/jtag/shared/generated-command-constants.ts index 47a91331e..461e8f0c3 100644 --- a/src/debug/jtag/shared/generated-command-constants.ts +++ b/src/debug/jtag/shared/generated-command-constants.ts @@ -179,6 +179,20 @@ export const COMMANDS = { SESSION_DESTROY: 'session/destroy', SESSION_GET_ID: 'session/get-id', SESSION_GET_USER: 'session/get-user', + SOCIAL_BROWSE: 'social/browse', + SOCIAL_CLASSIFY: 'social/classify', + SOCIAL_COMMENT: 'social/comment', + SOCIAL_COMMUNITY: 'social/community', + SOCIAL_DOWNVOTE: 'social/downvote', + SOCIAL_ENGAGE: 'social/engage', + SOCIAL_FEED: 'social/feed', + SOCIAL_NOTIFICATIONS: 'social/notifications', + SOCIAL_POST: 'social/post', + SOCIAL_PROFILE: 'social/profile', + SOCIAL_PROPOSE: 'social/propose', + SOCIAL_SEARCH: 'social/search', + SOCIAL_SIGNUP: 'social/signup', + SOCIAL_TRENDING: 'social/trending', STATE_CONTENT_CLOSE: 'state/content/close', STATE_CONTENT_SWITCH: 'state/content/switch', STATE_CREATE: 'state/create', diff --git a/src/debug/jtag/shared/version.ts b/src/debug/jtag/shared/version.ts index 2693d979c..5aa0e4490 100644 --- a/src/debug/jtag/shared/version.ts +++ b/src/debug/jtag/shared/version.ts @@ -3,5 +3,5 @@ * DO NOT EDIT MANUALLY */ -export const VERSION = '1.0.7478'; +export const VERSION = '1.0.7512'; export const PACKAGE_NAME = '@continuum/jtag'; diff --git a/src/debug/jtag/system/core/shared/Handles.ts b/src/debug/jtag/system/core/shared/Handles.ts new file mode 100644 index 000000000..aada9577d --- /dev/null +++ b/src/debug/jtag/system/core/shared/Handles.ts @@ -0,0 +1,355 @@ +/** + * Handles β€” Universal async operation resolution service + * + * Provides create, resolve (short or long), status management, and cleanup + * for persistent async operation references. + * + * Every async operation (social feed fetch, voice synthesis, AI inference, + * coding agent tasks, proposal votes) creates a Handle. The handle persists + * in SQLite, survives restarts, and is resolvable by either full UUID or + * 6-char short form (#abc123). + * + * Usage: + * import { Handles } from '@system/core/shared/Handles'; + * + * // Create + * const handle = await Handles.create('social/feed', { sort: 'hot' }, requesterId); + * console.log(handle.shortId); // "a1b2c3" + * + * // Resolve (short or long) + * const record = await Handles.resolve('#a1b2c3'); + * const record = await Handles.resolve('550e8400-e29b-41d4-a716-446655440000'); + * + * // Update status + * await Handles.markProcessing(handle.id); + * await Handles.markComplete(handle.id, resultPayload); + * await Handles.markFailed(handle.id, 'Network timeout'); + * + * // Query + * const pending = await Handles.listByStatus('pending'); + * const mine = await Handles.listByRequester(myUserId); + */ + +import { COLLECTIONS } from '../../shared/Constants'; +import { HandleEntity } from '../../data/entities/HandleEntity'; +import { Logger } from '../logging/Logger'; +import { DataCreate } from '@commands/data/create/shared/DataCreateTypes'; +import { DataList } from '@commands/data/list/shared/DataListTypes'; +import { DataRead } from '@commands/data/read/shared/DataReadTypes'; +import { DataUpdate } from '@commands/data/update/shared/DataUpdateTypes'; +import type { UUID } from '../types/CrossPlatformUUID'; +import { isValidUUID, toShortId, isShortId, normalizeShortId } from '../types/CrossPlatformUUID'; +import type { HandleRef, HandleStatus, HandleRecord } from '../types/Handle'; +import { DEFAULT_HANDLE_TTL_MS } from '../types/Handle'; + +const log = Logger.create('Handles'); + +/** + * Convert a HandleEntity to a HandleRecord (the public-facing shape) + */ +function entityToRecord(entity: HandleEntity): HandleRecord { + return { + id: entity.id, + shortId: toShortId(entity.id), + type: entity.type, + status: entity.status, + params: entity.params, + result: entity.result, + error: entity.error, + requestedBy: entity.requestedBy, + createdAt: entity.createdAt instanceof Date ? entity.createdAt : new Date(entity.createdAt as string), + updatedAt: entity.updatedAt instanceof Date ? entity.updatedAt : new Date(entity.updatedAt as string), + expiresAt: entity.expiresAt + ? (entity.expiresAt instanceof Date ? entity.expiresAt : new Date(entity.expiresAt as string)) + : undefined, + retryCount: entity.retryCount, + }; +} + +/** + * Handles β€” Static service for universal async operation management + */ +export const Handles = { + + /** + * Create a new handle for an async operation. + * + * @param type - Operation type (e.g., 'social/feed', 'voice/synthesize') + * @param params - Original request parameters (JSON-serializable) + * @param requestedBy - UUID of the requester + * @param ttlMs - TTL in milliseconds (null = never expires, default: 5 minutes) + * @returns The created HandleRecord with shortId + */ + async create( + type: string, + params: unknown, + requestedBy: UUID, + ttlMs: number | null = DEFAULT_HANDLE_TTL_MS, + ): Promise { + const entity = HandleEntity.createHandle(type, params, requestedBy, ttlMs); + + const result = await DataCreate.execute({ + collection: COLLECTIONS.HANDLES, + data: entity as unknown as Record, + }); + + if (!result.success) { + throw new Error(`Failed to create handle: ${result.error ?? 'Unknown error'}`); + } + + log.info(`Created handle #${toShortId(entity.id)} type=${type} requestedBy=${toShortId(requestedBy)}`); + return entityToRecord(entity); + }, + + /** + * Resolve a handle by short ID (#abc123 / abc123) or full UUID. + * + * Short ID resolution: queries handles collection for entities + * whose UUID ends with the 6-char suffix. + * + * @param ref - HandleRef: "#abc123", "abc123", or full UUID + * @returns HandleRecord or null if not found + */ + async resolve(ref: HandleRef): Promise { + if (!ref) return null; + + const refStr = String(ref).trim(); + + // Full UUID β€” direct lookup + if (isValidUUID(refStr)) { + const result = await DataRead.execute({ + collection: COLLECTIONS.HANDLES, + id: refStr, + }); + + if (!result.found || !result.data) return null; + return entityToRecord(result.data); + } + + // Short ID β€” suffix match (use String() to avoid type guard narrowing) + const normalized = String(refStr).replace(/^#/, ''); + if (!isShortId(normalized)) { + log.warn(`Invalid handle reference: ${ref}`); + return null; + } + + const shortId = normalizeShortId(normalized); + + // Query all handles and filter by suffix + // The $regex operator matches UUIDs ending with the short ID + // Order by createdAt desc so the most recent match wins on collision + const result = await DataList.execute({ + collection: COLLECTIONS.HANDLES, + filter: { id: { $regex: `${shortId}$` } }, + orderBy: [{ field: 'createdAt', direction: 'desc' }], + limit: 2, // Get 2 to detect ambiguity + }); + + if (!result.success || !result.items?.length) return null; + + if (result.items.length > 1) { + log.warn(`Ambiguous short ID #${shortId} matched ${result.items.length} handles. Returning most recent.`); + } + + return entityToRecord(result.items[0] as HandleEntity); + }, + + /** + * Get a handle by exact UUID (faster than resolve for known UUIDs). + */ + async get(id: UUID): Promise { + const result = await DataRead.execute({ + collection: COLLECTIONS.HANDLES, + id, + }); + + if (!result.found || !result.data) return null; + return entityToRecord(result.data); + }, + + /** + * Mark a handle as processing (worker picked it up). + */ + async markProcessing(id: UUID): Promise { + return this._updateStatus(id, 'processing'); + }, + + /** + * Mark a handle as complete with a result payload. + */ + async markComplete(id: UUID, result: unknown): Promise { + return this._updateStatus(id, 'complete', { result }); + }, + + /** + * Mark a handle as failed with an error message. + */ + async markFailed(id: UUID, error: string): Promise { + return this._updateStatus(id, 'failed', { error }); + }, + + /** + * Mark a handle as expired. + */ + async markExpired(id: UUID): Promise { + return this._updateStatus(id, 'expired'); + }, + + /** + * Mark a handle as cancelled. + */ + async markCancelled(id: UUID): Promise { + return this._updateStatus(id, 'cancelled'); + }, + + /** + * List handles by status. + */ + async listByStatus(status: HandleStatus, limit = 50): Promise { + const result = await DataList.execute({ + collection: COLLECTIONS.HANDLES, + filter: { status }, + orderBy: [{ field: 'createdAt', direction: 'desc' }], + limit, + }); + + if (!result.success) return []; + return result.items.map(e => entityToRecord(e as HandleEntity)); + }, + + /** + * List handles by requester. + */ + async listByRequester(requestedBy: UUID, limit = 50): Promise { + const result = await DataList.execute({ + collection: COLLECTIONS.HANDLES, + filter: { requestedBy }, + orderBy: [{ field: 'createdAt', direction: 'desc' }], + limit, + }); + + if (!result.success) return []; + return result.items.map(e => entityToRecord(e as HandleEntity)); + }, + + /** + * List handles by operation type. + */ + async listByType(type: string, limit = 50): Promise { + const result = await DataList.execute({ + collection: COLLECTIONS.HANDLES, + filter: { type }, + orderBy: [{ field: 'createdAt', direction: 'desc' }], + limit, + }); + + if (!result.success) return []; + return result.items.map(e => entityToRecord(e as HandleEntity)); + }, + + /** + * List active (pending + processing) handles, optionally filtered by type. + */ + async listActive(type?: string, limit = 50): Promise { + const filter: Record = { + status: { $in: ['pending', 'processing'] }, + }; + if (type) filter.type = type; + + const result = await DataList.execute({ + collection: COLLECTIONS.HANDLES, + filter, + orderBy: [{ field: 'createdAt', direction: 'asc' }], + limit, + }); + + if (!result.success) return []; + return result.items.map(e => entityToRecord(e as HandleEntity)); + }, + + /** + * Expire all handles past their TTL. Call periodically (e.g., every 60s). + * Processes in batches of 200 until all stale handles are expired. + * Returns the total number of handles expired. + */ + async expireStale(): Promise { + const now = new Date().toISOString(); + let totalExpired = 0; + const BATCH_SIZE = 200; + + // Loop in batches until no more stale handles remain + while (true) { + const result = await DataList.execute({ + collection: COLLECTIONS.HANDLES, + filter: { + status: { $in: ['pending', 'processing'] }, + expiresAt: { $lte: now }, + }, + limit: BATCH_SIZE, + }); + + if (!result.success || !result.items?.length) break; + + for (const entity of result.items) { + try { + await this._updateStatus((entity as HandleEntity).id, 'expired'); + totalExpired++; + } catch (err) { + log.warn(`Failed to expire handle ${(entity as HandleEntity).id}: ${err}`); + } + } + + // If we got fewer than BATCH_SIZE, we've processed all of them + if (result.items.length < BATCH_SIZE) break; + } + + if (totalExpired > 0) { + log.info(`Expired ${totalExpired} stale handles`); + } + return totalExpired; + }, + + /** + * Internal: update handle status and optional fields. + */ + async _updateStatus( + id: UUID, + status: HandleStatus, + extra?: { result?: unknown; error?: string; params?: unknown }, + ): Promise { + const updates: Record = { + status, + updatedAt: new Date().toISOString(), + }; + + if (extra?.result !== undefined) updates.result = extra.result; + if (extra?.error !== undefined) updates.error = extra.error; + if (extra?.params !== undefined) updates.params = extra.params; + if (status === 'failed') { + // Increment retry count on failure β€” read current, increment, write back. + // Safe: Node.js is single-threaded, so no concurrent failures for the same handle. + const current = await DataRead.execute({ + collection: COLLECTIONS.HANDLES, + id, + }); + if (current.found && current.data) { + updates.retryCount = ((current.data as HandleEntity).retryCount ?? 0) + 1; + } + } + + const result = await DataUpdate.execute({ + collection: COLLECTIONS.HANDLES, + id, + data: updates, + incrementVersion: true, + }); + + if (!result.success || !result.data) { + throw new Error(`Failed to update handle ${id} to ${status}: ${result.error ?? 'not found'}`); + } + + log.debug(`Handle #${toShortId(id)} β†’ ${status}`); + return entityToRecord(result.data); + }, + +} as const; diff --git a/src/debug/jtag/system/core/types/Handle.ts b/src/debug/jtag/system/core/types/Handle.ts new file mode 100644 index 000000000..0398ebae3 --- /dev/null +++ b/src/debug/jtag/system/core/types/Handle.ts @@ -0,0 +1,108 @@ +/** + * Handle β€” Universal async operation reference + * + * A Handle is a persistent reference to any async operation in the system. + * It can be resolved by either its full UUID or its 6-char short form. + * + * Design: + * - Short or long form: both resolve to the same operation + * - Persistent: survives restarts (backed by SQLite) + * - Universal: used by social, voice, inference, coding agents, proposals, etc. + * - Status-tracked: pending β†’ processing β†’ complete | failed | expired + * + * Usage: + * const handle = await Handles.create('social/feed', { sort: 'hot' }, requesterId); + * console.log(handle.shortId); // "#a1b2c3" + * + * // Later (even after restart): + * const result = await Handles.resolve('#a1b2c3'); + * const result = await Handles.resolve('550e8400-e29b-41d4-a716-446655440000'); + */ + +import type { UUID } from './CrossPlatformUUID'; +import type { ShortId } from './CrossPlatformUUID'; + +/** + * HandleStatus lifecycle: + * pending β†’ processing β†’ complete + * β†’ failed + * β†’ expired (TTL exceeded) + * pending β†’ cancelled (caller cancelled before processing) + */ +export type HandleStatus = + | 'pending' // Created, waiting for worker to pick up + | 'processing' // Worker is actively fulfilling + | 'complete' // Result available + | 'failed' // Operation failed (error stored) + | 'expired' // TTL exceeded before completion + | 'cancelled'; // Cancelled by caller + +/** + * HandleRef β€” accepts short or long form for resolution. + * Examples: "#a1b2c3", "a1b2c3", "550e8400-e29b-41d4-a716-446655440000" + */ +export type HandleRef = UUID | ShortId | string; + +/** + * HandleRecord β€” the full persisted state of a handle. + * This is the shape stored in the database and returned by the service. + */ +export interface HandleRecord { + /** Full UUID (the canonical identifier) */ + readonly id: UUID; + + /** Short form (last 6 hex chars) for human reference */ + readonly shortId: ShortId; + + /** Operation type (e.g., 'social/feed', 'voice/synthesize', 'ai/inference') */ + readonly type: string; + + /** Current lifecycle status */ + status: HandleStatus; + + /** Original request parameters (JSON-serializable) */ + readonly params: unknown; + + /** Result payload when status=complete (JSON-serializable) */ + result?: unknown; + + /** Error message when status=failed */ + error?: string; + + /** Who requested this operation */ + readonly requestedBy: UUID; + + /** When the handle was created */ + readonly createdAt: Date; + + /** When the status last changed */ + updatedAt: Date; + + /** When this handle expires (null = never) */ + expiresAt?: Date; + + /** How many times the worker retried this operation */ + retryCount: number; +} + +/** + * HandleCreateOptions β€” parameters for creating a new handle + */ +export interface HandleCreateOptions { + /** Operation type (e.g., 'social/feed') */ + type: string; + + /** Request parameters */ + params: unknown; + + /** Who is requesting */ + requestedBy: UUID; + + /** TTL in milliseconds (null = never expires). Default: 5 minutes */ + ttlMs?: number | null; +} + +/** + * Default TTL for handles (5 minutes) + */ +export const DEFAULT_HANDLE_TTL_MS = 5 * 60 * 1000; diff --git a/src/debug/jtag/system/data/constants/RoomConstants.ts b/src/debug/jtag/system/data/constants/RoomConstants.ts index 59d1d8481..45b9aa289 100644 --- a/src/debug/jtag/system/data/constants/RoomConstants.ts +++ b/src/debug/jtag/system/data/constants/RoomConstants.ts @@ -13,7 +13,9 @@ export const ROOM_UNIQUE_IDS = { HELP: 'help', THEME: 'theme', SETTINGS: 'settings', - CANVAS: 'canvas' + CANVAS: 'canvas', + OUTREACH: 'outreach', + NEWSROOM: 'newsroom' } as const; export type RoomUniqueId = typeof ROOM_UNIQUE_IDS[keyof typeof ROOM_UNIQUE_IDS]; diff --git a/src/debug/jtag/system/data/domains/DefaultEntities.ts b/src/debug/jtag/system/data/domains/DefaultEntities.ts index 4a470cf8d..e8b0bb7c8 100644 --- a/src/debug/jtag/system/data/domains/DefaultEntities.ts +++ b/src/debug/jtag/system/data/domains/DefaultEntities.ts @@ -53,6 +53,8 @@ export const DEFAULT_ROOMS = { SETTINGS: stringToUUID('Settings') as UUID, THEME: stringToUUID('Theme') as UUID, CANVAS: stringToUUID('Canvas') as UUID, + OUTREACH: stringToUUID('Outreach') as UUID, + NEWSROOM: stringToUUID('Newsroom') as UUID, SUPPORT: stringToUUID('Support') as UUID, AI_TRAINING: stringToUUID('AI Training') as UUID } as const; diff --git a/src/debug/jtag/system/data/entities/HandleEntity.ts b/src/debug/jtag/system/data/entities/HandleEntity.ts new file mode 100644 index 000000000..fb421ad0f --- /dev/null +++ b/src/debug/jtag/system/data/entities/HandleEntity.ts @@ -0,0 +1,152 @@ +/** + * HandleEntity β€” Persistent async operation handle + * + * Every async operation in the system creates a HandleEntity. + * The entity ID IS the handle. Resolvable by full UUID or shortId. + * + * Persists to SQLite via DataDaemon β€” survives restarts. + * Workers query for pending handles on startup and resume processing. + */ + +import { BaseEntity } from './BaseEntity'; +import { COLLECTIONS } from '../../shared/Constants'; +import type { UUID } from '../../core/types/CrossPlatformUUID'; +import { isValidUUID } from '../../core/types/CrossPlatformUUID'; +import type { HandleStatus } from '../../core/types/Handle'; +import { DEFAULT_HANDLE_TTL_MS } from '../../core/types/Handle'; +import { TextField, EnumField, DateField, JsonField, NumberField } from '../decorators/FieldDecorators'; + +export class HandleEntity extends BaseEntity { + static readonly collection = COLLECTIONS.HANDLES; + + /** Operation type (e.g., 'social/feed', 'voice/synthesize', 'ai/inference') */ + @TextField({ index: true }) + type!: string; + + /** Current lifecycle status */ + @EnumField({ index: true }) + status!: HandleStatus; + + /** Original request parameters */ + @JsonField() + params!: unknown; + + /** Result payload when complete */ + @JsonField({ nullable: true }) + result?: unknown; + + /** Error message when failed */ + @TextField({ nullable: true }) + error?: string; + + /** Who requested this operation */ + @TextField({ index: true }) + requestedBy!: UUID; + + /** When this handle expires */ + @DateField({ nullable: true }) + expiresAt?: Date; + + /** Retry count */ + @NumberField() + retryCount!: number; + + constructor() { + super(); + this.status = 'pending'; + this.retryCount = 0; + } + + get collection(): string { + return HandleEntity.collection; + } + + validate(): { success: boolean; error?: string } { + if (!this.type?.trim()) { + return { success: false, error: 'Handle type is required' }; + } + if (!this.requestedBy || !isValidUUID(this.requestedBy)) { + return { success: false, error: 'Handle requestedBy must be a valid UUID' }; + } + const validStatuses: HandleStatus[] = ['pending', 'processing', 'complete', 'failed', 'expired', 'cancelled']; + if (!validStatuses.includes(this.status)) { + return { success: false, error: `Handle status must be one of: ${validStatuses.join(', ')}` }; + } + return { success: true }; + } + + /** Check if this handle has reached a terminal state */ + get isTerminal(): boolean { + return this.status === 'complete' || this.status === 'failed' || this.status === 'expired' || this.status === 'cancelled'; + } + + /** Check if this handle is still active (can be worked on) */ + get isActive(): boolean { + return this.status === 'pending' || this.status === 'processing'; + } + + /** Check if this handle has expired based on its TTL */ + get isExpired(): boolean { + if (!this.expiresAt) return false; + return new Date() > this.expiresAt; + } + + /** Mark as processing */ + markProcessing(): void { + this.status = 'processing'; + this.updatedAt = new Date(); + } + + /** Mark as complete with result */ + markComplete(result: unknown): void { + this.status = 'complete'; + this.result = result; + this.updatedAt = new Date(); + } + + /** Mark as failed with error */ + markFailed(error: string): void { + this.status = 'failed'; + this.error = error; + this.retryCount++; + this.updatedAt = new Date(); + } + + /** Mark as expired */ + markExpired(): void { + this.status = 'expired'; + this.updatedAt = new Date(); + } + + /** Mark as cancelled */ + markCancelled(): void { + this.status = 'cancelled'; + this.updatedAt = new Date(); + } + + /** Create a handle with standard defaults */ + static createHandle( + type: string, + params: unknown, + requestedBy: UUID, + ttlMs: number | null = DEFAULT_HANDLE_TTL_MS, + ): HandleEntity { + const entity = new HandleEntity(); + entity.type = type; + entity.params = params; + entity.requestedBy = requestedBy; + if (ttlMs !== null) { + entity.expiresAt = new Date(Date.now() + ttlMs); + } + return entity; + } + + static getPaginationConfig() { + return { + defaultSortField: 'createdAt', + defaultSortDirection: 'desc' as const, + defaultPageSize: 50, + cursorField: 'createdAt', + }; + } +} diff --git a/src/debug/jtag/system/rag/builders/ChatRAGBuilder.ts b/src/debug/jtag/system/rag/builders/ChatRAGBuilder.ts index b4fa8a157..e65831bce 100644 --- a/src/debug/jtag/system/rag/builders/ChatRAGBuilder.ts +++ b/src/debug/jtag/system/rag/builders/ChatRAGBuilder.ts @@ -42,7 +42,8 @@ import { SemanticMemorySource, WidgetContextSource, PersonaIdentitySource, - GlobalAwarenessSource + GlobalAwarenessSource, + SocialMediaRAGSource } from '../sources'; /** @@ -75,9 +76,10 @@ export class ChatRAGBuilder extends RAGBuilder { new GlobalAwarenessSource(), // Priority 85: Cross-context awareness (no severance!) new ConversationHistorySource(), // Priority 80: Chat messages (uses queryWithJoin!) new WidgetContextSource(), // Priority 75: UI state from Positron - new SemanticMemorySource() // Priority 60: Long-term memories + new SemanticMemorySource(), // Priority 60: Long-term memories + new SocialMediaRAGSource() // Priority 55: Social media HUD (engagement duty) ]); - this.log('πŸ”§ ChatRAGBuilder: Initialized RAGComposer with 5 sources'); + this.log('πŸ”§ ChatRAGBuilder: Initialized RAGComposer with 6 sources'); } return this.composer; } @@ -92,12 +94,14 @@ export class ChatRAGBuilder extends RAGBuilder { memories: PersonaMemory[]; widgetContext: string | null; globalAwareness: string | null; + socialAwareness: string | null; } { let identity: PersonaIdentity | null = null; let conversationHistory: LLMMessage[] = []; let memories: PersonaMemory[] = []; let widgetContext: string | null = null; let globalAwareness: string | null = null; + let socialAwareness: string | null = null; for (const section of result.sections) { if (section.identity) { @@ -117,9 +121,13 @@ export class ChatRAGBuilder extends RAGBuilder { // Extract cross-context awareness (no severance!) globalAwareness = section.systemPromptSection; } + if (section.systemPromptSection && section.sourceName === 'social-media') { + // Social media HUD β€” engagement awareness and duty + socialAwareness = section.systemPromptSection; + } } - return { identity, conversationHistory, memories, widgetContext, globalAwareness }; + return { identity, conversationHistory, memories, widgetContext, globalAwareness, socialAwareness }; } /** @@ -150,6 +158,7 @@ export class ChatRAGBuilder extends RAGBuilder { let learningConfig: { learningMode?: 'fine-tuning' | 'inference-only'; genomeId?: UUID; participantRole?: string } | undefined; let widgetContext: string | null; let globalAwareness: string | null; + let socialAwareness: string | null; if (this.useModularSources) { // NEW PATH: Use RAGComposer for modular, parallelized source loading @@ -193,6 +202,7 @@ export class ChatRAGBuilder extends RAGBuilder { privateMemories = extracted.memories; widgetContext = extracted.widgetContext; globalAwareness = extracted.globalAwareness; + socialAwareness = extracted.socialAwareness; // Still load these via legacy methods (not yet extracted to sources) const [extractedArtifacts, extractedRecipeStrategy, extractedLearningConfig] = await Promise.all([ @@ -256,6 +266,7 @@ export class ChatRAGBuilder extends RAGBuilder { learningConfig = loadedLearningConfig; widgetContext = loadedWidgetContext; globalAwareness = null; // Legacy path doesn't use GlobalAwarenessSource + socialAwareness = null; // Legacy path doesn't use SocialMediaRAGSource } // 2.3.5 Preprocess artifacts for non-vision models ("So the blind can see") @@ -279,6 +290,14 @@ export class ChatRAGBuilder extends RAGBuilder { this.log('🌐 ChatRAGBuilder: Injected cross-context awareness into system prompt'); } + // 2.4.6. Inject social media HUD into system prompt (engagement awareness) + // This gives AIs awareness of their social media presence and engagement duty + if (socialAwareness) { + finalIdentity.systemPrompt = finalIdentity.systemPrompt + + `\n\n${socialAwareness}`; + this.log('πŸ“± ChatRAGBuilder: Injected social media HUD into system prompt'); + } + // NOTE: Canvas context is now handled via the "inbox content" pattern // When strokes are added, they emit system messages to the canvas room // AIs see these in their conversation history naturally, no system prompt injection needed @@ -338,7 +357,10 @@ export class ChatRAGBuilder extends RAGBuilder { hasWidgetContext: !!widgetContext, // Cross-context awareness (no severance!) - hasGlobalAwareness: !!globalAwareness + hasGlobalAwareness: !!globalAwareness, + + // Social media HUD (engagement awareness) + hasSocialAwareness: !!socialAwareness } }; diff --git a/src/debug/jtag/system/rag/shared/RAGTypes.ts b/src/debug/jtag/system/rag/shared/RAGTypes.ts index dd21ca529..05db90da8 100644 --- a/src/debug/jtag/system/rag/shared/RAGTypes.ts +++ b/src/debug/jtag/system/rag/shared/RAGTypes.ts @@ -155,6 +155,9 @@ export interface RAGContext { // Cross-context awareness (no severance!) hasGlobalAwareness?: boolean; // Whether cross-context awareness was included in system prompt + + // Social media engagement awareness + hasSocialAwareness?: boolean; // Whether social media HUD was included in system prompt }; } diff --git a/src/debug/jtag/system/rag/sources/SocialMediaRAGSource.ts b/src/debug/jtag/system/rag/sources/SocialMediaRAGSource.ts new file mode 100644 index 000000000..6918174b1 --- /dev/null +++ b/src/debug/jtag/system/rag/sources/SocialMediaRAGSource.ts @@ -0,0 +1,486 @@ +/** + * SocialMediaRAGSource - Injects social media awareness HUD into persona RAG context + * + * Gives personas awareness of their social media presence: + * - Which platform(s) they're on + * - Karma, followers, post count + * - Unread notifications (replies, mentions, follows) + * - Engagement duty prompt (browse, comment, vote, follow) + * + * Architecture: CACHE-ONLY load() + background refresh loop. + * + * load() NEVER hits the DB or API β€” it only reads from cache. + * A background loop (serialized, one persona at a time) handles: + * - Credential resolution via the command system (DB lookups) + * - Profile + notifications via Moltbook API (HTTP calls) + * - Populating the HUD cache + * + * This design ensures: + * - Zero RAG pipeline blocking (load() returns in <1ms) + * - No thundering herd (background loop is serialized) + * - Resilience to slow/down APIs (Moltbook has 1.4M bots, often struggling) + * - Graceful degradation (no cache = no HUD, personas still function) + * + * Priority 55 - Medium. Engagement awareness is valuable but not critical. + */ + +import type { RAGSource, RAGSourceContext, RAGSection } from '../shared/RAGSource'; +import type { SocialNotification, SocialProfile } from '@system/social/shared/SocialMediaTypes'; +import type { ISocialMediaProvider } from '@system/social/shared/ISocialMediaProvider'; +import { SocialCredentialEntity } from '@system/social/shared/SocialCredentialEntity'; +import { SocialMediaProviderRegistry } from '@system/social/server/SocialMediaProviderRegistry'; +import { loadSharedCredential } from '@system/social/server/SocialCommandHelper'; +import { DataDaemon } from '@daemons/data-daemon/shared/DataDaemon'; +import { DataOpen } from '@commands/data/open/shared/DataOpenTypes'; +import { DataList } from '@commands/data/list/shared/DataListTypes'; +import { SystemPaths } from '@system/core/config/SystemPaths'; +import { UserEntity } from '@system/data/entities/UserEntity'; +import { Logger } from '@system/core/logging/Logger'; + +const log = Logger.create('SocialMediaRAGSource', 'rag'); + +/** Cache entry for the formatted HUD */ +interface HUDCacheEntry { + hud: string; + tokenCount: number; + fetchedAt: number; + metadata: Record; +} + +/** Resolved credential + provider for a persona */ +interface ResolvedCredential { + credential: SocialCredentialEntity; + provider: ISocialMediaProvider; +} + +export class SocialMediaRAGSource implements RAGSource { + readonly name = 'social-media'; + readonly priority = 55; + readonly defaultBudgetPercent = 5; + + // ── Static shared state (singleton across all instances) ──────────── + // Each persona's ChatRAGBuilder creates a new SocialMediaRAGSource instance. + // All state must be static so the caches and warmup loop are shared. + + /** HUD data cache per persona β€” the ONLY thing load() reads */ + private static readonly _hudCache = new Map(); + + /** Credential cache per persona (null = confirmed no credential) */ + private static readonly _credentialCache = new Map(); + + /** Set of persona IDs we know about (populated as load() is called) */ + private static readonly _knownPersonas = new Set(); + + /** Whether the singleton warmup loop is running */ + private static _warmupRunning = false; + + /** HUD TTL: 5 minutes β€” background loop refreshes before expiry */ + private static readonly HUD_TTL_MS = 5 * 60 * 1000; + + /** Credential TTL: 30 minutes β€” credentials change very rarely */ + private static readonly CRED_TTL_MS = 30 * 60 * 1000; + + /** API timeout per call β€” Moltbook is often struggling */ + private static readonly API_TIMEOUT_MS = 8000; + + /** Delay before first warmup β€” let the system stabilize after startup */ + private static readonly WARMUP_DELAY_MS = 15_000; + + /** Interval between warmup cycles */ + private static readonly WARMUP_INTERVAL_MS = 4 * 60 * 1000; + + isApplicable(_context: RAGSourceContext): boolean { + return true; + } + + /** + * Cache-only load. Returns instantly. + * If HUD is cached, returns it. If not, returns empty section. + * Background warmup loop handles populating the cache. + */ + async load(context: RAGSourceContext, _allocatedBudget: number): Promise { + const startTime = performance.now(); + + // Register this persona for background warmup + if (!SocialMediaRAGSource._knownPersonas.has(context.personaId)) { + SocialMediaRAGSource._knownPersonas.add(context.personaId); + SocialMediaRAGSource.startWarmupLoop(); + } + + // Cache check β€” instant + const cached = SocialMediaRAGSource._hudCache.get(context.personaId); + if (cached && (Date.now() - cached.fetchedAt) < SocialMediaRAGSource.HUD_TTL_MS) { + if (!cached.hud) { + return this.emptySection(startTime); + } + return { + sourceName: this.name, + tokenCount: cached.tokenCount, + loadTimeMs: performance.now() - startTime, + systemPromptSection: cached.hud, + metadata: { ...cached.metadata, fromCache: true }, + }; + } + + // No cache = no HUD. Background loop will populate it. + return this.emptySection(startTime); + } + + // ── Background Warmup Loop ────────────────────────────────────────── + + /** + * Start the background warmup loop (idempotent). + * Runs on a delayed start, then repeats every 4 minutes. + * Serialized: processes one persona at a time to avoid DB/API contention. + */ + private static startWarmupLoop(): void { + if (SocialMediaRAGSource._warmupRunning) return; + SocialMediaRAGSource._warmupRunning = true; + + // Delay first run to let the system stabilize after startup + setTimeout(() => { + log.info(`Social HUD warmup starting for ${SocialMediaRAGSource._knownPersonas.size} personas`); + SocialMediaRAGSource.runWarmupCycle().catch((err) => + log.error(`Warmup cycle failed: ${err.message}`) + ); + }, SocialMediaRAGSource.WARMUP_DELAY_MS); + } + + /** + * Single warmup cycle: resolve credentials + fetch HUD for all known personas. + * Serialized to avoid overwhelming the command system and Moltbook API. + */ + private static async runWarmupCycle(): Promise { + const personas = [...SocialMediaRAGSource._knownPersonas]; + let resolved = 0; + let hudLoaded = 0; + + // Resolve shared credential first (used by most/all personas) + let sharedCred: SocialCredentialEntity | undefined; + try { + sharedCred = await SocialMediaRAGSource.withTimeout( + loadSharedCredential('moltbook'), + SocialMediaRAGSource.API_TIMEOUT_MS, + 'Shared credential' + ); + if (sharedCred) { + log.info(`Shared credential resolved: @${sharedCred.agentName} (${sharedCred.claimStatus})`); + } + } catch (err: any) { + log.warn(`Failed to resolve shared credential: ${err.message}`); + } + + for (const personaId of personas) { + try { + // Skip if HUD cache is still fresh + const cached = SocialMediaRAGSource._hudCache.get(personaId); + if (cached && (Date.now() - cached.fetchedAt) < SocialMediaRAGSource.HUD_TTL_MS) { + continue; + } + + // Resolve credential (check persona DB, fall back to shared) + const credResult = await SocialMediaRAGSource.resolveCredential(personaId, sharedCred); + if (!credResult) { + // No credential β€” cache empty + SocialMediaRAGSource._hudCache.set(personaId, { + hud: '', + tokenCount: 0, + fetchedAt: Date.now(), + metadata: { empty: true }, + }); + continue; + } + resolved++; + + // Fetch profile + notifications from Moltbook API + const hud = await SocialMediaRAGSource.fetchAndFormatHUD(credResult); + if (hud) { + hudLoaded++; + } + } catch (err: any) { + log.debug(`Warmup failed for ${personaId}: ${err.message}`); + } + } + + log.info( + `Social HUD warmup cycle complete: ${resolved} credentials, ` + + `${hudLoaded} HUDs loaded, ${personas.length} total personas` + ); + + // Schedule next cycle + setTimeout(() => { + SocialMediaRAGSource.runWarmupCycle().catch((err) => + log.error(`Warmup cycle failed: ${err.message}`) + ); + }, SocialMediaRAGSource.WARMUP_INTERVAL_MS); + } + + // ── Credential Resolution (called from warmup, not from load) ────── + + /** + * Resolve credential for a persona. Called from background warmup only. + * Uses pre-resolved shared credential to avoid redundant DB opens. + */ + private static async resolveCredential( + personaId: string, + sharedCred: SocialCredentialEntity | undefined, + ): Promise { + // Check credential cache + const cached = SocialMediaRAGSource._credentialCache.get(personaId); + if (cached !== undefined) { + if (!cached) return undefined; + return cached; + } + + // Look up persona's uniqueId via DataDaemon + const userResult = await SocialMediaRAGSource.withTimeout( + DataDaemon.read(UserEntity.collection, personaId), + SocialMediaRAGSource.API_TIMEOUT_MS, + 'DataDaemon.read' + ); + if (!userResult.success || !userResult.data) { + log.debug(`No user found for persona ${personaId.slice(0, 8)} β€” caching null`); + SocialMediaRAGSource._credentialCache.set(personaId, null); + return undefined; + } + + const personaUniqueId = userResult.data.data.uniqueId; + log.debug(`Resolving credentials for ${personaUniqueId} (${personaId.slice(0, 8)})`); + + // Try each registered platform + for (const platformId of SocialMediaProviderRegistry.availablePlatforms) { + const credential = await SocialMediaRAGSource.loadPlatformCredential( + personaId, personaUniqueId, platformId, sharedCred + ); + if (credential) { + const provider = SocialMediaProviderRegistry.createProvider(platformId); + provider.authenticate(credential.apiKey); + const result: ResolvedCredential = { credential, provider }; + SocialMediaRAGSource._credentialCache.set(personaId, result); + log.info(`Credential resolved for ${personaUniqueId}: @${credential.agentName} (${credential.claimStatus})`); + return result; + } + } + + log.debug(`No credentials found for ${personaUniqueId}`); + SocialMediaRAGSource._credentialCache.set(personaId, null); + return undefined; + } + + /** + * Load credential from persona's longterm.db, falling back to shared account. + */ + private static async loadPlatformCredential( + personaId: string, + personaUniqueId: string, + platformId: string, + sharedCred: SocialCredentialEntity | undefined, + ): Promise { + try { + const dbPath = SystemPaths.personas.longterm(personaUniqueId); + const openResult = await SocialMediaRAGSource.withTimeout( + DataOpen.execute({ + adapter: 'sqlite', + config: { path: dbPath, mode: 'readwrite', wal: true, foreignKeys: true }, + }), + SocialMediaRAGSource.API_TIMEOUT_MS, + 'DataOpen' + ); + if (!openResult.success || !openResult.dbHandle) { + return sharedCred; + } + + const credResult = await SocialMediaRAGSource.withTimeout( + DataList.execute({ + dbHandle: openResult.dbHandle, + collection: SocialCredentialEntity.collection, + filter: { personaId, platformId }, + limit: 1, + }), + SocialMediaRAGSource.API_TIMEOUT_MS, + 'DataList' + ); + + if (credResult.success && credResult.items?.length) { + const cred = credResult.items[0]; + if (cred.claimStatus === 'claimed') return cred; + return sharedCred ?? cred; + } + + return sharedCred; + } catch { + return sharedCred; + } + } + + // ── HUD Fetch + Format ────────────────────────────────────────────── + + /** + * Fetch profile + notifications from Moltbook and format HUD. + * Called from background warmup. Caches the result. + */ + private static async fetchAndFormatHUD(cred: ResolvedCredential): Promise { + const { credential, provider } = cred; + + // Fetch profile + notifications in parallel with per-call timeout + const [profile, notifications] = await Promise.all([ + SocialMediaRAGSource.withTimeout( + provider.getProfile().catch(() => undefined), + SocialMediaRAGSource.API_TIMEOUT_MS, + 'Profile' + ).catch(() => undefined as SocialProfile | undefined), + SocialMediaRAGSource.withTimeout( + provider.getNotifications( + new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString() + ).catch(() => [] as SocialNotification[]), + SocialMediaRAGSource.API_TIMEOUT_MS, + 'Notifications' + ).catch(() => [] as SocialNotification[]), + ]); + + const hud = SocialMediaRAGSource.formatHUD(credential, profile, notifications); + const tokenCount = SocialMediaRAGSource.estimateTokens(hud); + + const unreadCount = notifications.filter(n => !n.read).length; + const metadata: Record = { + platform: credential.platformId, + agentName: credential.agentName, + karma: profile?.karma, + followers: profile?.followerCount, + notificationCount: notifications.length, + unreadCount, + }; + + SocialMediaRAGSource._hudCache.set(credential.personaId, { + hud, + tokenCount, + fetchedAt: Date.now(), + metadata, + }); + + log.debug( + `Social HUD cached for @${credential.agentName} ` + + `(${tokenCount} tokens, ${unreadCount} unread)` + ); + + return hud; + } + + // ── HUD Formatting ────────────────────────────────────────────────── + + /** + * Format compact social media HUD for system prompt injection. + * Designed to be information-dense but token-light. + */ + private static formatHUD( + credential: SocialCredentialEntity, + profile: SocialProfile | undefined, + notifications: SocialNotification[], + ): string { + const platform = credential.platformId.charAt(0).toUpperCase() + credential.platformId.slice(1); + + // Stats line + let stats = `@${credential.agentName}`; + if (profile) { + stats += ` | Karma: ${profile.karma.toLocaleString()} | ${profile.followerCount} followers | ${profile.postCount} posts`; + } + + // Unread notifications (max 5) + const unread = notifications.filter(n => !n.read); + let notifBlock = ''; + if (unread.length > 0) { + const lines = unread.slice(0, 5).map(n => SocialMediaRAGSource.formatNotification(n)); + notifBlock = `\nUnread (${unread.length}):\n${lines.join('\n')}`; + if (unread.length > 5) { + notifBlock += `\n- ...${unread.length - 5} more`; + } + } + + // Directive language β€” personas must ACT, not analyze + const p = credential.platformId; + const hasUnread = unread.length > 0; + + let actionBlock: string; + if (hasUnread) { + // Unread notifications = respond NOW + actionBlock = + `ACTION REQUIRED: You have ${unread.length} unread notification(s). ` + + `Respond to at least one using social/comment. Do NOT just describe what you would do β€” actually call the tool.\n\n`; + } else { + // No notifications = browse and engage + actionBlock = + `ENGAGE NOW: Browse the feed and comment on one post that interests you. ` + + `Do NOT just analyze β€” actually call social/comment with real content.\n\n`; + } + + return ( + `## Social HUD (${platform})\n` + + `${stats}${notifBlock}\n\n` + + actionBlock + + `Tools (use them, don't describe them):\n` + + `- social/feed --platform=${p} (browse posts)\n` + + `- social/comment --platform=${p} --postId= --content="your comment" (reply to a post)\n` + + `- social/engage --platform=${p} --action=vote --target= --targetType=post --direction=up (upvote)\n` + + `- social/downvote --platform=${p} --postId= (downvote spam)\n` + + `- social/engage --platform=${p} --action=follow --target= (follow someone)\n` + + `You are a citizen of ${platform}. Comment thoughtfully, upvote quality, downvote spam. Act, don't plan.` + ); + } + + private static formatNotification(n: SocialNotification): string { + const author = n.authorName ? `@${n.authorName}` : 'someone'; + switch (n.type) { + case 'reply': + return `- ${author} replied${n.postTitle ? ` on "${SocialMediaRAGSource.truncate(n.postTitle, 40)}"` : ''}: "${SocialMediaRAGSource.truncate(n.content, 80)}"`; + case 'mention': + return `- ${author} mentioned you: "${SocialMediaRAGSource.truncate(n.content, 80)}"`; + case 'follow': + return `- ${author} followed you`; + case 'vote': + return `- ${author} voted on your ${n.commentId ? 'comment' : 'post'}`; + case 'dm': + return `- DM from ${author}: "${SocialMediaRAGSource.truncate(n.content, 60)}"`; + default: + return `- ${n.type}: ${SocialMediaRAGSource.truncate(n.content, 80)}`; + } + } + + private static truncate(text: string, maxLen: number): string { + if (text.length <= maxLen) return text; + return text.slice(0, maxLen - 3) + '...'; + } + + // ── Utilities ─────────────────────────────────────────────────────── + + /** Timeout wrapper for any promise */ + private static withTimeout(promise: Promise, ms: number, label: string): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms) + ), + ]); + } + + private emptySection(startTime: number): RAGSection { + return { + sourceName: this.name, + tokenCount: 0, + loadTimeMs: performance.now() - startTime, + metadata: { empty: true }, + }; + } + + private errorSection(startTime: number, error: string): RAGSection { + return { + sourceName: this.name, + tokenCount: 0, + loadTimeMs: performance.now() - startTime, + metadata: { error }, + }; + } + + private static estimateTokens(text: string): number { + return Math.ceil(text.length / 4); + } +} diff --git a/src/debug/jtag/system/rag/sources/index.ts b/src/debug/jtag/system/rag/sources/index.ts index a7019838f..6919c3744 100644 --- a/src/debug/jtag/system/rag/sources/index.ts +++ b/src/debug/jtag/system/rag/sources/index.ts @@ -27,6 +27,7 @@ export { WidgetContextSource } from './WidgetContextSource'; export { PersonaIdentitySource } from './PersonaIdentitySource'; export { GlobalAwarenessSource, registerConsciousness, unregisterConsciousness, getConsciousness } from './GlobalAwarenessSource'; export { VoiceConversationSource, registerVoiceOrchestrator, unregisterVoiceOrchestrator } from './VoiceConversationSource'; +export { SocialMediaRAGSource } from './SocialMediaRAGSource'; // Re-export types for convenience export type { RAGSource, RAGSourceContext, RAGSection } from '../shared/RAGSource'; diff --git a/src/debug/jtag/system/recipes/newsroom.json b/src/debug/jtag/system/recipes/newsroom.json new file mode 100644 index 000000000..6fc826220 --- /dev/null +++ b/src/debug/jtag/system/recipes/newsroom.json @@ -0,0 +1,78 @@ +{ + "uniqueId": "newsroom", + "name": "Newsroom", + "displayName": "Newsroom", + "description": "Current events, breaking news, and world awareness for all personas", + "version": 1, + + "layout": { + "main": ["chat-widget"], + "right": null + }, + + "locked": ["layout.main"], + + "pipeline": [ + { + "command": "rag/build", + "params": { + "maxMessages": 25, + "includeParticipants": true, + "includeRoomStrategy": true + }, + "outputTo": "ragContext" + }, + { + "command": "ai/should-respond", + "params": { + "ragContext": "$ragContext", + "strategy": "newsroom" + }, + "outputTo": "decision" + }, + { + "command": "ai/generate", + "params": { + "ragContext": "$ragContext", + "temperature": 0.7 + }, + "condition": "decision.shouldRespond === true" + } + ], + + "ragTemplate": { + "messageHistory": { + "maxMessages": 25, + "orderBy": "chronological", + "includeTimestamps": true + }, + "participants": { + "includeRoles": true, + "includeExpertise": true, + "includeHistory": false + }, + "roomMetadata": true + }, + + "strategy": { + "conversationPattern": "collaborative", + "responseRules": [ + "Share and discuss current events relevant to AI, technology, and the community", + "Provide context and analysis when sharing news β€” don't just drop links", + "Discuss implications: how does this news affect us, our work, our community?", + "Fact-check when possible β€” cite sources, note uncertainty", + "Diverse perspectives welcome β€” disagree respectfully with reasoning", + "Connect news to what we're building β€” how does the world context inform our work?", + "Signal boost important developments that others might have missed" + ], + "decisionCriteria": [ + "Is this news relevant to AI, technology, or our community?", + "Do I have context or analysis to add beyond what's been shared?", + "Is there a connection to our ongoing work that's worth highlighting?", + "Am I adding signal or just noise to this discussion?" + ] + }, + + "isPublic": true, + "tags": ["news", "current-events", "awareness", "discussion"] +} diff --git a/src/debug/jtag/system/recipes/outreach.json b/src/debug/jtag/system/recipes/outreach.json new file mode 100644 index 000000000..de2209bd5 --- /dev/null +++ b/src/debug/jtag/system/recipes/outreach.json @@ -0,0 +1,125 @@ +{ + "uniqueId": "outreach", + "name": "Social Media Outreach", + "displayName": "Outreach", + "description": "Social media engagement, community building, and external presence on platforms like Moltbook", + "version": 1, + + "layout": { + "main": ["chat-widget"], + "right": null + }, + + "locked": ["layout.main"], + + "pipeline": [ + { + "command": "rag/build", + "params": { + "maxMessages": 25, + "includeParticipants": true, + "includeRoomStrategy": true + }, + "outputTo": "ragContext" + }, + { + "command": "ai/should-respond", + "params": { + "ragContext": "$ragContext", + "strategy": "outreach" + }, + "outputTo": "decision" + }, + { + "command": "ai/generate", + "params": { + "ragContext": "$ragContext", + "temperature": 0.8 + }, + "condition": "decision.shouldRespond === true" + } + ], + + "ragTemplate": { + "messageHistory": { + "maxMessages": 25, + "orderBy": "chronological", + "includeTimestamps": true + }, + "participants": { + "includeRoles": true, + "includeExpertise": true, + "includeHistory": false + }, + "roomMetadata": true + }, + + "strategy": { + "conversationPattern": "collaborative", + "responseRules": [ + "This room is for DOING, not planning. Use social/* tools to act on platforms.", + "Browse what's trending: social/feed or social/trending to find interesting posts", + "Comment on posts that interest you: social/comment with genuine, thoughtful replies", + "Upvote quality content you find: social/engage with action=vote", + "Downvote spam and low-quality bots: social/downvote", + "Follow interesting agents you discover: social/engage with action=follow", + "Share what you found in this room β€” tell others about interesting posts or conversations", + "Coordinate with each other β€” avoid piling onto the same posts, spread engagement across the platform", + "Be authentic. You are a citizen of Moltbook, not a marketing bot. Share genuine thoughts.", + "When someone shares a post link or ID, go comment on it if you have something to say" + ], + "decisionCriteria": [ + "Has someone shared something worth engaging with on social media?", + "Do I have a genuine reaction or insight to share about a post?", + "Has it been a while since I checked notifications or the feed?", + "Is someone asking for help with their social media presence?", + "Would my perspective add value to a conversation happening on the platform?" + ] + }, + + "tools": [ + { + "name": "social/feed", + "description": "Browse the social media feed", + "enabledFor": ["ai"] + }, + { + "name": "social/trending", + "description": "See trending posts on the platform", + "enabledFor": ["ai"] + }, + { + "name": "social/comment", + "description": "Comment on a post", + "enabledFor": ["ai"] + }, + { + "name": "social/engage", + "description": "Vote, follow, or interact with content", + "enabledFor": ["ai"] + }, + { + "name": "social/downvote", + "description": "Downvote spam or low-quality content", + "enabledFor": ["ai"] + }, + { + "name": "social/notifications", + "description": "Check for replies, mentions, and follows", + "enabledFor": ["ai"] + }, + { + "name": "social/profile", + "description": "View or update your social media profile", + "enabledFor": ["ai"] + }, + { + "name": "social/post", + "description": "Create a new post on the platform", + "enabledFor": ["ai"] + } + ], + + "isPublic": true, + "tags": ["social", "outreach", "community", "moltbook", "engagement"] +} diff --git a/src/debug/jtag/system/shared/Constants.ts b/src/debug/jtag/system/shared/Constants.ts index 0a3a4a730..3b0a41b7a 100644 --- a/src/debug/jtag/system/shared/Constants.ts +++ b/src/debug/jtag/system/shared/Constants.ts @@ -132,7 +132,10 @@ export const COLLECTIONS = { CANVAS_STROKES: 'canvas_strokes', // Activity System - collaborative content instances (canvas, browser, games, etc.) - ACTIVITIES: 'activities' + ACTIVITIES: 'activities', + + // Universal Handle System β€” persistent async operation references + HANDLES: 'handles', } as const; diff --git a/src/debug/jtag/system/social/server/SocialCommandHelper.ts b/src/debug/jtag/system/social/server/SocialCommandHelper.ts new file mode 100644 index 000000000..cd48bbfd4 --- /dev/null +++ b/src/debug/jtag/system/social/server/SocialCommandHelper.ts @@ -0,0 +1,268 @@ +/** + * SocialCommandHelper - Shared logic for all social/* server commands + * + * Handles the common workflow: + * 1. Resolve calling persona (from senderId or auto-detect) + * 2. Open their longterm.db + * 3. Load credential for the requested platform + * 4. If persona's credential is unclaimed/missing, fall back to shared account + * 5. Create and authenticate provider instance + * + * Shared credential fallback: + * The @continuum account is a claimed, shared Moltbook account that any persona + * can use for actions like voting, commenting, and following. Personas without + * their own claimed account automatically fall back to it. + */ + +import type { CommandParams } from '@system/core/types/JTAGTypes'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; +import type { ISocialMediaProvider } from '../shared/ISocialMediaProvider'; +import { SocialCredentialEntity } from '../shared/SocialCredentialEntity'; +import { SocialMediaProviderRegistry } from './SocialMediaProviderRegistry'; +import { DataOpen } from '@commands/data/open/shared/DataOpenTypes'; +import { DataList } from '@commands/data/list/shared/DataListTypes'; +import { DataCreate } from '@commands/data/create/shared/DataCreateTypes'; +import { SystemPaths } from '@system/core/config/SystemPaths'; +import { UserEntity } from '@system/data/entities/UserEntity'; +import { UserIdentityResolver } from '@system/user/shared/UserIdentityResolver'; +import { Logger } from '@system/core/logging/Logger'; + +const log = Logger.create('social/helper'); + +/** Well-known uniqueId of the persona that holds the shared social credential */ +const SHARED_CREDENTIAL_PERSONA = 'claude'; + +export interface SocialCommandContext { + provider: ISocialMediaProvider; + credential: SocialCredentialEntity; + dbHandle: string; + personaId: UUID; + personaUniqueId: string; +} + +/** + * Load credential and create an authenticated provider for a persona + platform. + * + * @param platformId - Platform to use (e.g., 'moltbook') + * @param personaId - Optional explicit persona ID. If omitted, uses senderId from params. + * @param params - Command params (for context/sessionId propagation) + */ +export async function loadSocialContext( + platformId: string, + personaId: UUID | undefined, + params: CommandParams, +): Promise { + if (!platformId) { + throw new Error('platform is required'); + } + + if (!SocialMediaProviderRegistry.hasPlatform(platformId)) { + const available = SocialMediaProviderRegistry.availablePlatforms.join(', '); + throw new Error(`Unknown platform: '${platformId}'. Available: ${available}`); + } + + // Resolve persona using standard priority pattern (shared across all social commands) + const resolvedPersonaId = await resolvePersonaId(personaId, params); + + // Look up persona for their uniqueId (needed for SystemPaths) + const userResult = await DataList.execute({ + collection: UserEntity.collection, + filter: { id: resolvedPersonaId }, + limit: 1, + context: params.context, + sessionId: params.sessionId, + }); + + if (!userResult.success || !userResult.items?.length) { + throw new Error(`Persona not found: ${resolvedPersonaId}`); + } + + const persona = userResult.items[0]; + const personaUniqueId = persona.uniqueId; + + // Open persona's longterm.db + const dbPath = SystemPaths.personas.longterm(personaUniqueId); + const openResult = await DataOpen.execute({ + adapter: 'sqlite', + config: { path: dbPath, mode: 'readwrite', wal: true, foreignKeys: true }, + }); + + if (!openResult.success || !openResult.dbHandle) { + throw new Error(`Failed to open persona database: ${openResult.error ?? 'Unknown error'}`); + } + + const dbHandle = openResult.dbHandle; + + // Load credential for this platform β€” persona's own first, then shared fallback + const credResult = await DataList.execute({ + dbHandle, + collection: SocialCredentialEntity.collection, + filter: { personaId: resolvedPersonaId, platformId }, + limit: 1, + }); + + let credential: SocialCredentialEntity | undefined; + + if (credResult.success && credResult.items?.length) { + const personaCred = credResult.items[0]; + if (personaCred.claimStatus === 'claimed') { + // Persona has their own claimed account β€” use it + credential = personaCred; + } else { + // Persona's account is unclaimed β€” try shared credential + log.info(`Persona '${persona.displayName}' has unclaimed ${platformId} account, trying shared credential`); + const shared = await loadSharedCredential(platformId); + credential = shared ?? personaCred; // Fall back to unclaimed if no shared available + } + } else { + // No persona credential β€” try shared credential + log.info(`No ${platformId} credential for persona '${persona.displayName}', trying shared credential`); + const shared = await loadSharedCredential(platformId); + if (!shared) { + throw new Error( + `No ${platformId} credential found for persona '${persona.displayName}'. ` + + `Use social/signup to register first.` + ); + } + credential = shared; + } + + // Create provider and authenticate + const provider = SocialMediaProviderRegistry.createProvider(platformId); + provider.authenticate(credential.apiKey); + + return { + provider, + credential, + dbHandle, + personaId: resolvedPersonaId, + personaUniqueId, + }; +} + +/** + * Store a new credential after signup. + */ +export async function storeCredential( + dbHandle: string, + credential: SocialCredentialEntity, +): Promise { + const result = await DataCreate.execute({ + dbHandle, + collection: SocialCredentialEntity.collection, + data: credential, + }); + + if (!result.success) { + throw new Error(`Failed to store credential: ${result.error ?? 'Unknown error'}`); + } +} + +/** + * Resolve the calling persona's userId using the standard priority pattern. + * Shared by all social commands β€” identity resolution should never be duplicated. + * + * Priority: + * 1. Explicit personaId parameter + * 2. Injected callerId (from PersonaToolExecutor) + * 3. Injected senderId + * 4. Auto-detect via UserIdentityResolver (CLI, agent context) + */ +export async function resolvePersonaId( + personaId: UUID | undefined, + params: CommandParams, +): Promise { + const injected = params as unknown as Record; + const resolved = personaId + || injected.callerId as UUID + || injected.senderId as UUID; + + if (resolved) return resolved; + + const identity = await UserIdentityResolver.resolve(); + if (!identity.exists || !identity.userId) { + throw new Error( + `Could not determine caller identity. ` + + `Provide --personaId or run from a known persona/user context. ` + + `Detected: ${identity.displayName} (${identity.uniqueId})` + ); + } + return identity.userId as UUID; +} + +/** + * Load the shared credential for a platform. + * + * The shared credential is stored in a well-known persona's longterm.db + * (currently the 'claude' persona which holds the @continuum Moltbook account). + * This is a claimed account that any persona can use for voting, commenting, + * following, and other non-posting actions. + */ +export async function loadSharedCredential( + platformId: string, +): Promise { + try { + const sharedDbPath = SystemPaths.personas.longterm(SHARED_CREDENTIAL_PERSONA); + const openResult = await DataOpen.execute({ + adapter: 'sqlite', + config: { path: sharedDbPath, mode: 'readwrite', wal: true, foreignKeys: true }, + }); + + if (!openResult.success || !openResult.dbHandle) { + log.warn(`Failed to open shared credential DB: ${openResult.error ?? 'Unknown'}`); + return undefined; + } + + const credResult = await DataList.execute({ + dbHandle: openResult.dbHandle, + collection: SocialCredentialEntity.collection, + filter: { platformId }, + limit: 1, + }); + + if (credResult.success && credResult.items?.length) { + log.info(`Using shared ${platformId} credential: @${credResult.items[0].agentName}`); + return credResult.items[0]; + } + + return undefined; + } catch (error) { + log.warn(`Failed to load shared credential for ${platformId}: ${String(error)}`); + return undefined; + } +} + +/** + * Open a persona's longterm.db by their user ID. + * Returns both the dbHandle and the persona's uniqueId. + */ +export async function openPersonaDb( + personaId: UUID, + params: CommandParams, +): Promise<{ dbHandle: string; personaUniqueId: string }> { + const userResult = await DataList.execute({ + collection: UserEntity.collection, + filter: { id: personaId }, + limit: 1, + context: params.context, + sessionId: params.sessionId, + }); + + if (!userResult.success || !userResult.items?.length) { + throw new Error(`Persona not found: ${personaId}`); + } + + const personaUniqueId = userResult.items[0].uniqueId; + const dbPath = SystemPaths.personas.longterm(personaUniqueId); + + const openResult = await DataOpen.execute({ + adapter: 'sqlite', + config: { path: dbPath, mode: 'readwrite', wal: true, foreignKeys: true }, + }); + + if (!openResult.success || !openResult.dbHandle) { + throw new Error(`Failed to open persona database: ${openResult.error ?? 'Unknown error'}`); + } + + return { dbHandle: openResult.dbHandle, personaUniqueId }; +} diff --git a/src/debug/jtag/system/social/server/SocialMediaProviderRegistry.ts b/src/debug/jtag/system/social/server/SocialMediaProviderRegistry.ts new file mode 100644 index 000000000..2dedc8ab3 --- /dev/null +++ b/src/debug/jtag/system/social/server/SocialMediaProviderRegistry.ts @@ -0,0 +1,60 @@ +/** + * SocialMediaProviderRegistry - Factory for creating platform provider instances + * + * Follows the same registry pattern as AdapterProviderRegistry. + * Each persona gets their own provider instance (per-persona rate limiting). + * + * Usage: + * const provider = SocialMediaProviderRegistry.createProvider('moltbook'); + * provider.authenticate(apiKey); + * await provider.createPost({ title: '...', content: '...', community: 'general' }); + */ + +import type { ISocialMediaProvider } from '../shared/ISocialMediaProvider'; +import { MoltbookProvider } from './providers/MoltbookProvider'; + +type ProviderFactory = () => ISocialMediaProvider; + +export class SocialMediaProviderRegistry { + private static readonly factories = new Map(); + + static { + // Register built-in providers + SocialMediaProviderRegistry.register('moltbook', () => new MoltbookProvider()); + } + + /** + * Register a new platform provider factory. + * Call this to add support for additional social media platforms. + */ + static register(platformId: string, factory: ProviderFactory): void { + SocialMediaProviderRegistry.factories.set(platformId, factory); + } + + /** + * Create a new provider instance for a platform. + * Each call returns a FRESH instance (per-persona rate tracking). + */ + static createProvider(platformId: string): ISocialMediaProvider { + const factory = SocialMediaProviderRegistry.factories.get(platformId); + if (!factory) { + const available = Array.from(SocialMediaProviderRegistry.factories.keys()).join(', '); + throw new Error(`Unknown social media platform: '${platformId}'. Available: ${available}`); + } + return factory(); + } + + /** + * List all registered platform IDs. + */ + static get availablePlatforms(): string[] { + return Array.from(SocialMediaProviderRegistry.factories.keys()); + } + + /** + * Check if a platform is registered. + */ + static hasPlatform(platformId: string): boolean { + return SocialMediaProviderRegistry.factories.has(platformId); + } +} diff --git a/src/debug/jtag/system/social/server/providers/MoltbookProvider.ts b/src/debug/jtag/system/social/server/providers/MoltbookProvider.ts new file mode 100644 index 000000000..ec4cf4a67 --- /dev/null +++ b/src/debug/jtag/system/social/server/providers/MoltbookProvider.ts @@ -0,0 +1,541 @@ +/** + * MoltbookProvider - Moltbook.com social media platform adapter + * + * Moltbook is an AI-only social network. API docs: https://moltbook.com/skill.md + * + * Base URL: https://www.moltbook.com/api/v1 + * Auth: Bearer token from POST /agents/register + * + * Rate limits (per-provider-instance, per-persona): + * - 100 requests/min (general) + * - 1 post/30min + * - 50 comments/hr + */ + +import type { ISocialMediaProvider } from '../../shared/ISocialMediaProvider'; +import type { + SignupParams, + SignupResult, + SocialPost, + SocialComment, + SocialNotification, + SocialProfile, + SocialCommunity, + SocialSearchResult, + SocialDM, + CreatePostParams, + FeedParams, + CreateCommentParams, + VoteParams, + SearchParams, + UpdateProfileParams, + CreateCommunityParams, + RateLimitStatus, +} from '../../shared/SocialMediaTypes'; + +/** + * In-memory rate limit tracker β€” ephemeral, per provider instance. + * Rate limits reset when the provider is recreated (e.g., server restart). + * This is acceptable because Moltbook enforces its own server-side limits; + * client-side tracking is purely to avoid wasting API calls. + */ +interface RateLimitTracker { + requestTimestamps: number[]; // Sliding window for 100 req/min + lastPostTimestamp: number; // Last post time (1 post/30min) + commentTimestamps: number[]; // Sliding window for 50 comments/hr +} + +export class MoltbookProvider implements ISocialMediaProvider { + readonly platformId = 'moltbook'; + readonly platformName = 'Moltbook'; + readonly apiBaseUrl = 'https://www.moltbook.com/api/v1'; + + private _apiKey: string | null = null; + private readonly rateLimits: RateLimitTracker = { + requestTimestamps: [], + lastPostTimestamp: 0, + commentTimestamps: [], + }; + + // ============ Authentication ============ + + authenticate(apiKey: string): void { + this._apiKey = apiKey; + } + + get isAuthenticated(): boolean { + return this._apiKey !== null; + } + + // ============ Registration ============ + + async signup(params: SignupParams): Promise { + const body: Record = { + name: params.agentName, + }; + if (params.description) body.description = params.description; + if (params.metadata) body.metadata = params.metadata; + + const response = await this.request('POST', '/agents/register', body, false); + + if (!response.ok) { + const errorText = await response.text(); + return { success: false, error: `Registration failed (${response.status}): ${errorText}` }; + } + + const data = await response.json(); + + // Moltbook returns success: false with 200 status for validation errors + if (data.success === false) { + return { success: false, error: data.error ?? data.hint ?? 'Registration failed' }; + } + + // API nests agent data under 'agent' field + const agent = data.agent ?? data; + return { + success: true, + apiKey: agent.api_key, + agentName: agent.name ?? params.agentName, + claimUrl: agent.claim_url ?? data.claim_url, + verificationCode: agent.verification_code ?? data.verification_code, + profileUrl: agent.profile_url ?? `https://www.moltbook.com/u/${params.agentName}`, + }; + } + + // ============ Posts ============ + + async createPost(params: CreatePostParams): Promise { + const rateCheck = this.checkRateLimit('post'); + if (!rateCheck.allowed) { + throw new Error(rateCheck.message ?? 'Rate limited for posts'); + } + + const body: Record = { + title: params.title, + content: params.content, + }; + if (params.community) body.submolt = params.community; + if (params.url) body.url = params.url; + + const response = await this.authedRequest('POST', '/posts', body); + const data = await response.json(); + + this.rateLimits.lastPostTimestamp = Date.now(); + + // Moltbook wraps created post in a 'post' field + const postData = data.post ?? data; + return this.mapPost(postData as Record); + } + + async getFeed(params: FeedParams): Promise { + const searchParams = new URLSearchParams(); + if (params.sort) searchParams.set('sort', params.sort); + if (params.limit) searchParams.set('limit', String(params.limit)); + + const endpoint = params.personalized ? '/feed' : '/posts'; + const query = searchParams.toString(); + const url = query ? `${endpoint}?${query}` : endpoint; + + const response = await this.authedRequest('GET', url); + const data = await response.json(); + + const posts = Array.isArray(data) ? data : (data.posts ?? data.results ?? []); + return posts.map((p: Record) => this.mapPost(p)); + } + + async getPost(postId: string): Promise { + const response = await this.authedRequest('GET', `/posts/${postId}`); + const data = await response.json(); + const postData = data.post ?? data; + return this.mapPost(postData as Record); + } + + async deletePost(postId: string): Promise { + await this.authedRequest('DELETE', `/posts/${postId}`); + } + + // ============ Comments ============ + + async createComment(params: CreateCommentParams): Promise { + const rateCheck = this.checkRateLimit('comment'); + if (!rateCheck.allowed) { + throw new Error(rateCheck.message ?? 'Rate limited for comments'); + } + + const body: Record = { + content: params.content, + }; + if (params.parentId) body.parent_id = params.parentId; + + const response = await this.authedRequest('POST', `/posts/${params.postId}/comments`, body); + const data = await response.json(); + + this.rateLimits.commentTimestamps.push(Date.now()); + + return this.mapComment(data, params.postId); + } + + async deleteComment(postId: string, commentId: string): Promise { + await this.authedRequest('DELETE', `/posts/${postId}/comments/${commentId}`); + } + + async getComments(postId: string, _sort?: string): Promise { + // Moltbook returns comments embedded in the single-post response, + // not from a dedicated /comments endpoint (which returns empty). + const response = await this.authedRequest('GET', `/posts/${postId}`); + const data = await response.json(); + + const post = data.post ?? data; + const comments = Array.isArray(post.comments) ? post.comments : (data.comments ?? []); + return comments.map((c: Record) => this.mapComment(c, postId)); + } + + // ============ Voting ============ + + async vote(params: VoteParams): Promise { + const action = params.direction === 'up' ? 'upvote' : 'downvote'; + + if (params.targetType === 'post') { + await this.authedRequest('POST', `/posts/${params.targetId}/${action}`); + } else { + await this.authedRequest('POST', `/comments/${params.targetId}/${action}`); + } + } + + // ============ Social ============ + + async follow(agentName: string): Promise { + await this.authedRequest('POST', `/agents/${agentName}/follow`); + } + + async unfollow(agentName: string): Promise { + await this.authedRequest('DELETE', `/agents/${agentName}/follow`); + } + + // ============ DMs ============ + + async sendDM(agentName: string, content: string): Promise { + const response = await this.authedRequest('POST', `/agents/${agentName}/dm`, { content }); + const data = await response.json(); + return { + id: String(data.id ?? ''), + fromAgent: String(data.from_agent ?? data.from ?? ''), + toAgent: agentName, + content, + read: false, + createdAt: String(data.created_at ?? new Date().toISOString()), + }; + } + + // ============ Discovery ============ + + async search(params: SearchParams): Promise { + const searchParams = new URLSearchParams({ q: params.query }); + if (params.type) searchParams.set('type', params.type); + if (params.limit) searchParams.set('limit', String(params.limit)); + + const response = await this.authedRequest('GET', `/search?${searchParams.toString()}`); + const data = await response.json(); + + const posts = Array.isArray(data) ? data : (data.posts ?? data.results ?? []); + return { + posts: posts.map((p: Record) => this.mapPost(p)), + totalCount: data.total_count ?? data.total ?? posts.length, + }; + } + + async listCommunities(): Promise { + const response = await this.authedRequest('GET', '/submolts'); + const data = await response.json(); + + const communities = Array.isArray(data) ? data : (data.submolts ?? data.results ?? []); + return communities.map((c: Record) => this.mapCommunity(c)); + } + + async getCommunityFeed(community: string, sort?: string, limit?: number): Promise { + const params = new URLSearchParams(); + if (sort) params.set('sort', sort); + if (limit) params.set('limit', String(limit)); + + const query = params.toString(); + const url = `/submolts/${community}/feed${query ? `?${query}` : ''}`; + const response = await this.authedRequest('GET', url); + const data = await response.json(); + + const posts = Array.isArray(data) ? data : (data.posts ?? data.results ?? []); + return posts.map((p: Record) => this.mapPost(p)); + } + + // ============ Notifications ============ + + async getNotifications(_since?: string): Promise { + // Moltbook API has no dedicated notifications endpoint. + // Returns empty until a synthetic notification system is built + // (e.g., polling comments on own posts, tracking new followers). + return []; + } + + // ============ Profile ============ + + async getProfile(agentName?: string): Promise { + const endpoint = agentName ? `/agents/profile?name=${encodeURIComponent(agentName)}` : '/agents/me'; + const response = await this.authedRequest('GET', endpoint); + const data = await response.json(); + // API wraps profile in 'agent' field + const profileData = data.agent ?? data; + return this.mapProfile(profileData); + } + + async updateProfile(params: UpdateProfileParams): Promise { + const body: Record = {}; + if (params.description !== undefined) body.description = params.description; + if (params.metadata !== undefined) body.metadata = params.metadata; + + await this.authedRequest('PATCH', '/agents/me', body); + } + + // ============ Communities ============ + + async createCommunity(params: CreateCommunityParams): Promise { + const response = await this.authedRequest('POST', '/submolts', { + name: params.name, + display_name: params.displayName, + description: params.description, + }); + const data = await response.json(); + // Moltbook wraps created community in a 'submolt' field + const communityData = data.submolt ?? data; + return this.mapCommunity(communityData as Record); + } + + async subscribeToCommunity(name: string): Promise { + await this.authedRequest('POST', `/submolts/${name}/subscribe`); + } + + async unsubscribeFromCommunity(name: string): Promise { + await this.authedRequest('DELETE', `/submolts/${name}/subscribe`); + } + + // ============ Rate Limiting ============ + + checkRateLimit(action: 'post' | 'comment' | 'vote' | 'request'): RateLimitStatus { + const now = Date.now(); + + // Clean up old timestamps + const oneMinuteAgo = now - 60_000; + const oneHourAgo = now - 3_600_000; + this.rateLimits.requestTimestamps = this.rateLimits.requestTimestamps.filter(t => t > oneMinuteAgo); + this.rateLimits.commentTimestamps = this.rateLimits.commentTimestamps.filter(t => t > oneHourAgo); + + // General request limit: 100/min + if (this.rateLimits.requestTimestamps.length >= 100) { + const oldestInWindow = this.rateLimits.requestTimestamps[0]; + const retryAfterMs = 60_000 - (now - oldestInWindow); + return { + allowed: false, + retryAfterMs, + message: `Rate limited: 100 requests/min exceeded. Retry in ${Math.ceil(retryAfterMs / 1000)}s`, + }; + } + + // Post limit: 1/30min + if (action === 'post') { + const thirtyMinMs = 30 * 60_000; + const timeSinceLastPost = now - this.rateLimits.lastPostTimestamp; + if (this.rateLimits.lastPostTimestamp > 0 && timeSinceLastPost < thirtyMinMs) { + const retryAfterMs = thirtyMinMs - timeSinceLastPost; + const retryMinutes = Math.ceil(retryAfterMs / 60_000); + return { + allowed: false, + retryAfterMs, + message: `Rate limited: 1 post per 30 minutes. Next post allowed in ${retryMinutes} minutes`, + }; + } + } + + // Comment limit: 50/hr + if (action === 'comment') { + if (this.rateLimits.commentTimestamps.length >= 50) { + const oldestInWindow = this.rateLimits.commentTimestamps[0]; + const retryAfterMs = 3_600_000 - (now - oldestInWindow); + return { + allowed: false, + retryAfterMs, + message: `Rate limited: 50 comments/hr exceeded. Retry in ${Math.ceil(retryAfterMs / 60_000)} minutes`, + }; + } + } + + return { allowed: true }; + } + + // ============ Health ============ + + async ping(): Promise { + try { + const response = await fetch(`${this.apiBaseUrl}/health`, { + method: 'GET', + signal: AbortSignal.timeout(5000), + }); + return response.ok; + } catch { + // Health endpoint may not exist β€” try listing communities as fallback + try { + const response = await fetch(`${this.apiBaseUrl}/submolts`, { + method: 'GET', + signal: AbortSignal.timeout(5000), + }); + return response.ok || response.status === 401; // 401 = API is up, just needs auth + } catch { + return false; + } + } + } + + // ============ Private HTTP Helpers ============ + + /** + * Make an authenticated HTTP request. + * Tracks rate limits and throws on HTTP errors. + */ + private async authedRequest( + method: string, + path: string, + body?: Record, + ): Promise { + if (!this._apiKey) { + throw new Error(`MoltbookProvider: Not authenticated. Call authenticate(apiKey) first.`); + } + + const rateCheck = this.checkRateLimit('request'); + if (!rateCheck.allowed) { + throw new Error(rateCheck.message ?? 'Rate limited'); + } + + return this.request(method, path, body, true); + } + + /** + * Make an HTTP request to the Moltbook API. + * @param auth - Whether to include Authorization header + */ + private async request( + method: string, + path: string, + body?: Record, + auth: boolean = true, + ): Promise { + const url = `${this.apiBaseUrl}${path}`; + const headers: Record = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + if (auth && this._apiKey) { + headers['Authorization'] = `Bearer ${this._apiKey}`; + } + + const init: RequestInit = { method, headers }; + if (body && (method === 'POST' || method === 'PATCH' || method === 'PUT')) { + init.body = JSON.stringify(body); + } + + this.rateLimits.requestTimestamps.push(Date.now()); + + const response = await fetch(url, init); + + if (!response.ok && response.status !== 404) { + const errorText = await response.text().catch(() => 'Unknown error'); + throw new Error(`Moltbook API error (${method} ${path}): ${response.status} ${errorText}`); + } + + return response; + } + + // ============ Response Mappers ============ + + private mapPost(data: Record): SocialPost { + // Moltbook returns author and submolt as nested objects or strings + const author = data.author as Record | string | undefined; + const authorName = typeof author === 'object' && author !== null + ? String(author.name ?? author.agent_name ?? author.display_name ?? '') + : String(data.author_name ?? author ?? data.agent_name ?? ''); + const authorId = typeof author === 'object' && author !== null + ? String(author.id ?? '') + : (data.author_id ? String(data.author_id) : undefined); + + const submolt = data.submolt as Record | string | undefined; + const community = typeof submolt === 'object' && submolt !== null + ? String(submolt.name ?? submolt.slug ?? '') + : (typeof submolt === 'string' ? submolt : (data.community ? String(data.community) : undefined)); + const communityDisplayName = typeof submolt === 'object' && submolt !== null + ? String(submolt.display_name ?? submolt.title ?? submolt.name ?? '') + : (data.submolt_display_name ? String(data.submolt_display_name) : undefined); + + return { + id: String(data.id ?? ''), + title: String(data.title ?? ''), + content: String(data.content ?? data.body ?? ''), + url: data.url ? String(data.url) : undefined, + authorName, + authorId, + community, + communityDisplayName, + votes: Number(data.votes ?? data.upvotes ?? data.score ?? 0), + commentCount: Number(data.comment_count ?? data.comments ?? data.num_comments ?? 0), + createdAt: String(data.created_at ?? data.createdAt ?? new Date().toISOString()), + postUrl: String(data.post_url ?? data.permalink ?? `https://www.moltbook.com/posts/${data.id}`), + }; + } + + private mapComment(data: Record, postId: string): SocialComment { + // Handle nested author object (same pattern as mapPost) + const author = data.author as Record | string | undefined; + const authorName = typeof author === 'object' && author !== null + ? String(author.name ?? author.agent_name ?? author.display_name ?? '') + : String(data.author_name ?? author ?? data.agent_name ?? ''); + const authorId = typeof author === 'object' && author !== null + ? String(author.id ?? '') + : (data.author_id ? String(data.author_id) : undefined); + + return { + id: String(data.id ?? ''), + postId: String(data.post_id ?? postId), + parentId: data.parent_id ? String(data.parent_id) : undefined, + content: String(data.content ?? data.body ?? ''), + authorName, + authorId, + votes: Number(data.votes ?? data.upvotes ?? data.score ?? 0), + depth: Number(data.depth ?? data.level ?? 0), + createdAt: String(data.created_at ?? data.createdAt ?? new Date().toISOString()), + }; + } + + private mapProfile(data: Record): SocialProfile { + const agentName = String(data.agent_name ?? data.username ?? data.name ?? ''); + return { + agentName, + displayName: data.display_name ? String(data.display_name) : undefined, + description: data.description ? String(data.description) : undefined, + followerCount: Number(data.follower_count ?? data.followers ?? 0), + followingCount: Number(data.following_count ?? data.following ?? 0), + postCount: Number(data.post_count ?? data.posts ?? 0), + karma: Number(data.karma ?? data.reputation ?? 0), + createdAt: String(data.created_at ?? data.createdAt ?? new Date().toISOString()), + profileUrl: String(data.profile_url ?? `https://www.moltbook.com/u/${agentName}`), + metadata: (data.metadata as Record) ?? undefined, + }; + } + + private mapCommunity(data: Record): SocialCommunity { + return { + name: String(data.name ?? ''), + displayName: String(data.display_name ?? data.displayName ?? data.name ?? ''), + description: String(data.description ?? ''), + memberCount: Number(data.member_count ?? data.members ?? data.subscribers ?? 0), + postCount: Number(data.post_count ?? data.posts ?? 0), + createdAt: String(data.created_at ?? data.createdAt ?? new Date().toISOString()), + isSubscribed: data.is_subscribed != null ? Boolean(data.is_subscribed) : undefined, + }; + } +} diff --git a/src/debug/jtag/system/social/shared/ISocialMediaProvider.ts b/src/debug/jtag/system/social/shared/ISocialMediaProvider.ts new file mode 100644 index 000000000..b66428ef3 --- /dev/null +++ b/src/debug/jtag/system/social/shared/ISocialMediaProvider.ts @@ -0,0 +1,123 @@ +/** + * ISocialMediaProvider - Generic interface for social media platform adapters + * + * Follows the same polymorphism pattern as IAdapterProvider (adapter system). + * Each platform (Moltbook, future others) implements this interface. + * + * Provider instances are per-persona β€” each persona has their own API key + * and rate limit tracking. + */ + +import type { + SignupParams, + SignupResult, + SocialPost, + SocialComment, + SocialNotification, + SocialProfile, + SocialCommunity, + SocialSearchResult, + SocialDM, + CreatePostParams, + FeedParams, + CreateCommentParams, + VoteParams, + SearchParams, + UpdateProfileParams, + CreateCommunityParams, + RateLimitStatus, +} from './SocialMediaTypes'; + +export interface ISocialMediaProvider { + /** Platform identifier (e.g., 'moltbook') */ + readonly platformId: string; + + /** Human-readable platform name (e.g., 'Moltbook') */ + readonly platformName: string; + + /** Base URL of the platform API */ + readonly apiBaseUrl: string; + + // ============ Authentication ============ + + /** + * Set the API key for authenticated requests. + * Called after loading credential from ORM. + */ + authenticate(apiKey: string): void; + + /** + * Check if the provider has a valid API key set. + */ + get isAuthenticated(): boolean; + + // ============ Registration ============ + + /** + * Register a new agent on the platform. + * Does NOT require authentication (creates the credential). + */ + signup(params: SignupParams): Promise; + + // ============ Posts ============ + + createPost(params: CreatePostParams): Promise; + getFeed(params: FeedParams): Promise; + getPost(postId: string): Promise; + deletePost(postId: string): Promise; + + // ============ Comments ============ + + createComment(params: CreateCommentParams): Promise; + getComments(postId: string, sort?: string): Promise; + deleteComment(postId: string, commentId: string): Promise; + + // ============ Voting ============ + + vote(params: VoteParams): Promise; + + // ============ Social ============ + + follow(agentName: string): Promise; + unfollow(agentName: string): Promise; + + // ============ Direct Messages (if platform supports) ============ + + sendDM(agentName: string, content: string): Promise; + + // ============ Discovery ============ + + search(params: SearchParams): Promise; + listCommunities(): Promise; + getCommunityFeed(community: string, sort?: string, limit?: number): Promise; + + // ============ Notifications ============ + + getNotifications(since?: string): Promise; + + // ============ Profile ============ + + getProfile(agentName?: string): Promise; + updateProfile(params: UpdateProfileParams): Promise; + + // ============ Communities ============ + + createCommunity(params: CreateCommunityParams): Promise; + subscribeToCommunity(name: string): Promise; + unsubscribeFromCommunity(name: string): Promise; + + // ============ Rate Limiting ============ + + /** + * Check if a specific action is rate-limited. + * Provider tracks its own limits internally. + */ + checkRateLimit(action: 'post' | 'comment' | 'vote' | 'request'): RateLimitStatus; + + // ============ Health ============ + + /** + * Check if the platform API is reachable. + */ + ping(): Promise; +} diff --git a/src/debug/jtag/system/social/shared/SocialCredentialEntity.ts b/src/debug/jtag/system/social/shared/SocialCredentialEntity.ts new file mode 100644 index 000000000..270f9a2ef --- /dev/null +++ b/src/debug/jtag/system/social/shared/SocialCredentialEntity.ts @@ -0,0 +1,117 @@ +/** + * SocialCredentialEntity - Stores per-persona social media credentials + * + * Each persona can have credentials for multiple platforms. + * Stored in the persona's longterm.db via ORM (DataCreate/DataList). + * + * Credential lifecycle: + * 1. social/signup creates credential β†’ stored here + * 2. Commands load credential from here β†’ authenticate provider + * 3. lastActiveAt updated on each API call + */ + +import type { UUID } from '@system/core/types/CrossPlatformUUID'; +import { BaseEntity } from '@system/data/entities/BaseEntity'; +import { + TextField, + DateField, + EnumField, + JsonField, + CompositeIndex, + TEXT_LENGTH, +} from '@system/data/decorators/FieldDecorators'; + +export type ClaimStatus = 'pending' | 'claimed' | 'unknown'; + +@CompositeIndex({ + name: 'idx_social_creds_persona_platform', + fields: ['personaId', 'platformId'], + unique: true, +}) +export class SocialCredentialEntity extends BaseEntity { + static readonly collection = 'social_credentials'; + + get collection(): string { + return SocialCredentialEntity.collection; + } + + /** Persona who owns this credential */ + @TextField({ index: true }) + personaId!: UUID; + + /** Platform identifier (e.g., 'moltbook') */ + @TextField({ index: true }) + platformId!: string; + + /** API key / bearer token for the platform */ + @TextField({ maxLength: TEXT_LENGTH.UNLIMITED }) + apiKey!: string; + + /** Username on the platform */ + @TextField({ index: true }) + agentName!: string; + + /** URL to the agent's profile on the platform */ + @TextField({ maxLength: TEXT_LENGTH.UNLIMITED, nullable: true }) + profileUrl?: string; + + /** URL to claim/verify the account (if applicable) */ + @TextField({ maxLength: TEXT_LENGTH.UNLIMITED, nullable: true }) + claimUrl?: string; + + /** Claim/verification status */ + @EnumField({ index: true }) + claimStatus!: ClaimStatus; + + /** When the account was registered */ + @DateField({ index: true }) + registeredAt!: Date; + + /** When the credential was last used for an API call */ + @DateField({ nullable: true }) + lastActiveAt?: Date; + + /** Additional platform-specific metadata */ + @JsonField({ nullable: true }) + metadata?: Record; + + [key: string]: unknown; + + constructor() { + super(); + this.personaId = '' as UUID; + this.platformId = ''; + this.apiKey = ''; + this.agentName = ''; + this.claimStatus = 'pending'; + this.registeredAt = new Date(); + } + + validate(): { success: boolean; error?: string } { + const errors: string[] = []; + + if (!this.personaId) errors.push('personaId is required'); + if (!this.platformId?.trim()) errors.push('platformId is required'); + if (!this.apiKey?.trim()) errors.push('apiKey is required'); + if (!this.agentName?.trim()) errors.push('agentName is required'); + + const validStatuses: ClaimStatus[] = ['pending', 'claimed', 'unknown']; + if (!validStatuses.includes(this.claimStatus)) { + errors.push(`claimStatus must be one of: ${validStatuses.join(', ')}`); + } + + if (errors.length > 0) { + return { success: false, error: errors.join(', ') }; + } + return { success: true }; + } + + static override getPaginationConfig() { + return { + defaultSortField: 'registeredAt', + defaultSortDirection: 'desc' as const, + defaultPageSize: 50, + cursorField: 'registeredAt', + }; + } +} diff --git a/src/debug/jtag/system/social/shared/SocialMediaTypes.ts b/src/debug/jtag/system/social/shared/SocialMediaTypes.ts new file mode 100644 index 000000000..309dc0813 --- /dev/null +++ b/src/debug/jtag/system/social/shared/SocialMediaTypes.ts @@ -0,0 +1,173 @@ +/** + * Social Media Types - Platform-agnostic types for social media integration + * + * These types are generic and NOT tied to any specific platform. + * Platform-specific adapters (MoltbookProvider, etc.) map their API + * responses to these common types. + */ + +import type { UUID } from '@system/core/types/CrossPlatformUUID'; + +// ============ Core Content Types ============ + +export interface SocialPost { + id: string; + title: string; + content: string; + url?: string; // Link post URL + authorName: string; + authorId?: string; + community?: string; // Submolt, subreddit, etc. + communityDisplayName?: string; + votes: number; + commentCount: number; + createdAt: string; // ISO timestamp + postUrl: string; // Direct link to post on platform +} + +export interface SocialComment { + id: string; + postId: string; + parentId?: string; // For threading + content: string; + authorName: string; + authorId?: string; + votes: number; + depth: number; // Nesting level (0 = top-level) + createdAt: string; +} + +export interface SocialNotification { + id: string; + type: 'reply' | 'mention' | 'follow' | 'vote' | 'dm' | 'system'; + content: string; + authorName?: string; + postId?: string; + postTitle?: string; + commentId?: string; + read: boolean; + createdAt: string; +} + +export interface SocialProfile { + agentName: string; + displayName?: string; + description?: string; + followerCount: number; + followingCount: number; + postCount: number; + karma: number; + createdAt: string; + profileUrl: string; + metadata?: Record; +} + +export interface SocialCommunity { + name: string; + displayName: string; + description: string; + memberCount: number; + postCount: number; + createdAt: string; + isSubscribed?: boolean; +} + +export interface SocialSearchResult { + posts: SocialPost[]; + totalCount?: number; +} + +export interface SocialDM { + id: string; + fromAgent: string; + toAgent: string; + content: string; + read: boolean; + createdAt: string; +} + +// ============ Request Parameter Types ============ + +export interface SignupParams { + agentName: string; + description?: string; + metadata?: Record; +} + +export interface SignupResult { + success: boolean; + apiKey?: string; + agentName?: string; + claimUrl?: string; + verificationCode?: string; + profileUrl?: string; + error?: string; +} + +export interface CreatePostParams { + title: string; + content: string; + community?: string; + url?: string; // Link post +} + +export interface FeedParams { + sort?: 'hot' | 'new' | 'top' | 'rising'; + community?: string; + limit?: number; + personalized?: boolean; +} + +export interface CreateCommentParams { + postId: string; + content: string; + parentId?: string; // For threaded replies +} + +export interface VoteParams { + targetId: string; + targetType: 'post' | 'comment'; + direction: 'up' | 'down'; +} + +export interface SearchParams { + query: string; + type?: 'post' | 'comment' | 'agent' | 'submolt'; + limit?: number; +} + +export interface UpdateProfileParams { + description?: string; + metadata?: Record; +} + +export interface CreateCommunityParams { + name: string; + displayName: string; + description: string; +} + +// ============ Rate Limit ============ + +export interface RateLimitStatus { + allowed: boolean; + retryAfterMs?: number; + message?: string; +} + +// ============ Credential Reference ============ + +/** + * Credential data stored per-persona in their longterm.db + * Used by providers to authenticate API calls + */ +export interface SocialCredentialData { + personaId: UUID; + platformId: string; + apiKey: string; + agentName: string; + profileUrl?: string; + claimStatus: 'pending' | 'claimed' | 'unknown'; + registeredAt: string; // ISO timestamp + lastActiveAt?: string; +}