mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-26 15:03:34 +08:00
feat(settings): add Project Rules (CLAUDE.md) management UI (#72)
* feat(settings): add Project Rules (CLAUDE.md) management UI Add a new "Project Rules" tab to the desktop settings page that lets users view, create, and open their CLAUDE.md files directly from the UI. This provides a visual entry point for managing project-specific instructions that are auto-injected into every conversation. - New settings tab "Project Rules" with i18n across all 5 locales - ProjectRulesSettings page showing project-level and user-level CLAUDE.md - Server API: GET/POST /api/project-rules for file status and creation - "Open" button launches the file in the system editor - "Create" button scaffolds a template CLAUDE.md if it doesn't exist Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: resolve TypeScript errors in ProjectRulesSettings - Remove unused 'query' variable - Fix translation function type to use TranslationKey Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: 你的姓名 <you@example.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
d4df7e2109
commit
5c612bd967
8
desktop/src/__tests__/projectRulesSettings.test.tsx
Normal file
8
desktop/src/__tests__/projectRulesSettings.test.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { ProjectRulesSettings } from '../pages/ProjectRulesSettings'
|
||||
|
||||
describe('ProjectRulesSettings', () => {
|
||||
it('exports ProjectRulesSettings component', () => {
|
||||
expect(typeof ProjectRulesSettings).toBe('function')
|
||||
})
|
||||
})
|
||||
@ -181,6 +181,18 @@ export const en = {
|
||||
'settings.tab.mcp': 'MCP',
|
||||
'settings.tab.plugins': 'Plugins',
|
||||
'settings.tab.diagnostics': 'Diagnostics',
|
||||
'settings.tab.projectRules': 'Project Rules',
|
||||
|
||||
// Settings > Project Rules
|
||||
'settings.projectRules.title': 'Project Rules (CLAUDE.md)',
|
||||
'settings.projectRules.description': 'CLAUDE.md files define project-specific instructions that are automatically loaded into every conversation. They control coding style, tool preferences, and project conventions.',
|
||||
'settings.projectRules.projectFile': 'Project rules',
|
||||
'settings.projectRules.projectFileDesc': 'Rules for the current project directory (.claude/CLAUDE.md)',
|
||||
'settings.projectRules.userFile': 'User rules',
|
||||
'settings.projectRules.userFileDesc': 'Global rules applied to all projects (~/.claude/CLAUDE.md)',
|
||||
'settings.projectRules.open': 'Open',
|
||||
'settings.projectRules.create': 'Create',
|
||||
'settings.projectRules.notFound': 'File does not exist yet',
|
||||
|
||||
// Settings > Usage
|
||||
'settings.activity.title': 'Token usage',
|
||||
|
||||
@ -183,6 +183,16 @@ export const jp: Record<TranslationKey, string> = {
|
||||
'settings.tab.mcp': 'MCP',
|
||||
'settings.tab.plugins': 'プラグイン',
|
||||
'settings.tab.diagnostics': '診断',
|
||||
'settings.tab.projectRules': 'プロジェクトルール',
|
||||
'settings.projectRules.title': 'プロジェクトルール (CLAUDE.md)',
|
||||
'settings.projectRules.description': 'CLAUDE.mdファイルはプロジェクト固有の指示を定義し、会話開始時に自動的に読み込まれます。コーディングスタイル、ツール設定、プロジェクト規約を制御します。',
|
||||
'settings.projectRules.projectFile': 'プロジェクトルール',
|
||||
'settings.projectRules.projectFileDesc': '現在のプロジェクトディレクトリのルール (.claude/CLAUDE.md)',
|
||||
'settings.projectRules.userFile': 'ユーザールール',
|
||||
'settings.projectRules.userFileDesc': 'すべてのプロジェクトに適用されるグローバルルール (~/.claude/CLAUDE.md)',
|
||||
'settings.projectRules.open': '開く',
|
||||
'settings.projectRules.create': '作成',
|
||||
'settings.projectRules.notFound': 'ファイルがまだ存在しません',
|
||||
|
||||
// Settings > Usage
|
||||
'settings.activity.title': 'トークン使用量',
|
||||
|
||||
@ -183,6 +183,16 @@ export const kr: Record<TranslationKey, string> = {
|
||||
'settings.tab.mcp': 'MCP',
|
||||
'settings.tab.plugins': '플러그인',
|
||||
'settings.tab.diagnostics': '진단',
|
||||
'settings.tab.projectRules': '프로젝트 규칙',
|
||||
'settings.projectRules.title': '프로젝트 규칙 (CLAUDE.md)',
|
||||
'settings.projectRules.description': 'CLAUDE.md 파일은 프로젝트별 지침을 정의하며, 대화 시작 시 자동으로 로드됩니다. 코딩 스타일, 도구 설정 및 프로젝트 규약을 제어합니다.',
|
||||
'settings.projectRules.projectFile': '프로젝트 규칙',
|
||||
'settings.projectRules.projectFileDesc': '현재 프로젝트 디렉토리의 규칙 (.claude/CLAUDE.md)',
|
||||
'settings.projectRules.userFile': '사용자 규칙',
|
||||
'settings.projectRules.userFileDesc': '모든 프로젝트에 적용되는 전역 규칙 (~/.claude/CLAUDE.md)',
|
||||
'settings.projectRules.open': '열기',
|
||||
'settings.projectRules.create': '생성',
|
||||
'settings.projectRules.notFound': '파일이 아직 존재하지 않습니다',
|
||||
|
||||
// Settings > Usage
|
||||
'settings.activity.title': '토큰 사용량',
|
||||
|
||||
@ -183,6 +183,16 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.tab.mcp': 'MCP',
|
||||
'settings.tab.plugins': '外掛',
|
||||
'settings.tab.diagnostics': '診斷',
|
||||
'settings.tab.projectRules': '專案規則',
|
||||
'settings.projectRules.title': '專案規則 (CLAUDE.md)',
|
||||
'settings.projectRules.description': 'CLAUDE.md 檔案定義了專案特定的指令,每次對話開始時自動載入。它們控制編碼風格、工具偏好和專案約定。',
|
||||
'settings.projectRules.projectFile': '專案規則',
|
||||
'settings.projectRules.projectFileDesc': '當前專案目錄的規則 (.claude/CLAUDE.md)',
|
||||
'settings.projectRules.userFile': '使用者規則',
|
||||
'settings.projectRules.userFileDesc': '套用於所有專案的全域規則 (~/.claude/CLAUDE.md)',
|
||||
'settings.projectRules.open': '開啟',
|
||||
'settings.projectRules.create': '建立',
|
||||
'settings.projectRules.notFound': '檔案尚不存在',
|
||||
|
||||
// Settings > Usage
|
||||
'settings.activity.title': 'Token 用量',
|
||||
|
||||
@ -183,6 +183,18 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.tab.mcp': 'MCP',
|
||||
'settings.tab.plugins': '插件',
|
||||
'settings.tab.diagnostics': '诊断',
|
||||
'settings.tab.projectRules': '项目规则',
|
||||
|
||||
// Settings > Project Rules
|
||||
'settings.projectRules.title': '项目规则 (CLAUDE.md)',
|
||||
'settings.projectRules.description': 'CLAUDE.md 文件定义了项目特定的指令,每次对话开始时自动加载。它们控制编码风格、工具偏好和项目约定。',
|
||||
'settings.projectRules.projectFile': '项目规则',
|
||||
'settings.projectRules.projectFileDesc': '当前项目目录的规则 (.claude/CLAUDE.md)',
|
||||
'settings.projectRules.userFile': '用户规则',
|
||||
'settings.projectRules.userFileDesc': '应用于所有项目的全局规则 (~/.claude/CLAUDE.md)',
|
||||
'settings.projectRules.open': '打开',
|
||||
'settings.projectRules.create': '创建',
|
||||
'settings.projectRules.notFound': '文件尚不存在',
|
||||
|
||||
// Settings > Usage
|
||||
'settings.activity.title': 'Token 用量',
|
||||
|
||||
149
desktop/src/pages/ProjectRulesSettings.tsx
Normal file
149
desktop/src/pages/ProjectRulesSettings.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation, type TranslationKey } from '../i18n'
|
||||
import { Button } from '../components/shared/Button'
|
||||
import { useSessionStore } from '../stores/sessionStore'
|
||||
import { getDesktopHost } from '../lib/desktopHost'
|
||||
import { api } from '../api/client'
|
||||
|
||||
type RulesFileStatus = {
|
||||
path: string
|
||||
exists: boolean
|
||||
}
|
||||
|
||||
type ProjectRulesResponse = {
|
||||
projectFile: RulesFileStatus | null
|
||||
userFile: RulesFileStatus
|
||||
}
|
||||
|
||||
export function ProjectRulesSettings() {
|
||||
const t = useTranslation()
|
||||
const [rules, setRules] = useState<ProjectRulesResponse | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const sessions = useSessionStore((s) => s.sessions)
|
||||
const activeSessionId = useSessionStore((s) => s.activeSessionId)
|
||||
const activeSession = sessions.find((s) => s.id === activeSessionId)
|
||||
const cwd = activeSession?.workDir || activeSession?.projectPath || undefined
|
||||
|
||||
const fetchRules = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const query = cwd ? `?cwd=${encodeURIComponent(cwd)}` : ''
|
||||
const res = await api.get<ProjectRulesResponse>(`/api/project-rules${query}`)
|
||||
setRules(res)
|
||||
} catch {
|
||||
setRules(null)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchRules()
|
||||
}, [cwd])
|
||||
|
||||
const handleOpen = async (path: string) => {
|
||||
try {
|
||||
await getDesktopHost().shell.openPath(path)
|
||||
} catch {
|
||||
// fallback: ignore
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreate = async (scope: 'project' | 'user') => {
|
||||
try {
|
||||
await api.post(`/api/project-rules/create`, { scope, cwd })
|
||||
await fetchRules()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<span className="material-symbols-outlined animate-spin text-[var(--color-text-muted)]">progress_activity</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-[var(--color-text)]">{t('settings.projectRules.title')}</h2>
|
||||
<p className="text-sm text-[var(--color-text-muted)] mt-1">{t('settings.projectRules.description')}</p>
|
||||
</div>
|
||||
|
||||
{/* Project-level CLAUDE.md */}
|
||||
{rules?.projectFile && (
|
||||
<RuleFileCard
|
||||
title={t('settings.projectRules.projectFile')}
|
||||
description={t('settings.projectRules.projectFileDesc')}
|
||||
path={rules.projectFile.path}
|
||||
exists={rules.projectFile.exists}
|
||||
onOpen={() => handleOpen(rules.projectFile!.path)}
|
||||
onCreate={() => handleCreate('project')}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* User-level CLAUDE.md */}
|
||||
{rules?.userFile && (
|
||||
<RuleFileCard
|
||||
title={t('settings.projectRules.userFile')}
|
||||
description={t('settings.projectRules.userFileDesc')}
|
||||
path={rules.userFile.path}
|
||||
exists={rules.userFile.exists}
|
||||
onOpen={() => handleOpen(rules.userFile.path)}
|
||||
onCreate={() => handleCreate('user')}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RuleFileCard({
|
||||
title,
|
||||
description,
|
||||
path,
|
||||
exists,
|
||||
onOpen,
|
||||
onCreate,
|
||||
t,
|
||||
}: {
|
||||
title: string
|
||||
description: string
|
||||
path: string
|
||||
exists: boolean
|
||||
onOpen: () => void
|
||||
onCreate: () => void
|
||||
t: (key: TranslationKey, params?: Record<string, string | number>) => string
|
||||
}) {
|
||||
return (
|
||||
<div className="border border-[var(--color-border)] rounded-lg p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-medium text-[var(--color-text)]">{title}</h3>
|
||||
<p className="text-xs text-[var(--color-text-muted)] mt-0.5">{description}</p>
|
||||
<p className="text-xs text-[var(--color-text-muted)] mt-1 font-mono truncate" title={path}>{path}</p>
|
||||
</div>
|
||||
<div className="ml-4 flex-shrink-0">
|
||||
{exists ? (
|
||||
<Button size="sm" variant="secondary" onClick={onOpen}>
|
||||
<span className="material-symbols-outlined text-base mr-1">open_in_new</span>
|
||||
{t('settings.projectRules.open')}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-[var(--color-text-muted)]">{t('settings.projectRules.notFound')}</span>
|
||||
<Button size="sm" variant="primary" onClick={onCreate}>
|
||||
<span className="material-symbols-outlined text-base mr-1">add</span>
|
||||
{t('settings.projectRules.create')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -49,6 +49,7 @@ import { DiagnosticsSettings } from './DiagnosticsSettings'
|
||||
import { TraceList } from './TraceList'
|
||||
import { ActivitySettings } from './ActivitySettings'
|
||||
import { MemorySettings } from './MemorySettings'
|
||||
import { ProjectRulesSettings } from './ProjectRulesSettings'
|
||||
import { useUIStore, type SettingsTab } from '../stores/uiStore'
|
||||
import { ClaudeOfficialLogin } from '../components/settings/ClaudeOfficialLogin'
|
||||
import { ChatGPTOfficialLogin } from '../components/settings/ChatGPTOfficialLogin'
|
||||
@ -207,6 +208,7 @@ export function Settings() {
|
||||
<TabButton icon="smart_toy" label={t('settings.tab.agents')} active={activeTab === 'agents'} onClick={() => setActiveTab('agents')} />
|
||||
<TabButton icon="auto_awesome" label={t('settings.tab.skills')} active={activeTab === 'skills'} onClick={() => setActiveTab('skills')} />
|
||||
<TabButton icon="history_edu" label={t('settings.tab.memory')} active={activeTab === 'memory'} onClick={() => setActiveTab('memory')} />
|
||||
<TabButton icon="description" label={t('settings.tab.projectRules')} active={activeTab === 'projectRules'} onClick={() => setActiveTab('projectRules')} />
|
||||
<TabButton icon="extension" label={t('settings.tab.plugins')} active={activeTab === 'plugins'} onClick={() => setActiveTab('plugins')} />
|
||||
<TabButton icon="mouse" label={t('settings.tab.computerUse')} active={activeTab === 'computerUse'} onClick={() => setActiveTab('computerUse')} />
|
||||
<TabButton icon="monitoring" label={t('settings.tab.activity')} active={activeTab === 'activity'} onClick={() => setActiveTab('activity')} />
|
||||
@ -230,6 +232,7 @@ export function Settings() {
|
||||
{activeTab === 'agents' && <AgentsSettings />}
|
||||
{activeTab === 'skills' && <SkillSettings />}
|
||||
{activeTab === 'memory' && <MemorySettings />}
|
||||
{activeTab === 'projectRules' && <ProjectRulesSettings />}
|
||||
{activeTab === 'plugins' && <PluginSettings />}
|
||||
{activeTab === 'computerUse' && <ComputerUseSettings />}
|
||||
{activeTab === 'trace' && <TraceList />}
|
||||
|
||||
@ -80,6 +80,7 @@ export type SettingsTab =
|
||||
| 'computerUse'
|
||||
| 'trace'
|
||||
| 'diagnostics'
|
||||
| 'projectRules'
|
||||
| 'about'
|
||||
|
||||
type ActiveView = 'code' | 'scheduled' | 'terminal' | 'history' | 'settings'
|
||||
|
||||
121
src/server/__tests__/project-rules.test.ts
Normal file
121
src/server/__tests__/project-rules.test.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { describe, it, expect, mock, beforeEach } from 'bun:test'
|
||||
import * as path from 'path'
|
||||
|
||||
// Mock dependencies
|
||||
mock.module('../../utils/envUtils.js', () => ({
|
||||
getClaudeConfigHomeDir: () => '/mock/home/.claude',
|
||||
}))
|
||||
|
||||
mock.module('../../utils/cwd.js', () => ({
|
||||
getCwd: () => '/mock/project',
|
||||
}))
|
||||
|
||||
const mockFiles = new Set<string>()
|
||||
|
||||
mock.module('fs/promises', () => ({
|
||||
access: async (filePath: string) => {
|
||||
if (!mockFiles.has(filePath)) {
|
||||
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
|
||||
}
|
||||
},
|
||||
mkdir: async () => {},
|
||||
writeFile: async (filePath: string) => {
|
||||
mockFiles.add(filePath)
|
||||
},
|
||||
}))
|
||||
|
||||
import { handleProjectRulesApi } from '../api/project-rules'
|
||||
|
||||
function makeReq(method: string, body?: unknown): Request {
|
||||
return new Request('http://localhost/api/project-rules', {
|
||||
method,
|
||||
...(body ? { body: JSON.stringify(body) } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
describe('project-rules API', () => {
|
||||
beforeEach(() => {
|
||||
mockFiles.clear()
|
||||
})
|
||||
|
||||
it('GET /api/project-rules returns file paths with existence status', async () => {
|
||||
const url = new URL('http://localhost/api/project-rules?cwd=/mock/project')
|
||||
const res = await handleProjectRulesApi(
|
||||
makeReq('GET'),
|
||||
url,
|
||||
['api', 'project-rules'],
|
||||
)
|
||||
const data = await res.json() as { projectFile: { path: string; exists: boolean }; userFile: { path: string; exists: boolean } }
|
||||
|
||||
expect(data.projectFile.path).toContain('CLAUDE.md')
|
||||
expect(data.projectFile.exists).toBe(false)
|
||||
expect(data.userFile.path).toContain('CLAUDE.md')
|
||||
expect(data.userFile.exists).toBe(false)
|
||||
})
|
||||
|
||||
it('GET /api/project-rules shows existing files', async () => {
|
||||
const projectPath = path.join('/mock/project', '.claude', 'CLAUDE.md')
|
||||
mockFiles.add(projectPath)
|
||||
|
||||
const url = new URL('http://localhost/api/project-rules?cwd=/mock/project')
|
||||
const res = await handleProjectRulesApi(
|
||||
makeReq('GET'),
|
||||
url,
|
||||
['api', 'project-rules'],
|
||||
)
|
||||
const data = await res.json() as { projectFile: { path: string; exists: boolean }; userFile: { path: string; exists: boolean } }
|
||||
|
||||
expect(data.projectFile.exists).toBe(true)
|
||||
expect(data.userFile.exists).toBe(false)
|
||||
})
|
||||
|
||||
it('POST /api/project-rules/create creates user file', async () => {
|
||||
const url = new URL('http://localhost/api/project-rules/create')
|
||||
const res = await handleProjectRulesApi(
|
||||
new Request('http://localhost/api/project-rules/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ scope: 'user' }),
|
||||
}),
|
||||
url,
|
||||
['api', 'project-rules', 'create'],
|
||||
)
|
||||
const data = await res.json() as { ok: boolean; created: boolean; path: string }
|
||||
|
||||
expect(data.ok).toBe(true)
|
||||
expect(data.created).toBe(true)
|
||||
expect(data.path).toContain('CLAUDE.md')
|
||||
})
|
||||
|
||||
it('POST /api/project-rules/create with invalid scope returns 400', async () => {
|
||||
const url = new URL('http://localhost/api/project-rules/create')
|
||||
const res = await handleProjectRulesApi(
|
||||
new Request('http://localhost/api/project-rules/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ scope: 'invalid' }),
|
||||
}),
|
||||
url,
|
||||
['api', 'project-rules', 'create'],
|
||||
)
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('POST /api/project-rules/create does not overwrite existing file', async () => {
|
||||
const userPath = path.join('/mock/home/.claude', 'CLAUDE.md')
|
||||
mockFiles.add(userPath)
|
||||
|
||||
const url = new URL('http://localhost/api/project-rules/create')
|
||||
const res = await handleProjectRulesApi(
|
||||
new Request('http://localhost/api/project-rules/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ scope: 'user' }),
|
||||
}),
|
||||
url,
|
||||
['api', 'project-rules', 'create'],
|
||||
)
|
||||
const data = await res.json() as { ok: boolean; created: boolean }
|
||||
|
||||
expect(data.ok).toBe(true)
|
||||
expect(data.created).toBe(false)
|
||||
})
|
||||
})
|
||||
97
src/server/api/project-rules.ts
Normal file
97
src/server/api/project-rules.ts
Normal file
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Project Rules (CLAUDE.md) REST API
|
||||
*
|
||||
* GET /api/project-rules — Get CLAUDE.md file paths and existence status
|
||||
* POST /api/project-rules/create — Create a CLAUDE.md file if it doesn't exist
|
||||
*/
|
||||
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs/promises'
|
||||
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
|
||||
import { getCwd } from '../../utils/cwd.js'
|
||||
|
||||
export async function handleProjectRulesApi(
|
||||
req: Request,
|
||||
url: URL,
|
||||
segments: string[],
|
||||
): Promise<Response> {
|
||||
const sub = segments[2]
|
||||
|
||||
if (req.method === 'GET' && !sub) {
|
||||
return await getProjectRules(url)
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && sub === 'create') {
|
||||
return await createProjectRulesFile(req, url)
|
||||
}
|
||||
|
||||
return Response.json(
|
||||
{ error: 'Not Found', message: `Unknown project-rules endpoint` },
|
||||
{ status: 404 },
|
||||
)
|
||||
}
|
||||
|
||||
async function getProjectRules(url: URL): Promise<Response> {
|
||||
const cwd = url.searchParams.get('cwd') || getCwd()
|
||||
|
||||
const userFile = path.join(getClaudeConfigHomeDir(), 'CLAUDE.md')
|
||||
const projectFile = path.join(cwd, '.claude', 'CLAUDE.md')
|
||||
|
||||
const [userExists, projectExists] = await Promise.all([
|
||||
fileExists(userFile),
|
||||
fileExists(projectFile),
|
||||
])
|
||||
|
||||
return Response.json({
|
||||
projectFile: { path: projectFile, exists: projectExists },
|
||||
userFile: { path: userFile, exists: userExists },
|
||||
})
|
||||
}
|
||||
|
||||
async function createProjectRulesFile(req: Request, url: URL): Promise<Response> {
|
||||
let body: { scope?: string; cwd?: string }
|
||||
try {
|
||||
body = await req.json() as { scope?: string; cwd?: string }
|
||||
} catch {
|
||||
return Response.json({ error: 'Invalid JSON body' }, { status: 400 })
|
||||
}
|
||||
|
||||
const scope = body.scope
|
||||
const cwd = body.cwd || url.searchParams.get('cwd') || getCwd()
|
||||
|
||||
let filePath: string
|
||||
if (scope === 'project') {
|
||||
filePath = path.join(cwd, '.claude', 'CLAUDE.md')
|
||||
} else if (scope === 'user') {
|
||||
filePath = path.join(getClaudeConfigHomeDir(), 'CLAUDE.md')
|
||||
} else {
|
||||
return Response.json({ error: 'Invalid scope, must be "project" or "user"' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Don't overwrite existing files
|
||||
if (await fileExists(filePath)) {
|
||||
return Response.json({ ok: true, path: filePath, created: false })
|
||||
}
|
||||
|
||||
// Create directory if needed
|
||||
const dir = path.dirname(filePath)
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
|
||||
// Create file with template content
|
||||
const template = scope === 'project'
|
||||
? '# Project Rules\n\n<!-- Add project-specific instructions here. These are loaded into every conversation. -->\n'
|
||||
: '# User Rules\n\n<!-- Add global instructions here. These apply to all projects. -->\n'
|
||||
|
||||
await fs.writeFile(filePath, template, 'utf-8')
|
||||
|
||||
return Response.json({ ok: true, path: filePath, created: true })
|
||||
}
|
||||
|
||||
async function fileExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -29,6 +29,7 @@ import { handleMemoryApi } from './api/memory.js'
|
||||
import { handleDesktopUiApi } from './api/desktop-ui.js'
|
||||
import { handleProjectsApi } from './api/projects.js'
|
||||
import { handleTracesApi } from './api/traces.js'
|
||||
import { handleProjectRulesApi } from './api/project-rules.js'
|
||||
|
||||
export async function handleApiRequest(req: Request, url: URL): Promise<Response> {
|
||||
const path = url.pathname
|
||||
@ -118,6 +119,9 @@ export async function handleApiRequest(req: Request, url: URL): Promise<Response
|
||||
case 'memory':
|
||||
return handleMemoryApi(req, url, segments)
|
||||
|
||||
case 'project-rules':
|
||||
return handleProjectRulesApi(req, url, segments)
|
||||
|
||||
case 'desktop-ui':
|
||||
return handleDesktopUiApi(req, url, segments)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user