From 81f3cf3530bb9a6fe7eb6c54a28ff3231ad8af0f Mon Sep 17 00:00:00 2001 From: Bernhard Merkle Date: Sat, 31 Jan 2026 01:04:42 +0100 Subject: [PATCH 01/11] - replaced empty dummy implementation of calendar with working version (based on V2 behaviour) - enhanced calendar client login with better authentication and error handling - fixed some arguments (e.g. passing of dates/search strings( --- .../graphUtils/src/calendarClient.ts | 6 +- .../agentUtils/graphUtils/src/graphClient.ts | 20 +- .../calendar/src/calendarActionHandlerV3.ts | 491 +++++++++++++++--- 3 files changed, 445 insertions(+), 72 deletions(-) diff --git a/ts/packages/agents/agentUtils/graphUtils/src/calendarClient.ts b/ts/packages/agents/agentUtils/graphUtils/src/calendarClient.ts index e3369d5fe4..bb36e25c82 100644 --- a/ts/packages/agents/agentUtils/graphUtils/src/calendarClient.ts +++ b/ts/packages/agents/agentUtils/graphUtils/src/calendarClient.ts @@ -20,7 +20,11 @@ export class CalendarClient extends GraphClient { constructor() { super("@calendar login"); - this.login(); + // Try silent login - will succeed if there's a valid cached token + this.login().catch(() => { + // Silently ignore - user will need to run @calendar login + debug("No cached credentials, user needs to run @calendar login"); + }); this.on("connected", (client: Client) => { this.startSyncThread(client); }); diff --git a/ts/packages/agents/agentUtils/graphUtils/src/graphClient.ts b/ts/packages/agents/agentUtils/graphUtils/src/graphClient.ts index 2d3f028219..f748854bf4 100644 --- a/ts/packages/agents/agentUtils/graphUtils/src/graphClient.ts +++ b/ts/packages/agents/agentUtils/graphUtils/src/graphClient.ts @@ -159,7 +159,8 @@ export class GraphClient extends EventEmitter { const options: DeviceCodeCredentialOptions = { clientId: this._settings.clientId, tenantId: this._settings.tenantId, - disableAutomaticAuthentication: true, + // Only disable automatic auth for silent login attempts + disableAutomaticAuthentication: cb === undefined, tokenCachePersistenceOptions: { enabled: true, @@ -184,11 +185,22 @@ export class GraphClient extends EventEmitter { const credential = new DeviceCodeCredential(options); if (cb === undefined) { + // Silent auth - only possible if we have a cached auth record + if (options.authenticationRecord === undefined) { + throw new Error( + `No cached credentials. Run ${this.authCommand} to authenticate.`, + ); + } // getToken to make sure we can authenticate silently - await credential.getToken(this.MSGRAPH_AUTH_URL); - if (options.authenticationRecord !== undefined) { - return this.createClient(credential); + try { + await credential.getToken(this.MSGRAPH_AUTH_URL); + } catch (e: any) { + // Token cache may be expired - need interactive auth + throw new Error( + `Cached credentials expired. Run ${this.authCommand} to re-authenticate.`, + ); } + return this.createClient(credential); } // This will ask for user interaction diff --git a/ts/packages/agents/calendar/src/calendarActionHandlerV3.ts b/ts/packages/agents/calendar/src/calendarActionHandlerV3.ts index d71acff2e7..834304b2df 100644 --- a/ts/packages/agents/calendar/src/calendarActionHandlerV3.ts +++ b/ts/packages/agents/calendar/src/calendarActionHandlerV3.ts @@ -6,138 +6,495 @@ import { AppAgent, ActionContext, ActionResult, + SessionContext, } from "@typeagent/agent-sdk"; +import { + CommandHandlerNoParams, + CommandHandlerTable, + getCommandInterface, +} from "@typeagent/agent-sdk/helpers/command"; +import { + displayStatus, + displaySuccess, + displayWarn, +} from "@typeagent/agent-sdk/helpers/display"; +import { + createActionResultFromHtmlDisplay, + createActionResultFromError, +} from "@typeagent/agent-sdk/helpers/action"; import { CalendarActionV3 } from "./calendarActionsSchemaV3.js"; +import { + createCalendarGraphClient, + CalendarClient, +} from "graph-utils"; +import { getNWeeksDateRangeISO, generateQueryFromFuzzyDay } from "./calendarQueryHelper.js"; +import { + getDateRelativeToDayV2, + parseFuzzyDateString, + parseTimeString, +} from "typechat-utils"; import chalk from "chalk"; -// Calendar action handler V3 - simplified, grammar-friendly version +// Calendar context to hold the client +export type CalendarActionContext = { + calendarClient: CalendarClient | undefined; +}; + +// Login command handler +export class CalendarClientLoginCommandHandler + implements CommandHandlerNoParams +{ + public readonly description = "Log into MS Graph to access calendar"; + public async run(context: ActionContext) { + const calendarClient: CalendarClient | undefined = + context.sessionContext.agentContext.calendarClient; + if (calendarClient === undefined) { + throw new Error("Calendar client not initialized"); + } + if (calendarClient.isAuthenticated()) { + const name = await calendarClient.getUserAsync(); + displayWarn( + `Already logged in as ${name.displayName}<${name.mail}>`, + context, + ); + return; + } + + await calendarClient.login((prompt) => { + displayStatus(prompt, context); + }); + + const name = await calendarClient.getUserAsync(); + displaySuccess( + `Successfully logged in as ${name.displayName}<${name.mail}>`, + context, + ); + } +} + +// Logout command handler +export class CalendarClientLogoutCommandHandler + implements CommandHandlerNoParams +{ + public readonly description = "Log out of MS Graph to access calendar"; + public async run(context: ActionContext) { + const calendarClient: CalendarClient | undefined = + context.sessionContext.agentContext.calendarClient; + if (calendarClient === undefined) { + throw new Error("Calendar client not initialized"); + } + if (calendarClient.logout()) { + displaySuccess("Successfully logged out", context); + } else { + displayWarn("Already logged out", context); + } + } +} + +const handlers: CommandHandlerTable = { + description: "Calendar login command", + defaultSubCommand: "login", + commands: { + login: new CalendarClientLoginCommandHandler(), + logout: new CalendarClientLogoutCommandHandler(), + }, +}; + +// Helper function to format events as HTML +function formatEventsAsHtml(events: any[]): string { + if (!events || events.length === 0) { + return "

No events found.

"; + } + + let html = "
    "; + for (const event of events) { + const start = event.start?.dateTime + ? new Date(event.start.dateTime).toLocaleString() + : "Unknown"; + const end = event.end?.dateTime + ? new Date(event.end.dateTime).toLocaleString() + : "Unknown"; + html += `
  • ${event.subject || "No subject"}
    `; + html += `${start} - ${end}`; + if (event.location?.displayName) { + html += `
    Location: ${event.location.displayName}`; + } + html += "
  • "; + } + html += "
"; + return html; +} + +// Calendar action handler V3 - with Graph API integration export class CalendarActionHandlerV3 implements AppAgent { + public async initializeAgentContext(): Promise { + return { + calendarClient: undefined, + }; + } + + public async updateAgentContext( + enable: boolean, + context: SessionContext, + ): Promise { + if (enable) { + context.agentContext.calendarClient = await createCalendarGraphClient(); + } else { + context.agentContext.calendarClient = undefined; + } + } + public async executeAction( action: AppAction, - context: ActionContext, + context: ActionContext, ): Promise { const calendarAction = action as CalendarActionV3; + const calendarClient = context.sessionContext.agentContext.calendarClient; console.log( chalk.cyan( `\n[Calendar V3] Executing action: ${calendarAction.actionName}`, ), ); - console.log( - chalk.gray( - `Parameters: ${JSON.stringify(calendarAction.parameters, null, 2)}`, - ), - ); + + if (!calendarClient) { + return createActionResultFromError( + "Calendar client not initialized. Please run '@calendar login' first.", + ); + } + + if (!calendarClient.isAuthenticated()) { + return createActionResultFromError( + "Not logged in. Please run '@calendar login' first.", + ); + } switch (calendarAction.actionName) { case "scheduleEvent": - await this.handleScheduleEvent(calendarAction, context); - break; + return await this.handleScheduleEvent(calendarAction, context, calendarClient); case "findEvents": - await this.handleFindEvents(calendarAction, context); - break; + return await this.handleFindEvents(calendarAction, context, calendarClient); case "addParticipant": - await this.handleAddParticipant(calendarAction, context); - break; + return await this.handleAddParticipant(calendarAction, context, calendarClient); case "findTodaysEvents": - await this.handleFindTodaysEvents(context); - break; + return await this.handleFindTodaysEvents(context, calendarClient); case "findThisWeeksEvents": - await this.handleFindThisWeeksEvents(context); - break; + return await this.handleFindThisWeeksEvents(context, calendarClient); default: console.log( chalk.red( `Unknown action: ${(calendarAction as any).actionName}`, ), ); + return createActionResultFromError( + `Unknown action: ${(calendarAction as any).actionName}`, + ); } - - return undefined; } private async handleScheduleEvent( action: CalendarActionV3 & { actionName: "scheduleEvent" }, - context: ActionContext, - ): Promise { - const { description, date, time, location, participant } = + context: ActionContext, + client: CalendarClient, + ): Promise { + const { description, date, time, participant } = action.parameters; - console.log(chalk.green(`\n✓ Would schedule event:`)); - console.log(chalk.white(` Description: ${description}`)); - console.log(chalk.white(` Date: ${date}`)); - if (time) { - console.log(chalk.white(` Time: ${time}`)); + console.log(chalk.green(`\n✓ Scheduling event: ${description}`)); + + try { + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + // Parse the natural language date + let eventDate = this.parseNaturalDate(date); + if (!eventDate) { + return createActionResultFromError(`Could not parse date: ${date}`); + } + + // Parse the time and set it on the date + let startHour = 9, startMinute = 0; + if (time) { + // Try simple parsing like "2pm", "14:00" + const simpleTime = this.parseSimpleTime(time); + if (simpleTime) { + startHour = simpleTime.hours; + startMinute = simpleTime.minutes; + } else { + // Try parseTimeString which returns "HH:mm:ss" format + try { + const parsedTime = parseTimeString(time); + const [h, m] = parsedTime.split(":").map(Number); + startHour = h; + startMinute = m; + } catch { + // Fall back to default 9am + console.log(chalk.yellow(`Could not parse time: ${time}, using 9am`)); + } + } + } + + eventDate.setHours(startHour, startMinute, 0, 0); + const endDate = new Date(eventDate); + endDate.setHours(startHour + 1); // Default 1 hour duration + + const startDateTime = eventDate.toISOString(); + const endDateTime = endDate.toISOString(); + const attendees = participant ? [participant] : undefined; + + // Create the event via Graph API using correct signature + const eventId = await client.createCalendarEvent( + description, // subject + "", // body + startDateTime, // startDateTime + endDateTime, // endDateTime + timeZone, // timeZone + attendees, // attendees + ); + + if (eventId) { + const dateStr = eventDate.toLocaleDateString(); + const timeStr = eventDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + return createActionResultFromHtmlDisplay( + `

✓ Event created: ${description} on ${dateStr} at ${timeStr}

`, + ); + } else { + return createActionResultFromError("Failed to create event"); + } + } catch (error: any) { + console.error(chalk.red(`Error creating event: ${error.message}`)); + return createActionResultFromError(`Failed to create event: ${error.message}`); + } + } + + private parseNaturalDate(dateStr: string): Date | undefined { + // Handle special keywords + const lowerDate = dateStr.toLowerCase().trim(); + + if (lowerDate === "today") { + return new Date(); + } + if (lowerDate === "tomorrow") { + const d = new Date(); + d.setDate(d.getDate() + 1); + return d; + } + + // Try parsing relative day ("next Monday", "this Friday") + const relativeDate = getDateRelativeToDayV2(dateStr); + if (relativeDate) { + return relativeDate; } - if (location) { - console.log(chalk.white(` Location: ${location}`)); + + // Try parsing fuzzy date string ("July 15", "2026-03-15") + const fuzzyDate = parseFuzzyDateString(dateStr); + if (fuzzyDate) { + return fuzzyDate; } - if (participant) { - console.log(chalk.white(` Participant: ${participant}`)); + + // Try ISO format directly + const isoDate = new Date(dateStr); + if (!isNaN(isoDate.getTime())) { + return isoDate; } + + return undefined; + } - context.actionIO.appendDisplay( - `Scheduled: ${description} on ${date}${time ? ` at ${time}` : ""}`, - ); + private parseSimpleTime(timeStr: string): { hours: number; minutes: number } | undefined { + // Handle "2pm", "2:30pm", "14:00", "noon", "midnight" + const lowerTime = timeStr.toLowerCase().trim(); + + if (lowerTime === "noon") { + return { hours: 12, minutes: 0 }; + } + if (lowerTime === "midnight") { + return { hours: 0, minutes: 0 }; + } + + // Match patterns like "2pm", "2:30pm", "14:00" + const amPmMatch = lowerTime.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/i); + if (amPmMatch) { + let hours = parseInt(amPmMatch[1], 10); + const minutes = amPmMatch[2] ? parseInt(amPmMatch[2], 10) : 0; + const period = amPmMatch[3]?.toLowerCase(); + + if (period === "pm" && hours < 12) { + hours += 12; + } else if (period === "am" && hours === 12) { + hours = 0; + } + + return { hours, minutes }; + } + + return undefined; } private async handleFindEvents( action: CalendarActionV3 & { actionName: "findEvents" }, - context: ActionContext, - ): Promise { - const { date, description, participant } = action.parameters; + context: ActionContext, + client: CalendarClient, + ): Promise { + const { date, description } = action.parameters; - console.log(chalk.green(`\n✓ Would search for events:`)); - if (date) { - console.log(chalk.white(` Date: ${date}`)); - } - if (description) { - console.log(chalk.white(` Description: ${description}`)); - } - if (participant) { - console.log(chalk.white(` Participant: ${participant}`)); - } + console.log(chalk.green(`\n✓ Searching for events`)); - context.actionIO.appendDisplay( - `Searching for events${date ? ` on ${date}` : ""}${description ? ` matching "${description}"` : ""}`, - ); + try { + let events: any[] = []; + + if (description) { + // Search by description using embeddings + events = await client.findEventsFromEmbeddings(description) as any[]; + } else if (date) { + // Try to use generateQueryFromFuzzyDay for natural language dates + const query = generateQueryFromFuzzyDay(date); + if (query) { + events = await client.findCalendarEventsByDateRange(query); + } else { + // Fall back to parsing the date manually + const parsedDate = this.parseNaturalDate(date); + if (parsedDate) { + const startDate = new Date(parsedDate); + startDate.setHours(0, 0, 0, 0); + const endDate = new Date(parsedDate); + endDate.setHours(23, 59, 59, 999); + const manualQuery = `startDateTime=${startDate.toISOString()}&endDateTime=${endDate.toISOString()}`; + events = await client.findCalendarEventsByDateRange(manualQuery); + } else { + return createActionResultFromError(`Could not parse date: ${date}`); + } + } + } else { + // Default: get this week's events + const dateRange = getNWeeksDateRangeISO(1); + const query = `startDateTime=${dateRange.startDateTime}&endDateTime=${dateRange.endDateTime}`; + events = await client.findCalendarEventsByDateRange(query); + } + + if (!events || events.length === 0) { + return createActionResultFromHtmlDisplay( + "

No events found matching your criteria.

", + ); + } + + return createActionResultFromHtmlDisplay(formatEventsAsHtml(events)); + } catch (error: any) { + console.error(chalk.red(`Error finding events: ${error.message}`)); + return createActionResultFromError(`Failed to find events: ${error.message}`); + } } private async handleAddParticipant( action: CalendarActionV3 & { actionName: "addParticipant" }, - context: ActionContext, - ): Promise { + context: ActionContext, + client: CalendarClient, + ): Promise { const { description, participant } = action.parameters; - console.log(chalk.green(`\n✓ Would add participant:`)); - console.log(chalk.white(` Event: ${description}`)); - console.log(chalk.white(` Participant: ${participant}`)); + console.log(chalk.green(`\n✓ Adding participant ${participant} to ${description}`)); - context.actionIO.appendDisplay( - `Added ${participant} to ${description}`, - ); + try { + // Find the event first - returns event objects despite type saying string[] + const events = await client.findEventsFromEmbeddings(description) as any[]; + if (!events || events.length === 0) { + return createActionResultFromError(`Could not find event: ${description}`); + } + + const event = events[0]; + // Add participant to the event using the correct API + await client.addParticipantsToExistingMeeting( + event.id, + event.attendees || [], + [participant], + ); + + return createActionResultFromHtmlDisplay( + `

✓ Added ${participant} to ${description}

`, + ); + } catch (error: any) { + console.error(chalk.red(`Error adding participant: ${error.message}`)); + return createActionResultFromError(`Failed to add participant: ${error.message}`); + } } private async handleFindTodaysEvents( - context: ActionContext, - ): Promise { - console.log(chalk.green(`\n✓ Would find today's events`)); + context: ActionContext, + client: CalendarClient, + ): Promise { + console.log(chalk.green(`\n✓ Finding today's events`)); - context.actionIO.appendDisplay(`Showing today's schedule`); + try { + const today = new Date(); + const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0); + const endOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59, 999); + + const query = `startDateTime=${startOfDay.toISOString()}&endDateTime=${endOfDay.toISOString()}`; + const events = await client.findCalendarEventsByDateRange(query); + + if (!events || events.length === 0) { + return createActionResultFromHtmlDisplay( + "

No events scheduled for today.

", + ); + } + + return createActionResultFromHtmlDisplay( + `

Today's Schedule

${formatEventsAsHtml(events)}`, + ); + } catch (error: any) { + console.error(chalk.red(`Error finding today's events: ${error.message}`)); + return createActionResultFromError(`Failed to find today's events: ${error.message}`); + } } private async handleFindThisWeeksEvents( - context: ActionContext, - ): Promise { - console.log(chalk.green(`\n✓ Would find this week's events`)); + context: ActionContext, + client: CalendarClient, + ): Promise { + console.log(chalk.green(`\n✓ Finding this week's events`)); + + try { + const dateRange = getNWeeksDateRangeISO(1); + const query = `startDateTime=${dateRange.startDateTime}&endDateTime=${dateRange.endDateTime}`; + const events = await client.findCalendarEventsByDateRange(query); - context.actionIO.appendDisplay(`Showing this week's schedule`); + if (!events || events.length === 0) { + return createActionResultFromHtmlDisplay( + "

No events scheduled for this week.

", + ); + } + + return createActionResultFromHtmlDisplay( + `

This Week's Schedule

${formatEventsAsHtml(events)}`, + ); + } catch (error: any) { + console.error(chalk.red(`Error finding this week's events: ${error.message}`)); + return createActionResultFromError(`Failed to find this week's events: ${error.message}`); + } } } // Instantiate function required by the agent loader export function instantiate(): AppAgent { - return new CalendarActionHandlerV3(); + return { + initializeAgentContext: async () => ({ + calendarClient: undefined, + }), + updateAgentContext: async ( + enable: boolean, + context: SessionContext, + ) => { + if (enable) { + context.agentContext.calendarClient = await createCalendarGraphClient(); + } else { + context.agentContext.calendarClient = undefined; + } + }, + executeAction: async (action: AppAction, context: ActionContext) => { + const handler = new CalendarActionHandlerV3(); + return handler.executeAction(action, context); + }, + ...getCommandInterface(handlers), + }; } // Validation functions for entity types From eb73a40f23547f5c72234c0f07c1e68c9e734649 Mon Sep 17 00:00:00 2001 From: Bernhard Merkle Date: Wed, 4 Feb 2026 00:42:45 +0100 Subject: [PATCH 02/11] - implemented a local Player - there are currently problems with spotify to get a developer API key and application, hence here is a local Player basic implementation as alternative --- ts/packages/agents/playerLocal/README.md | 127 ++++++ ts/packages/agents/playerLocal/package.json | 42 ++ .../src/agent/localPlayerCommands.ts | 388 +++++++++++++++++ .../src/agent/localPlayerHandlers.ts | 376 ++++++++++++++++ .../src/agent/localPlayerManifest.json | 14 + .../src/agent/localPlayerSchema.agr | 95 ++++ .../src/agent/localPlayerSchema.json | 6 + .../src/agent/localPlayerSchema.ts | 184 ++++++++ .../playerLocal/src/localPlayerService.ts | 408 ++++++++++++++++++ .../agents/playerLocal/src/tsconfig.json | 10 + .../defaultAgentProvider/data/config.json | 3 + ts/packages/defaultAgentProvider/package.json | 1 + ts/pnpm-lock.yaml | 57 +++ 13 files changed, 1711 insertions(+) create mode 100644 ts/packages/agents/playerLocal/README.md create mode 100644 ts/packages/agents/playerLocal/package.json create mode 100644 ts/packages/agents/playerLocal/src/agent/localPlayerCommands.ts create mode 100644 ts/packages/agents/playerLocal/src/agent/localPlayerHandlers.ts create mode 100644 ts/packages/agents/playerLocal/src/agent/localPlayerManifest.json create mode 100644 ts/packages/agents/playerLocal/src/agent/localPlayerSchema.agr create mode 100644 ts/packages/agents/playerLocal/src/agent/localPlayerSchema.json create mode 100644 ts/packages/agents/playerLocal/src/agent/localPlayerSchema.ts create mode 100644 ts/packages/agents/playerLocal/src/localPlayerService.ts create mode 100644 ts/packages/agents/playerLocal/src/tsconfig.json diff --git a/ts/packages/agents/playerLocal/README.md b/ts/packages/agents/playerLocal/README.md new file mode 100644 index 0000000000..b1ed6b59f3 --- /dev/null +++ b/ts/packages/agents/playerLocal/README.md @@ -0,0 +1,127 @@ +# Local Music Player TypeAgent + +A TypeAgent for playing local audio files without requiring any external service like Spotify. + +## Features + +- **Play local audio files** - MP3, WAV, OGG, FLAC, M4A, AAC, WMA +- **Queue management** - Add files to queue, show queue, clear queue +- **Playback controls** - Play, pause, resume, stop, next, previous +- **Volume control** - Set volume, mute/unmute +- **Shuffle and repeat** - Shuffle mode, repeat one/all +- **File search** - Search for files in your music folder +- **Cross-platform** - Works on Windows, macOS, and Linux + +## Setup + +No external API keys required! The agent uses the system's built-in audio capabilities: + +- **Windows**: Uses PowerShell with Windows Media Player +- **macOS**: Uses `afplay` +- **Linux**: Uses `mpv` (install with `sudo apt install mpv`) + +## Configuration + +Set your music folder using the command: +``` +@localPlayer folder set /path/to/music +``` + +Or use natural language: +``` +set music folder to C:\Users\Me\Music +``` + +## Usage + +### Enable the agent + +In the shell or interactive mode: +``` +@config localPlayer on +``` + +### Example commands + +**Play music:** +``` +play some music +play song.mp3 +play all songs in the folder +``` + +**Control playback:** +``` +pause +resume +stop +next track +previous track +``` + +**Volume:** +``` +set volume to 50 +turn up the volume +mute +``` + +**Queue management:** +``` +show the queue +add rock song to queue +clear the queue +play the third track +``` + +**Browse files:** +``` +list files +search for beethoven +show music folder +``` + +## Available Actions + +| Action | Description | +|--------|-------------| +| `playFile` | Play a specific audio file | +| `playFolder` | Play all audio files in a folder | +| `playFromQueue` | Play a track from the queue by number | +| `status` | Show current playback status | +| `pause` | Pause playback | +| `resume` | Resume playback | +| `stop` | Stop playback | +| `next` | Skip to next track | +| `previous` | Go to previous track | +| `shuffle` | Turn shuffle on/off | +| `repeat` | Set repeat mode (off/one/all) | +| `setVolume` | Set volume level (0-100) | +| `changeVolume` | Adjust volume by amount | +| `mute` | Mute audio | +| `unmute` | Unmute audio | +| `listFiles` | List audio files in folder | +| `searchFiles` | Search for files by name | +| `addToQueue` | Add file to playback queue | +| `clearQueue` | Clear the queue | +| `showQueue` | Display the queue | +| `setMusicFolder` | Set default music folder | +| `showMusicFolder` | Show current music folder | + +## Supported Audio Formats + +- MP3 (.mp3) +- WAV (.wav) +- OGG (.ogg) +- FLAC (.flac) +- M4A (.m4a) +- AAC (.aac) +- WMA (.wma) + +## Trademarks + +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft +trademarks or logos is subject to and must follow +[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). +Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. +Any use of third-party trademarks or logos are subject to those third-party's policies. diff --git a/ts/packages/agents/playerLocal/package.json b/ts/packages/agents/playerLocal/package.json new file mode 100644 index 0000000000..85db5f7dec --- /dev/null +++ b/ts/packages/agents/playerLocal/package.json @@ -0,0 +1,42 @@ +{ + "name": "music-local", + "version": "0.0.1", + "private": true, + "description": "Local media player agent for TypeAgent", + "homepage": "https://github.com/microsoft/TypeAgent#readme", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/TypeAgent.git", + "directory": "ts/packages/agents/playerLocal" + }, + "license": "MIT", + "author": "Microsoft", + "type": "module", + "exports": { + "./agent/manifest": "./src/agent/localPlayerManifest.json", + "./agent/handlers": "./dist/agent/localPlayerHandlers.js" + }, + "scripts": { + "agc": "agc -i ./src/agent/localPlayerSchema.agr -o ./dist/agent/localPlayerSchema.ag.json", + "asc": "asc -i ./src/agent/localPlayerSchema.ts -o ./dist/agent/localPlayerSchema.pas.json -t LocalPlayerActions -e LocalPlayerEntities", + "build": "concurrently npm:tsc npm:asc npm:agc", + "clean": "rimraf --glob dist *.tsbuildinfo *.done.build.log", + "tsc": "tsc -p src" + }, + "dependencies": { + "@typeagent/agent-sdk": "workspace:*", + "@typeagent/common-utils": "workspace:*", + "chalk": "^5.4.1", + "debug": "^4.4.0", + "dotenv": "^16.3.1", + "play-sound": "^1.1.6" + }, + "devDependencies": { + "@typeagent/action-schema-compiler": "workspace:*", + "@types/debug": "^4.1.12", + "action-grammar-compiler": "workspace:*", + "concurrently": "^9.1.2", + "rimraf": "^6.0.1", + "typescript": "~5.4.5" + } +} diff --git a/ts/packages/agents/playerLocal/src/agent/localPlayerCommands.ts b/ts/packages/agents/playerLocal/src/agent/localPlayerCommands.ts new file mode 100644 index 0000000000..5266af0fc2 --- /dev/null +++ b/ts/packages/agents/playerLocal/src/agent/localPlayerCommands.ts @@ -0,0 +1,388 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + ActionContext, + AppAgentCommandInterface, + ParsedCommandParams, +} from "@typeagent/agent-sdk"; +import { + CommandHandler, + CommandHandlerNoParams, + CommandHandlerTable, + getCommandInterface, +} from "@typeagent/agent-sdk/helpers/command"; +import { + displayStatus, + displaySuccess, + displayWarn, + displayError, +} from "@typeagent/agent-sdk/helpers/display"; +import { LocalPlayerActionContext } from "./localPlayerHandlers.js"; + +// Helper to get service with error handling +function getService(context: ActionContext) { + const service = context.sessionContext.agentContext.playerService; + if (!service) { + displayError("Local player not initialized. Enable it with: @config localPlayer on", context); + return undefined; + } + return service; +} + +// Status command handler +class StatusCommandHandler implements CommandHandlerNoParams { + public readonly description = "Show local player status"; + + public async run(context: ActionContext) { + const service = getService(context); + if (!service) return; + + const state = service.getState(); + + if (state.currentTrack) { + const status = state.isPlaying ? "▶️ Playing" : state.isPaused ? "⏸️ Paused" : "⏹️ Stopped"; + displaySuccess( + `${status}: ${state.currentTrack.name}\n` + + `Volume: ${state.volume}%${state.isMuted ? " (muted)" : ""}\n` + + `Shuffle: ${state.shuffle ? "On" : "Off"} | Repeat: ${state.repeat}\n` + + `Queue: ${state.currentIndex + 1}/${state.queue.length} tracks`, + context, + ); + } else { + displayWarn("No track loaded. Use '@localPlayer play' to start.", context); + } + } +} + +// Play command handler with optional file parameter +const playParameters = { + args: { + file: { + description: "File name or path to play (optional - plays first file if not specified)", + optional: true, + }, + }, +} as const; + +const playHandler: CommandHandler = { + description: "Play an audio file or resume playback", + parameters: playParameters, + run: async ( + context: ActionContext, + params: ParsedCommandParams, + ) => { + const service = getService(context); + if (!service) return; + + const fileName = params.args.file; + + if (fileName) { + const success = await service.playFile(fileName); + if (success) { + const state = service.getState(); + displaySuccess(`▶️ Playing: ${state.currentTrack?.name}`, context); + } else { + displayError(`Could not find or play: ${fileName}`, context); + } + } else { + // Resume or play first file + const state = service.getState(); + if (state.isPaused) { + service.resume(); + displaySuccess(`▶️ Resumed: ${state.currentTrack?.name}`, context); + } else if (state.queue.length > 0) { + await service.playFromQueue(state.currentIndex + 1); + displaySuccess(`▶️ Playing: ${service.getState().currentTrack?.name}`, context); + } else { + // Play first file from folder + const success = await service.playFolder(); + if (success) { + displaySuccess(`▶️ Playing: ${service.getState().currentTrack?.name}`, context); + } else { + displayWarn("No audio files found. Set music folder with: @localPlayer setfolder ", context); + } + } + } + }, +}; + +// Pause command +class PauseCommandHandler implements CommandHandlerNoParams { + public readonly description = "Pause playback"; + + public async run(context: ActionContext) { + const service = getService(context); + if (!service) return; + + service.pause(); + displaySuccess("⏸️ Paused", context); + } +} + +// Resume command +class ResumeCommandHandler implements CommandHandlerNoParams { + public readonly description = "Resume playback"; + + public async run(context: ActionContext) { + const service = getService(context); + if (!service) return; + + service.resume(); + const state = service.getState(); + displaySuccess(`▶️ Resumed: ${state.currentTrack?.name || ""}`, context); + } +} + +// Stop command +class StopCommandHandler implements CommandHandlerNoParams { + public readonly description = "Stop playback"; + + public async run(context: ActionContext) { + const service = getService(context); + if (!service) return; + + service.stop(); + displaySuccess("⏹️ Stopped", context); + } +} + +// Next command +class NextCommandHandler implements CommandHandlerNoParams { + public readonly description = "Play next track"; + + public async run(context: ActionContext) { + const service = getService(context); + if (!service) return; + + const success = await service.next(); + if (success) { + const state = service.getState(); + displaySuccess(`⏭️ Next: ${state.currentTrack?.name}`, context); + } else { + displayWarn("No next track available", context); + } + } +} + +// Previous command +class PrevCommandHandler implements CommandHandlerNoParams { + public readonly description = "Play previous track"; + + public async run(context: ActionContext) { + const service = getService(context); + if (!service) return; + + const success = await service.previous(); + if (success) { + const state = service.getState(); + displaySuccess(`⏮️ Previous: ${state.currentTrack?.name}`, context); + } else { + displayWarn("No previous track available", context); + } + } +} + +// Folder command - show current folder +class FolderCommandHandler implements CommandHandlerNoParams { + public readonly description = "Show current music folder"; + + public async run(context: ActionContext) { + const service = getService(context); + if (!service) return; + + const folder = service.getMusicFolder(); + displayStatus(`📁 Music folder: ${folder}`, context); + } +} + +// Set folder command with parameter +const setFolderParameters = { + args: { + path: { + description: "Path to the music folder", + }, + }, +} as const; + +const setFolderHandler: CommandHandler = { + description: "Set the music folder path", + parameters: setFolderParameters, + run: async ( + context: ActionContext, + params: ParsedCommandParams, + ) => { + const service = getService(context); + if (!service) return; + + const folderPath = params.args.path; + const success = service.setMusicFolder(folderPath); + + if (success) { + const files = service.listFiles(); + displaySuccess(`📁 Music folder set to: ${folderPath}\nFound ${files.length} audio files`, context); + } else { + displayError(`Invalid folder path: ${folderPath}`, context); + } + }, +}; + +// List command +class ListCommandHandler implements CommandHandlerNoParams { + public readonly description = "List audio files in music folder"; + + public async run(context: ActionContext) { + const service = getService(context); + if (!service) return; + + const files = service.listFiles(); + + if (files.length === 0) { + displayWarn("No audio files found in music folder", context); + return; + } + + const fileList = files.slice(0, 20).map((f, i) => + `${i + 1}. ${f.name}` + ).join("\n"); + + let message = `🎵 Found ${files.length} audio files:\n${fileList}`; + if (files.length > 20) { + message += `\n...and ${files.length - 20} more`; + } + + displaySuccess(message, context); + } +} + +// Queue command +class QueueCommandHandler implements CommandHandlerNoParams { + public readonly description = "Show playback queue"; + + public async run(context: ActionContext) { + const service = getService(context); + if (!service) return; + + const queue = service.getQueue(); + const state = service.getState(); + + if (queue.length === 0) { + displayWarn("Queue is empty", context); + return; + } + + const queueList = queue.slice(0, 20).map((track, i) => { + const current = i === state.currentIndex ? " ▶️" : ""; + return `${i + 1}. ${track.name}${current}`; + }).join("\n"); + + let message = `📋 Queue (${queue.length} tracks):\n${queueList}`; + if (queue.length > 20) { + message += `\n...and ${queue.length - 20} more`; + } + + displaySuccess(message, context); + } +} + +// Clear command +class ClearCommandHandler implements CommandHandlerNoParams { + public readonly description = "Clear playback queue"; + + public async run(context: ActionContext) { + const service = getService(context); + if (!service) return; + + service.clearQueue(); + displaySuccess("🗑️ Queue cleared", context); + } +} + +// Shuffle command +class ShuffleCommandHandler implements CommandHandlerNoParams { + public readonly description = "Toggle shuffle mode"; + + public async run(context: ActionContext) { + const service = getService(context); + if (!service) return; + + const state = service.getState(); + service.setShuffle(!state.shuffle); + displaySuccess(`🔀 Shuffle: ${!state.shuffle ? "On" : "Off"}`, context); + } +} + +// Volume command with parameter +const volumeParameters = { + args: { + level: { + description: "Volume level (0-100)", + }, + }, +} as const; + +const volumeHandler: CommandHandler = { + description: "Set volume level (0-100)", + parameters: volumeParameters, + run: async ( + context: ActionContext, + params: ParsedCommandParams, + ) => { + const service = getService(context); + if (!service) return; + + const level = parseInt(params.args.level, 10); + if (isNaN(level) || level < 0 || level > 100) { + displayError("Volume must be a number between 0 and 100", context); + return; + } + + service.setVolume(level); + displaySuccess(`🔊 Volume: ${level}%`, context); + }, +}; + +// Mute command +class MuteCommandHandler implements CommandHandlerNoParams { + public readonly description = "Toggle mute"; + + public async run(context: ActionContext) { + const service = getService(context); + if (!service) return; + + const state = service.getState(); + if (state.isMuted) { + service.unmute(); + displaySuccess(`🔊 Unmuted (Volume: ${state.volume}%)`, context); + } else { + service.mute(); + displaySuccess("🔇 Muted", context); + } + } +} + +const handlers: CommandHandlerTable = { + description: "Local music player commands", + defaultSubCommand: "status", + commands: { + status: new StatusCommandHandler(), + play: playHandler, + pause: new PauseCommandHandler(), + resume: new ResumeCommandHandler(), + stop: new StopCommandHandler(), + next: new NextCommandHandler(), + prev: new PrevCommandHandler(), + folder: new FolderCommandHandler(), + setfolder: setFolderHandler, + list: new ListCommandHandler(), + queue: new QueueCommandHandler(), + clear: new ClearCommandHandler(), + shuffle: new ShuffleCommandHandler(), + volume: volumeHandler, + mute: new MuteCommandHandler(), + }, +}; + +export function getLocalPlayerCommandInterface(): AppAgentCommandInterface { + return getCommandInterface(handlers); +} diff --git a/ts/packages/agents/playerLocal/src/agent/localPlayerHandlers.ts b/ts/packages/agents/playerLocal/src/agent/localPlayerHandlers.ts new file mode 100644 index 0000000000..124f9be5db --- /dev/null +++ b/ts/packages/agents/playerLocal/src/agent/localPlayerHandlers.ts @@ -0,0 +1,376 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import chalk from "chalk"; +import { + AppAgent, + SessionContext, + ActionContext, + AppAgentEvent, + TypeAgentAction, +} from "@typeagent/agent-sdk"; +import { + createActionResultFromHtmlDisplay, + createActionResultFromError, +} from "@typeagent/agent-sdk/helpers/action"; +import { LocalPlayerActions } from "./localPlayerSchema.js"; +import { getLocalPlayerService, LocalPlayerService } from "../localPlayerService.js"; +import { getLocalPlayerCommandInterface } from "./localPlayerCommands.js"; +import registerDebug from "debug"; + +const debug = registerDebug("typeagent:localPlayer"); + +export function instantiate(): AppAgent { + return { + initializeAgentContext: initializeLocalPlayerContext, + updateAgentContext: updateLocalPlayerContext, + executeAction: executeLocalPlayerAction, + ...getLocalPlayerCommandInterface(), + }; +} + +export type LocalPlayerActionContext = { + playerService: LocalPlayerService | undefined; +}; + +async function initializeLocalPlayerContext(): Promise { + return { + playerService: undefined, + }; +} + +async function updateLocalPlayerContext( + enable: boolean, + context: SessionContext, +): Promise { + if (enable) { + if (context.agentContext.playerService) { + return; + } + try { + context.agentContext.playerService = getLocalPlayerService(); + const musicFolder = context.agentContext.playerService.getMusicFolder(); + const message = `Local player enabled. Music folder: ${musicFolder}`; + debug(message); + context.notify(AppAgentEvent.Info, chalk.green(message)); + } catch (e: any) { + const message = `Failed to initialize local player: ${e.message}`; + debug(message); + context.notify(AppAgentEvent.Error, chalk.red(message)); + } + } else { + if (context.agentContext.playerService) { + context.agentContext.playerService.stop(); + context.agentContext.playerService = undefined; + } + } +} + +async function executeLocalPlayerAction( + action: TypeAgentAction, + context: ActionContext, +) { + const playerService = context.sessionContext.agentContext.playerService; + + if (!playerService) { + return createActionResultFromError( + "Local player not initialized. Use '@localPlayer on' to enable.", + ); + } + + try { + switch (action.actionName) { + case "playFile": + return handlePlayFile(playerService, action.parameters.fileName); + + case "playFolder": + return handlePlayFolder( + playerService, + action.parameters?.folderPath, + action.parameters?.shuffle, + ); + + case "playFromQueue": + return handlePlayFromQueue(playerService, action.parameters.trackNumber); + + case "status": + return handleStatus(playerService); + + case "pause": + return handlePause(playerService); + + case "resume": + return handleResume(playerService); + + case "stop": + return handleStop(playerService); + + case "next": + return handleNext(playerService); + + case "previous": + return handlePrevious(playerService); + + case "shuffle": + return handleShuffle(playerService, action.parameters.on); + + case "repeat": + return handleRepeat(playerService, action.parameters.mode); + + case "setVolume": + return handleSetVolume(playerService, action.parameters.level); + + case "changeVolume": + return handleChangeVolume(playerService, action.parameters.amount); + + case "mute": + return handleMute(playerService); + + case "unmute": + return handleUnmute(playerService); + + case "listFiles": + return handleListFiles(playerService, action.parameters?.folderPath); + + case "searchFiles": + return handleSearchFiles(playerService, action.parameters.query); + + case "addToQueue": + return handleAddToQueue(playerService, action.parameters.fileName); + + case "clearQueue": + return handleClearQueue(playerService); + + case "showQueue": + return handleShowQueue(playerService); + + case "setMusicFolder": + return handleSetMusicFolder(playerService, action.parameters.folderPath); + + case "showMusicFolder": + return handleShowMusicFolder(playerService); + + default: + return createActionResultFromError( + `Unknown action: ${(action as any).actionName}`, + ); + } + } catch (error: any) { + return createActionResultFromError(`Error: ${error.message}`); + } +} + +// Action handlers + +async function handlePlayFile(service: LocalPlayerService, fileName: string) { + const success = await service.playFile(fileName); + if (success) { + const state = service.getState(); + return createActionResultFromHtmlDisplay( + `

▶️ Now playing: ${state.currentTrack?.name || fileName}

`, + ); + } + return createActionResultFromError(`Could not find or play: ${fileName}`); +} + +async function handlePlayFolder(service: LocalPlayerService, folderPath?: string, shuffle?: boolean) { + const success = await service.playFolder(folderPath, shuffle || false); + if (success) { + const state = service.getState(); + return createActionResultFromHtmlDisplay( + `

▶️ Playing ${state.queue.length} tracks from folder${shuffle ? " (shuffled)" : ""}

+

Now playing: ${state.currentTrack?.name}

`, + ); + } + return createActionResultFromError("No audio files found in the specified folder"); +} + +async function handlePlayFromQueue(service: LocalPlayerService, trackNumber: number) { + const success = await service.playFromQueue(trackNumber); + if (success) { + const state = service.getState(); + return createActionResultFromHtmlDisplay( + `

▶️ Now playing track ${trackNumber}: ${state.currentTrack?.name}

`, + ); + } + return createActionResultFromError(`Invalid track number: ${trackNumber}`); +} + +function handleStatus(service: LocalPlayerService) { + const state = service.getState(); + + let statusHtml = "

🎵 Local Player Status

"; + + if (state.currentTrack) { + const playIcon = state.isPlaying ? "▶️" : state.isPaused ? "⏸️" : "⏹️"; + statusHtml += `

${playIcon} ${state.currentTrack.name}

`; + } else { + statusHtml += "

No track loaded

"; + } + + statusHtml += `

Volume: ${state.volume}%${state.isMuted ? " (muted)" : ""}

`; + statusHtml += `

Shuffle: ${state.shuffle ? "On" : "Off"} | Repeat: ${state.repeat}

`; + statusHtml += `

Queue: ${state.queue.length} tracks

`; + + return createActionResultFromHtmlDisplay(statusHtml); +} + +function handlePause(service: LocalPlayerService) { + service.pause(); + return createActionResultFromHtmlDisplay("

⏸️ Paused

"); +} + +function handleResume(service: LocalPlayerService) { + service.resume(); + return createActionResultFromHtmlDisplay("

▶️ Resumed

"); +} + +function handleStop(service: LocalPlayerService) { + service.stop(); + return createActionResultFromHtmlDisplay("

⏹️ Stopped

"); +} + +async function handleNext(service: LocalPlayerService) { + const success = await service.next(); + if (success) { + const state = service.getState(); + return createActionResultFromHtmlDisplay( + `

⏭️ Next: ${state.currentTrack?.name}

`, + ); + } + return createActionResultFromError("No next track available"); +} + +async function handlePrevious(service: LocalPlayerService) { + const success = await service.previous(); + if (success) { + const state = service.getState(); + return createActionResultFromHtmlDisplay( + `

⏮️ Previous: ${state.currentTrack?.name}

`, + ); + } + return createActionResultFromError("No previous track available"); +} + +function handleShuffle(service: LocalPlayerService, on: boolean) { + service.setShuffle(on); + return createActionResultFromHtmlDisplay( + `

🔀 Shuffle: ${on ? "On" : "Off"}

`, + ); +} + +function handleRepeat(service: LocalPlayerService, mode: "off" | "one" | "all") { + service.setRepeat(mode); + const modeText = mode === "one" ? "Repeat One 🔂" : mode === "all" ? "Repeat All 🔁" : "Off"; + return createActionResultFromHtmlDisplay(`

Repeat: ${modeText}

`); +} + +function handleSetVolume(service: LocalPlayerService, level: number) { + service.setVolume(level); + return createActionResultFromHtmlDisplay(`

🔊 Volume: ${level}%

`); +} + +function handleChangeVolume(service: LocalPlayerService, amount: number) { + service.changeVolume(amount); + const state = service.getState(); + return createActionResultFromHtmlDisplay(`

🔊 Volume: ${state.volume}%

`); +} + +function handleMute(service: LocalPlayerService) { + service.mute(); + return createActionResultFromHtmlDisplay("

🔇 Muted

"); +} + +function handleUnmute(service: LocalPlayerService) { + service.unmute(); + return createActionResultFromHtmlDisplay("

🔊 Unmuted

"); +} + +function handleListFiles(service: LocalPlayerService, folderPath?: string) { + const files = service.listFiles(folderPath); + + if (files.length === 0) { + return createActionResultFromHtmlDisplay("

No audio files found

"); + } + + const fileListHtml = files.slice(0, 20).map((f, i) => + `
  • ${i + 1}. ${f.name}
  • ` + ).join(""); + + let html = `

    📁 Audio Files (${files.length} total)

      ${fileListHtml}
    `; + if (files.length > 20) { + html += `

    ...and ${files.length - 20} more

    `; + } + + return createActionResultFromHtmlDisplay(html); +} + +function handleSearchFiles(service: LocalPlayerService, query: string) { + const files = service.searchFiles(query); + + if (files.length === 0) { + return createActionResultFromHtmlDisplay(`

    No files found matching: ${query}

    `); + } + + const fileListHtml = files.slice(0, 20).map((f, i) => + `
  • ${i + 1}. ${f.name}
  • ` + ).join(""); + + let html = `

    🔍 Search Results for "${query}" (${files.length} found)

      ${fileListHtml}
    `; + + return createActionResultFromHtmlDisplay(html); +} + +function handleAddToQueue(service: LocalPlayerService, fileName: string) { + const success = service.addFileToQueue(fileName); + if (success) { + const state = service.getState(); + return createActionResultFromHtmlDisplay( + `

    ➕ Added to queue: ${fileName} (${state.queue.length} in queue)

    `, + ); + } + return createActionResultFromError(`Could not find: ${fileName}`); +} + +function handleClearQueue(service: LocalPlayerService) { + service.clearQueue(); + return createActionResultFromHtmlDisplay("

    🗑️ Queue cleared

    "); +} + +function handleShowQueue(service: LocalPlayerService) { + const queue = service.getQueue(); + const state = service.getState(); + + if (queue.length === 0) { + return createActionResultFromHtmlDisplay("

    Queue is empty

    "); + } + + const queueHtml = queue.slice(0, 20).map((track, i) => { + const current = i === state.currentIndex ? " ▶️" : ""; + return `
  • ${i + 1}. ${track.name}${current}
  • `; + }).join(""); + + let html = `

    📋 Playback Queue (${queue.length} tracks)

      ${queueHtml}
    `; + if (queue.length > 20) { + html += `

    ...and ${queue.length - 20} more

    `; + } + + return createActionResultFromHtmlDisplay(html); +} + +function handleSetMusicFolder(service: LocalPlayerService, folderPath: string) { + const success = service.setMusicFolder(folderPath); + if (success) { + return createActionResultFromHtmlDisplay( + `

    📁 Music folder set to: ${folderPath}

    `, + ); + } + return createActionResultFromError(`Invalid folder path: ${folderPath}`); +} + +function handleShowMusicFolder(service: LocalPlayerService) { + const folder = service.getMusicFolder(); + return createActionResultFromHtmlDisplay( + `

    📁 Music folder: ${folder}

    `, + ); +} diff --git a/ts/packages/agents/playerLocal/src/agent/localPlayerManifest.json b/ts/packages/agents/playerLocal/src/agent/localPlayerManifest.json new file mode 100644 index 0000000000..ed06df7b62 --- /dev/null +++ b/ts/packages/agents/playerLocal/src/agent/localPlayerManifest.json @@ -0,0 +1,14 @@ +{ + "emojiChar": "🎵", + "description": "Agent to play local music files", + "schema": { + "description": "Local Music Player agent that lets you play and control local audio files.", + "schemaFile": "./localPlayerSchema.ts", + "compiledSchemaFile": "../../dist/agent/localPlayerSchema.pas.json", + "grammarFile": "../../dist/agent/localPlayerSchema.ag.json", + "schemaType": { + "action": "LocalPlayerActions", + "entity": "LocalPlayerEntities" + } + } +} diff --git a/ts/packages/agents/playerLocal/src/agent/localPlayerSchema.agr b/ts/packages/agents/playerLocal/src/agent/localPlayerSchema.agr new file mode 100644 index 0000000000..340d4fa78e --- /dev/null +++ b/ts/packages/agents/playerLocal/src/agent/localPlayerSchema.agr @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Grammar rules for Local Music Player + +@ = + | + | + | + | + | + | + | + | + | + | + | + +@ = pause ((the)? music)? -> { actionName: "pause" } + +@ = resume ((the)? music)? -> { actionName: "resume" } + +@ = stop ((the)? music)? -> { actionName: "stop" } + +@ = (next | skip) (track | song)? -> { actionName: "next" } + +@ = (previous | back | last) (track | song)? -> { actionName: "previous" } + +@ = (what('s | is) | show) (playing | status | now playing)? -> { actionName: "status" } + +@ = + +@ = + play (the)? $(n:) (track | song)? -> { + actionName: "playFromQueue", + parameters: { + trackNumber: $(n) + } + } +| play track $(n:) -> { + actionName: "playFromQueue", + parameters: { + trackNumber: $(n) + } + } +| play track #$(n:number) -> { + actionName: "playFromQueue", + parameters: { + trackNumber: $(n) + } + } + +@ = | | + +@ = (turn up | increase | raise) (the)? volume + -> { actionName: "changeVolume", parameters: { amount: 10 } } + +@ = (turn down | decrease | lower) (the)? volume + -> { actionName: "changeVolume", parameters: { amount: -10 } } + +@ = set volume (to)? $(n:number) (percent)? + -> { actionName: "setVolume", parameters: { level: $(n) } } + +@ = mute (the)? (music | sound | audio)? -> { actionName: "mute" } + +@ = unmute (the)? (music | sound | audio)? -> { actionName: "unmute" } + +@ = (show | list | display) (the)? queue -> { actionName: "showQueue" } + +@ = clear (the)? queue -> { actionName: "clearQueue" } + +@ = + $(x:number) +| one -> 1 +| two -> 2 +| three -> 3 +| four -> 4 +| five -> 5 +| six -> 6 +| seven -> 7 +| eight -> 8 +| nine -> 9 +| ten -> 10 + +@ = + first -> 1 +| second -> 2 +| third -> 3 +| fourth -> 4 +| fifth -> 5 +| sixth -> 6 +| seventh -> 7 +| eighth -> 8 +| ninth -> 9 +| tenth -> 10 diff --git a/ts/packages/agents/playerLocal/src/agent/localPlayerSchema.json b/ts/packages/agents/playerLocal/src/agent/localPlayerSchema.json new file mode 100644 index 0000000000..81cee21c19 --- /dev/null +++ b/ts/packages/agents/playerLocal/src/agent/localPlayerSchema.json @@ -0,0 +1,6 @@ +{ + "schemaName": "LocalPlayerActions", + "fullSchemaType": "LocalPlayerActions", + "entityName": "LocalPlayerEntities", + "actionNamespace": "localPlayer" +} diff --git a/ts/packages/agents/playerLocal/src/agent/localPlayerSchema.ts b/ts/packages/agents/playerLocal/src/agent/localPlayerSchema.ts new file mode 100644 index 0000000000..d8dc078275 --- /dev/null +++ b/ts/packages/agents/playerLocal/src/agent/localPlayerSchema.ts @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export type LocalPlayerActions = + | PlayFileAction + | PlayFolderAction + | PlayFromQueueAction + | StatusAction + | PauseAction + | ResumeAction + | StopAction + | NextAction + | PreviousAction + | ShuffleAction + | RepeatAction + | SetVolumeAction + | ChangeVolumeAction + | MuteAction + | UnmuteAction + | ListFilesAction + | SearchFilesAction + | AddToQueueAction + | ClearQueueAction + | ShowQueueAction + | SetMusicFolderAction + | ShowMusicFolderAction; + +export type LocalPlayerEntities = FilePath; +export type FilePath = string; + +// Play a specific audio file by path or name +export interface PlayFileAction { + actionName: "playFile"; + parameters: { + // File name or path to play (can be partial match) + fileName: string; + }; +} + +// Play all audio files in a folder +export interface PlayFolderAction { + actionName: "playFolder"; + parameters: { + // Folder path to play from + folderPath?: string; + // Whether to shuffle the tracks + shuffle?: boolean; + }; +} + +// Play a specific track from the current queue by index +export interface PlayFromQueueAction { + actionName: "playFromQueue"; + parameters: { + // 1-based index of the track in the queue + trackNumber: number; + }; +} + +// Show now playing status including track information and playback state +export interface StatusAction { + actionName: "status"; +} + +// Pause playback +export interface PauseAction { + actionName: "pause"; +} + +// Resume playback +export interface ResumeAction { + actionName: "resume"; +} + +// Stop playback completely +export interface StopAction { + actionName: "stop"; +} + +// Skip to next track +export interface NextAction { + actionName: "next"; +} + +// Go to previous track +export interface PreviousAction { + actionName: "previous"; +} + +// Turn shuffle on or off +export interface ShuffleAction { + actionName: "shuffle"; + parameters: { + on: boolean; + }; +} + +// Set repeat mode +export interface RepeatAction { + actionName: "repeat"; + parameters: { + // Repeat mode: "off", "one" (repeat current track), "all" (repeat queue) + mode: "off" | "one" | "all"; + }; +} + +// Set volume to a specific level (0-100) +export interface SetVolumeAction { + actionName: "setVolume"; + parameters: { + // New volume level (0-100) + level: number; + }; +} + +// Change volume by a relative amount +export interface ChangeVolumeAction { + actionName: "changeVolume"; + parameters: { + // Amount to change volume by (can be negative) + amount: number; + }; +} + +// Mute audio +export interface MuteAction { + actionName: "mute"; +} + +// Unmute audio +export interface UnmuteAction { + actionName: "unmute"; +} + +// List audio files in the music folder or a specified folder +export interface ListFilesAction { + actionName: "listFiles"; + parameters?: { + // Folder to list (defaults to music folder) + folderPath?: string; + }; +} + +// Search for audio files by name +export interface SearchFilesAction { + actionName: "searchFiles"; + parameters: { + // Search query to match against file names + query: string; + }; +} + +// Add a file or files to the playback queue +export interface AddToQueueAction { + actionName: "addToQueue"; + parameters: { + // File name or path to add to queue + fileName: string; + }; +} + +// Clear the playback queue +export interface ClearQueueAction { + actionName: "clearQueue"; +} + +// Show the current playback queue +export interface ShowQueueAction { + actionName: "showQueue"; +} + +// Set the default music folder path +export interface SetMusicFolderAction { + actionName: "setMusicFolder"; + parameters: { + // Path to the music folder + folderPath: FilePath; + }; +} + +// Show the current music folder path +export interface ShowMusicFolderAction { + actionName: "showMusicFolder"; +} diff --git a/ts/packages/agents/playerLocal/src/localPlayerService.ts b/ts/packages/agents/playerLocal/src/localPlayerService.ts new file mode 100644 index 0000000000..a05e01f8d3 --- /dev/null +++ b/ts/packages/agents/playerLocal/src/localPlayerService.ts @@ -0,0 +1,408 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import { spawn, ChildProcess } from "child_process"; +import registerDebug from "debug"; + +const debug = registerDebug("typeagent:localPlayer"); +const debugError = registerDebug("typeagent:localPlayer:error"); + +// Supported audio file extensions +const AUDIO_EXTENSIONS = [".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac", ".wma"]; + +export interface Track { + name: string; + path: string; + duration?: number; + artist?: string; + album?: string; +} + +export interface PlaybackState { + isPlaying: boolean; + isPaused: boolean; + currentTrack: Track | null; + currentIndex: number; + volume: number; + isMuted: boolean; + shuffle: boolean; + repeat: "off" | "one" | "all"; + queue: Track[]; +} + +export class LocalPlayerService { + private state: PlaybackState; + private musicFolder: string; + private playerProcess: ChildProcess | null = null; + + constructor() { + // Default music folder + this.musicFolder = path.join(os.homedir(), "Music"); + + this.state = { + isPlaying: false, + isPaused: false, + currentTrack: null, + currentIndex: -1, + volume: 50, + isMuted: false, + shuffle: false, + repeat: "off", + queue: [], + }; + } + + public getMusicFolder(): string { + return this.musicFolder; + } + + public setMusicFolder(folderPath: string): boolean { + if (fs.existsSync(folderPath) && fs.statSync(folderPath).isDirectory()) { + this.musicFolder = folderPath; + debug(`Music folder set to: ${folderPath}`); + return true; + } + debugError(`Invalid folder path: ${folderPath}`); + return false; + } + + public getState(): PlaybackState { + return { ...this.state }; + } + + public listFiles(folderPath?: string): Track[] { + const folder = folderPath || this.musicFolder; + const tracks: Track[] = []; + + try { + if (!fs.existsSync(folder)) { + debug(`Folder does not exist: ${folder}`); + return tracks; + } + + const files = fs.readdirSync(folder); + for (const file of files) { + const ext = path.extname(file).toLowerCase(); + if (AUDIO_EXTENSIONS.includes(ext)) { + tracks.push({ + name: path.basename(file, ext), + path: path.join(folder, file), + }); + } + } + } catch (error) { + debugError(`Error listing files: ${error}`); + } + + return tracks; + } + + public searchFiles(query: string): Track[] { + const allFiles = this.listFilesRecursive(this.musicFolder); + const lowerQuery = query.toLowerCase(); + + return allFiles.filter(track => + track.name.toLowerCase().includes(lowerQuery) + ); + } + + private listFilesRecursive(folder: string, maxDepth: number = 3, currentDepth: number = 0): Track[] { + const tracks: Track[] = []; + + if (currentDepth >= maxDepth) { + return tracks; + } + + try { + const entries = fs.readdirSync(folder, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(folder, entry.name); + + if (entry.isDirectory()) { + tracks.push(...this.listFilesRecursive(fullPath, maxDepth, currentDepth + 1)); + } else if (entry.isFile()) { + const ext = path.extname(entry.name).toLowerCase(); + if (AUDIO_EXTENSIONS.includes(ext)) { + tracks.push({ + name: path.basename(entry.name, ext), + path: fullPath, + }); + } + } + } + } catch (error) { + debugError(`Error reading folder ${folder}: ${error}`); + } + + return tracks; + } + + public async playFile(fileName: string): Promise { + // Search for the file + const tracks = this.searchFiles(fileName); + + if (tracks.length === 0) { + debugError(`No files found matching: ${fileName}`); + return false; + } + + // Play the first match + return this.playTrack(tracks[0]); + } + + public async playTrack(track: Track): Promise { + // Stop any current playback + this.stop(); + + try { + debug(`Playing: ${track.path}`); + + if (process.platform === "win32") { + // Use Windows Media Player via PowerShell + this.playerProcess = spawn("powershell", [ + "-Command", + `Add-Type -AssemblyName presentationCore; $player = New-Object System.Windows.Media.MediaPlayer; $player.Open('${track.path}'); $player.Volume = ${this.state.volume / 100}; $player.Play(); Start-Sleep -Seconds 3600` + ], { stdio: "ignore" }); + } else if (process.platform === "darwin") { + // macOS: use afplay + this.playerProcess = spawn("afplay", [track.path], { stdio: "ignore" }); + } else { + // Linux: use mpv or similar + this.playerProcess = spawn("mpv", ["--no-video", track.path], { stdio: "ignore" }); + } + + this.playerProcess.on("error", (error) => { + debugError(`Player error: ${error}`); + }); + + this.playerProcess.on("exit", () => { + debug("Playback ended"); + this.handlePlaybackEnd(); + }); + + this.state.isPlaying = true; + this.state.isPaused = false; + this.state.currentTrack = track; + + // Find index in queue + const index = this.state.queue.findIndex(t => t.path === track.path); + if (index >= 0) { + this.state.currentIndex = index; + } + + return true; + } catch (error) { + debugError(`Error playing track: ${error}`); + return false; + } + } + + private handlePlaybackEnd(): void { + this.state.isPlaying = false; + + if (this.state.repeat === "one" && this.state.currentTrack) { + // Repeat current track + this.playTrack(this.state.currentTrack); + } else if (this.state.queue.length > 0 && this.state.currentIndex < this.state.queue.length - 1) { + // Play next in queue + this.next(); + } else if (this.state.repeat === "all" && this.state.queue.length > 0) { + // Repeat all - go back to start + this.state.currentIndex = 0; + this.playTrack(this.state.queue[0]); + } + } + + public pause(): boolean { + if (this.playerProcess && this.state.isPlaying) { + // Note: Simple pause isn't supported by all players + // For a real implementation, use a library with better control + if (process.platform === "win32") { + // Send Ctrl+C to pause (not ideal) + this.playerProcess.kill("SIGSTOP"); + } + this.state.isPaused = true; + this.state.isPlaying = false; + debug("Paused"); + return true; + } + return false; + } + + public resume(): boolean { + if (this.state.isPaused && this.state.currentTrack) { + if (process.platform === "win32" && this.playerProcess) { + this.playerProcess.kill("SIGCONT"); + } else if (this.state.currentTrack) { + // Restart playback (not ideal, but works) + this.playTrack(this.state.currentTrack); + } + this.state.isPaused = false; + this.state.isPlaying = true; + debug("Resumed"); + return true; + } + return false; + } + + public stop(): boolean { + if (this.playerProcess) { + this.playerProcess.kill(); + this.playerProcess = null; + } + this.state.isPlaying = false; + this.state.isPaused = false; + debug("Stopped"); + return true; + } + + public async next(): Promise { + if (this.state.queue.length === 0) { + return false; + } + + let nextIndex = this.state.currentIndex + 1; + + if (this.state.shuffle) { + nextIndex = Math.floor(Math.random() * this.state.queue.length); + } + + if (nextIndex >= this.state.queue.length) { + if (this.state.repeat === "all") { + nextIndex = 0; + } else { + return false; + } + } + + this.state.currentIndex = nextIndex; + return await this.playTrack(this.state.queue[nextIndex]); + } + + public async previous(): Promise { + if (this.state.queue.length === 0) { + return false; + } + + let prevIndex = this.state.currentIndex - 1; + + if (prevIndex < 0) { + if (this.state.repeat === "all") { + prevIndex = this.state.queue.length - 1; + } else { + prevIndex = 0; + } + } + + this.state.currentIndex = prevIndex; + return await this.playTrack(this.state.queue[prevIndex]); + } + + public setVolume(level: number): boolean { + this.state.volume = Math.max(0, Math.min(100, level)); + debug(`Volume set to: ${this.state.volume}`); + // Note: Changing volume during playback requires player-specific implementation + return true; + } + + public changeVolume(amount: number): boolean { + return this.setVolume(this.state.volume + amount); + } + + public mute(): boolean { + this.state.isMuted = true; + debug("Muted"); + return true; + } + + public unmute(): boolean { + this.state.isMuted = false; + debug("Unmuted"); + return true; + } + + public setShuffle(on: boolean): boolean { + this.state.shuffle = on; + debug(`Shuffle: ${on}`); + return true; + } + + public setRepeat(mode: "off" | "one" | "all"): boolean { + this.state.repeat = mode; + debug(`Repeat mode: ${mode}`); + return true; + } + + public addToQueue(track: Track): boolean { + this.state.queue.push(track); + debug(`Added to queue: ${track.name}`); + return true; + } + + public addFileToQueue(fileName: string): boolean { + const tracks = this.searchFiles(fileName); + if (tracks.length > 0) { + return this.addToQueue(tracks[0]); + } + return false; + } + + public clearQueue(): boolean { + this.state.queue = []; + this.state.currentIndex = -1; + debug("Queue cleared"); + return true; + } + + public getQueue(): Track[] { + return [...this.state.queue]; + } + + public async playFromQueue(index: number): Promise { + // Convert from 1-based to 0-based index + const zeroIndex = index - 1; + + if (zeroIndex >= 0 && zeroIndex < this.state.queue.length) { + this.state.currentIndex = zeroIndex; + return await this.playTrack(this.state.queue[zeroIndex]); + } + return false; + } + + public async playFolder(folderPath?: string, shuffle: boolean = false): Promise { + const tracks = this.listFiles(folderPath); + + if (tracks.length === 0) { + return false; + } + + this.state.queue = shuffle ? this.shuffleArray(tracks) : tracks; + this.state.shuffle = shuffle; + this.state.currentIndex = 0; + + return await this.playTrack(this.state.queue[0]); + } + + private shuffleArray(array: T[]): T[] { + const shuffled = [...array]; + for (let i = shuffled.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; + } + return shuffled; + } +} + +// Singleton instance +let playerInstance: LocalPlayerService | null = null; + +export function getLocalPlayerService(): LocalPlayerService { + if (!playerInstance) { + playerInstance = new LocalPlayerService(); + } + return playerInstance; +} diff --git a/ts/packages/agents/playerLocal/src/tsconfig.json b/ts/packages/agents/playerLocal/src/tsconfig.json new file mode 100644 index 0000000000..c40e7f2583 --- /dev/null +++ b/ts/packages/agents/playerLocal/src/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "rootDir": ".", + "outDir": "../dist", + "composite": true + }, + "include": ["./**/*.ts", "./**/*.mts", "./**/*.json"], + "exclude": ["../dist/**/*", "../node_modules/**/*"], + "extends": "../../../../tsconfig.base.json" +} diff --git a/ts/packages/defaultAgentProvider/data/config.json b/ts/packages/defaultAgentProvider/data/config.json index ad74901c54..e5f199f31f 100644 --- a/ts/packages/defaultAgentProvider/data/config.json +++ b/ts/packages/defaultAgentProvider/data/config.json @@ -3,6 +3,9 @@ "player": { "name": "music" }, + "localPlayer": { + "name": "music-local" + }, "calendar": { "name": "calendar" }, diff --git a/ts/packages/defaultAgentProvider/package.json b/ts/packages/defaultAgentProvider/package.json index 3379092309..300e8966a2 100644 --- a/ts/packages/defaultAgentProvider/package.json +++ b/ts/packages/defaultAgentProvider/package.json @@ -64,6 +64,7 @@ "markdown-agent": "workspace:*", "montage-agent": "workspace:*", "music": "workspace:*", + "music-local": "workspace:*", "oracle-agent": "workspace:*", "photo-agent": "workspace:*", "proper-lockfile": "^4.1.2", diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index 6c8e46b701..26f776c06d 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -2188,6 +2188,46 @@ importers: specifier: ~5.4.5 version: 5.4.5 + packages/agents/playerLocal: + dependencies: + '@typeagent/agent-sdk': + specifier: workspace:* + version: link:../../agentSdk + '@typeagent/common-utils': + specifier: workspace:* + version: link:../../utils/commonUtils + chalk: + specifier: ^5.4.1 + version: 5.6.2 + debug: + specifier: ^4.4.0 + version: 4.4.3(supports-color@8.1.1) + dotenv: + specifier: ^16.3.1 + version: 16.5.0 + play-sound: + specifier: ^1.1.6 + version: 1.1.6 + devDependencies: + '@typeagent/action-schema-compiler': + specifier: workspace:* + version: link:../../actionSchemaCompiler + '@types/debug': + specifier: ^4.1.12 + version: 4.1.12 + action-grammar-compiler: + specifier: workspace:* + version: link:../../actionGrammarCompiler + concurrently: + specifier: ^9.1.2 + version: 9.1.2 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + typescript: + specifier: ~5.4.5 + version: 5.4.5 + packages/agents/settings: dependencies: '@typeagent/agent-sdk': @@ -3034,6 +3074,9 @@ importers: music: specifier: workspace:* version: link:../agents/player + music-local: + specifier: workspace:* + version: link:../agents/playerLocal oracle-agent: specifier: workspace:* version: link:../agents/oracle @@ -9260,6 +9303,9 @@ packages: resolution: {integrity: sha512-Z+suHH+7LSE40WfUeZPIxSxypCWvrzdVc60xAjUShZeT5eMWM0/FQUduq3HjluyfAHWvC/aOBkT1pTZktyF/jg==} engines: {node: '>= 0.12'} + find-exec@1.0.3: + resolution: {integrity: sha512-gnG38zW90mS8hm5smNcrBnakPEt+cGJoiMkJwCU0IYnEb0H2NQk0NIljhNW+48oniCriFek/PH6QXbwsJo/qug==} + find-replace@3.0.0: resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} engines: {node: '>=4.0.0'} @@ -11688,6 +11734,9 @@ packages: pkg-types@2.2.0: resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==} + play-sound@1.1.6: + resolution: {integrity: sha512-09eO4QiXNFXJffJaOW5P6x6F5RLihpLUkXttvUZeWml0fU6x6Zp7AjG9zaeMpgH2ZNvq4GR1ytB22ddYcqJIZA==} + playwright-core@1.57.0: resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} engines: {node: '>=18'} @@ -20538,6 +20587,10 @@ snapshots: dependencies: user-home: 2.0.0 + find-exec@1.0.3: + dependencies: + shell-quote: 1.8.1 + find-replace@3.0.0: dependencies: array-back: 3.1.0 @@ -23696,6 +23749,10 @@ snapshots: exsolve: 1.0.7 pathe: 2.0.3 + play-sound@1.1.6: + dependencies: + find-exec: 1.0.3 + playwright-core@1.57.0: {} playwright@1.57.0: From c4659a3b943ce41da267c3618668b552fe8f8d4b Mon Sep 17 00:00:00 2001 From: Bernhard Merkle Date: Sat, 7 Feb 2026 20:05:53 +0100 Subject: [PATCH 03/11] Update ts/packages/agents/playerLocal/src/localPlayerService.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ts/packages/agents/playerLocal/src/localPlayerService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ts/packages/agents/playerLocal/src/localPlayerService.ts b/ts/packages/agents/playerLocal/src/localPlayerService.ts index a05e01f8d3..c964a6fa24 100644 --- a/ts/packages/agents/playerLocal/src/localPlayerService.ts +++ b/ts/packages/agents/playerLocal/src/localPlayerService.ts @@ -165,7 +165,9 @@ export class LocalPlayerService { // Use Windows Media Player via PowerShell this.playerProcess = spawn("powershell", [ "-Command", - `Add-Type -AssemblyName presentationCore; $player = New-Object System.Windows.Media.MediaPlayer; $player.Open('${track.path}'); $player.Volume = ${this.state.volume / 100}; $player.Play(); Start-Sleep -Seconds 3600` + "& { Add-Type -AssemblyName presentationCore; $player = New-Object System.Windows.Media.MediaPlayer; $player.Open($args[0]); $player.Volume = [double]$args[1]; $player.Play(); Start-Sleep -Seconds 3600 }", + track.path, + String(this.state.volume / 100) ], { stdio: "ignore" }); } else if (process.platform === "darwin") { // macOS: use afplay From 1dd99a57174d5eb942f5611c4e63c8cd3cd025f6 Mon Sep 17 00:00:00 2001 From: Bernhard Merkle Date: Sat, 7 Feb 2026 20:06:44 +0100 Subject: [PATCH 04/11] Update ts/packages/agents/playerLocal/src/localPlayerService.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../agents/playerLocal/src/localPlayerService.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ts/packages/agents/playerLocal/src/localPlayerService.ts b/ts/packages/agents/playerLocal/src/localPlayerService.ts index c964a6fa24..8bd2614191 100644 --- a/ts/packages/agents/playerLocal/src/localPlayerService.ts +++ b/ts/packages/agents/playerLocal/src/localPlayerService.ts @@ -270,7 +270,17 @@ export class LocalPlayerService { let nextIndex = this.state.currentIndex + 1; if (this.state.shuffle) { - nextIndex = Math.floor(Math.random() * this.state.queue.length); + const queueLength = this.state.queue.length; + if (queueLength > 1) { + let randomIndex: number; + do { + randomIndex = Math.floor(Math.random() * queueLength); + } while (randomIndex === this.state.currentIndex); + nextIndex = randomIndex; + } else { + // Only one track in the queue; keep index at 0 + nextIndex = 0; + } } if (nextIndex >= this.state.queue.length) { From 380a4a25ab42be69742c5bdfbf7985082a4a890a Mon Sep 17 00:00:00 2001 From: Bernhard Merkle Date: Sat, 7 Feb 2026 20:07:26 +0100 Subject: [PATCH 05/11] Update ts/packages/agents/playerLocal/src/localPlayerService.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../agents/playerLocal/src/localPlayerService.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ts/packages/agents/playerLocal/src/localPlayerService.ts b/ts/packages/agents/playerLocal/src/localPlayerService.ts index 8bd2614191..e7b8779a38 100644 --- a/ts/packages/agents/playerLocal/src/localPlayerService.ts +++ b/ts/packages/agents/playerLocal/src/localPlayerService.ts @@ -237,10 +237,14 @@ export class LocalPlayerService { public resume(): boolean { if (this.state.isPaused && this.state.currentTrack) { - if (process.platform === "win32" && this.playerProcess) { + if (!this.playerProcess) { + // No active player process; restart playback + this.playTrack(this.state.currentTrack); + } else if (process.platform !== "win32") { + // On non-Windows platforms, attempt to continue the existing process this.playerProcess.kill("SIGCONT"); - } else if (this.state.currentTrack) { - // Restart playback (not ideal, but works) + } else { + // On Windows, SIGCONT isn't supported; restart playback instead this.playTrack(this.state.currentTrack); } this.state.isPaused = false; From d8d1c2489477492752e1300febdb40d8dabb3f89 Mon Sep 17 00:00:00 2001 From: Bernhard Merkle Date: Sat, 7 Feb 2026 20:08:13 +0100 Subject: [PATCH 06/11] Update ts/packages/agents/playerLocal/package.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ts/packages/agents/playerLocal/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/ts/packages/agents/playerLocal/package.json b/ts/packages/agents/playerLocal/package.json index 85db5f7dec..21936940a3 100644 --- a/ts/packages/agents/playerLocal/package.json +++ b/ts/packages/agents/playerLocal/package.json @@ -28,7 +28,6 @@ "@typeagent/common-utils": "workspace:*", "chalk": "^5.4.1", "debug": "^4.4.0", - "dotenv": "^16.3.1", "play-sound": "^1.1.6" }, "devDependencies": { From 9a0887c7eca795b5e2c3f4f5eb5963061be22ed2 Mon Sep 17 00:00:00 2001 From: Bernhard Merkle Date: Sat, 7 Feb 2026 20:08:43 +0100 Subject: [PATCH 07/11] Update ts/packages/agents/playerLocal/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ts/packages/agents/playerLocal/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/packages/agents/playerLocal/README.md b/ts/packages/agents/playerLocal/README.md index b1ed6b59f3..a426bba7b0 100644 --- a/ts/packages/agents/playerLocal/README.md +++ b/ts/packages/agents/playerLocal/README.md @@ -18,7 +18,7 @@ No external API keys required! The agent uses the system's built-in audio capabi - **Windows**: Uses PowerShell with Windows Media Player - **macOS**: Uses `afplay` -- **Linux**: Uses `mpv` (install with `sudo apt install mpv`) +- **Linux**: Uses `mpv` (must be installed separately; for example: Debian/Ubuntu: `sudo apt install mpv`, Fedora: `sudo dnf install mpv`, Arch: `sudo pacman -S mpv`) ## Configuration From 1c54e01cd3b752c09eece5fde927608e5210076c Mon Sep 17 00:00:00 2001 From: Bernhard Merkle Date: Sat, 7 Feb 2026 20:38:05 +0100 Subject: [PATCH 08/11] - code fixes - layout fixes - build, prettier, test run fine now --- ts/examples/memoryProviders/package.json | 2 +- ts/package.json | 1 + .../calendar/src/calendarActionHandlerV3.ts | 8 - .../agents/calendar/src/calendarSchema.agr | 28 -- ts/packages/agents/playerLocal/README.md | 56 ++-- .../src/agent/localPlayerCommands.ts | 150 ++++++---- .../src/agent/localPlayerHandlers.ts | 183 +++++++----- .../src/agent/localPlayerSchema.json | 8 +- .../playerLocal/src/localPlayerService.ts | 97 +++++-- .../agents/playerLocal/src/tsconfig.json | 16 +- ts/pnpm-lock.yaml | 268 +++++++++--------- 11 files changed, 463 insertions(+), 354 deletions(-) diff --git a/ts/examples/memoryProviders/package.json b/ts/examples/memoryProviders/package.json index c2fc773cb3..6835499490 100644 --- a/ts/examples/memoryProviders/package.json +++ b/ts/examples/memoryProviders/package.json @@ -28,7 +28,7 @@ "tsc": "tsc -b" }, "dependencies": { - "@elastic/elasticsearch": "^8.17.0", + "@elastic/elasticsearch": "^8.19.1", "aiclient": "workspace:*", "better-sqlite3": "12.2.0", "debug": "^4.4.0", diff --git a/ts/package.json b/ts/package.json index 58d41c51c9..68cae25470 100644 --- a/ts/package.json +++ b/ts/package.json @@ -54,6 +54,7 @@ "@fluidframework/build-tools": "^0.57.0", "@types/node": "^20.17.28", "markdown-link-check": "^3.14.2", + "prebuild-install": "^7.1.3", "prettier": "^3.5.3", "shx": "^0.4.0" }, diff --git a/ts/packages/agents/calendar/src/calendarActionHandlerV3.ts b/ts/packages/agents/calendar/src/calendarActionHandlerV3.ts index 3689bfe357..6a3175ada5 100644 --- a/ts/packages/agents/calendar/src/calendarActionHandlerV3.ts +++ b/ts/packages/agents/calendar/src/calendarActionHandlerV3.ts @@ -23,11 +23,6 @@ import { createActionResultFromError, } from "@typeagent/agent-sdk/helpers/action"; import { CalendarActionV3 } from "./calendarActionsSchemaV3.js"; -import { - createCalendarGraphClient, - CalendarClient, -} from "graph-utils"; -import { getNWeeksDateRangeISO, generateQueryFromFuzzyDay } from "./calendarQueryHelper.js"; import { createCalendarGraphClient, CalendarClient } from "graph-utils"; import { getNWeeksDateRangeISO, @@ -314,9 +309,6 @@ export class CalendarActionHandlerV3 implements AppAgent { d.setDate(d.getDate() + 1); return d; } - - return undefined; - } // Try parsing relative day ("next Monday", "this Friday") const relativeDate = getDateRelativeToDayV2(dateStr); diff --git a/ts/packages/agents/calendar/src/calendarSchema.agr b/ts/packages/agents/calendar/src/calendarSchema.agr index 8ec576df8a..4ea768bf2a 100644 --- a/ts/packages/agents/calendar/src/calendarSchema.agr +++ b/ts/packages/agents/calendar/src/calendarSchema.agr @@ -105,31 +105,3 @@ | (can you)? find what I have scheduled this week -> { actionName: "findThisWeeksEvents" } - -// Common entity types and patterns - -@ = - $(x:number) -| one -> 1 -| two -> 2 -| three -> 3 -| four -> 4 -| five -> 5 -| six -> 6 -| seven -> 7 -| eight -> 8 -| nine -> 9 -| ten -> 10 - -@ = - first -> 1 -| second -> 2 -| third -> 3 -| fourth -> 4 -| fifth -> 5 -| sixth -> 6 -| seventh -> 7 -| eighth -> 8 -| ninth -> 9 -| tenth -> 10 - diff --git a/ts/packages/agents/playerLocal/README.md b/ts/packages/agents/playerLocal/README.md index a426bba7b0..394d6cf0f2 100644 --- a/ts/packages/agents/playerLocal/README.md +++ b/ts/packages/agents/playerLocal/README.md @@ -23,11 +23,13 @@ No external API keys required! The agent uses the system's built-in audio capabi ## Configuration Set your music folder using the command: + ``` @localPlayer folder set /path/to/music ``` Or use natural language: + ``` set music folder to C:\Users\Me\Music ``` @@ -37,6 +39,7 @@ set music folder to C:\Users\Me\Music ### Enable the agent In the shell or interactive mode: + ``` @config localPlayer on ``` @@ -44,6 +47,7 @@ In the shell or interactive mode: ### Example commands **Play music:** + ``` play some music play song.mp3 @@ -51,6 +55,7 @@ play all songs in the folder ``` **Control playback:** + ``` pause resume @@ -60,6 +65,7 @@ previous track ``` **Volume:** + ``` set volume to 50 turn up the volume @@ -67,6 +73,7 @@ mute ``` **Queue management:** + ``` show the queue add rock song to queue @@ -75,6 +82,7 @@ play the third track ``` **Browse files:** + ``` list files search for beethoven @@ -83,30 +91,30 @@ show music folder ## Available Actions -| Action | Description | -|--------|-------------| -| `playFile` | Play a specific audio file | -| `playFolder` | Play all audio files in a folder | -| `playFromQueue` | Play a track from the queue by number | -| `status` | Show current playback status | -| `pause` | Pause playback | -| `resume` | Resume playback | -| `stop` | Stop playback | -| `next` | Skip to next track | -| `previous` | Go to previous track | -| `shuffle` | Turn shuffle on/off | -| `repeat` | Set repeat mode (off/one/all) | -| `setVolume` | Set volume level (0-100) | -| `changeVolume` | Adjust volume by amount | -| `mute` | Mute audio | -| `unmute` | Unmute audio | -| `listFiles` | List audio files in folder | -| `searchFiles` | Search for files by name | -| `addToQueue` | Add file to playback queue | -| `clearQueue` | Clear the queue | -| `showQueue` | Display the queue | -| `setMusicFolder` | Set default music folder | -| `showMusicFolder` | Show current music folder | +| Action | Description | +| ----------------- | ------------------------------------- | +| `playFile` | Play a specific audio file | +| `playFolder` | Play all audio files in a folder | +| `playFromQueue` | Play a track from the queue by number | +| `status` | Show current playback status | +| `pause` | Pause playback | +| `resume` | Resume playback | +| `stop` | Stop playback | +| `next` | Skip to next track | +| `previous` | Go to previous track | +| `shuffle` | Turn shuffle on/off | +| `repeat` | Set repeat mode (off/one/all) | +| `setVolume` | Set volume level (0-100) | +| `changeVolume` | Adjust volume by amount | +| `mute` | Mute audio | +| `unmute` | Unmute audio | +| `listFiles` | List audio files in folder | +| `searchFiles` | Search for files by name | +| `addToQueue` | Add file to playback queue | +| `clearQueue` | Clear the queue | +| `showQueue` | Display the queue | +| `setMusicFolder` | Set default music folder | +| `showMusicFolder` | Show current music folder | ## Supported Audio Formats diff --git a/ts/packages/agents/playerLocal/src/agent/localPlayerCommands.ts b/ts/packages/agents/playerLocal/src/agent/localPlayerCommands.ts index 5266af0fc2..19cc60ca52 100644 --- a/ts/packages/agents/playerLocal/src/agent/localPlayerCommands.ts +++ b/ts/packages/agents/playerLocal/src/agent/localPlayerCommands.ts @@ -24,7 +24,10 @@ import { LocalPlayerActionContext } from "./localPlayerHandlers.js"; function getService(context: ActionContext) { const service = context.sessionContext.agentContext.playerService; if (!service) { - displayError("Local player not initialized. Enable it with: @config localPlayer on", context); + displayError( + "Local player not initialized. Enable it with: @config localPlayer on", + context, + ); return undefined; } return service; @@ -33,24 +36,31 @@ function getService(context: ActionContext) { // Status command handler class StatusCommandHandler implements CommandHandlerNoParams { public readonly description = "Show local player status"; - + public async run(context: ActionContext) { const service = getService(context); if (!service) return; - + const state = service.getState(); - + if (state.currentTrack) { - const status = state.isPlaying ? "▶️ Playing" : state.isPaused ? "⏸️ Paused" : "⏹️ Stopped"; + const status = state.isPlaying + ? "▶️ Playing" + : state.isPaused + ? "⏸️ Paused" + : "⏹️ Stopped"; displaySuccess( `${status}: ${state.currentTrack.name}\n` + - `Volume: ${state.volume}%${state.isMuted ? " (muted)" : ""}\n` + - `Shuffle: ${state.shuffle ? "On" : "Off"} | Repeat: ${state.repeat}\n` + - `Queue: ${state.currentIndex + 1}/${state.queue.length} tracks`, + `Volume: ${state.volume}%${state.isMuted ? " (muted)" : ""}\n` + + `Shuffle: ${state.shuffle ? "On" : "Off"} | Repeat: ${state.repeat}\n` + + `Queue: ${state.currentIndex + 1}/${state.queue.length} tracks`, context, ); } else { - displayWarn("No track loaded. Use '@localPlayer play' to start.", context); + displayWarn( + "No track loaded. Use '@localPlayer play' to start.", + context, + ); } } } @@ -59,7 +69,8 @@ class StatusCommandHandler implements CommandHandlerNoParams { const playParameters = { args: { file: { - description: "File name or path to play (optional - plays first file if not specified)", + description: + "File name or path to play (optional - plays first file if not specified)", optional: true, }, }, @@ -76,12 +87,15 @@ const playHandler: CommandHandler = { if (!service) return; const fileName = params.args.file; - + if (fileName) { const success = await service.playFile(fileName); if (success) { const state = service.getState(); - displaySuccess(`▶️ Playing: ${state.currentTrack?.name}`, context); + displaySuccess( + `▶️ Playing: ${state.currentTrack?.name}`, + context, + ); } else { displayError(`Could not find or play: ${fileName}`, context); } @@ -90,17 +104,29 @@ const playHandler: CommandHandler = { const state = service.getState(); if (state.isPaused) { service.resume(); - displaySuccess(`▶️ Resumed: ${state.currentTrack?.name}`, context); + displaySuccess( + `▶️ Resumed: ${state.currentTrack?.name}`, + context, + ); } else if (state.queue.length > 0) { await service.playFromQueue(state.currentIndex + 1); - displaySuccess(`▶️ Playing: ${service.getState().currentTrack?.name}`, context); + displaySuccess( + `▶️ Playing: ${service.getState().currentTrack?.name}`, + context, + ); } else { // Play first file from folder const success = await service.playFolder(); if (success) { - displaySuccess(`▶️ Playing: ${service.getState().currentTrack?.name}`, context); + displaySuccess( + `▶️ Playing: ${service.getState().currentTrack?.name}`, + context, + ); } else { - displayWarn("No audio files found. Set music folder with: @localPlayer setfolder ", context); + displayWarn( + "No audio files found. Set music folder with: @localPlayer setfolder ", + context, + ); } } } @@ -110,11 +136,11 @@ const playHandler: CommandHandler = { // Pause command class PauseCommandHandler implements CommandHandlerNoParams { public readonly description = "Pause playback"; - + public async run(context: ActionContext) { const service = getService(context); if (!service) return; - + service.pause(); displaySuccess("⏸️ Paused", context); } @@ -123,25 +149,28 @@ class PauseCommandHandler implements CommandHandlerNoParams { // Resume command class ResumeCommandHandler implements CommandHandlerNoParams { public readonly description = "Resume playback"; - + public async run(context: ActionContext) { const service = getService(context); if (!service) return; - + service.resume(); const state = service.getState(); - displaySuccess(`▶️ Resumed: ${state.currentTrack?.name || ""}`, context); + displaySuccess( + `▶️ Resumed: ${state.currentTrack?.name || ""}`, + context, + ); } } // Stop command class StopCommandHandler implements CommandHandlerNoParams { public readonly description = "Stop playback"; - + public async run(context: ActionContext) { const service = getService(context); if (!service) return; - + service.stop(); displaySuccess("⏹️ Stopped", context); } @@ -150,11 +179,11 @@ class StopCommandHandler implements CommandHandlerNoParams { // Next command class NextCommandHandler implements CommandHandlerNoParams { public readonly description = "Play next track"; - + public async run(context: ActionContext) { const service = getService(context); if (!service) return; - + const success = await service.next(); if (success) { const state = service.getState(); @@ -168,11 +197,11 @@ class NextCommandHandler implements CommandHandlerNoParams { // Previous command class PrevCommandHandler implements CommandHandlerNoParams { public readonly description = "Play previous track"; - + public async run(context: ActionContext) { const service = getService(context); if (!service) return; - + const success = await service.previous(); if (success) { const state = service.getState(); @@ -186,11 +215,11 @@ class PrevCommandHandler implements CommandHandlerNoParams { // Folder command - show current folder class FolderCommandHandler implements CommandHandlerNoParams { public readonly description = "Show current music folder"; - + public async run(context: ActionContext) { const service = getService(context); if (!service) return; - + const folder = service.getMusicFolder(); displayStatus(`📁 Music folder: ${folder}`, context); } @@ -217,10 +246,13 @@ const setFolderHandler: CommandHandler = { const folderPath = params.args.path; const success = service.setMusicFolder(folderPath); - + if (success) { const files = service.listFiles(); - displaySuccess(`📁 Music folder set to: ${folderPath}\nFound ${files.length} audio files`, context); + displaySuccess( + `📁 Music folder set to: ${folderPath}\nFound ${files.length} audio files`, + context, + ); } else { displayError(`Invalid folder path: ${folderPath}`, context); } @@ -230,27 +262,28 @@ const setFolderHandler: CommandHandler = { // List command class ListCommandHandler implements CommandHandlerNoParams { public readonly description = "List audio files in music folder"; - + public async run(context: ActionContext) { const service = getService(context); if (!service) return; - + const files = service.listFiles(); - + if (files.length === 0) { displayWarn("No audio files found in music folder", context); return; } - - const fileList = files.slice(0, 20).map((f, i) => - `${i + 1}. ${f.name}` - ).join("\n"); - + + const fileList = files + .slice(0, 20) + .map((f, i) => `${i + 1}. ${f.name}`) + .join("\n"); + let message = `🎵 Found ${files.length} audio files:\n${fileList}`; if (files.length > 20) { message += `\n...and ${files.length - 20} more`; } - + displaySuccess(message, context); } } @@ -258,29 +291,32 @@ class ListCommandHandler implements CommandHandlerNoParams { // Queue command class QueueCommandHandler implements CommandHandlerNoParams { public readonly description = "Show playback queue"; - + public async run(context: ActionContext) { const service = getService(context); if (!service) return; - + const queue = service.getQueue(); const state = service.getState(); - + if (queue.length === 0) { displayWarn("Queue is empty", context); return; } - - const queueList = queue.slice(0, 20).map((track, i) => { - const current = i === state.currentIndex ? " ▶️" : ""; - return `${i + 1}. ${track.name}${current}`; - }).join("\n"); - + + const queueList = queue + .slice(0, 20) + .map((track, i) => { + const current = i === state.currentIndex ? " ▶️" : ""; + return `${i + 1}. ${track.name}${current}`; + }) + .join("\n"); + let message = `📋 Queue (${queue.length} tracks):\n${queueList}`; if (queue.length > 20) { message += `\n...and ${queue.length - 20} more`; } - + displaySuccess(message, context); } } @@ -288,11 +324,11 @@ class QueueCommandHandler implements CommandHandlerNoParams { // Clear command class ClearCommandHandler implements CommandHandlerNoParams { public readonly description = "Clear playback queue"; - + public async run(context: ActionContext) { const service = getService(context); if (!service) return; - + service.clearQueue(); displaySuccess("🗑️ Queue cleared", context); } @@ -301,11 +337,11 @@ class ClearCommandHandler implements CommandHandlerNoParams { // Shuffle command class ShuffleCommandHandler implements CommandHandlerNoParams { public readonly description = "Toggle shuffle mode"; - + public async run(context: ActionContext) { const service = getService(context); if (!service) return; - + const state = service.getState(); service.setShuffle(!state.shuffle); displaySuccess(`🔀 Shuffle: ${!state.shuffle ? "On" : "Off"}`, context); @@ -336,7 +372,7 @@ const volumeHandler: CommandHandler = { displayError("Volume must be a number between 0 and 100", context); return; } - + service.setVolume(level); displaySuccess(`🔊 Volume: ${level}%`, context); }, @@ -345,11 +381,11 @@ const volumeHandler: CommandHandler = { // Mute command class MuteCommandHandler implements CommandHandlerNoParams { public readonly description = "Toggle mute"; - + public async run(context: ActionContext) { const service = getService(context); if (!service) return; - + const state = service.getState(); if (state.isMuted) { service.unmute(); diff --git a/ts/packages/agents/playerLocal/src/agent/localPlayerHandlers.ts b/ts/packages/agents/playerLocal/src/agent/localPlayerHandlers.ts index 124f9be5db..77acb11451 100644 --- a/ts/packages/agents/playerLocal/src/agent/localPlayerHandlers.ts +++ b/ts/packages/agents/playerLocal/src/agent/localPlayerHandlers.ts @@ -14,7 +14,10 @@ import { createActionResultFromError, } from "@typeagent/agent-sdk/helpers/action"; import { LocalPlayerActions } from "./localPlayerSchema.js"; -import { getLocalPlayerService, LocalPlayerService } from "../localPlayerService.js"; +import { + getLocalPlayerService, + LocalPlayerService, +} from "../localPlayerService.js"; import { getLocalPlayerCommandInterface } from "./localPlayerCommands.js"; import registerDebug from "debug"; @@ -49,7 +52,8 @@ async function updateLocalPlayerContext( } try { context.agentContext.playerService = getLocalPlayerService(); - const musicFolder = context.agentContext.playerService.getMusicFolder(); + const musicFolder = + context.agentContext.playerService.getMusicFolder(); const message = `Local player enabled. Music folder: ${musicFolder}`; debug(message); context.notify(AppAgentEvent.Info, chalk.green(message)); @@ -71,7 +75,7 @@ async function executeLocalPlayerAction( context: ActionContext, ) { const playerService = context.sessionContext.agentContext.playerService; - + if (!playerService) { return createActionResultFromError( "Local player not initialized. Use '@localPlayer on' to enable.", @@ -81,75 +85,96 @@ async function executeLocalPlayerAction( try { switch (action.actionName) { case "playFile": - return handlePlayFile(playerService, action.parameters.fileName); - + return handlePlayFile( + playerService, + action.parameters.fileName, + ); + case "playFolder": return handlePlayFolder( - playerService, + playerService, action.parameters?.folderPath, action.parameters?.shuffle, ); - + case "playFromQueue": - return handlePlayFromQueue(playerService, action.parameters.trackNumber); - + return handlePlayFromQueue( + playerService, + action.parameters.trackNumber, + ); + case "status": return handleStatus(playerService); - + case "pause": return handlePause(playerService); - + case "resume": return handleResume(playerService); - + case "stop": return handleStop(playerService); - + case "next": return handleNext(playerService); - + case "previous": return handlePrevious(playerService); - + case "shuffle": return handleShuffle(playerService, action.parameters.on); - + case "repeat": return handleRepeat(playerService, action.parameters.mode); - + case "setVolume": return handleSetVolume(playerService, action.parameters.level); - + case "changeVolume": - return handleChangeVolume(playerService, action.parameters.amount); - + return handleChangeVolume( + playerService, + action.parameters.amount, + ); + case "mute": return handleMute(playerService); - + case "unmute": return handleUnmute(playerService); - + case "listFiles": - return handleListFiles(playerService, action.parameters?.folderPath); - + return handleListFiles( + playerService, + action.parameters?.folderPath, + ); + case "searchFiles": - return handleSearchFiles(playerService, action.parameters.query); - + return handleSearchFiles( + playerService, + action.parameters.query, + ); + case "addToQueue": - return handleAddToQueue(playerService, action.parameters.fileName); - + return handleAddToQueue( + playerService, + action.parameters.fileName, + ); + case "clearQueue": return handleClearQueue(playerService); - + case "showQueue": return handleShowQueue(playerService); - + case "setMusicFolder": - return handleSetMusicFolder(playerService, action.parameters.folderPath); - + return handleSetMusicFolder( + playerService, + action.parameters.folderPath, + ); + case "showMusicFolder": return handleShowMusicFolder(playerService); - + default: return createActionResultFromError( `Unknown action: ${(action as any).actionName}`, @@ -173,7 +198,11 @@ async function handlePlayFile(service: LocalPlayerService, fileName: string) { return createActionResultFromError(`Could not find or play: ${fileName}`); } -async function handlePlayFolder(service: LocalPlayerService, folderPath?: string, shuffle?: boolean) { +async function handlePlayFolder( + service: LocalPlayerService, + folderPath?: string, + shuffle?: boolean, +) { const success = await service.playFolder(folderPath, shuffle || false); if (success) { const state = service.getState(); @@ -182,10 +211,15 @@ async function handlePlayFolder(service: LocalPlayerService, folderPath?: string

    Now playing: ${state.currentTrack?.name}

    `, ); } - return createActionResultFromError("No audio files found in the specified folder"); + return createActionResultFromError( + "No audio files found in the specified folder", + ); } -async function handlePlayFromQueue(service: LocalPlayerService, trackNumber: number) { +async function handlePlayFromQueue( + service: LocalPlayerService, + trackNumber: number, +) { const success = await service.playFromQueue(trackNumber); if (success) { const state = service.getState(); @@ -198,20 +232,20 @@ async function handlePlayFromQueue(service: LocalPlayerService, trackNumber: num function handleStatus(service: LocalPlayerService) { const state = service.getState(); - + let statusHtml = "

    🎵 Local Player Status

    "; - + if (state.currentTrack) { const playIcon = state.isPlaying ? "▶️" : state.isPaused ? "⏸️" : "⏹️"; statusHtml += `

    ${playIcon} ${state.currentTrack.name}

    `; } else { statusHtml += "

    No track loaded

    "; } - + statusHtml += `

    Volume: ${state.volume}%${state.isMuted ? " (muted)" : ""}

    `; statusHtml += `

    Shuffle: ${state.shuffle ? "On" : "Off"} | Repeat: ${state.repeat}

    `; statusHtml += `

    Queue: ${state.queue.length} tracks

    `; - + return createActionResultFromHtmlDisplay(statusHtml); } @@ -259,9 +293,17 @@ function handleShuffle(service: LocalPlayerService, on: boolean) { ); } -function handleRepeat(service: LocalPlayerService, mode: "off" | "one" | "all") { +function handleRepeat( + service: LocalPlayerService, + mode: "off" | "one" | "all", +) { service.setRepeat(mode); - const modeText = mode === "one" ? "Repeat One 🔂" : mode === "all" ? "Repeat All 🔁" : "Off"; + const modeText = + mode === "one" + ? "Repeat One 🔂" + : mode === "all" + ? "Repeat All 🔁" + : "Off"; return createActionResultFromHtmlDisplay(`

    Repeat: ${modeText}

    `); } @@ -273,7 +315,9 @@ function handleSetVolume(service: LocalPlayerService, level: number) { function handleChangeVolume(service: LocalPlayerService, amount: number) { service.changeVolume(amount); const state = service.getState(); - return createActionResultFromHtmlDisplay(`

    🔊 Volume: ${state.volume}%

    `); + return createActionResultFromHtmlDisplay( + `

    🔊 Volume: ${state.volume}%

    `, + ); } function handleMute(service: LocalPlayerService) { @@ -288,36 +332,40 @@ function handleUnmute(service: LocalPlayerService) { function handleListFiles(service: LocalPlayerService, folderPath?: string) { const files = service.listFiles(folderPath); - + if (files.length === 0) { return createActionResultFromHtmlDisplay("

    No audio files found

    "); } - - const fileListHtml = files.slice(0, 20).map((f, i) => - `
  • ${i + 1}. ${f.name}
  • ` - ).join(""); - + + const fileListHtml = files + .slice(0, 20) + .map((f, i) => `
  • ${i + 1}. ${f.name}
  • `) + .join(""); + let html = `

    📁 Audio Files (${files.length} total)

      ${fileListHtml}
    `; if (files.length > 20) { html += `

    ...and ${files.length - 20} more

    `; } - + return createActionResultFromHtmlDisplay(html); } function handleSearchFiles(service: LocalPlayerService, query: string) { const files = service.searchFiles(query); - + if (files.length === 0) { - return createActionResultFromHtmlDisplay(`

    No files found matching: ${query}

    `); + return createActionResultFromHtmlDisplay( + `

    No files found matching: ${query}

    `, + ); } - - const fileListHtml = files.slice(0, 20).map((f, i) => - `
  • ${i + 1}. ${f.name}
  • ` - ).join(""); - + + const fileListHtml = files + .slice(0, 20) + .map((f, i) => `
  • ${i + 1}. ${f.name}
  • `) + .join(""); + let html = `

    🔍 Search Results for "${query}" (${files.length} found)

      ${fileListHtml}
    `; - + return createActionResultFromHtmlDisplay(html); } @@ -340,21 +388,24 @@ function handleClearQueue(service: LocalPlayerService) { function handleShowQueue(service: LocalPlayerService) { const queue = service.getQueue(); const state = service.getState(); - + if (queue.length === 0) { return createActionResultFromHtmlDisplay("

    Queue is empty

    "); } - - const queueHtml = queue.slice(0, 20).map((track, i) => { - const current = i === state.currentIndex ? " ▶️" : ""; - return `
  • ${i + 1}. ${track.name}${current}
  • `; - }).join(""); - + + const queueHtml = queue + .slice(0, 20) + .map((track, i) => { + const current = i === state.currentIndex ? " ▶️" : ""; + return `
  • ${i + 1}. ${track.name}${current}
  • `; + }) + .join(""); + let html = `

    📋 Playback Queue (${queue.length} tracks)

      ${queueHtml}
    `; if (queue.length > 20) { html += `

    ...and ${queue.length - 20} more

    `; } - + return createActionResultFromHtmlDisplay(html); } diff --git a/ts/packages/agents/playerLocal/src/agent/localPlayerSchema.json b/ts/packages/agents/playerLocal/src/agent/localPlayerSchema.json index 81cee21c19..8c5a5664ae 100644 --- a/ts/packages/agents/playerLocal/src/agent/localPlayerSchema.json +++ b/ts/packages/agents/playerLocal/src/agent/localPlayerSchema.json @@ -1,6 +1,6 @@ { - "schemaName": "LocalPlayerActions", - "fullSchemaType": "LocalPlayerActions", - "entityName": "LocalPlayerEntities", - "actionNamespace": "localPlayer" + "schemaName": "LocalPlayerActions", + "fullSchemaType": "LocalPlayerActions", + "entityName": "LocalPlayerEntities", + "actionNamespace": "localPlayer" } diff --git a/ts/packages/agents/playerLocal/src/localPlayerService.ts b/ts/packages/agents/playerLocal/src/localPlayerService.ts index e7b8779a38..e1d28b5de4 100644 --- a/ts/packages/agents/playerLocal/src/localPlayerService.ts +++ b/ts/packages/agents/playerLocal/src/localPlayerService.ts @@ -11,7 +11,15 @@ const debug = registerDebug("typeagent:localPlayer"); const debugError = registerDebug("typeagent:localPlayer:error"); // Supported audio file extensions -const AUDIO_EXTENSIONS = [".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac", ".wma"]; +const AUDIO_EXTENSIONS = [ + ".mp3", + ".wav", + ".ogg", + ".flac", + ".m4a", + ".aac", + ".wma", +]; export interface Track { name: string; @@ -41,7 +49,7 @@ export class LocalPlayerService { constructor() { // Default music folder this.musicFolder = path.join(os.homedir(), "Music"); - + this.state = { isPlaying: false, isPaused: false, @@ -60,7 +68,10 @@ export class LocalPlayerService { } public setMusicFolder(folderPath: string): boolean { - if (fs.existsSync(folderPath) && fs.statSync(folderPath).isDirectory()) { + if ( + fs.existsSync(folderPath) && + fs.statSync(folderPath).isDirectory() + ) { this.musicFolder = folderPath; debug(`Music folder set to: ${folderPath}`); return true; @@ -103,13 +114,17 @@ export class LocalPlayerService { public searchFiles(query: string): Track[] { const allFiles = this.listFilesRecursive(this.musicFolder); const lowerQuery = query.toLowerCase(); - - return allFiles.filter(track => - track.name.toLowerCase().includes(lowerQuery) + + return allFiles.filter((track) => + track.name.toLowerCase().includes(lowerQuery), ); } - private listFilesRecursive(folder: string, maxDepth: number = 3, currentDepth: number = 0): Track[] { + private listFilesRecursive( + folder: string, + maxDepth: number = 3, + currentDepth: number = 0, + ): Track[] { const tracks: Track[] = []; if (currentDepth >= maxDepth) { @@ -118,12 +133,18 @@ export class LocalPlayerService { try { const entries = fs.readdirSync(folder, { withFileTypes: true }); - + for (const entry of entries) { const fullPath = path.join(folder, entry.name); - + if (entry.isDirectory()) { - tracks.push(...this.listFilesRecursive(fullPath, maxDepth, currentDepth + 1)); + tracks.push( + ...this.listFilesRecursive( + fullPath, + maxDepth, + currentDepth + 1, + ), + ); } else if (entry.isFile()) { const ext = path.extname(entry.name).toLowerCase(); if (AUDIO_EXTENSIONS.includes(ext)) { @@ -144,7 +165,7 @@ export class LocalPlayerService { public async playFile(fileName: string): Promise { // Search for the file const tracks = this.searchFiles(fileName); - + if (tracks.length === 0) { debugError(`No files found matching: ${fileName}`); return false; @@ -160,21 +181,29 @@ export class LocalPlayerService { try { debug(`Playing: ${track.path}`); - + if (process.platform === "win32") { // Use Windows Media Player via PowerShell - this.playerProcess = spawn("powershell", [ - "-Command", - "& { Add-Type -AssemblyName presentationCore; $player = New-Object System.Windows.Media.MediaPlayer; $player.Open($args[0]); $player.Volume = [double]$args[1]; $player.Play(); Start-Sleep -Seconds 3600 }", - track.path, - String(this.state.volume / 100) - ], { stdio: "ignore" }); + this.playerProcess = spawn( + "powershell", + [ + "-Command", + "& { Add-Type -AssemblyName presentationCore; $player = New-Object System.Windows.Media.MediaPlayer; $player.Open($args[0]); $player.Volume = [double]$args[1]; $player.Play(); Start-Sleep -Seconds 3600 }", + track.path, + String(this.state.volume / 100), + ], + { stdio: "ignore" }, + ); } else if (process.platform === "darwin") { // macOS: use afplay - this.playerProcess = spawn("afplay", [track.path], { stdio: "ignore" }); + this.playerProcess = spawn("afplay", [track.path], { + stdio: "ignore", + }); } else { // Linux: use mpv or similar - this.playerProcess = spawn("mpv", ["--no-video", track.path], { stdio: "ignore" }); + this.playerProcess = spawn("mpv", ["--no-video", track.path], { + stdio: "ignore", + }); } this.playerProcess.on("error", (error) => { @@ -189,9 +218,11 @@ export class LocalPlayerService { this.state.isPlaying = true; this.state.isPaused = false; this.state.currentTrack = track; - + // Find index in queue - const index = this.state.queue.findIndex(t => t.path === track.path); + const index = this.state.queue.findIndex( + (t) => t.path === track.path, + ); if (index >= 0) { this.state.currentIndex = index; } @@ -205,11 +236,14 @@ export class LocalPlayerService { private handlePlaybackEnd(): void { this.state.isPlaying = false; - + if (this.state.repeat === "one" && this.state.currentTrack) { // Repeat current track this.playTrack(this.state.currentTrack); - } else if (this.state.queue.length > 0 && this.state.currentIndex < this.state.queue.length - 1) { + } else if ( + this.state.queue.length > 0 && + this.state.currentIndex < this.state.queue.length - 1 + ) { // Play next in queue this.next(); } else if (this.state.repeat === "all" && this.state.queue.length > 0) { @@ -272,7 +306,7 @@ export class LocalPlayerService { } let nextIndex = this.state.currentIndex + 1; - + if (this.state.shuffle) { const queueLength = this.state.queue.length; if (queueLength > 1) { @@ -305,7 +339,7 @@ export class LocalPlayerService { } let prevIndex = this.state.currentIndex - 1; - + if (prevIndex < 0) { if (this.state.repeat === "all") { prevIndex = this.state.queue.length - 1; @@ -381,7 +415,7 @@ export class LocalPlayerService { public async playFromQueue(index: number): Promise { // Convert from 1-based to 0-based index const zeroIndex = index - 1; - + if (zeroIndex >= 0 && zeroIndex < this.state.queue.length) { this.state.currentIndex = zeroIndex; return await this.playTrack(this.state.queue[zeroIndex]); @@ -389,9 +423,12 @@ export class LocalPlayerService { return false; } - public async playFolder(folderPath?: string, shuffle: boolean = false): Promise { + public async playFolder( + folderPath?: string, + shuffle: boolean = false, + ): Promise { const tracks = this.listFiles(folderPath); - + if (tracks.length === 0) { return false; } @@ -399,7 +436,7 @@ export class LocalPlayerService { this.state.queue = shuffle ? this.shuffleArray(tracks) : tracks; this.state.shuffle = shuffle; this.state.currentIndex = 0; - + return await this.playTrack(this.state.queue[0]); } diff --git a/ts/packages/agents/playerLocal/src/tsconfig.json b/ts/packages/agents/playerLocal/src/tsconfig.json index c40e7f2583..2ea48741dc 100644 --- a/ts/packages/agents/playerLocal/src/tsconfig.json +++ b/ts/packages/agents/playerLocal/src/tsconfig.json @@ -1,10 +1,10 @@ { - "compilerOptions": { - "rootDir": ".", - "outDir": "../dist", - "composite": true - }, - "include": ["./**/*.ts", "./**/*.mts", "./**/*.json"], - "exclude": ["../dist/**/*", "../node_modules/**/*"], - "extends": "../../../../tsconfig.base.json" + "compilerOptions": { + "rootDir": ".", + "outDir": "../dist", + "composite": true + }, + "include": ["./**/*.ts", "./**/*.mts", "./**/*.json"], + "exclude": ["../dist/**/*", "../node_modules/**/*"], + "extends": "../../../../tsconfig.base.json" } diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index 32aabc715e..122b24ef39 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: markdown-link-check: specifier: ^3.14.2 version: 3.14.2 + prebuild-install: + specifier: ^7.1.3 + version: 7.1.3 prettier: specifier: ^3.5.3 version: 3.5.3 @@ -139,7 +142,7 @@ importers: version: 8.18.1 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -475,8 +478,8 @@ importers: examples/memoryProviders: dependencies: '@elastic/elasticsearch': - specifier: ^8.17.0 - version: 8.18.2 + specifier: ^8.19.1 + version: 8.19.1 aiclient: specifier: workspace:* version: link:../../packages/aiclient @@ -513,7 +516,7 @@ importers: version: 16.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -540,7 +543,7 @@ importers: version: 23.11.1(typescript@5.4.5) ts-node: specifier: ^10.9.1 - version: 10.9.2(@types/node@22.15.18)(typescript@5.4.5) + version: 10.9.2(@types/node@24.10.11)(typescript@5.4.5) xml2js: specifier: ^0.6.2 version: 0.6.2 @@ -726,7 +729,7 @@ importers: version: 12.0.2(webpack@5.105.0) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -864,7 +867,7 @@ importers: version: 12.0.2(webpack@5.105.0) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -922,7 +925,7 @@ importers: version: 2.0.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -969,7 +972,7 @@ importers: version: 29.5.14 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -1013,7 +1016,7 @@ importers: version: 29.5.14 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -1041,7 +1044,7 @@ importers: version: 29.5.14 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -1280,7 +1283,7 @@ importers: version: 2.4.1 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -2002,7 +2005,7 @@ importers: version: 2.4.1 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -2017,7 +2020,7 @@ importers: version: 5.4.5 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.15.18)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(yaml@2.7.0) + version: 6.4.1(@types/node@24.10.11)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(yaml@2.7.0) packages/agents/montage: dependencies: @@ -2093,7 +2096,7 @@ importers: version: 12.0.2(webpack@5.105.0) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.2.5 version: 3.5.3 @@ -2214,9 +2217,6 @@ importers: debug: specifier: ^4.4.0 version: 4.4.3(supports-color@8.1.1) - dotenv: - specifier: ^16.3.1 - version: 16.5.0 play-sound: specifier: ^1.1.6 version: 1.1.6 @@ -2374,7 +2374,7 @@ importers: version: 12.0.2(webpack@5.105.0) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -2476,7 +2476,7 @@ importers: version: 16.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -2561,7 +2561,7 @@ importers: version: 8.18.1 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -2622,7 +2622,7 @@ importers: version: 16.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -2683,7 +2683,7 @@ importers: version: 2.0.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -2720,7 +2720,7 @@ importers: version: 5.6.3(webpack@5.105.0) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -2807,7 +2807,7 @@ importers: version: 10.1.2 ts-node: specifier: ^10.9.1 - version: 10.9.2(@types/node@22.15.18)(typescript@5.4.5) + version: 10.9.2(@types/node@24.10.11)(typescript@5.4.5) typechat: specifier: ^0.1.1 version: 0.1.1(typescript@5.4.5)(zod@3.25.76) @@ -2826,7 +2826,7 @@ importers: version: 29.5.14 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -2998,7 +2998,7 @@ importers: version: 29.5.14 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.2.5 version: 3.5.3 @@ -3007,7 +3007,7 @@ importers: version: 5.0.10 ts-jest: specifier: ^29.1.2 - version: 29.3.3(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(jest@29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)))(typescript@5.4.5) + version: 29.3.3(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(jest@29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)))(typescript@5.4.5) typescript: specifier: ~5.4.5 version: 5.4.5 @@ -3170,7 +3170,7 @@ importers: version: 16.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -3306,7 +3306,7 @@ importers: version: 16.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -3346,7 +3346,7 @@ importers: version: 29.5.14 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -3452,7 +3452,7 @@ importers: version: 16.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -3547,7 +3547,7 @@ importers: version: 16.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -3587,7 +3587,7 @@ importers: version: 12.0.2(webpack@5.105.0) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -3694,7 +3694,7 @@ importers: version: 16.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -3810,7 +3810,7 @@ importers: version: 16.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.2.5 version: 3.5.3 @@ -3901,7 +3901,7 @@ importers: version: 16.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -4023,7 +4023,7 @@ importers: devDependencies: '@electron-toolkit/tsconfig': specifier: ^1.0.1 - version: 1.0.1(@types/node@22.15.18) + version: 1.0.1(@types/node@24.10.11) '@fontsource/lato': specifier: ^5.2.5 version: 5.2.5 @@ -4047,7 +4047,7 @@ importers: version: 26.3.1(electron-builder-squirrel-windows@26.3.1) electron-vite: specifier: ^4.0.1 - version: 4.0.1(vite@6.4.1(@types/node@22.15.18)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(yaml@2.7.0)) + version: 4.0.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(yaml@2.7.0)) less: specifier: ^4.2.0 version: 4.3.0 @@ -4065,7 +4065,7 @@ importers: version: 5.4.5 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.15.18)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(yaml@2.7.0) + version: 6.4.1(@types/node@24.10.11)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(yaml@2.7.0) packages/telemetry: dependencies: @@ -4096,7 +4096,7 @@ importers: version: 29.5.14 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -4127,7 +4127,7 @@ importers: version: 16.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -4195,7 +4195,7 @@ importers: version: 16.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -4223,7 +4223,7 @@ importers: version: 0.25.11 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -4254,7 +4254,7 @@ importers: version: 29.5.14 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -4300,7 +4300,7 @@ importers: version: 29.5.14 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -5066,12 +5066,12 @@ packages: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} - '@elastic/elasticsearch@8.18.2': - resolution: {integrity: sha512-2pOc/hGdxkbaDavfAlnUfjJdVsFRCGqg7fpsWJfJ2UzpgViIyojdViHg8zOCT1J14lAwvDgb9CNETWa3SBZRfw==} + '@elastic/elasticsearch@8.19.1': + resolution: {integrity: sha512-+1j9NnQVOX+lbWB8LhCM7IkUmjU05Y4+BmSLfusq0msCsQb1Va+OUKFCoOXjCJqQrcgdRdQCjYYyolQ/npQALQ==} engines: {node: '>=18'} - '@elastic/transport@8.9.6': - resolution: {integrity: sha512-v71jgmZtgPg2ouXF5KTPxU1a6z7YYc8nazAS7jLySteC/vrShs1OJh6oEEeo5oDc19MYUofV/JV1h5vqJVBXOw==} + '@elastic/transport@8.10.1': + resolution: {integrity: sha512-xo2lPBAJEt81fQRAKa9T/gUq1SPGBHpSnVUXhoSpL996fPZRAfQwFA4BZtEUQL1p8Dezodd3ZN8Wwno+mYyKuw==} engines: {node: '>=18'} '@electron-toolkit/preload@3.0.2': @@ -6769,8 +6769,8 @@ packages: resolution: {integrity: sha512-JtaY3FxmD+te+KSI2FJuEcfNC9T/DGGVf551babM7fAaXhjJUt7oSYurH1Devxd2+BOSUACCgt3buinx4UnmEA==} engines: {node: '>=18.0.0'} - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/helpers@0.5.18': + resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} '@szmarczak/http-timer@4.0.6': resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} @@ -7155,6 +7155,9 @@ packages: '@types/node@22.15.18': resolution: {integrity: sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==} + '@types/node@24.10.11': + resolution: {integrity: sha512-/Af7O8r1frCVgOz0I62jWUtMohJ0/ZQU/ZoketltOJPZpnb17yoNc9BSoVuV9qlaIXJiPNOpsfq4ByFajSArNQ==} + '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -7623,8 +7626,8 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - apache-arrow@18.1.0: - resolution: {integrity: sha512-v/ShMp57iBnBp4lDgV8Jx3d3Q5/Hac25FWmQ98eMahUiHPXcvwIMKJD0hBIgclm/FCG+LwPkAKtkRO1O/W0YGg==} + apache-arrow@21.1.0: + resolution: {integrity: sha512-kQrYLxhC+NTVVZ4CCzGF6L/uPVOzJmD1T3XgbiUnP7oTeVFOFgEUu6IKNwCDkpFoBVqDKQivlX4RUFqqnWFlEA==} hasBin: true app-builder-bin@5.0.0-alpha.12: @@ -7666,10 +7669,6 @@ packages: resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} engines: {node: '>=0.10.0'} - array-back@3.1.0: - resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} - engines: {node: '>=6'} - array-back@6.2.2: resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==} engines: {node: '>=12.17'} @@ -8236,9 +8235,14 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} - command-line-args@5.2.1: - resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} - engines: {node: '>=4.0.0'} + command-line-args@6.0.1: + resolution: {integrity: sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==} + engines: {node: '>=12.20'} + peerDependencies: + '@75lb/nature': latest + peerDependenciesMeta: + '@75lb/nature': + optional: true command-line-usage@7.0.3: resolution: {integrity: sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==} @@ -9329,9 +9333,14 @@ packages: find-exec@1.0.3: resolution: {integrity: sha512-gnG38zW90mS8hm5smNcrBnakPEt+cGJoiMkJwCU0IYnEb0H2NQk0NIljhNW+48oniCriFek/PH6QXbwsJo/qug==} - find-replace@3.0.0: - resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} - engines: {node: '>=4.0.0'} + find-replace@5.0.2: + resolution: {integrity: sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==} + engines: {node: '>=14'} + peerDependencies: + '@75lb/nature': latest + peerDependenciesMeta: + '@75lb/nature': + optional: true find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} @@ -9349,8 +9358,8 @@ packages: resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} hasBin: true - flatbuffers@24.3.25: - resolution: {integrity: sha512-3HDgPbgiwWMI9zVB7VYBHaMrbOO7Gm0v+yD2FV/sCKj+9NDeVL7BOBYUuhWAQGKWOzBo8S9WdMvV0eixO233XQ==} + flatbuffers@25.9.23: + resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} follow-redirects@1.15.9: resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} @@ -13199,10 +13208,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - typical@4.0.0: - resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} - engines: {node: '>=8'} - typical@7.3.0: resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==} engines: {node: '>=12.17'} @@ -13232,8 +13237,11 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@6.21.3: - resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + undici@6.23.0: + resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} engines: {node: '>=18.17'} undici@7.11.0: @@ -13644,8 +13652,8 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - wordwrapjs@5.1.0: - resolution: {integrity: sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==} + wordwrapjs@5.1.1: + resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} engines: {node: '>=12.17'} workerpool@6.5.1: @@ -15264,23 +15272,25 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} - '@elastic/elasticsearch@8.18.2': + '@elastic/elasticsearch@8.19.1': dependencies: - '@elastic/transport': 8.9.6 - apache-arrow: 18.1.0 - tslib: 2.6.2 + '@elastic/transport': 8.10.1 + apache-arrow: 21.1.0 + tslib: 2.8.1 transitivePeerDependencies: + - '@75lb/nature' - supports-color - '@elastic/transport@8.9.6': + '@elastic/transport@8.10.1': dependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) debug: 4.4.3(supports-color@8.1.1) hpagent: 1.2.0 ms: 2.1.3 secure-json-parse: 3.0.2 tslib: 2.8.1 - undici: 6.21.3 + undici: 6.23.0 transitivePeerDependencies: - supports-color @@ -15288,9 +15298,9 @@ snapshots: dependencies: electron: 37.4.0 - '@electron-toolkit/tsconfig@1.0.1(@types/node@22.15.18)': + '@electron-toolkit/tsconfig@1.0.1(@types/node@24.10.11)': dependencies: - '@types/node': 22.15.18 + '@types/node': 24.10.11 '@electron/asar@3.4.1': dependencies: @@ -15941,7 +15951,7 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5))': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -15955,7 +15965,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + jest-config: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -17558,7 +17568,7 @@ snapshots: '@smithy/types': 4.2.0 tslib: 2.8.1 - '@swc/helpers@0.5.15': + '@swc/helpers@0.5.18': dependencies: tslib: 2.8.1 @@ -18046,6 +18056,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@24.10.11': + dependencies: + undici-types: 7.16.0 + '@types/normalize-package-data@2.4.4': {} '@types/plist@3.0.5': @@ -18572,17 +18586,19 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 - apache-arrow@18.1.0: + apache-arrow@21.1.0: dependencies: - '@swc/helpers': 0.5.15 + '@swc/helpers': 0.5.18 '@types/command-line-args': 5.2.3 '@types/command-line-usage': 5.0.4 - '@types/node': 20.19.25 - command-line-args: 5.2.1 + '@types/node': 24.10.11 + command-line-args: 6.0.1 command-line-usage: 7.0.3 - flatbuffers: 24.3.25 + flatbuffers: 25.9.23 json-bignum: 0.0.3 tslib: 2.8.1 + transitivePeerDependencies: + - '@75lb/nature' app-builder-bin@5.0.0-alpha.12: {} @@ -18680,8 +18696,6 @@ snapshots: arr-union@3.1.0: {} - array-back@3.1.0: {} - array-back@6.2.2: {} array-buffer-byte-length@1.0.2: @@ -19360,12 +19374,12 @@ snapshots: dependencies: delayed-stream: 1.0.0 - command-line-args@5.2.1: + command-line-args@6.0.1: dependencies: - array-back: 3.1.0 - find-replace: 3.0.0 + array-back: 6.2.2 + find-replace: 5.0.2 lodash.camelcase: 4.3.0 - typical: 4.0.0 + typical: 7.3.0 command-line-usage@7.0.3: dependencies: @@ -19571,13 +19585,13 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)): + create-jest@29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + jest-config: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -20181,7 +20195,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-vite@4.0.1(vite@6.4.1(@types/node@22.15.18)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(yaml@2.7.0)): + electron-vite@4.0.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(yaml@2.7.0)): dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.4) @@ -20189,7 +20203,7 @@ snapshots: esbuild: 0.25.11 magic-string: 0.30.17 picocolors: 1.1.1 - vite: 6.4.1(@types/node@22.15.18)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(yaml@2.7.0) + vite: 6.4.1(@types/node@24.10.11)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(yaml@2.7.0) transitivePeerDependencies: - supports-color @@ -20724,9 +20738,7 @@ snapshots: dependencies: shell-quote: 1.8.1 - find-replace@3.0.0: - dependencies: - array-back: 3.1.0 + find-replace@5.0.2: {} find-up@4.1.0: dependencies: @@ -20746,7 +20758,7 @@ snapshots: flat@5.0.2: {} - flatbuffers@24.3.25: {} + flatbuffers@25.9.23: {} follow-redirects@1.15.9(debug@4.4.1): optionalDependencies: @@ -21827,16 +21839,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)): + jest-cli@29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + create-jest: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + jest-config: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -21939,7 +21951,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)): + jest-config@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)): dependencies: '@babel/core': 7.28.4 '@jest/test-sequencer': 29.7.0 @@ -21965,12 +21977,12 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.19.25 - ts-node: 10.9.2(@types/node@22.15.18)(typescript@5.4.5) + ts-node: 10.9.2(@types/node@24.10.11)(typescript@5.4.5) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)): + jest-config@29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)): dependencies: '@babel/core': 7.28.4 '@jest/test-sequencer': 29.7.0 @@ -21995,8 +22007,8 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.15.18 - ts-node: 10.9.2(@types/node@22.15.18)(typescript@5.4.5) + '@types/node': 24.10.11 + ts-node: 10.9.2(@types/node@24.10.11)(typescript@5.4.5) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -22261,12 +22273,12 @@ snapshots: - supports-color - ts-node - jest@29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)): + jest@29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + jest-cli: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -23447,7 +23459,7 @@ snapshots: node-abi@3.77.0: dependencies: - semver: 7.7.2 + semver: 7.7.3 node-abi@4.24.0: dependencies: @@ -25285,7 +25297,7 @@ snapshots: table-layout@4.1.1: dependencies: array-back: 6.2.2 - wordwrapjs: 5.1.0 + wordwrapjs: 5.1.1 table@6.9.0: dependencies: @@ -25554,12 +25566,12 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.28.4) esbuild: 0.25.11 - ts-jest@29.3.3(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(jest@29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)))(typescript@5.4.5): + ts-jest@29.3.3(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(jest@29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)))(typescript@5.4.5): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)) + jest: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -25632,14 +25644,14 @@ snapshots: yn: 3.1.1 optional: true - ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5): + ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.9 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.15.18 + '@types/node': 24.10.11 acorn: 8.11.1 acorn-walk: 8.3.0 arg: 4.1.3 @@ -25739,8 +25751,6 @@ snapshots: typescript@5.9.3: {} - typical@4.0.0: {} - typical@7.3.0: {} uc.micro@2.1.0: {} @@ -25767,7 +25777,9 @@ snapshots: undici-types@6.21.0: {} - undici@6.21.3: {} + undici-types@7.16.0: {} + + undici@6.23.0: {} undici@7.11.0: {} @@ -25937,7 +25949,7 @@ snapshots: terser: 5.39.2 yaml: 2.7.0 - vite@6.4.1(@types/node@22.15.18)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(yaml@2.7.0): + vite@6.4.1(@types/node@24.10.11)(jiti@2.5.1)(less@4.3.0)(terser@5.39.2)(yaml@2.7.0): dependencies: esbuild: 0.25.11 fdir: 6.5.0(picomatch@4.0.3) @@ -25946,7 +25958,7 @@ snapshots: rollup: 4.52.5 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.15.18 + '@types/node': 24.10.11 fsevents: 2.3.3 jiti: 2.5.1 less: 4.3.0 @@ -26267,7 +26279,7 @@ snapshots: wordwrap@1.0.0: {} - wordwrapjs@5.1.0: {} + wordwrapjs@5.1.1: {} workerpool@6.5.1: {} From 30d3236a6c6e4f755b116b43bf80a9440a3003f5 Mon Sep 17 00:00:00 2001 From: Bernhard Merkle Date: Sat, 7 Feb 2026 20:44:49 +0100 Subject: [PATCH 09/11] fixed dependencies --- ts/packages/mcp/thoughts/package.json | 1 + ts/pnpm-lock.yaml | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ts/packages/mcp/thoughts/package.json b/ts/packages/mcp/thoughts/package.json index 419ed6c667..4f414e3bcb 100644 --- a/ts/packages/mcp/thoughts/package.json +++ b/ts/packages/mcp/thoughts/package.json @@ -42,6 +42,7 @@ }, "devDependencies": { "@types/node": "^20.11.19", + "rimraf": "^6.0.1", "typescript": "^5.5.4" } } diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index 122b24ef39..c69fb4e5ba 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -516,7 +516,7 @@ importers: version: 16.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -867,7 +867,7 @@ importers: version: 12.0.2(webpack@5.105.0) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -3631,6 +3631,9 @@ importers: '@types/node': specifier: ^20.11.19 version: 20.19.25 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 typescript: specifier: ^5.5.4 version: 5.9.3 From 9e766ca479356f13c4e2f7e2dfd5302db5fce04c Mon Sep 17 00:00:00 2001 From: Bernhard Merkle Date: Sat, 7 Feb 2026 21:40:17 +0100 Subject: [PATCH 10/11] file.ts - Fixed ensureDir function to use synchronous fs.mkdirSync instead of async fs.promises.mkdir without await. This fixes the race condition causing "ENOENT: no such file or directory" errors when tests try to write files immediately after creating directories. grammarIntegration.spec.ts: Added describeIf and hasTestKeys imports from test-lib Wrapped the "Grammar Generation via populateCache" describe block with describeIf(..., () => hasTestKeys(), ...) to skip API-dependent tests when no API keys are configured Increased the timeout from 60 seconds to 180 seconds (3 minutes) for the API call test, as LLM calls can be slow Added test-lib to package.json devDependencies --- ts/packages/cache/package.json | 1 + ts/packages/cache/test/grammarIntegration.spec.ts | 11 ++++++++--- ts/packages/testLib/src/file.ts | 2 +- ts/pnpm-lock.yaml | 7 +++++-- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/ts/packages/cache/package.json b/ts/packages/cache/package.json index 08800e8e7f..1e460da9c8 100644 --- a/ts/packages/cache/package.json +++ b/ts/packages/cache/package.json @@ -55,6 +55,7 @@ "jest": "^29.7.0", "prettier": "^3.5.3", "rimraf": "^6.0.1", + "test-lib": "workspace:*", "typescript": "~5.4.5" } } diff --git a/ts/packages/cache/test/grammarIntegration.spec.ts b/ts/packages/cache/test/grammarIntegration.spec.ts index e0f47c0601..bc5846c620 100644 --- a/ts/packages/cache/test/grammarIntegration.spec.ts +++ b/ts/packages/cache/test/grammarIntegration.spec.ts @@ -22,6 +22,7 @@ import { compileGrammarToNFA, loadGrammarRules, } from "action-grammar"; +import { describeIf, hasTestKeys } from "test-lib"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -841,7 +842,10 @@ describe("Grammar Integration", () => { }); }); - describe("Grammar Generation via populateCache", () => { + describeIf( + "Grammar Generation via populateCache", + () => hasTestKeys(), + () => { it("should generate and add grammar rules from request/action pairs", async () => { const staticGrammarText = `@ = @ = play $(track:string) -> { @@ -971,7 +975,7 @@ describe("Grammar Integration", () => { console.error("Grammar generation error:", error.message); throw error; } - }, 60000); // 60 second timeout for API call + }, 180000); // 3 minute timeout for API call (LLM can be slow) it("should handle grammar generation errors gracefully", async () => { // Test that populateCache handles errors gracefully (e.g., invalid schema path) @@ -1006,7 +1010,8 @@ describe("Grammar Integration", () => { expect(error).toBeDefined(); } }); - }); + }, + ); describe("Grammar Merging - Comprehensive Tests", () => { it("should handle multi-token sequences correctly after merging", () => { diff --git a/ts/packages/testLib/src/file.ts b/ts/packages/testLib/src/file.ts index 4cb854f123..363905de54 100644 --- a/ts/packages/testLib/src/file.ts +++ b/ts/packages/testLib/src/file.ts @@ -48,7 +48,7 @@ export function readTestJsonFile(filePath: string): any { export function ensureDir(folderPath: string): string { if (!fs.existsSync(folderPath)) { - fs.promises.mkdir(folderPath, { recursive: true }); + fs.mkdirSync(folderPath, { recursive: true }); } return folderPath; } diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index c69fb4e5ba..8d8e63e736 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -142,7 +142,7 @@ importers: version: 8.18.1 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) + version: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -867,7 +867,7 @@ importers: version: 12.0.2(webpack@5.105.0) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.10.11)(typescript@5.4.5)) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -2690,6 +2690,9 @@ importers: rimraf: specifier: ^6.0.1 version: 6.0.1 + test-lib: + specifier: workspace:* + version: link:../testLib typescript: specifier: ~5.4.5 version: 5.4.5 From daf06f1b9fb9d671acdb2bb41f8a05f854a6fb13 Mon Sep 17 00:00:00 2001 From: Bernhard Merkle Date: Sat, 7 Feb 2026 21:46:25 +0100 Subject: [PATCH 11/11] format --- .../cache/test/grammarIntegration.spec.ts | 282 +++++++++--------- 1 file changed, 146 insertions(+), 136 deletions(-) diff --git a/ts/packages/cache/test/grammarIntegration.spec.ts b/ts/packages/cache/test/grammarIntegration.spec.ts index bc5846c620..3ffb6714e8 100644 --- a/ts/packages/cache/test/grammarIntegration.spec.ts +++ b/ts/packages/cache/test/grammarIntegration.spec.ts @@ -846,171 +846,181 @@ describe("Grammar Integration", () => { "Grammar Generation via populateCache", () => hasTestKeys(), () => { - it("should generate and add grammar rules from request/action pairs", async () => { - const staticGrammarText = `@ = + it("should generate and add grammar rules from request/action pairs", async () => { + const staticGrammarText = `@ = @ = play $(track:string) -> { actionName: "play", parameters: { track: $(track) } }`; - const grammar = loadGrammarRules("player", staticGrammarText, []); - const cache = new AgentCache( - "test", - mockExplainerFactory, - undefined, - ); - const agentGrammarRegistry = new AgentGrammarRegistry(); - const persistedStore = new PersistedGrammarStore(); - - await persistedStore.newStore(grammarStoreFile); + const grammar = loadGrammarRules( + "player", + staticGrammarText, + [], + ); + const cache = new AgentCache( + "test", + mockExplainerFactory, + undefined, + ); + const agentGrammarRegistry = new AgentGrammarRegistry(); + const persistedStore = new PersistedGrammarStore(); - cache.grammarStore.addGrammar("player", grammar!); - agentGrammarRegistry.registerAgent( - "player", - grammar!, - compileGrammarToNFA(grammar!, "player"), - ); + await persistedStore.newStore(grammarStoreFile); - // Use real player schema file from the agents package - const playerSchemaPath = path.join( - __dirname, - "../../../agents/player/dist/agent/playerSchema.pas.json", - ); + cache.grammarStore.addGrammar("player", grammar!); + agentGrammarRegistry.registerAgent( + "player", + grammar!, + compileGrammarToNFA(grammar!, "player"), + ); - // Verify schema file exists - if (!fs.existsSync(playerSchemaPath)) { - console.log( - `⚠ Player schema not found at ${playerSchemaPath}`, + // Use real player schema file from the agents package + const playerSchemaPath = path.join( + __dirname, + "../../../agents/player/dist/agent/playerSchema.pas.json", ); - console.log( - "Run 'npm run build' in packages/agents/player to generate the schema", + + // Verify schema file exists + if (!fs.existsSync(playerSchemaPath)) { + console.log( + `⚠ Player schema not found at ${playerSchemaPath}`, + ); + console.log( + "Run 'npm run build' in packages/agents/player to generate the schema", + ); + return; // Skip test if schema not built + } + + console.log(`Using player schema: ${playerSchemaPath}`); + + // Configure with schema path getter + cache.configureGrammarGeneration( + agentGrammarRegistry, + persistedStore, + true, + (schemaName: string) => playerSchemaPath, ); - return; // Skip test if schema not built - } - console.log(`Using player schema: ${playerSchemaPath}`); + const namespaceKeys = cache.getNamespaceKeys( + ["player"], + undefined, + ); - // Configure with schema path getter - cache.configureGrammarGeneration( - agentGrammarRegistry, - persistedStore, - true, - (schemaName: string) => playerSchemaPath, - ); + // Before generation, "pause" should not match + const matchesBefore = cache.match("pause", { namespaceKeys }); + expect(matchesBefore.length).toBe(0); - const namespaceKeys = cache.getNamespaceKeys(["player"], undefined); + // Import populateCache dynamically + const { populateCache } = await import( + "action-grammar/generation" + ); - // Before generation, "pause" should not match - const matchesBefore = cache.match("pause", { namespaceKeys }); - expect(matchesBefore.length).toBe(0); - - // Import populateCache dynamically - const { populateCache } = await import("action-grammar/generation"); - - try { - // Generate grammar rule for a new action - const genResult = await populateCache({ - request: "pause", - schemaName: "player", - action: { - actionName: "pause", - parameters: {}, - }, - schemaPath: playerSchemaPath, - }); - - console.log("populateCache result:", genResult); - - // If generation succeeded, add the rule - if (genResult.success && genResult.generatedRule) { - await persistedStore.addRule({ + try { + // Generate grammar rule for a new action + const genResult = await populateCache({ + request: "pause", schemaName: "player", - grammarText: genResult.generatedRule, + action: { + actionName: "pause", + parameters: {}, + }, + schemaPath: playerSchemaPath, }); - const agentGrammar = - agentGrammarRegistry.getAgent("player"); - const addResult = agentGrammar!.addGeneratedRules( - genResult.generatedRule, - ); - console.log( - "addGeneratedRules result:", - JSON.stringify(addResult, null, 2), - ); - if (!addResult.success) { + console.log("populateCache result:", genResult); + + // If generation succeeded, add the rule + if (genResult.success && genResult.generatedRule) { + await persistedStore.addRule({ + schemaName: "player", + grammarText: genResult.generatedRule, + }); + + const agentGrammar = + agentGrammarRegistry.getAgent("player"); + const addResult = agentGrammar!.addGeneratedRules( + genResult.generatedRule, + ); console.log( - "Failed to add generated rules - this may be expected if the rule format is invalid", + "addGeneratedRules result:", + JSON.stringify(addResult, null, 2), ); - // Don't fail test if rule addition fails - focus on validating the generation worked + if (!addResult.success) { + console.log( + "Failed to add generated rules - this may be expected if the rule format is invalid", + ); + // Don't fail test if rule addition fails - focus on validating the generation worked + } else { + console.log( + "✓ Successfully added generated rule to agent grammar", + ); + + cache.syncAgentGrammar("player"); + + // After generation, "pause" should match + const matchesAfter = cache.match("pause", { + namespaceKeys, + }); + expect(matchesAfter.length).toBeGreaterThan(0); + expect( + matchesAfter[0].match.actions[0].action + .actionName, + ).toBe("pause"); + + console.log( + "✓ Grammar generation and integration successful", + ); + } } else { console.log( - "✓ Successfully added generated rule to agent grammar", + "Grammar generation was rejected:", + genResult.rejectionReason, ); + // Don't fail test if generation was legitimately rejected + } + } catch (error: any) { + // Let all errors throw so tests fail properly + console.error("Grammar generation error:", error.message); + throw error; + } + }, 180000); // 3 minute timeout for API call (LLM can be slow) - cache.syncAgentGrammar("player"); + it("should handle grammar generation errors gracefully", async () => { + // Test that populateCache handles errors gracefully (e.g., invalid schema path) + try { + const { populateCache } = await import( + "action-grammar/generation" + ); - // After generation, "pause" should match - const matchesAfter = cache.match("pause", { - namespaceKeys, - }); - expect(matchesAfter.length).toBeGreaterThan(0); - expect( - matchesAfter[0].match.actions[0].action.actionName, - ).toBe("pause"); + const result = await populateCache({ + request: "test request", + schemaName: "test", + action: { + actionName: "testAction", + parameters: {}, + }, + schemaPath: "/nonexistent/mock/path.pas.json", // Invalid path + }); - console.log( - "✓ Grammar generation and integration successful", - ); - } - } else { + // Should fail gracefully with invalid schema path + expect(result.success).toBe(false); + expect(result.rejectionReason).toBeDefined(); console.log( - "Grammar generation was rejected:", - genResult.rejectionReason, + "Handled error gracefully:", + result.rejectionReason, ); - // Don't fail test if generation was legitimately rejected + } catch (error: any) { + // Error during file reading is expected and acceptable + console.log( + "Expected error for invalid schema path:", + error.message, + ); + expect(error).toBeDefined(); } - } catch (error: any) { - // Let all errors throw so tests fail properly - console.error("Grammar generation error:", error.message); - throw error; - } - }, 180000); // 3 minute timeout for API call (LLM can be slow) - - it("should handle grammar generation errors gracefully", async () => { - // Test that populateCache handles errors gracefully (e.g., invalid schema path) - try { - const { populateCache } = await import( - "action-grammar/generation" - ); - - const result = await populateCache({ - request: "test request", - schemaName: "test", - action: { - actionName: "testAction", - parameters: {}, - }, - schemaPath: "/nonexistent/mock/path.pas.json", // Invalid path - }); - - // Should fail gracefully with invalid schema path - expect(result.success).toBe(false); - expect(result.rejectionReason).toBeDefined(); - console.log( - "Handled error gracefully:", - result.rejectionReason, - ); - } catch (error: any) { - // Error during file reading is expected and acceptable - console.log( - "Expected error for invalid schema path:", - error.message, - ); - expect(error).toBeDefined(); - } - }); - }, + }); + }, ); describe("Grammar Merging - Comprehensive Tests", () => {