- Implement SSHandle struct with comprehensive Git service handling capabilities - Add support for multiple authentication methods including password, public key and certificate - Integrate Git command parsing and execution with proper channel management - Implement branch protection rules enforcement during Git operations - Add robust error handling and logging for SSH connections and Git processes - Create secure Git command execution with environment isolation - Implement proper resource cleanup for channels and subprocesses - Add support for receive-pack, upload-pack and upload-archive services - Integrate with existing authz and database services for permission checks - Implement proper data forwarding between SSH channels and Git processes fix(config): improve environment loading with error reporting - Replace silent dotenv loading failures with informative error messages - Handle global config race conditions safely during application startup - Improve config loading reliability and debugging capabilities fix(link-unfurl): handle server-side rendering compatibility - Add undefined window object check for SSR environments - Prevent client-side only code from breaking server-side rendering refactor(agent): improve tool registry error handling - Replace panics with graceful error logging for duplicate tool registrations - Add proper error type definitions for tool registry operations - Implement safe merging of registries with duplicate detection fix(room-context): enhance WebSocket connection reliability - Add proper error handling for room subscription operations - Improve connection management with better error suppression - Add console warnings for debugging connection issues feat(ws-client): add comprehensive WebSocket client implementation - Create RoomWsClient class with complete WebSocket communication layer - Implement request-response pattern with timeout handling - Add support for various room-related events and actions - Include proper connection status tracking and management - Implement callback system for different event types - Add automatic reconnection and error recovery mechanisms
145 lines
3.7 KiB
TypeScript
145 lines
3.7 KiB
TypeScript
/**
|
|
* URL pattern detection and unfurling for smart link previews.
|
|
* Supports internal URLs (issues, PRs, commits, repos) and external URLs.
|
|
*/
|
|
|
|
|
|
export type LinkType =
|
|
| 'issue'
|
|
| 'pull_request'
|
|
| 'commit'
|
|
| 'repository'
|
|
| 'room'
|
|
| 'project'
|
|
| 'external';
|
|
|
|
export interface UnfurlResult {
|
|
type: LinkType;
|
|
url: string;
|
|
/** Parsed entity ID fields */
|
|
ids: {
|
|
project?: string;
|
|
namespace?: string;
|
|
repo?: string;
|
|
issueNumber?: number;
|
|
prNumber?: number;
|
|
commitOid?: string;
|
|
roomId?: string;
|
|
};
|
|
/** Display title (populated after fetch) */
|
|
title?: string;
|
|
/** Extra metadata (populated after fetch) */
|
|
meta?: Record<string, unknown>;
|
|
/** Whether the URL is external */
|
|
isExternal?: boolean;
|
|
/** Domain for external URLs */
|
|
domain?: string;
|
|
}
|
|
|
|
/** Internal URL patterns */
|
|
const INTERNAL_PATTERNS: Array<{
|
|
type: LinkType;
|
|
regex: RegExp;
|
|
extract: (match: RegExpMatchArray) => UnfurlResult['ids'];
|
|
}> = [
|
|
{
|
|
type: 'issue',
|
|
regex: /^\/project\/([^/]+)\/issues\/(\d+)/,
|
|
extract: (m) => ({ project: m[1], issueNumber: parseInt(m[2], 10) }),
|
|
},
|
|
{
|
|
type: 'pull_request',
|
|
regex: /^\/repository\/([^/]+)\/([^/]+)\/pulls?\/(\d+)/,
|
|
extract: (m) => ({ namespace: m[1], repo: m[2], prNumber: parseInt(m[3], 10) }),
|
|
},
|
|
{
|
|
type: 'commit',
|
|
regex: /^\/repository\/([^/]+)\/([^/]+)\/commit\/([a-f0-9]+)/,
|
|
extract: (m) => ({ namespace: m[1], repo: m[2], commitOid: m[3] }),
|
|
},
|
|
{
|
|
type: 'repository',
|
|
regex: /^\/repository\/([^/]+)\/([^/]+)/,
|
|
extract: (m) => ({ namespace: m[1], repo: m[2] }),
|
|
},
|
|
{
|
|
type: 'project',
|
|
regex: /^\/project\/([^/]+)/,
|
|
extract: (m) => ({ project: m[1] }),
|
|
},
|
|
{
|
|
type: 'room',
|
|
regex: /^\/project\/([^/]+)\/room(?:\/([^/?#]+))?/,
|
|
extract: (m) => ({ project: m[1], roomId: m[2] }),
|
|
},
|
|
];
|
|
|
|
/** Detect the type and IDs of a URL */
|
|
export function detectLinkType(url: string): UnfurlResult | null {
|
|
// Remove trailing slashes and hash
|
|
const normalized = url.split('?')[0].split('#')[0].replace(/\/+$/, '');
|
|
|
|
// Check internal patterns
|
|
for (const pattern of INTERNAL_PATTERNS) {
|
|
const match = normalized.match(pattern.regex);
|
|
if (match) {
|
|
return {
|
|
type: pattern.type,
|
|
url: `/${normalized.replace(/^\//, '')}`,
|
|
ids: pattern.extract(match),
|
|
};
|
|
}
|
|
}
|
|
|
|
// External URL
|
|
try {
|
|
const parsed = new URL(url);
|
|
const isExternal = typeof window === 'undefined'
|
|
? true
|
|
: !parsed.hostname.includes(window.location.hostname);
|
|
return {
|
|
type: 'external',
|
|
url,
|
|
ids: {},
|
|
isExternal,
|
|
domain: parsed.hostname,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Extract all URLs from a text string */
|
|
export function extractUrls(text: string): Array<{ url: string; index: number }> {
|
|
// Match URLs (including internal paths starting with /)
|
|
const urlPattern = /(?:https?:\/\/[^\s<>"]+|^\/[^\s<>"]+|\/[^\s<>"]+)/gm;
|
|
const results: Array<{ url: string; index: number }> = [];
|
|
let match;
|
|
|
|
while ((match = urlPattern.exec(text)) !== null) {
|
|
const url = match[0];
|
|
// Filter out very short or malformed URLs
|
|
if (url.length > 3) {
|
|
results.push({ url, index: match.index });
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/** Simple cache for unfurl results */
|
|
const unfurlCache = new Map<string, UnfurlResult & { expiresAt: number }>();
|
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
|
|
export function getCachedUnfurl(url: string): (UnfurlResult & { expiresAt: number }) | undefined {
|
|
const cached = unfurlCache.get(url);
|
|
if (cached && cached.expiresAt > Date.now()) {
|
|
return cached;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function setCachedUnfurl(url: string, result: UnfurlResult): void {
|
|
unfurlCache.set(url, { ...result, expiresAt: Date.now() + CACHE_TTL });
|
|
}
|