mirror of
https://github.com/aljazceru/mcp-code.git
synced 2025-12-17 12:45:28 +01:00
initial commit
This commit is contained in:
30
lib/README.md
Normal file
30
lib/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# MCP-NDK Library Structure
|
||||
|
||||
This directory contains reusable code organized into modules to reduce duplication and improve maintainability.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- `/lib` - Core libraries and utilities
|
||||
- `/types` - TypeScript type definitions used across the application
|
||||
- `/nostr` - Nostr-related functionality
|
||||
- `utils.ts` - Utility functions for working with Nostr
|
||||
- `snippets.ts` - Functions for managing code snippets
|
||||
- `/utils` - General utility functions
|
||||
- `log.ts` - Logging functionality
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Single Responsibility**: Each module has a specific, focused purpose
|
||||
2. **DRY (Don't Repeat Yourself)**: Common code is extracted into reusable functions
|
||||
3. **Separation of Concerns**: Clear separation between types, utilities, and business logic
|
||||
4. **Consistency**: Consistent patterns and naming conventions throughout the codebase
|
||||
|
||||
## How to Use
|
||||
|
||||
When adding new functionality, follow these guidelines:
|
||||
|
||||
1. Place type definitions in `/lib/types`
|
||||
2. Place general utilities in `/lib/utils`
|
||||
3. Organize Nostr-specific code under `/lib/nostr`
|
||||
4. Aim to minimize duplication by leveraging existing utilities
|
||||
5. Keep command files focused on their specific command, delegating to library code for implementation
|
||||
136
lib/nostr/snippets.ts
Normal file
136
lib/nostr/snippets.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import type { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk";
|
||||
import { ndk } from "../../ndk.js";
|
||||
import { knownUsers } from "../../users.js";
|
||||
import type { CodeSnippet, FindSnippetsParams } from "../types/index.js";
|
||||
import { log } from "../utils/log.js";
|
||||
import { SNIPPET_KIND, eventToSnippet, identifierToPubkeys } from "./utils.js";
|
||||
|
||||
/**
|
||||
* Get code snippets from Nostr events of kind 1337
|
||||
*
|
||||
* @param params - Parameters to filter snippets
|
||||
* @returns Array of code snippets
|
||||
*/
|
||||
export async function getSnippets(params: FindSnippetsParams = {}): Promise<{
|
||||
snippets: CodeSnippet[];
|
||||
otherSnippets: CodeSnippet[];
|
||||
}> {
|
||||
// Construct filter based on params
|
||||
const filter: NDKFilter = {
|
||||
kinds: [SNIPPET_KIND as number],
|
||||
limit: params.limit || 500,
|
||||
};
|
||||
|
||||
// Add optional filters
|
||||
if (params.since) {
|
||||
filter.since = params.since;
|
||||
}
|
||||
|
||||
if (params.until) {
|
||||
filter.until = params.until;
|
||||
}
|
||||
|
||||
if (params.authors && params.authors.length > 0) {
|
||||
for (const author of params.authors) {
|
||||
const pubkeys = identifierToPubkeys(author);
|
||||
if (pubkeys.length) {
|
||||
filter.authors ??= [];
|
||||
filter.authors.push(...pubkeys);
|
||||
} else {
|
||||
log(`Unknown author: ${author}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add custom tag filters for languages and tags
|
||||
if (params.languages && params.languages.length > 0) {
|
||||
filter["#l"] = params.languages;
|
||||
}
|
||||
|
||||
if (params.tags && params.tags.length > 0) {
|
||||
filter["#t"] = params.tags;
|
||||
}
|
||||
|
||||
log(`Fetching snippets with filter: ${JSON.stringify(filter, null, 2)}`);
|
||||
|
||||
// Fetch events
|
||||
const events = await ndk.fetchEvents(filter);
|
||||
|
||||
let maxMatchCount = 0;
|
||||
|
||||
function getMatchCount(event: NDKEvent) {
|
||||
if (!params.tags || params.tags.length === 0) return 1;
|
||||
|
||||
const aTags = event.tags
|
||||
.filter((tag) => tag[0] === "t")
|
||||
.map((tag) => tag[1])
|
||||
.filter((t) => t !== undefined);
|
||||
return params.tags.filter((tag) =>
|
||||
aTags.some((t) => t.match(new RegExp(tag, "i")))
|
||||
).length;
|
||||
}
|
||||
|
||||
for (const event of events) {
|
||||
const aMatches = getMatchCount(event);
|
||||
if (aMatches > maxMatchCount) maxMatchCount = aMatches;
|
||||
}
|
||||
|
||||
const selectedEvents: NDKEvent[] = [];
|
||||
const notSelectedEvents: NDKEvent[] = [];
|
||||
|
||||
for (const event of events) {
|
||||
if (getMatchCount(event) === maxMatchCount) {
|
||||
selectedEvents.push(event);
|
||||
} else {
|
||||
notSelectedEvents.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert events to snippets
|
||||
const snippets = selectedEvents.map(eventToSnippet);
|
||||
const otherSnippets = notSelectedEvents.map(eventToSnippet);
|
||||
|
||||
return { snippets, otherSnippets };
|
||||
}
|
||||
|
||||
/**
|
||||
* Format snippets for display
|
||||
* @param snippets Array of code snippets
|
||||
* @returns Formatted string representation
|
||||
*/
|
||||
export function formatSnippets(snippets: CodeSnippet[]): string {
|
||||
return snippets
|
||||
.map((snippet) => {
|
||||
const author = knownUsers[snippet.pubkey];
|
||||
const keys: Record<string, string> = {
|
||||
Title: snippet.title,
|
||||
Language: snippet.language,
|
||||
Tags: snippet.tags.join(", "),
|
||||
Code: snippet.code,
|
||||
};
|
||||
if (author?.profile?.name) keys.Author = author.profile.name;
|
||||
return Object.entries(keys)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join("\n");
|
||||
})
|
||||
.join("\n\n---\n\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Format partial match snippets for display
|
||||
* @param snippets Array of code snippets
|
||||
* @returns Formatted string representation
|
||||
*/
|
||||
export function formatPartialMatches(snippets: CodeSnippet[]): string {
|
||||
if (snippets.length === 0) return "";
|
||||
|
||||
let text =
|
||||
"\n\nSome other events not included in this result since they had less in common with your search, here is a list of the events that had partial matches:\n\n";
|
||||
text += snippets
|
||||
.map((snippet) => {
|
||||
return ` * ${snippet.title}:\n Tags: ${snippet.tags.join(", ")}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return text;
|
||||
}
|
||||
76
lib/nostr/utils.ts
Normal file
76
lib/nostr/utils.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { NDKEvent, NDKSigner } from "@nostr-dev-kit/ndk";
|
||||
import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { ndk } from "../../ndk.js";
|
||||
import { queryUser } from "../../users.js";
|
||||
import type { CodeSnippet } from "../types/index.js";
|
||||
import { getUser } from "../../config.js";
|
||||
|
||||
export const SNIPPET_KIND = 1337;
|
||||
|
||||
/**
|
||||
* Gets the appropriate signer based on the username
|
||||
* @param username Username to get signer for (or "main" for default)
|
||||
* @returns The signer to use
|
||||
* @throws Error if user not found or missing nsec
|
||||
*/
|
||||
export async function getSigner(username?: string): Promise<NDKSigner> {
|
||||
// If no username or "main", return the default signer
|
||||
if (!username || username === "main") {
|
||||
if (!ndk.signer) {
|
||||
throw new Error("No default signer configured");
|
||||
}
|
||||
return ndk.signer;
|
||||
}
|
||||
|
||||
// Otherwise, get the user's signer
|
||||
const userData = getUser(username);
|
||||
|
||||
if (!userData?.nsec) {
|
||||
throw new Error(`User "${username}" not found in config or missing nsec`);
|
||||
}
|
||||
|
||||
return new NDKPrivateKeySigner(userData.nsec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an identifier (pubkey, npub, or name) to pubkeys
|
||||
* @param identifier The identifier to convert
|
||||
* @returns Array of pubkeys
|
||||
*/
|
||||
export function identifierToPubkeys(identifier: string): string[] {
|
||||
// If it's an npub, convert directly
|
||||
if (identifier.startsWith("npub")) {
|
||||
return [ndk.getUser({ npub: identifier }).pubkey];
|
||||
}
|
||||
|
||||
// If it's a hex pubkey, return as is
|
||||
if (identifier.length === 64 && /^[0-9a-f]+$/i.test(identifier)) {
|
||||
return [identifier];
|
||||
}
|
||||
|
||||
// Otherwise, search by profile name or other attributes
|
||||
return queryUser(identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an NDKEvent into a CodeSnippet
|
||||
* @param event NDKEvent of kind 1337
|
||||
* @returns CodeSnippet object
|
||||
*/
|
||||
export function eventToSnippet(event: NDKEvent): CodeSnippet {
|
||||
const title = event.tagValue("title") ?? event.tagValue("name");
|
||||
const language = event.tagValue("l");
|
||||
const tags = event.tags
|
||||
.filter((tag) => tag[0] === "t" && tag[1] !== undefined)
|
||||
.map((tag) => tag[1] as string);
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
title: title || "Untitled",
|
||||
code: event.content,
|
||||
language: language || "text",
|
||||
pubkey: event.pubkey,
|
||||
createdAt: event.created_at || 0,
|
||||
tags,
|
||||
};
|
||||
}
|
||||
25
lib/types/index.ts
Normal file
25
lib/types/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { NDKUserProfile } from "@nostr-dev-kit/ndk";
|
||||
|
||||
export type UserEntry = {
|
||||
profile: NDKUserProfile;
|
||||
data: string;
|
||||
};
|
||||
|
||||
export interface FindSnippetsParams {
|
||||
limit?: number;
|
||||
since?: number;
|
||||
until?: number;
|
||||
authors?: string[];
|
||||
languages?: string[];
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export type CodeSnippet = {
|
||||
id: string;
|
||||
title: string;
|
||||
code: string;
|
||||
language: string;
|
||||
pubkey: string;
|
||||
createdAt: number;
|
||||
tags: string[];
|
||||
};
|
||||
5
lib/utils/log.ts
Normal file
5
lib/utils/log.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Simple logging utility
|
||||
* @param message Message to log
|
||||
*/
|
||||
export function log(_message: string): void {}
|
||||
Reference in New Issue
Block a user