mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-05-09 00:20:09 +00:00
feat: update custom mode draft
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
export const ROOT_DIR = '.infio_json_db'
|
||||
export const COMMAND_DIR = 'commands'
|
||||
export const CHAT_DIR = 'chats'
|
||||
export const CUSTOM_MODE_DIR = 'custom_modes'
|
||||
export const INITIAL_MIGRATION_MARKER = '.initial_migration_completed'
|
||||
|
||||
153
src/database/json/custom-mode/CustomModeManager.ts
Executable file
153
src/database/json/custom-mode/CustomModeManager.ts
Executable file
@@ -0,0 +1,153 @@
|
||||
import fuzzysort from 'fuzzysort'
|
||||
import { App } from 'obsidian'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { AbstractJsonRepository } from '../base'
|
||||
import { CUSTOM_MODE_DIR, ROOT_DIR } from '../constants'
|
||||
import {
|
||||
DuplicateCustomModeException,
|
||||
EmptyCustomModeNameException,
|
||||
} from '../exception'
|
||||
|
||||
import { CUSTOM_MODE_SCHEMA_VERSION, CustomMode, CustomModeMetadata } from './types'
|
||||
|
||||
export class CustomModeManager extends AbstractJsonRepository<
|
||||
CustomMode,
|
||||
CustomModeMetadata
|
||||
> {
|
||||
constructor(app: App) {
|
||||
super(app, `${ROOT_DIR}/${CUSTOM_MODE_DIR}`)
|
||||
}
|
||||
|
||||
protected generateFileName(mode: CustomMode): string {
|
||||
// Format: v{schemaVersion}_name_id.json (with name encoded)
|
||||
const encodedName = encodeURIComponent(mode.name)
|
||||
return `v${CUSTOM_MODE_SCHEMA_VERSION}_${encodedName}_${mode.id}.json`
|
||||
}
|
||||
|
||||
protected parseFileName(fileName: string): CustomModeMetadata | null {
|
||||
const match = fileName.match(
|
||||
new RegExp(`^v${CUSTOM_MODE_SCHEMA_VERSION}_(.+)_([0-9a-f-]+)\\.json$`),
|
||||
)
|
||||
if (!match) return null
|
||||
|
||||
const encodedName = match[1]
|
||||
const id = match[2]
|
||||
const name = decodeURIComponent(encodedName)
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
updatedAt: Date.now(),
|
||||
schemaVersion: CUSTOM_MODE_SCHEMA_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
public async createCustomMode(
|
||||
customMode: Omit<
|
||||
CustomMode,
|
||||
'id' | 'slug' | 'createdAt' | 'updatedAt' | 'schemaVersion'
|
||||
>,
|
||||
): Promise<CustomMode> {
|
||||
if (customMode.name !== undefined && customMode.name.length === 0) {
|
||||
throw new EmptyCustomModeNameException()
|
||||
}
|
||||
|
||||
const existingCustomMode = await this.findByName(customMode.name)
|
||||
if (existingCustomMode) {
|
||||
throw new DuplicateCustomModeException(customMode.name)
|
||||
}
|
||||
|
||||
const newCustomMode: CustomMode = {
|
||||
id: uuidv4(),
|
||||
...customMode,
|
||||
slug: customMode.name.toLowerCase().replace(/ /g, '-'),
|
||||
updatedAt: Date.now(),
|
||||
schemaVersion: CUSTOM_MODE_SCHEMA_VERSION,
|
||||
}
|
||||
|
||||
await this.create(newCustomMode)
|
||||
return newCustomMode
|
||||
}
|
||||
|
||||
public async ListCustomModes(): Promise<CustomMode[]> {
|
||||
const allMetadata = await this.listMetadata()
|
||||
const allCustomModes = await Promise.all(allMetadata.map(async (meta) => this.read(meta.fileName)))
|
||||
return allCustomModes.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
}
|
||||
|
||||
public async findById(id: string): Promise<CustomMode | null> {
|
||||
const allMetadata = await this.listMetadata()
|
||||
const targetMetadata = allMetadata.find((meta) => meta.id === id)
|
||||
|
||||
if (!targetMetadata) return null
|
||||
|
||||
return this.read(targetMetadata.fileName)
|
||||
}
|
||||
|
||||
public async findByName(name: string): Promise<CustomMode | null> {
|
||||
const allMetadata = await this.listMetadata()
|
||||
const targetMetadata = allMetadata.find((meta) => meta.name === name)
|
||||
|
||||
if (!targetMetadata) return null
|
||||
|
||||
return this.read(targetMetadata.fileName)
|
||||
}
|
||||
|
||||
public async updateCustomMode(
|
||||
id: string,
|
||||
updates: Partial<
|
||||
Omit<CustomMode, 'id' | 'slug' | 'createdAt' | 'updatedAt' | 'schemaVersion'>
|
||||
>,
|
||||
): Promise<CustomMode | null> {
|
||||
if (updates.name !== undefined && updates.name.length === 0) {
|
||||
throw new EmptyCustomModeNameException()
|
||||
}
|
||||
|
||||
const customMode = await this.findById(id)
|
||||
if (!customMode) return null
|
||||
|
||||
if (updates.name && updates.name !== customMode.name) {
|
||||
const existingCustomMode = await this.findByName(updates.name)
|
||||
if (existingCustomMode) {
|
||||
throw new DuplicateCustomModeException(updates.name)
|
||||
}
|
||||
}
|
||||
|
||||
const updatedCustomMode: CustomMode = {
|
||||
...customMode,
|
||||
...updates,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
|
||||
await this.update(customMode, updatedCustomMode)
|
||||
return updatedCustomMode
|
||||
}
|
||||
|
||||
public async deleteCustomMode(id: string): Promise<boolean> {
|
||||
const customMode = await this.findById(id)
|
||||
if (!customMode) return false
|
||||
|
||||
const fileName = this.generateFileName(customMode)
|
||||
await this.delete(fileName)
|
||||
return true
|
||||
}
|
||||
|
||||
public async searchCustomModes(query: string): Promise<CustomMode[]> {
|
||||
const allMetadata = await this.listMetadata()
|
||||
const results = fuzzysort.go(query, allMetadata, {
|
||||
keys: ['name'],
|
||||
threshold: 0.2,
|
||||
limit: 20,
|
||||
all: true,
|
||||
})
|
||||
|
||||
const customModes = (
|
||||
await Promise.all(
|
||||
results.map(async (result) => this.read(result.obj.fileName)),
|
||||
)
|
||||
).filter((customMode): customMode is CustomMode => customMode !== null)
|
||||
|
||||
return customModes
|
||||
}
|
||||
}
|
||||
84
src/database/json/custom-mode/types.ts
Normal file
84
src/database/json/custom-mode/types.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const CUSTOM_MODE_SCHEMA_VERSION = 1
|
||||
|
||||
export const toolGroups = [
|
||||
"read",
|
||||
"edit",
|
||||
"research",
|
||||
// "browser",
|
||||
// "command",
|
||||
// "mcp",
|
||||
"modes",
|
||||
] as const
|
||||
|
||||
export const toolGroupsSchema = z.enum(toolGroups)
|
||||
|
||||
export type ToolGroup = z.infer<typeof toolGroupsSchema>
|
||||
|
||||
export const groupOptionsSchema = z.object({
|
||||
fileRegex: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(pattern) => {
|
||||
if (!pattern) {
|
||||
return true // Optional, so empty is valid.
|
||||
}
|
||||
|
||||
try {
|
||||
new RegExp(pattern)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
{ message: "Invalid regular expression pattern" },
|
||||
),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export const groupEntrySchema = z.union([toolGroupsSchema, z.tuple([toolGroupsSchema, groupOptionsSchema])])
|
||||
|
||||
export type GroupEntry = z.infer<typeof groupEntrySchema>
|
||||
|
||||
|
||||
const groupEntryArraySchema = z.array(groupEntrySchema).refine(
|
||||
(groups) => {
|
||||
const seen = new Set()
|
||||
|
||||
return groups.every((group) => {
|
||||
// For tuples, check the group name (first element).
|
||||
const groupName = Array.isArray(group) ? group[0] : group
|
||||
|
||||
if (seen.has(groupName)) {
|
||||
return false
|
||||
}
|
||||
|
||||
seen.add(groupName)
|
||||
return true
|
||||
})
|
||||
},
|
||||
{ message: "Duplicate groups are not allowed" },
|
||||
)
|
||||
|
||||
export const modeConfigSchema = z.object({
|
||||
id: z.string().uuid("Invalid ID"),
|
||||
slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"),
|
||||
name: z.string().min(1, "Name is required"),
|
||||
roleDefinition: z.string().min(1, "Role definition is required"),
|
||||
customInstructions: z.string().optional(),
|
||||
groups: groupEntryArraySchema,
|
||||
source: z.enum(["global", "project"]).optional(),
|
||||
updatedAt: z.number().int().positive(),
|
||||
schemaVersion: z.literal(CUSTOM_MODE_SCHEMA_VERSION),
|
||||
})
|
||||
|
||||
export type CustomMode = z.infer<typeof modeConfigSchema>
|
||||
|
||||
export type CustomModeMetadata = {
|
||||
id: string
|
||||
name: string
|
||||
updatedAt: number
|
||||
schemaVersion: number
|
||||
}
|
||||
@@ -5,6 +5,13 @@ export class DuplicateCommandException extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class DuplicateCustomModeException extends Error {
|
||||
constructor(customModeName: string) {
|
||||
super(`Custom mode with name "${customModeName}" already exists`)
|
||||
this.name = 'DuplicateCustomModeException'
|
||||
}
|
||||
}
|
||||
|
||||
export class EmptyCommandNameException extends Error {
|
||||
constructor() {
|
||||
super('Command name cannot be empty')
|
||||
@@ -18,3 +25,10 @@ export class EmptyChatTitleException extends Error {
|
||||
this.name = 'EmptyChatTitleException'
|
||||
}
|
||||
}
|
||||
|
||||
export class EmptyCustomModeNameException extends Error {
|
||||
constructor() {
|
||||
super('Custom mode name cannot be empty')
|
||||
this.name = 'EmptyCustomModeNameException'
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user