@assistkick/create 1.19.0 → 1.22.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/package.json +1 -1
- package/templates/assistkick-product-system/packages/backend/src/routes/mcp_config.ts +87 -0
- package/templates/assistkick-product-system/packages/backend/src/server.ts +8 -1
- package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +7 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +26 -1
- package/templates/assistkick-product-system/packages/backend/src/services/mcp_config_service.ts +157 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +37 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +14 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/McpConfigModal.tsx +307 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0001_superb_roxanne_simpson.sql +8 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0002_noisy_maelstrom.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0001_snapshot.json +1019 -23
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0002_snapshot.json +997 -22
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +14 -0
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +11 -0
- package/templates/assistkick-product-system/packages/shared/lib/app_use_flow.ts +484 -0
- package/templates/assistkick-product-system/packages/shared/package.json +2 -1
- package/templates/assistkick-product-system/packages/shared/tools/agent_builder.ts +341 -0
- package/templates/assistkick-product-system/packages/shared/tools/app_use_record.ts +268 -0
- package/templates/assistkick-product-system/packages/shared/tools/app_use_run.ts +348 -0
- package/templates/assistkick-product-system/packages/shared/tools/app_use_validate.ts +67 -0
- package/templates/assistkick-product-system/packages/shared/tools/workflow_builder.ts +754 -0
- package/templates/skills/assistkick-agent-builder/SKILL.md +168 -0
- package/templates/skills/assistkick-app-use/SKILL.md +296 -0
- package/templates/skills/assistkick-app-use/references/agent-browser.md +1156 -0
- package/templates/skills/assistkick-workflow-builder/SKILL.md +234 -0
package/templates/assistkick-product-system/packages/frontend/src/components/McpConfigModal.tsx
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* McpConfigModal — Modal dialog for editing per-user MCP server configurations.
|
|
3
|
+
* Scope toggle: Global (user-level) or Project (project-scoped).
|
|
4
|
+
* Two tabs per scope: Localhost (local dev only) and Remote (all environments).
|
|
5
|
+
* Each tab has a JSON textarea editor with validation and save functionality.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {useCallback, useEffect, useState} from 'react';
|
|
9
|
+
import {Globe, X} from 'lucide-react';
|
|
10
|
+
import {apiClient} from '../api/client';
|
|
11
|
+
import {useProjectStore} from '../stores/useProjectStore';
|
|
12
|
+
|
|
13
|
+
interface McpConfigModalProps {
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type ConfigTab = 'localhost' | 'remote';
|
|
18
|
+
type Scope = 'global' | 'project';
|
|
19
|
+
|
|
20
|
+
const PLACEHOLDER_CONFIG = JSON.stringify(
|
|
21
|
+
{
|
|
22
|
+
'example-server': {
|
|
23
|
+
url: 'http://localhost:8080/sse',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
null,
|
|
27
|
+
2,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export function McpConfigModal({onClose}: McpConfigModalProps) {
|
|
31
|
+
const selectedProjectId = useProjectStore((s) => s.selectedProjectId);
|
|
32
|
+
|
|
33
|
+
const [scope, setScope] = useState<Scope>('global');
|
|
34
|
+
const [activeTab, setActiveTab] = useState<ConfigTab>('localhost');
|
|
35
|
+
|
|
36
|
+
// Separate state for global and project configs
|
|
37
|
+
const [globalLocalhostJson, setGlobalLocalhostJson] = useState('{}');
|
|
38
|
+
const [globalRemoteJson, setGlobalRemoteJson] = useState('{}');
|
|
39
|
+
const [projectLocalhostJson, setProjectLocalhostJson] = useState('{}');
|
|
40
|
+
const [projectRemoteJson, setProjectRemoteJson] = useState('{}');
|
|
41
|
+
|
|
42
|
+
const [saving, setSaving] = useState(false);
|
|
43
|
+
const [error, setError] = useState<string | null>(null);
|
|
44
|
+
const [success, setSuccess] = useState<string | null>(null);
|
|
45
|
+
const [loading, setLoading] = useState(true);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const load = async () => {
|
|
49
|
+
try {
|
|
50
|
+
// Load global configs
|
|
51
|
+
const {configs: globalConfigs} = await apiClient.getMcpConfigs();
|
|
52
|
+
for (const cfg of globalConfigs) {
|
|
53
|
+
const formatted = JSON.stringify(JSON.parse(cfg.mcpServersJson), null, 2);
|
|
54
|
+
if (cfg.configType === 'localhost') setGlobalLocalhostJson(formatted);
|
|
55
|
+
if (cfg.configType === 'remote') setGlobalRemoteJson(formatted);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Load project configs if a project is selected
|
|
59
|
+
if (selectedProjectId) {
|
|
60
|
+
const {configs: projectConfigs} = await apiClient.getMcpConfigs(selectedProjectId);
|
|
61
|
+
for (const cfg of projectConfigs) {
|
|
62
|
+
const formatted = JSON.stringify(JSON.parse(cfg.mcpServersJson), null, 2);
|
|
63
|
+
if (cfg.configType === 'localhost') setProjectLocalhostJson(formatted);
|
|
64
|
+
if (cfg.configType === 'remote') setProjectRemoteJson(formatted);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch (err: any) {
|
|
68
|
+
setError(`Failed to load configs: ${err.message}`);
|
|
69
|
+
} finally {
|
|
70
|
+
setLoading(false);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
load();
|
|
74
|
+
}, [selectedProjectId]);
|
|
75
|
+
|
|
76
|
+
const getCurrentJson = (): string => {
|
|
77
|
+
if (scope === 'global') return activeTab === 'localhost' ? globalLocalhostJson : globalRemoteJson;
|
|
78
|
+
return activeTab === 'localhost' ? projectLocalhostJson : projectRemoteJson;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const setCurrentJson = (value: string) => {
|
|
82
|
+
if (scope === 'global') {
|
|
83
|
+
if (activeTab === 'localhost') setGlobalLocalhostJson(value);
|
|
84
|
+
else setGlobalRemoteJson(value);
|
|
85
|
+
} else {
|
|
86
|
+
if (activeTab === 'localhost') setProjectLocalhostJson(value);
|
|
87
|
+
else setProjectRemoteJson(value);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const currentJson = getCurrentJson();
|
|
92
|
+
|
|
93
|
+
const validateJson = useCallback((json: string): string | null => {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(json);
|
|
96
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
97
|
+
return 'Must be a JSON object (e.g. { "server-name": { ... } })';
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
} catch {
|
|
101
|
+
return 'Invalid JSON syntax';
|
|
102
|
+
}
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const handleSave = useCallback(async () => {
|
|
106
|
+
const jsonToSave = getCurrentJson();
|
|
107
|
+
const validationError = validateJson(jsonToSave);
|
|
108
|
+
if (validationError) {
|
|
109
|
+
setError(validationError);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
setSaving(true);
|
|
114
|
+
setError(null);
|
|
115
|
+
setSuccess(null);
|
|
116
|
+
|
|
117
|
+
const projectId = scope === 'project' ? selectedProjectId : null;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await apiClient.saveMcpConfig(activeTab, jsonToSave, projectId);
|
|
121
|
+
const scopeLabel = scope === 'project' ? 'Project' : 'Global';
|
|
122
|
+
const typeLabel = activeTab === 'localhost' ? 'localhost' : 'remote';
|
|
123
|
+
setSuccess(`${scopeLabel} ${typeLabel} config saved`);
|
|
124
|
+
setTimeout(() => setSuccess(null), 3000);
|
|
125
|
+
} catch (err: any) {
|
|
126
|
+
setError(err.message);
|
|
127
|
+
} finally {
|
|
128
|
+
setSaving(false);
|
|
129
|
+
}
|
|
130
|
+
}, [scope, activeTab, selectedProjectId, globalLocalhostJson, globalRemoteJson, projectLocalhostJson, projectRemoteJson, validateJson]);
|
|
131
|
+
|
|
132
|
+
const handleKeyDown = useCallback(
|
|
133
|
+
(e: React.KeyboardEvent) => {
|
|
134
|
+
if (e.key === 'Escape') onClose();
|
|
135
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
handleSave();
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
[onClose, handleSave],
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const jsonError = validateJson(currentJson);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div
|
|
147
|
+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
|
|
148
|
+
onClick={(e) => {
|
|
149
|
+
if (e.target === e.currentTarget) onClose();
|
|
150
|
+
}}
|
|
151
|
+
onKeyDown={handleKeyDown}
|
|
152
|
+
>
|
|
153
|
+
<div className="bg-surface border border-edge rounded-lg shadow-xl w-[640px] max-h-[80vh] flex flex-col">
|
|
154
|
+
{/* Header */}
|
|
155
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-edge">
|
|
156
|
+
<h2 className="text-sm font-semibold text-content">MCP Server Configuration</h2>
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
onClick={onClose}
|
|
160
|
+
className="text-content-muted hover:text-content transition-colors"
|
|
161
|
+
>
|
|
162
|
+
<X size={16} />
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{/* Scope toggle */}
|
|
167
|
+
<div className="flex items-center gap-1 px-4 py-2 border-b border-edge">
|
|
168
|
+
<span className="text-[11px] text-content-muted mr-2">Scope:</span>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
className={`h-6 px-2.5 rounded text-[11px] font-mono transition-colors ${
|
|
172
|
+
scope === 'global'
|
|
173
|
+
? 'bg-accent text-white'
|
|
174
|
+
: 'text-content-muted bg-transparent border border-edge hover:bg-surface-raised'
|
|
175
|
+
}`}
|
|
176
|
+
onClick={() => {
|
|
177
|
+
setScope('global');
|
|
178
|
+
setError(null);
|
|
179
|
+
setSuccess(null);
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
Global
|
|
183
|
+
</button>
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
className={`h-6 px-2.5 rounded text-[11px] font-mono transition-colors ${
|
|
187
|
+
scope === 'project'
|
|
188
|
+
? 'bg-accent text-white'
|
|
189
|
+
: 'text-content-muted bg-transparent border border-edge hover:bg-surface-raised'
|
|
190
|
+
}`}
|
|
191
|
+
onClick={() => {
|
|
192
|
+
setScope('project');
|
|
193
|
+
setError(null);
|
|
194
|
+
setSuccess(null);
|
|
195
|
+
}}
|
|
196
|
+
disabled={!selectedProjectId}
|
|
197
|
+
title={selectedProjectId ? `Project: ${selectedProjectId}` : 'No project selected'}
|
|
198
|
+
>
|
|
199
|
+
Project
|
|
200
|
+
</button>
|
|
201
|
+
{scope === 'project' && selectedProjectId && (
|
|
202
|
+
<span className="text-[10px] text-content-muted ml-1 font-mono truncate max-w-[200px]">
|
|
203
|
+
{selectedProjectId}
|
|
204
|
+
</span>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{/* Description */}
|
|
209
|
+
<div className="px-4 py-2 text-[11px] text-content-muted border-b border-edge">
|
|
210
|
+
{scope === 'global' ? (
|
|
211
|
+
<>
|
|
212
|
+
<strong>Global</strong> configs apply to all projects for your user.{' '}
|
|
213
|
+
Project-specific configs are merged on top (project overrides global).
|
|
214
|
+
</>
|
|
215
|
+
) : (
|
|
216
|
+
<>
|
|
217
|
+
<strong>Project</strong> configs only apply to this project and override global configs
|
|
218
|
+
with the same server name.
|
|
219
|
+
</>
|
|
220
|
+
)}
|
|
221
|
+
{' '}<strong>Localhost</strong> servers are excluded in Docker.{' '}
|
|
222
|
+
<strong>Remote</strong> servers are used everywhere.
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
{/* Tabs */}
|
|
226
|
+
<div className="flex border-b border-edge">
|
|
227
|
+
{(['localhost', 'remote'] as ConfigTab[]).map((tab) => (
|
|
228
|
+
<button
|
|
229
|
+
key={tab}
|
|
230
|
+
type="button"
|
|
231
|
+
className={`flex-1 px-4 py-2 text-xs font-mono transition-colors ${
|
|
232
|
+
activeTab === tab
|
|
233
|
+
? 'text-accent border-b-2 border-accent bg-surface-alt'
|
|
234
|
+
: 'text-content-muted hover:text-content hover:bg-surface-raised'
|
|
235
|
+
}`}
|
|
236
|
+
onClick={() => {
|
|
237
|
+
setActiveTab(tab);
|
|
238
|
+
setError(null);
|
|
239
|
+
setSuccess(null);
|
|
240
|
+
}}
|
|
241
|
+
>
|
|
242
|
+
{tab === 'localhost' ? 'Localhost' : 'Remote'}
|
|
243
|
+
</button>
|
|
244
|
+
))}
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
{/* Editor */}
|
|
248
|
+
<div className="flex-1 overflow-auto p-4">
|
|
249
|
+
{loading ? (
|
|
250
|
+
<div className="text-content-muted text-xs text-center py-8">Loading...</div>
|
|
251
|
+
) : (
|
|
252
|
+
<>
|
|
253
|
+
<label className="block text-[11px] text-content-muted mb-1.5">
|
|
254
|
+
mcpServers JSON ({activeTab === 'localhost' ? 'local dev only' : 'all environments'})
|
|
255
|
+
</label>
|
|
256
|
+
<textarea
|
|
257
|
+
className={`w-full h-64 bg-surface-alt border rounded font-mono text-xs text-content p-3 resize-y focus:outline-none focus:ring-1 ${
|
|
258
|
+
jsonError ? 'border-error focus:ring-error' : 'border-edge focus:ring-accent'
|
|
259
|
+
}`}
|
|
260
|
+
value={currentJson}
|
|
261
|
+
onChange={(e) => {
|
|
262
|
+
setCurrentJson(e.target.value);
|
|
263
|
+
setError(null);
|
|
264
|
+
}}
|
|
265
|
+
placeholder={PLACEHOLDER_CONFIG}
|
|
266
|
+
spellCheck={false}
|
|
267
|
+
/>
|
|
268
|
+
{jsonError && <p className="text-[11px] text-error mt-1">{jsonError}</p>}
|
|
269
|
+
<p className="text-[10px] text-content-muted mt-1.5">
|
|
270
|
+
Enter the content of the{' '}
|
|
271
|
+
<code className="bg-surface-raised px-1 rounded">mcpServers</code> object. Example:{' '}
|
|
272
|
+
<code className="bg-surface-raised px-1 rounded">
|
|
273
|
+
{'{ "jetbrains": { "url": "http://localhost:6637/sse" } }'}
|
|
274
|
+
</code>
|
|
275
|
+
</p>
|
|
276
|
+
</>
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
{/* Footer */}
|
|
281
|
+
<div className="flex items-center justify-between px-4 py-3 border-t border-edge">
|
|
282
|
+
<div className="text-[11px]">
|
|
283
|
+
{error && <span className="text-error">{error}</span>}
|
|
284
|
+
{success && <span className="text-accent">{success}</span>}
|
|
285
|
+
</div>
|
|
286
|
+
<div className="flex gap-2">
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
onClick={onClose}
|
|
290
|
+
className="h-7 px-3 rounded text-[11px] font-mono text-content-secondary bg-transparent border border-edge hover:bg-surface-raised transition-colors"
|
|
291
|
+
>
|
|
292
|
+
Close
|
|
293
|
+
</button>
|
|
294
|
+
<button
|
|
295
|
+
type="button"
|
|
296
|
+
onClick={handleSave}
|
|
297
|
+
disabled={saving || !!jsonError || loading}
|
|
298
|
+
className="h-7 px-3 rounded text-[11px] font-mono text-white bg-accent hover:bg-accent/90 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
299
|
+
>
|
|
300
|
+
{saving ? 'Saving...' : 'Save'}
|
|
301
|
+
</button>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|
package/templates/assistkick-product-system/packages/shared/db/migrations/0002_noisy_maelstrom.sql
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `user_mcp_configs` ADD `project_id` text;
|