Removed obsidian_search_with_context tool (not in CLI spec) Updated obsidian_search to use exact CLI parameter names: - query (required) - Search query text - path (optional) - Limit search to folder path - limit (optional) - Max number of files to return - total (optional) - Return match count instead of file list - case (optional) - Case sensitive search - format (optional) - Output format: text or json (default: text) Changed parameter names to match CLI: - folder → path - caseSensitive → case - Added: total flag for match counts - Removed: contextLines (not in CLI) Files updated: - src/tools/search.ts: Simplified to single search tool - src/validation/schemas.ts: Updated searchSchema parameters - manifest.json: Removed search_with_context, updated description - tasks.md: Marked T048 as REMOVED Total tools: 20 (was 21) - User Story 1: 9 tools - User Story 2: 11 tools (was 12) Build: ✅ 0 errors Validation: ✅ Manifest passes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
168 lines
4.6 KiB
TypeScript
168 lines
4.6 KiB
TypeScript
/**
|
|
* Zod Validation Schemas
|
|
* Constitutional Principle III: Local Execution Security
|
|
* Constitutional Principle IV: Defensive Programming
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
|
|
/**
|
|
* Common validation patterns
|
|
*/
|
|
|
|
// Vault name validation (alphanumeric, spaces, hyphens, underscores)
|
|
export const vaultNameSchema = z
|
|
.string()
|
|
.min(1, 'Vault name cannot be empty')
|
|
.max(255, 'Vault name too long')
|
|
.regex(/^[a-zA-Z0-9\s_-]+$/, 'Vault name contains invalid characters');
|
|
|
|
// Note name validation (allow most characters, but not path separators)
|
|
export const noteNameSchema = z
|
|
.string()
|
|
.min(1, 'Note name cannot be empty')
|
|
.max(255, 'Note name too long')
|
|
.regex(/^[^/\\]+$/, 'Note name cannot contain path separators');
|
|
|
|
// File path validation (allow subdirectories)
|
|
export const filePathSchema = z
|
|
.string()
|
|
.min(1, 'File path cannot be empty')
|
|
.max(1024, 'File path too long')
|
|
.regex(/^[^<>:"|?*]+$/, 'File path contains invalid characters');
|
|
|
|
// Content validation (allow any string, but limit size)
|
|
export const contentSchema = z
|
|
.string()
|
|
.max(1048576, 'Content too large (max 1MB)'); // 1MB limit
|
|
|
|
// Tag validation (must start with #)
|
|
export const tagSchema = z
|
|
.string()
|
|
.min(2, 'Tag too short')
|
|
.regex(/^#[a-zA-Z0-9_/-]+$/, 'Invalid tag format (must start with # and contain only alphanumeric, _, /, -)');
|
|
|
|
// Property key validation
|
|
export const propertyKeySchema = z
|
|
.string()
|
|
.min(1, 'Property key cannot be empty')
|
|
.max(100, 'Property key too long')
|
|
.regex(/^[a-zA-Z0-9_-]+$/, 'Property key contains invalid characters');
|
|
|
|
// Boolean flag validation
|
|
export const booleanFlagSchema = z
|
|
.union([z.boolean(), z.string()])
|
|
.transform((val) => {
|
|
if (typeof val === 'boolean') return val;
|
|
return val.toLowerCase() === 'true' || val === '1';
|
|
});
|
|
|
|
// Optional string that can be undefined or empty
|
|
export const optionalStringSchema = z.string().optional();
|
|
|
|
// Date string validation (ISO 8601)
|
|
export const dateSchema = z
|
|
.string()
|
|
.regex(/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?)?$/, 'Invalid date format (use ISO 8601)');
|
|
|
|
/**
|
|
* Common parameter schemas
|
|
*/
|
|
|
|
// File identifier (either file name or full path)
|
|
export const fileIdentifierSchema = z.object({
|
|
file: z.string().optional(),
|
|
path: z.string().optional(),
|
|
}).refine(
|
|
(data) => data.file || data.path,
|
|
{ message: 'Either file or path must be provided' }
|
|
);
|
|
|
|
// Pagination parameters
|
|
export const paginationSchema = z.object({
|
|
limit: z.number().int().positive().max(1000).optional(),
|
|
offset: z.number().int().nonnegative().optional(),
|
|
});
|
|
|
|
// Format options
|
|
export const formatSchema = z.object({
|
|
format: z.enum(['json', 'tsv', 'csv', 'text']).optional().default('text'),
|
|
});
|
|
|
|
/**
|
|
* Tool-specific schemas
|
|
*/
|
|
|
|
// Create note parameters
|
|
export const createNoteSchema = z.object({
|
|
name: noteNameSchema,
|
|
path: filePathSchema.optional(),
|
|
content: contentSchema.optional(),
|
|
template: z.string().optional(),
|
|
overwrite: booleanFlagSchema.optional(),
|
|
open: booleanFlagSchema.optional(),
|
|
});
|
|
|
|
// Read note parameters
|
|
export const readNoteSchema = z.union([
|
|
z.object({ file: noteNameSchema }),
|
|
z.object({ path: filePathSchema }),
|
|
]).refine(
|
|
(data) => ('file' in data && data.file) || ('path' in data && data.path),
|
|
{ message: 'Either file or path must be provided' }
|
|
);
|
|
|
|
// Append/Prepend parameters
|
|
export const appendPrependSchema = z.intersection(
|
|
fileIdentifierSchema,
|
|
z.object({
|
|
content: contentSchema,
|
|
inline: booleanFlagSchema.optional(),
|
|
})
|
|
);
|
|
|
|
// Delete note parameters
|
|
export const deleteNoteSchema = z.intersection(
|
|
fileIdentifierSchema,
|
|
z.object({
|
|
permanent: booleanFlagSchema.optional(),
|
|
})
|
|
);
|
|
|
|
// Move/Rename parameters
|
|
export const moveRenameSchema = z.intersection(
|
|
fileIdentifierSchema,
|
|
z.object({
|
|
newPath: filePathSchema.optional(),
|
|
newName: noteNameSchema.optional(),
|
|
})
|
|
).refine(
|
|
(data) => data.newPath || data.newName,
|
|
{ message: 'Either newPath or newName must be provided' }
|
|
);
|
|
|
|
// Search parameters
|
|
export const searchSchema = z.object({
|
|
query: z.string().min(1, 'Search query cannot be empty'),
|
|
path: optionalStringSchema, // Folder path to limit search
|
|
limit: z.number().int().positive().max(1000).optional(),
|
|
total: booleanFlagSchema.optional(), // Return match count instead of files
|
|
case: booleanFlagSchema.optional(), // Case sensitive search
|
|
format: z.enum(['text', 'json']).optional().default('text'),
|
|
});
|
|
|
|
// Tag search parameters
|
|
export const tagSearchSchema = z.object({
|
|
tag: tagSchema,
|
|
...formatSchema.shape,
|
|
});
|
|
|
|
// Property parameters
|
|
export const propertySchema = z.intersection(
|
|
fileIdentifierSchema,
|
|
z.object({
|
|
key: propertyKeySchema,
|
|
value: z.unknown().optional(),
|
|
})
|
|
);
|