init claude-code

This commit is contained in:
2026-04-01 17:32:37 +02:00
commit 73b208c009
1902 changed files with 513237 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
import { queryHaiku } from '../../services/api/claude.js'
import { logError } from '../log.js'
import { extractTextContent } from '../messages.js'
import { asSystemPrompt } from '../systemPromptType.js'
export type DateTimeParseResult =
| { success: true; value: string }
| { success: false; error: string }
/**
* Parse natural language date/time input into ISO 8601 format using Haiku.
*
* Examples:
* - "tomorrow at 3pm" → "2025-10-15T15:00:00-07:00"
* - "next Monday" → "2025-10-20"
* - "in 2 hours" → "2025-10-14T12:30:00-07:00"
*
* @param input The natural language date/time string from the user
* @param format Whether to parse as 'date' (YYYY-MM-DD) or 'date-time' (full ISO 8601 with time)
* @param signal AbortSignal for cancellation
* @returns Parsed ISO 8601 string or error message
*/
export async function parseNaturalLanguageDateTime(
input: string,
format: 'date' | 'date-time',
signal: AbortSignal,
): Promise<DateTimeParseResult> {
// Get current datetime with timezone for context
const now = new Date()
const currentDateTime = now.toISOString()
const timezoneOffset = -now.getTimezoneOffset() // minutes, inverted sign
const tzHours = Math.floor(Math.abs(timezoneOffset) / 60)
const tzMinutes = Math.abs(timezoneOffset) % 60
const tzSign = timezoneOffset >= 0 ? '+' : '-'
const timezone = `${tzSign}${String(tzHours).padStart(2, '0')}:${String(tzMinutes).padStart(2, '0')}`
const dayOfWeek = now.toLocaleDateString('en-US', { weekday: 'long' })
// Build system prompt with context
const systemPrompt = asSystemPrompt([
'You are a date/time parser that converts natural language into ISO 8601 format.',
'You MUST respond with ONLY the ISO 8601 formatted string, with no explanation or additional text.',
'If the input is ambiguous, prefer future dates over past dates.',
"For times without dates, use today's date.",
'For dates without times, do not include a time component.',
'If the input is incomplete or you cannot confidently parse it into a valid date, respond with exactly "INVALID" (nothing else).',
'Examples of INVALID input: partial dates like "2025-01-", lone numbers like "13", gibberish.',
'Examples of valid natural language: "tomorrow", "next Monday", "jan 1st 2025", "in 2 hours", "yesterday".',
])
// Build user prompt with rich context
const formatDescription =
format === 'date'
? 'YYYY-MM-DD (date only, no time)'
: `YYYY-MM-DDTHH:MM:SS${timezone} (full date-time with timezone)`
const userPrompt = `Current context:
- Current date and time: ${currentDateTime} (UTC)
- Local timezone: ${timezone}
- Day of week: ${dayOfWeek}
User input: "${input}"
Output format: ${formatDescription}
Parse the user's input into ISO 8601 format. Return ONLY the formatted string, or "INVALID" if the input is incomplete or unparseable.`
try {
const result = await queryHaiku({
systemPrompt,
userPrompt,
signal,
options: {
querySource: 'mcp_datetime_parse',
agents: [],
isNonInteractiveSession: false,
hasAppendSystemPrompt: false,
mcpTools: [],
enablePromptCaching: false,
},
})
// Extract text from result
const parsedText = extractTextContent(result.message.content).trim()
// Validate that we got something usable
if (!parsedText || parsedText === 'INVALID') {
return {
success: false,
error: 'Unable to parse date/time from input',
}
}
// Basic sanity check - should start with a digit (year)
if (!/^\d{4}/.test(parsedText)) {
return {
success: false,
error: 'Unable to parse date/time from input',
}
}
return { success: true, value: parsedText }
} catch (error) {
// Log error but don't expose details to user
logError(error)
return {
success: false,
error:
'Unable to parse date/time. Please enter in ISO 8601 format manually.',
}
}
}
/**
* Check if a string looks like it might be an ISO 8601 date/time.
* Used to decide whether to attempt NL parsing.
*/
export function looksLikeISO8601(input: string): boolean {
// ISO 8601 date: YYYY-MM-DD
// ISO 8601 datetime: YYYY-MM-DDTHH:MM:SS...
return /^\d{4}-\d{2}-\d{2}(T|$)/.test(input.trim())
}
+336
View File
@@ -0,0 +1,336 @@
import type {
EnumSchema,
MultiSelectEnumSchema,
PrimitiveSchemaDefinition,
StringSchema,
} from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod/v4'
import { jsonStringify } from '../slowOperations.js'
import { plural } from '../stringUtils.js'
import {
looksLikeISO8601,
parseNaturalLanguageDateTime,
} from './dateTimeParser.js'
export type ValidationResult = {
value?: string | number | boolean
isValid: boolean
error?: string
}
const STRING_FORMATS = {
email: {
description: 'email address',
example: 'user@example.com',
},
uri: {
description: 'URI',
example: 'https://example.com',
},
date: {
description: 'date',
example: '2024-03-15',
},
'date-time': {
description: 'date-time',
example: '2024-03-15T14:30:00Z',
},
}
/**
* Check if schema is a single-select enum (either legacy `enum` format or new `oneOf` format)
*/
export const isEnumSchema = (
schema: PrimitiveSchemaDefinition,
): schema is EnumSchema => {
return schema.type === 'string' && ('enum' in schema || 'oneOf' in schema)
}
/**
* Check if schema is a multi-select enum (`type: "array"` with `items.enum` or `items.anyOf`)
*/
export function isMultiSelectEnumSchema(
schema: PrimitiveSchemaDefinition,
): schema is MultiSelectEnumSchema {
return (
schema.type === 'array' &&
'items' in schema &&
typeof schema.items === 'object' &&
schema.items !== null &&
('enum' in schema.items || 'anyOf' in schema.items)
)
}
/**
* Get values from a multi-select enum schema
*/
export function getMultiSelectValues(schema: MultiSelectEnumSchema): string[] {
if ('anyOf' in schema.items) {
return schema.items.anyOf.map(item => item.const)
}
if ('enum' in schema.items) {
return schema.items.enum
}
return []
}
/**
* Get display labels from a multi-select enum schema
*/
export function getMultiSelectLabels(schema: MultiSelectEnumSchema): string[] {
if ('anyOf' in schema.items) {
return schema.items.anyOf.map(item => item.title)
}
if ('enum' in schema.items) {
return schema.items.enum
}
return []
}
/**
* Get label for a specific value in a multi-select enum
*/
export function getMultiSelectLabel(
schema: MultiSelectEnumSchema,
value: string,
): string {
const index = getMultiSelectValues(schema).indexOf(value)
return index >= 0 ? (getMultiSelectLabels(schema)[index] ?? value) : value
}
/**
* Get enum values from EnumSchema (handles both legacy `enum` and new `oneOf` formats)
*/
export function getEnumValues(schema: EnumSchema): string[] {
if ('oneOf' in schema) {
return schema.oneOf.map(item => item.const)
}
if ('enum' in schema) {
return schema.enum
}
return []
}
/**
* Get enum display labels from EnumSchema
*/
export function getEnumLabels(schema: EnumSchema): string[] {
if ('oneOf' in schema) {
return schema.oneOf.map(item => item.title)
}
if ('enum' in schema) {
return ('enumNames' in schema ? schema.enumNames : undefined) ?? schema.enum
}
return []
}
/**
* Get label for a specific enum value
*/
export function getEnumLabel(schema: EnumSchema, value: string): string {
const index = getEnumValues(schema).indexOf(value)
return index >= 0 ? (getEnumLabels(schema)[index] ?? value) : value
}
function getZodSchema(schema: PrimitiveSchemaDefinition): z.ZodTypeAny {
if (isEnumSchema(schema)) {
const [first, ...rest] = getEnumValues(schema)
if (!first) {
return z.never()
}
return z.enum([first, ...rest])
}
if (schema.type === 'string') {
let stringSchema = z.string()
if (schema.minLength !== undefined) {
stringSchema = stringSchema.min(schema.minLength, {
message: `Must be at least ${schema.minLength} ${plural(schema.minLength, 'character')}`,
})
}
if (schema.maxLength !== undefined) {
stringSchema = stringSchema.max(schema.maxLength, {
message: `Must be at most ${schema.maxLength} ${plural(schema.maxLength, 'character')}`,
})
}
switch (schema.format) {
case 'email':
stringSchema = stringSchema.email({
message: 'Must be a valid email address, e.g. user@example.com',
})
break
case 'uri':
stringSchema = stringSchema.url({
message: 'Must be a valid URI, e.g. https://example.com',
})
break
case 'date':
stringSchema = stringSchema.date(
'Must be a valid date, e.g. 2024-03-15, today, next Monday',
)
break
case 'date-time':
stringSchema = stringSchema.datetime({
offset: true,
message:
'Must be a valid date-time, e.g. 2024-03-15T14:30:00Z, tomorrow at 3pm',
})
break
default:
// No specific format validation
break
}
return stringSchema
}
if (schema.type === 'number' || schema.type === 'integer') {
const typeLabel = schema.type === 'integer' ? 'an integer' : 'a number'
const isInteger = schema.type === 'integer'
const formatNum = (n: number) =>
Number.isInteger(n) && !isInteger ? `${n}.0` : String(n)
// Build a single descriptive error message for range violations
const rangeMsg =
schema.minimum !== undefined && schema.maximum !== undefined
? `Must be ${typeLabel} between ${formatNum(schema.minimum)} and ${formatNum(schema.maximum)}`
: schema.minimum !== undefined
? `Must be ${typeLabel} >= ${formatNum(schema.minimum)}`
: schema.maximum !== undefined
? `Must be ${typeLabel} <= ${formatNum(schema.maximum)}`
: `Must be ${typeLabel}`
let numberSchema = z.coerce.number({
error: rangeMsg,
})
if (schema.type === 'integer') {
numberSchema = numberSchema.int({ message: rangeMsg })
}
if (schema.minimum !== undefined) {
numberSchema = numberSchema.min(schema.minimum, {
message: rangeMsg,
})
}
if (schema.maximum !== undefined) {
numberSchema = numberSchema.max(schema.maximum, {
message: rangeMsg,
})
}
return numberSchema
}
if (schema.type === 'boolean') {
return z.coerce.boolean()
}
throw new Error(`Unsupported schema: ${jsonStringify(schema)}`)
}
export function validateElicitationInput(
stringValue: string,
schema: PrimitiveSchemaDefinition,
): ValidationResult {
const zodSchema = getZodSchema(schema)
const parseResult = zodSchema.safeParse(stringValue)
if (parseResult.success) {
// zodSchema always produces primitive types for elicitation
return {
value: parseResult.data as string | number | boolean,
isValid: true,
}
}
return {
isValid: false,
error: parseResult.error.issues.map(e => e.message).join('; '),
}
}
const hasStringFormat = (
schema: PrimitiveSchemaDefinition,
): schema is StringSchema & { format: string } => {
return (
schema.type === 'string' &&
'format' in schema &&
typeof schema.format === 'string'
)
}
/**
* Returns a helpful placeholder/hint for a given format
*/
export function getFormatHint(
schema: PrimitiveSchemaDefinition,
): string | undefined {
if (schema.type === 'string') {
if (!hasStringFormat(schema)) {
return undefined
}
const { description, example } = STRING_FORMATS[schema.format] || {}
return `${description}, e.g. ${example}`
}
if (schema.type === 'number' || schema.type === 'integer') {
const isInteger = schema.type === 'integer'
const formatNum = (n: number) =>
Number.isInteger(n) && !isInteger ? `${n}.0` : String(n)
if (schema.minimum !== undefined && schema.maximum !== undefined) {
return `(${schema.type} between ${formatNum(schema.minimum!)} and ${formatNum(schema.maximum!)})`
} else if (schema.minimum !== undefined) {
return `(${schema.type} >= ${formatNum(schema.minimum!)})`
} else if (schema.maximum !== undefined) {
return `(${schema.type} <= ${formatNum(schema.maximum!)})`
} else {
const example = schema.type === 'integer' ? '42' : '3.14'
return `(${schema.type}, e.g. ${example})`
}
}
return undefined
}
/**
* Check if a schema is a date or date-time format that supports NL parsing
*/
export function isDateTimeSchema(
schema: PrimitiveSchemaDefinition,
): schema is StringSchema & { format: 'date' | 'date-time' } {
return (
schema.type === 'string' &&
'format' in schema &&
(schema.format === 'date' || schema.format === 'date-time')
)
}
/**
* Async validation that attempts NL date/time parsing via Haiku
* when the input doesn't look like ISO 8601.
*/
export async function validateElicitationInputAsync(
stringValue: string,
schema: PrimitiveSchemaDefinition,
signal: AbortSignal,
): Promise<ValidationResult> {
const syncResult = validateElicitationInput(stringValue, schema)
if (syncResult.isValid) {
return syncResult
}
if (isDateTimeSchema(schema) && !looksLikeISO8601(stringValue)) {
const parseResult = await parseNaturalLanguageDateTime(
stringValue,
schema.format,
signal,
)
if (parseResult.success) {
const validatedParsed = validateElicitationInput(
parseResult.value,
schema,
)
if (validatedParsed.isValid) {
return validatedParsed
}
}
}
return syncResult
}