idea-manager 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/package.json +2 -1
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +13 -3
- package/src/app/globals.css +72 -0
- package/src/app/projects/[id]/page.tsx +34 -2
- package/src/components/task/TaskChat.tsx +8 -5
- package/src/components/ui/AiPolicyModal.tsx +98 -0
- package/src/lib/ai/client.ts +45 -49
- package/src/lib/db/queries/projects.ts +3 -2
- package/src/lib/db/schema.ts +3 -0
- package/src/types/index.ts +1 -0
package/README.md
CHANGED
|
@@ -106,15 +106,14 @@ claude mcp add idea-manager -- npx -y idea-manager mcp
|
|
|
106
106
|
| Frontend | Next.js 15, React 19, TypeScript, Tailwind CSS 4 |
|
|
107
107
|
| Backend | Next.js API Routes |
|
|
108
108
|
| Database | SQLite (better-sqlite3) |
|
|
109
|
-
| AI |
|
|
109
|
+
| AI | Claude CLI (구독 기반, API 키 불필요) |
|
|
110
110
|
| MCP | Model Context Protocol (stdio) |
|
|
111
111
|
| CLI | Commander.js |
|
|
112
112
|
|
|
113
|
-
##
|
|
113
|
+
## 요구 사항
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
```
|
|
115
|
+
- **Node.js** 18+
|
|
116
|
+
- **Claude CLI** — AI 채팅/다듬기 기능 사용 시 필요 (Claude 구독 필요). 없어도 태스크 관리, 프롬프트 작성 등 기본 기능은 정상 동작합니다.
|
|
118
117
|
|
|
119
118
|
## 라이선스
|
|
120
119
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idea-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "AI 기반 브레인스토밍 → 구조화 → 프롬프트 생성 도구. MCP Server 내장.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"brainstorm",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"open": "^11.0.0",
|
|
47
47
|
"react": "19.2.3",
|
|
48
48
|
"react-dom": "19.2.3",
|
|
49
|
+
"react-markdown": "^10.1.0",
|
|
49
50
|
"tsx": "^4.21.0"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
@@ -3,6 +3,7 @@ import { getTaskConversations, addTaskConversation } from '@/lib/db/queries/task
|
|
|
3
3
|
import { getTask } from '@/lib/db/queries/tasks';
|
|
4
4
|
import { getTaskPrompt } from '@/lib/db/queries/task-prompts';
|
|
5
5
|
import { getBrainstorm } from '@/lib/db/queries/brainstorms';
|
|
6
|
+
import { getProject } from '@/lib/db/queries/projects';
|
|
6
7
|
import { runClaude } from '@/lib/ai/client';
|
|
7
8
|
|
|
8
9
|
export async function GET(
|
|
@@ -37,9 +38,12 @@ export async function POST(
|
|
|
37
38
|
const history = getTaskConversations(taskId);
|
|
38
39
|
const prompt = getTaskPrompt(taskId);
|
|
39
40
|
const brainstorm = getBrainstorm(projectId);
|
|
41
|
+
const project = getProject(projectId);
|
|
40
42
|
|
|
41
|
-
const
|
|
43
|
+
const aiPolicy = project?.ai_context ? `\n\nProject AI Policy:\n${project.ai_context}` : '';
|
|
42
44
|
|
|
45
|
+
const systemPrompt = `You are a helpful assistant helping refine a development task. Respond in Korean. Be concise.
|
|
46
|
+
${aiPolicy}
|
|
43
47
|
Task: ${task.title}
|
|
44
48
|
Description: ${task.description}
|
|
45
49
|
Status: ${task.status}
|
|
@@ -52,9 +56,15 @@ ${brainstorm?.content ? `\nBrainstorming context:\n${brainstorm.content.slice(0,
|
|
|
52
56
|
|
|
53
57
|
try {
|
|
54
58
|
const aiResponse = await runClaude(`${systemPrompt}\n\nConversation:\n${conversationText}`);
|
|
55
|
-
const
|
|
59
|
+
const trimmed = aiResponse.trim();
|
|
60
|
+
if (!trimmed) {
|
|
61
|
+
const fallbackMsg = addTaskConversation(taskId, 'assistant', '(AI 응답을 생성하지 못했습니다. 다시 시도해주세요.)');
|
|
62
|
+
return NextResponse.json({ userMessage: userMsg, aiMessage: fallbackMsg });
|
|
63
|
+
}
|
|
64
|
+
const aiMsg = addTaskConversation(taskId, 'assistant', trimmed);
|
|
56
65
|
return NextResponse.json({ userMessage: userMsg, aiMessage: aiMsg });
|
|
57
66
|
} catch {
|
|
58
|
-
|
|
67
|
+
const errorMsg = addTaskConversation(taskId, 'assistant', '(AI 호출에 실패했습니다. Claude CLI가 설치되어 있는지 확인해주세요.)');
|
|
68
|
+
return NextResponse.json({ userMessage: userMsg, aiMessage: errorMsg });
|
|
59
69
|
}
|
|
60
70
|
}
|
package/src/app/globals.css
CHANGED
|
@@ -886,6 +886,78 @@ textarea:focus {
|
|
|
886
886
|
background: hsl(var(--primary) / 0.6);
|
|
887
887
|
}
|
|
888
888
|
|
|
889
|
+
/* Chat markdown styling */
|
|
890
|
+
.chat-markdown p {
|
|
891
|
+
margin: 0.25em 0;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
.chat-markdown p:first-child {
|
|
895
|
+
margin-top: 0;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
.chat-markdown p:last-child {
|
|
899
|
+
margin-bottom: 0;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
.chat-markdown strong {
|
|
903
|
+
font-weight: 700;
|
|
904
|
+
color: hsl(var(--foreground));
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
.chat-markdown code {
|
|
908
|
+
background: hsl(var(--background));
|
|
909
|
+
padding: 1px 5px;
|
|
910
|
+
border-radius: 4px;
|
|
911
|
+
font-size: 0.9em;
|
|
912
|
+
font-family: var(--font-mono);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
.chat-markdown pre {
|
|
916
|
+
background: hsl(var(--background));
|
|
917
|
+
padding: 8px 12px;
|
|
918
|
+
border-radius: 6px;
|
|
919
|
+
overflow-x: auto;
|
|
920
|
+
margin: 0.5em 0;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.chat-markdown pre code {
|
|
924
|
+
background: none;
|
|
925
|
+
padding: 0;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
.chat-markdown ul, .chat-markdown ol {
|
|
929
|
+
padding-left: 1.4em;
|
|
930
|
+
margin: 0.3em 0;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
.chat-markdown li {
|
|
934
|
+
margin: 0.15em 0;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.chat-markdown h1, .chat-markdown h2, .chat-markdown h3,
|
|
938
|
+
.chat-markdown h4, .chat-markdown h5, .chat-markdown h6 {
|
|
939
|
+
font-weight: 700;
|
|
940
|
+
margin: 0.5em 0 0.25em;
|
|
941
|
+
line-height: 1.3;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
.chat-markdown h1 { font-size: 1.2em; }
|
|
945
|
+
.chat-markdown h2 { font-size: 1.1em; }
|
|
946
|
+
.chat-markdown h3 { font-size: 1.05em; }
|
|
947
|
+
|
|
948
|
+
.chat-markdown blockquote {
|
|
949
|
+
border-left: 3px solid hsl(var(--border));
|
|
950
|
+
padding-left: 10px;
|
|
951
|
+
margin: 0.4em 0;
|
|
952
|
+
color: hsl(var(--muted-foreground));
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.chat-markdown hr {
|
|
956
|
+
border: none;
|
|
957
|
+
border-top: 1px solid hsl(var(--border));
|
|
958
|
+
margin: 0.5em 0;
|
|
959
|
+
}
|
|
960
|
+
|
|
889
961
|
/* Dialog animation */
|
|
890
962
|
@keyframes dialogIn {
|
|
891
963
|
from { opacity: 0; transform: scale(0.95) translateY(4px); }
|
|
@@ -7,6 +7,7 @@ import ProjectTree from '@/components/task/ProjectTree';
|
|
|
7
7
|
import TaskDetail from '@/components/task/TaskDetail';
|
|
8
8
|
import DirectoryPicker from '@/components/DirectoryPicker';
|
|
9
9
|
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
|
10
|
+
import AiPolicyModal from '@/components/ui/AiPolicyModal';
|
|
10
11
|
import type { ISubProject, ITask, TaskStatus, ISubProjectWithStats } from '@/types';
|
|
11
12
|
|
|
12
13
|
interface IProject {
|
|
@@ -14,6 +15,7 @@ interface IProject {
|
|
|
14
15
|
name: string;
|
|
15
16
|
description: string;
|
|
16
17
|
project_path: string | null;
|
|
18
|
+
ai_context: string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
function WorkspaceInner({ id }: { id: string }) {
|
|
@@ -32,10 +34,11 @@ function WorkspaceInner({ id }: { id: string }) {
|
|
|
32
34
|
const [showAddSub, setShowAddSub] = useState(false);
|
|
33
35
|
const [showBrainstorm, setShowBrainstorm] = useState(true);
|
|
34
36
|
const [newSubName, setNewSubName] = useState('');
|
|
37
|
+
const [showAiPolicy, setShowAiPolicy] = useState(false);
|
|
35
38
|
|
|
36
39
|
// Resizable panel widths
|
|
37
|
-
const [leftWidth, setLeftWidth] = useState(
|
|
38
|
-
const [centerWidth, setCenterWidth] = useState(
|
|
40
|
+
const [leftWidth, setLeftWidth] = useState(500);
|
|
41
|
+
const [centerWidth, setCenterWidth] = useState(500);
|
|
39
42
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
40
43
|
const draggingRef = useRef<'left' | 'center' | null>(null);
|
|
41
44
|
const startXRef = useRef(0);
|
|
@@ -221,6 +224,18 @@ function WorkspaceInner({ id }: { id: string }) {
|
|
|
221
224
|
}
|
|
222
225
|
};
|
|
223
226
|
|
|
227
|
+
const handleSaveAiPolicy = async (aiContext: string) => {
|
|
228
|
+
const res = await fetch(`/api/projects/${id}`, {
|
|
229
|
+
method: 'PUT',
|
|
230
|
+
headers: { 'Content-Type': 'application/json' },
|
|
231
|
+
body: JSON.stringify({ ai_context: aiContext }),
|
|
232
|
+
});
|
|
233
|
+
if (res.ok) {
|
|
234
|
+
setProject(await res.json());
|
|
235
|
+
setShowAiPolicy(false);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
224
239
|
// Keyboard shortcuts (use e.code for Korean IME compatibility)
|
|
225
240
|
useEffect(() => {
|
|
226
241
|
const handler = (e: KeyboardEvent) => {
|
|
@@ -289,6 +304,16 @@ function WorkspaceInner({ id }: { id: string }) {
|
|
|
289
304
|
)}
|
|
290
305
|
</div>
|
|
291
306
|
<div className="flex items-center gap-2">
|
|
307
|
+
<button
|
|
308
|
+
onClick={() => setShowAiPolicy(true)}
|
|
309
|
+
className={`px-3 py-1.5 text-xs border rounded-md transition-colors ${
|
|
310
|
+
project.ai_context
|
|
311
|
+
? 'bg-accent/15 text-accent border-accent/30 hover:bg-accent/25'
|
|
312
|
+
: 'bg-muted hover:bg-card-hover text-muted-foreground border-border'
|
|
313
|
+
}`}
|
|
314
|
+
>
|
|
315
|
+
AI Policy{project.ai_context ? ' *' : ''}
|
|
316
|
+
</button>
|
|
292
317
|
{!project.project_path && (
|
|
293
318
|
<button
|
|
294
319
|
onClick={() => setShowDirPicker(true)}
|
|
@@ -418,6 +443,13 @@ function WorkspaceInner({ id }: { id: string }) {
|
|
|
418
443
|
onConfirm={handleConfirmAction}
|
|
419
444
|
onCancel={() => setConfirmAction(null)}
|
|
420
445
|
/>
|
|
446
|
+
|
|
447
|
+
<AiPolicyModal
|
|
448
|
+
open={showAiPolicy}
|
|
449
|
+
content={project.ai_context || ''}
|
|
450
|
+
onSave={handleSaveAiPolicy}
|
|
451
|
+
onClose={() => setShowAiPolicy(false)}
|
|
452
|
+
/>
|
|
421
453
|
</div>
|
|
422
454
|
);
|
|
423
455
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
4
|
import type { ITaskConversation } from '@/types';
|
|
5
|
+
import ReactMarkdown from 'react-markdown';
|
|
5
6
|
|
|
6
7
|
export default function TaskChat({
|
|
7
8
|
basePath,
|
|
@@ -79,14 +80,16 @@ export default function TaskChat({
|
|
|
79
80
|
Ask AI to help refine your task or prompt
|
|
80
81
|
</div>
|
|
81
82
|
)}
|
|
82
|
-
{messages.map((msg) => (
|
|
83
|
+
{messages.filter(msg => msg.content).map((msg) => (
|
|
83
84
|
<div key={msg.id} className={`flex flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}>
|
|
84
|
-
<div className={`max-w-[90%] px-3 py-2 rounded-lg text-sm leading-relaxed
|
|
85
|
+
<div className={`max-w-[90%] px-3 py-2 rounded-lg text-sm leading-relaxed ${
|
|
85
86
|
msg.role === 'user'
|
|
86
|
-
? 'bg-accent text-white rounded-br-sm'
|
|
87
|
-
: 'bg-muted text-foreground rounded-bl-sm'
|
|
87
|
+
? 'bg-accent text-white rounded-br-sm whitespace-pre-wrap'
|
|
88
|
+
: 'bg-muted text-foreground rounded-bl-sm chat-markdown'
|
|
88
89
|
}`}>
|
|
89
|
-
{msg.
|
|
90
|
+
{msg.role === 'assistant'
|
|
91
|
+
? <ReactMarkdown>{msg.content}</ReactMarkdown>
|
|
92
|
+
: msg.content}
|
|
90
93
|
</div>
|
|
91
94
|
{msg.role === 'assistant' && (
|
|
92
95
|
<button
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
export default function AiPolicyModal({
|
|
6
|
+
open,
|
|
7
|
+
content,
|
|
8
|
+
onSave,
|
|
9
|
+
onClose,
|
|
10
|
+
}: {
|
|
11
|
+
open: boolean;
|
|
12
|
+
content: string;
|
|
13
|
+
onSave: (content: string) => void;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
}) {
|
|
16
|
+
const [draft, setDraft] = useState(content);
|
|
17
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (open) {
|
|
21
|
+
setDraft(content);
|
|
22
|
+
setTimeout(() => textareaRef.current?.focus(), 50);
|
|
23
|
+
}
|
|
24
|
+
}, [open, content]);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!open) return;
|
|
28
|
+
const handler = (e: KeyboardEvent) => {
|
|
29
|
+
if (e.key === 'Escape') onClose();
|
|
30
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
onSave(draft);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
window.addEventListener('keydown', handler);
|
|
36
|
+
return () => window.removeEventListener('keydown', handler);
|
|
37
|
+
}, [open, draft, onSave, onClose]);
|
|
38
|
+
|
|
39
|
+
if (!open) return null;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
43
|
+
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
|
44
|
+
<div className="relative bg-card border border-border rounded-xl shadow-2xl w-[640px] max-h-[80vh] flex flex-col animate-dialog-in">
|
|
45
|
+
<div className="flex items-center justify-between px-5 py-3 border-b border-border">
|
|
46
|
+
<div>
|
|
47
|
+
<h3 className="text-sm font-semibold">AI Policy</h3>
|
|
48
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
49
|
+
AI 채팅과 프롬프트 다듬기에 항상 포함되는 프로젝트 컨텍스트
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
<button onClick={onClose} className="text-muted-foreground hover:text-foreground text-lg px-1">
|
|
53
|
+
x
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div className="flex-1 p-4 overflow-y-auto">
|
|
58
|
+
<textarea
|
|
59
|
+
ref={textareaRef}
|
|
60
|
+
value={draft}
|
|
61
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
62
|
+
placeholder={`프로젝트 컨텍스트와 AI 지침을 작성하세요.
|
|
63
|
+
|
|
64
|
+
예시:
|
|
65
|
+
- 이 프로젝트는 JABIS 스마트워크 시스템입니다
|
|
66
|
+
- 기술 스택: React + TypeScript + Vite (monorepo)
|
|
67
|
+
- DB: PostgreSQL (jabis 스키마)
|
|
68
|
+
- 한국어로 응답할 것
|
|
69
|
+
- 코드 제안 시 기존 컨벤션을 따를 것`}
|
|
70
|
+
className="w-full bg-input border border-border rounded-lg px-4 py-3 text-sm
|
|
71
|
+
text-foreground resize-none focus:border-primary focus:outline-none
|
|
72
|
+
leading-relaxed font-mono min-h-[300px]"
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div className="flex items-center justify-between px-5 py-3 border-t border-border">
|
|
77
|
+
<span className="text-xs text-muted-foreground">Cmd+Enter to save</span>
|
|
78
|
+
<div className="flex items-center gap-2">
|
|
79
|
+
<button
|
|
80
|
+
onClick={onClose}
|
|
81
|
+
className="px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground
|
|
82
|
+
border border-border rounded-md transition-colors"
|
|
83
|
+
>
|
|
84
|
+
Cancel
|
|
85
|
+
</button>
|
|
86
|
+
<button
|
|
87
|
+
onClick={() => onSave(draft)}
|
|
88
|
+
className="px-3 py-1.5 text-xs bg-primary text-white rounded-md
|
|
89
|
+
hover:bg-primary-hover transition-colors"
|
|
90
|
+
>
|
|
91
|
+
Save
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
package/src/lib/ai/client.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface IStructuredItem {
|
|
|
13
13
|
const CLI_PATH = 'claude';
|
|
14
14
|
const DEFAULT_ARGS = ['--dangerously-skip-permissions'];
|
|
15
15
|
const MODEL = 'sonnet';
|
|
16
|
-
const MAX_TURNS =
|
|
16
|
+
const MAX_TURNS = 80;
|
|
17
17
|
|
|
18
18
|
export type OnTextChunk = (text: string) => void;
|
|
19
19
|
export type OnRawEvent = (event: Record<string, unknown>) => void;
|
|
@@ -24,10 +24,11 @@ export type OnRawEvent = (event: Record<string, unknown>) => void;
|
|
|
24
24
|
*/
|
|
25
25
|
export function runClaude(prompt: string, onText?: OnTextChunk, onRawEvent?: OnRawEvent): Promise<string> {
|
|
26
26
|
return new Promise((resolve, reject) => {
|
|
27
|
+
const useStreamJson = !!(onText || onRawEvent);
|
|
27
28
|
const args = [
|
|
28
29
|
...DEFAULT_ARGS,
|
|
29
30
|
'--model', MODEL,
|
|
30
|
-
'--output-format', 'stream-json',
|
|
31
|
+
...(useStreamJson ? ['--output-format', 'stream-json', '--verbose'] : ['--output-format', 'text']),
|
|
31
32
|
'--max-turns', String(MAX_TURNS),
|
|
32
33
|
'-p', '-', // read prompt from stdin
|
|
33
34
|
];
|
|
@@ -57,56 +58,47 @@ export function runClaude(prompt: string, onText?: OnTextChunk, onRawEvent?: OnR
|
|
|
57
58
|
let resultText = '';
|
|
58
59
|
let stderrText = '';
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
else if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
82
|
-
let fullText = '';
|
|
83
|
-
for (const block of parsed.message.content) {
|
|
84
|
-
if (block.type === 'text') {
|
|
85
|
-
fullText += block.text;
|
|
61
|
+
if (useStreamJson) {
|
|
62
|
+
// stream-json mode: parse NDJSON events
|
|
63
|
+
proc.stdout?.on('data', (chunk: Buffer) => {
|
|
64
|
+
buffer += chunk.toString();
|
|
65
|
+
const lines = buffer.split('\n');
|
|
66
|
+
buffer = lines.pop() ?? '';
|
|
67
|
+
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
const trimmed = line.trim();
|
|
70
|
+
if (!trimmed) continue;
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(trimmed);
|
|
73
|
+
onRawEvent?.(parsed);
|
|
74
|
+
|
|
75
|
+
if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
|
|
76
|
+
resultText += parsed.delta.text;
|
|
77
|
+
onText?.(parsed.delta.text);
|
|
78
|
+
} else if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
79
|
+
let fullText = '';
|
|
80
|
+
for (const block of parsed.message.content) {
|
|
81
|
+
if (block.type === 'text') fullText += block.text;
|
|
86
82
|
}
|
|
83
|
+
if (fullText.length > resultText.length) {
|
|
84
|
+
onText?.(fullText.slice(resultText.length));
|
|
85
|
+
}
|
|
86
|
+
resultText = fullText;
|
|
87
|
+
} else if (parsed.type === 'result' && parsed.result) {
|
|
88
|
+
if (parsed.result.length > resultText.length) {
|
|
89
|
+
onText?.(parsed.result.slice(resultText.length));
|
|
90
|
+
}
|
|
91
|
+
resultText = parsed.result;
|
|
87
92
|
}
|
|
88
|
-
|
|
89
|
-
if (fullText.length > resultText.length) {
|
|
90
|
-
const newPart = fullText.slice(resultText.length);
|
|
91
|
-
onText?.(newPart);
|
|
92
|
-
}
|
|
93
|
-
resultText = fullText;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// result — final output
|
|
97
|
-
else if (parsed.type === 'result' && parsed.result) {
|
|
98
|
-
// Emit any remaining new text from result
|
|
99
|
-
if (parsed.result.length > resultText.length) {
|
|
100
|
-
const newPart = parsed.result.slice(resultText.length);
|
|
101
|
-
onText?.(newPart);
|
|
102
|
-
}
|
|
103
|
-
resultText = parsed.result;
|
|
104
|
-
}
|
|
105
|
-
} catch {
|
|
106
|
-
// ignore non-JSON lines
|
|
93
|
+
} catch { /* ignore non-JSON */ }
|
|
107
94
|
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
// text mode: stdout is the raw result
|
|
98
|
+
proc.stdout?.on('data', (chunk: Buffer) => {
|
|
99
|
+
resultText += chunk.toString();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
110
102
|
|
|
111
103
|
proc.stderr?.on('data', (chunk: Buffer) => {
|
|
112
104
|
stderrText += chunk.toString();
|
|
@@ -117,6 +109,10 @@ export function runClaude(prompt: string, onText?: OnTextChunk, onRawEvent?: OnR
|
|
|
117
109
|
});
|
|
118
110
|
|
|
119
111
|
proc.on('exit', (code, signal) => {
|
|
112
|
+
// Clean up known CLI noise from text output
|
|
113
|
+
if (!useStreamJson) {
|
|
114
|
+
resultText = resultText.replace(/Error: Reached max turns \(\d+\)\s*/g, '').trim();
|
|
115
|
+
}
|
|
120
116
|
if (code !== 0 && !resultText) {
|
|
121
117
|
const detail = stderrText.slice(0, 500) || (signal ? `killed by signal ${signal}` : 'no output');
|
|
122
118
|
reject(new Error(`Claude CLI exited with code ${code}: ${detail}`));
|
|
@@ -30,7 +30,7 @@ export function createProject(name: string, description: string = '', projectPat
|
|
|
30
30
|
return getProject(id)!;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export function updateProject(id: string, data: { name?: string; description?: string; project_path?: string | null }): IProject | undefined {
|
|
33
|
+
export function updateProject(id: string, data: { name?: string; description?: string; project_path?: string | null; ai_context?: string }): IProject | undefined {
|
|
34
34
|
const db = getDb();
|
|
35
35
|
const project = getProject(id);
|
|
36
36
|
if (!project) return undefined;
|
|
@@ -38,11 +38,12 @@ export function updateProject(id: string, data: { name?: string; description?: s
|
|
|
38
38
|
const now = new Date().toISOString();
|
|
39
39
|
|
|
40
40
|
db.prepare(
|
|
41
|
-
'UPDATE projects SET name = ?, description = ?, project_path = ?, updated_at = ? WHERE id = ?'
|
|
41
|
+
'UPDATE projects SET name = ?, description = ?, project_path = ?, ai_context = ?, updated_at = ? WHERE id = ?'
|
|
42
42
|
).run(
|
|
43
43
|
data.name ?? project.name,
|
|
44
44
|
data.description ?? project.description,
|
|
45
45
|
data.project_path !== undefined ? data.project_path : project.project_path,
|
|
46
|
+
data.ai_context !== undefined ? data.ai_context : (project.ai_context ?? ''),
|
|
46
47
|
now,
|
|
47
48
|
id,
|
|
48
49
|
);
|
package/src/lib/db/schema.ts
CHANGED
|
@@ -97,6 +97,9 @@ export function initSchema(db: Database.Database): void {
|
|
|
97
97
|
if (!projCols.some(c => c.name === 'project_path')) {
|
|
98
98
|
db.exec("ALTER TABLE projects ADD COLUMN project_path TEXT");
|
|
99
99
|
}
|
|
100
|
+
if (!projCols.some(c => c.name === 'ai_context')) {
|
|
101
|
+
db.exec("ALTER TABLE projects ADD COLUMN ai_context TEXT NOT NULL DEFAULT ''");
|
|
102
|
+
}
|
|
100
103
|
|
|
101
104
|
// v2 tables
|
|
102
105
|
db.exec(`
|