feat: base shortcut code
This commit is contained in:
25
utils/anthropicClient.ts
Normal file
25
utils/anthropicClient.ts
Normal 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)}.`);
|
||||
}
|
||||
}
|
||||
32
utils/commands/anthropic.ts
Normal file
32
utils/commands/anthropic.ts
Normal 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.'
|
||||
};
|
||||
}
|
||||
}
|
||||
11
utils/commands/ping.test.ts
Normal file
11
utils/commands/ping.test.ts
Normal 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
11
utils/commands/ping.ts
Normal 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()
|
||||
}
|
||||
};
|
||||
}
|
||||
12
utils/commands/time.test.ts
Normal file
12
utils/commands/time.test.ts
Normal 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
19
utils/commands/time.ts
Normal 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
54
utils/handler.test.ts
Normal 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
45
utils/handler.ts
Normal 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
31
utils/registry.ts
Normal 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
19
utils/types.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user