feat: base shortcut code

This commit is contained in:
2025-01-18 21:54:26 +01:00
parent a820b29ea9
commit 3abe88c7a2
29 changed files with 5982 additions and 123 deletions

25
utils/anthropicClient.ts Normal file
View File

@@ -0,0 +1,25 @@
import Anthropic from '@anthropic-ai/sdk';
export async function getMessage(text: string) {
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY
});
console.info('Anthropic request with text: ', text);
const response = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 2048,
messages: [{ role: 'user', content: text }]
});
console.info('Anthropic response: ', response);
try {
const data = response.content as [{ type: string; text: string }];
return data[0].text;
} catch (error) {
throw new Error(`Anthropic client error: ${JSON.stringify(error)}.`);
}
}

View File

@@ -0,0 +1,32 @@
import { getMessage } from '@utils/anthropicClient';
import { ShortcutsResponse } from '../types';
export async function anthropicCommand(
parameters: Record<string, string> | undefined
): Promise<ShortcutsResponse> {
try {
if (!parameters || !parameters['question']) {
return {
success: false,
message: 'Sorry. Need to provide a question.'
};
}
const response = await getMessage(
'I want to know ' +
parameters['question'] +
'. Structure the response in a manner suitable for spoken communication.'
);
return {
success: true,
message: response
};
} catch (error) {
console.error(error);
return {
success: false,
message: 'Sorry. There was a problem with Anthropic.'
};
}
}

View File

@@ -0,0 +1,11 @@
import { pingCommand } from './ping';
describe('Ping Command', () => {
it('should return success response', async () => {
const response = await pingCommand();
expect(response.success).toBe(true);
expect(response.message).toBe('The system is operational.');
expect(response.data).toHaveProperty('timestamp');
});
});

11
utils/commands/ping.ts Normal file
View File

@@ -0,0 +1,11 @@
import { ShortcutsResponse } from '../types';
export async function pingCommand(): Promise<ShortcutsResponse> {
return {
success: true,
message: 'The system is operational.',
data: {
timestamp: new Date().toISOString()
}
};
}

View File

@@ -0,0 +1,12 @@
import { timeCommand } from './time';
describe('Time Command', () => {
it('should return current time', async () => {
const response = await timeCommand();
expect(response.success).toBe(true);
expect(response.message).toMatch(/It's currently \d{1,2}:\d{2} [AP]M/);
expect(response.data).toHaveProperty('timestamp');
expect(response.data).toHaveProperty('formattedTime');
});
});

19
utils/commands/time.ts Normal file
View File

@@ -0,0 +1,19 @@
import { ShortcutsResponse } from '../types';
export async function timeCommand(): Promise<ShortcutsResponse> {
const now = new Date();
const timeString = now.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: 'numeric',
hour12: true
});
return {
success: true,
message: `It's currently ${timeString}`,
data: {
timestamp: now.toISOString(),
formattedTime: timeString
}
};
}

54
utils/handler.test.ts Normal file
View File

@@ -0,0 +1,54 @@
import { ShortcutsHandler } from './handler';
describe('ShortcutsHandler', () => {
let handler: ShortcutsHandler;
const originalEnv = process.env;
beforeEach(() => {
process.env = { ...originalEnv };
process.env.USER_KEY = 'test-key-123';
handler = new ShortcutsHandler();
});
afterEach(() => {
process.env = originalEnv;
});
describe('validateRequest', () => {
it('should validate correct API key', () => {
const isValid = handler.validateRequest({
command: 'test',
apiKey: 'test-key-123'
});
expect(isValid).toBe(true);
});
it('should reject invalid API key', () => {
const isValid = handler.validateRequest({
command: 'test',
apiKey: 'wrong-key'
});
expect(isValid).toBe(false);
});
});
describe('processCommand', () => {
it('should handle ping command', async () => {
const response = await handler.processCommand('ping');
expect(response.success).toBe(true);
expect(response.message).toContain('operational');
});
it('should handle time command', async () => {
const response = await handler.processCommand('time');
expect(response.success).toBe(true);
expect(response.message).toMatch(/currently/);
});
it('should handle unknown command', async () => {
const response = await handler.processCommand('unknown');
expect(response.success).toBe(false);
expect(response.message).toContain('Unknown command');
});
});
});

45
utils/handler.ts Normal file
View File

@@ -0,0 +1,45 @@
import { ShortcutsRequest, ShortcutsResponse } from './types';
import { CommandRegistry } from './registry';
export class ShortcutsHandler {
private registry: CommandRegistry;
constructor() {
this.registry = new CommandRegistry();
}
validateRequest(req: ShortcutsRequest): boolean {
try {
const isValidUser = req.apiKey == process.env.USER_KEY;
return !!isValidUser;
} catch (error) {
console.error('Error validating request:', error);
return false;
}
}
async processCommand(
command: string,
parameters?: Record<string, string>
): Promise<ShortcutsResponse> {
const handler = this.registry.getCommand(command);
if (!handler) {
return {
success: false,
message: `Unknown command: ${command}.`
};
}
try {
return await handler(parameters);
} catch (error) {
console.error(`Error processing command ${command}:`, error);
return {
success: false,
message: 'An error occurred while processing your command.'
};
}
}
}

31
utils/registry.ts Normal file
View File

@@ -0,0 +1,31 @@
import { ShortcutsResponse } from './types';
import { pingCommand } from './commands/ping';
import { timeCommand } from './commands/time';
import { anthropicCommand } from './commands/anthropic';
type CommandHandler = (
parameters?: Record<string, string>
) => Promise<ShortcutsResponse>;
export class CommandRegistry {
private commands: Map<string, CommandHandler>;
constructor() {
this.commands = new Map();
this.registerDefaultCommands();
}
private registerDefaultCommands() {
this.register('ping', pingCommand);
this.register('time', timeCommand);
this.register('anthropic', anthropicCommand);
}
register(command: string, handler: CommandHandler) {
this.commands.set(command.toLowerCase(), handler);
}
getCommand(command: string): CommandHandler | undefined {
return this.commands.get(command.toLowerCase());
}
}

19
utils/types.ts Normal file
View File

@@ -0,0 +1,19 @@
import { z } from 'zod';
export const RequestSchema = z.object({
command: z.string(),
parameters: z.record(z.string()).optional(),
apiKey: z.string()
});
export type ShortcutsRequest = z.infer<typeof RequestSchema>;
export interface ShortcutsResponse {
success: boolean;
message: string;
data?: unknown;
action?: {
type: 'notification' | 'openUrl' | 'runShortcut' | 'wait';
payload: unknown;
};
}