feat: add Cobra CLI, LSP/MCP registries, workflow engine, and enriched dashboard
All checks were successful
Beta Release / beta (push) Successful in 2m24s

Major changes:
- Refactor CLI entry point to Cobra commands (root, setup, scan, doctor, install, update, lsp, mcp, skills, config, version)
- Add LSP registry with health checks, auto-install, and editor config generation
- Add MCP registry with editor detection, status tracking, and per-editor configuration
- Add workflow engine with planner and step execution for automated task chains
- Add conversation search, export (Markdown/JSON), and detailed token counting
- Add streaming shell chat handler with tool call/result events
- Add skill validation, dry-run testing, and export endpoints
- Enrich dashboard with Tools/Activity/Status tabs and tool cards grid
- Add PRD documentation
- Complete i18n for both EN and FR

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-22 22:22:05 +02:00
parent 66b773ff86
commit 2e50366cd8
42 changed files with 6779 additions and 319 deletions

View File

@@ -26,6 +26,17 @@ const api = {
runScan: () => request('/scan', { method: 'POST' }),
installTools: (tools) => request('/install', { method: 'POST', body: JSON.stringify({ tools }) }),
configureMCP: () => request('/mcp/configure', { method: 'POST' }),
configureMCPForEditor: (editor) => request('/mcp/configure', { method: 'POST', body: JSON.stringify({ editor }) }),
getMCPStatus: () => request('/mcp/status'),
getMCPRegistry: () => request('/mcp/registry'),
getLSPHealth: () => request('/lsp/health'),
autoInstallLSP: (projectDir) => request('/lsp/auto-install', { method: 'POST', body: JSON.stringify({ project_dir: projectDir || '' }) }),
generateLSPConfig: (editor, names) => request('/lsp/editor-config', { method: 'POST', body: JSON.stringify({ editor, names }) }),
validateSkill: (name) => request('/skills/validate', { method: 'POST', body: JSON.stringify({ name }) }),
testSkill: (name, sampleTask) => request('/skills/test', { method: 'POST', body: JSON.stringify({ name, sample_task: sampleTask || '' }) }),
exportSkill: (name) => request('/skills/export', { method: 'POST', body: JSON.stringify({ name }) }),
importSkill: (path) => request('/skills/import', { method: 'POST', body: JSON.stringify({ import_path: path }) }),
getDashboardStatus: () => request('/dashboard/status'),
savePreferences: (prefs) => request('/preferences', { method: 'PUT', body: JSON.stringify(prefs) }),
saveProfile: (profile) => request('/config/profile', { method: 'PUT', body: JSON.stringify(profile) }),
saveProvider: (provider) => request('/config/provider', { method: 'PUT', body: JSON.stringify(provider) }),
@@ -84,6 +95,66 @@ const api = {
}).catch(reject)
})
},
sendShellChat: (message, context = {}, stream = true, onChunk) => {
const payload = {
message,
context: context.context || '',
history: context.history || [],
cwd: context.cwd || '',
platform: context.platform || '',
stream,
}
if (!stream) {
return request('/shell/chat', { method: 'POST', body: JSON.stringify(payload) })
}
return new Promise((resolve, reject) => {
fetch(`${API_BASE}/shell/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}).then(async (res) => {
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
reject(new Error(err.error || res.statusText))
return
}
const reader = res.body.getReader()
const decoder = new TextDecoder()
let full = ''
let toolCalls = []
while (true) {
const { done, value } = await reader.read()
if (done) break
const text = decoder.decode(value, { stream: true })
for (const line of text.split('\n')) {
if (!line.startsWith('data: ')) continue
try {
const data = JSON.parse(line.slice(6))
if (data.error) { reject(new Error(data.error)); return }
if (data.done) {
resolve({ content: full, tool_calls: toolCalls })
return
}
if (data.content) {
full += data.content
if (onChunk) onChunk(full, data)
} else if (data.tool_call) {
toolCalls.push(data.tool_call)
if (onChunk) onChunk(full, data, toolCalls)
} else if (data.tool_result) {
const idx = toolCalls.findIndex(tc => tc.tool_call_id === data.tool_result.id)
if (idx >= 0) {
toolCalls[idx].result = data.tool_result
}
if (onChunk) onChunk(full, data, toolCalls)
}
} catch {}
}
}
resolve({ content: full, tool_calls: toolCalls })
}).catch(reject)
})
},
}
export default api