sparkecoder 0.1.120 → 0.1.122

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.
Files changed (148) hide show
  1. package/dist/agent/index.js +182 -21
  2. package/dist/agent/index.js.map +1 -1
  3. package/dist/cli.js +800 -220
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.js +748 -168
  6. package/dist/index.js.map +1 -1
  7. package/dist/server/index.js +748 -168
  8. package/dist/server/index.js.map +1 -1
  9. package/dist/tools/index.js.map +1 -1
  10. package/package.json +1 -1
  11. package/web/.next/BUILD_ID +1 -1
  12. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  13. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  14. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  15. package/web/.next/standalone/web/.next/server/app/(main)/agents/page_client-reference-manifest.js +1 -1
  16. package/web/.next/standalone/web/.next/server/app/(main)/page_client-reference-manifest.js +1 -1
  17. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page.js.nft.json +1 -1
  18. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page_client-reference-manifest.js +1 -1
  19. package/web/.next/standalone/web/.next/server/app/(main)/settings/page.js.nft.json +1 -1
  20. package/web/.next/standalone/web/.next/server/app/(main)/settings/page_client-reference-manifest.js +1 -1
  21. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  22. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  23. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  24. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  25. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  26. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  28. package/web/.next/standalone/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  29. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  30. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +2 -2
  31. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  32. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  34. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  35. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  36. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  37. package/web/.next/standalone/web/.next/server/app/agents.html +1 -1
  38. package/web/.next/standalone/web/.next/server/app/agents.rsc +2 -2
  39. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents/__PAGE__.segment.rsc +1 -1
  40. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents.segment.rsc +1 -1
  41. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/agents.segments/_full.segment.rsc +2 -2
  43. package/web/.next/standalone/web/.next/server/app/agents.segments/_head.segment.rsc +1 -1
  44. package/web/.next/standalone/web/.next/server/app/agents.segments/_index.segment.rsc +2 -2
  45. package/web/.next/standalone/web/.next/server/app/agents.segments/_tree.segment.rsc +2 -2
  46. package/web/.next/standalone/web/.next/server/app/docs/installation/page_client-reference-manifest.js +1 -1
  47. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  48. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +2 -2
  49. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +2 -2
  50. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  51. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +2 -2
  52. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +2 -2
  53. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  54. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  55. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
  56. package/web/.next/standalone/web/.next/server/app/docs/page_client-reference-manifest.js +1 -1
  57. package/web/.next/standalone/web/.next/server/app/docs/skills/page_client-reference-manifest.js +1 -1
  58. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  59. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +2 -2
  60. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +2 -2
  61. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  62. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +2 -2
  63. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +2 -2
  64. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  65. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  66. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
  67. package/web/.next/standalone/web/.next/server/app/docs/tools/page_client-reference-manifest.js +1 -1
  68. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  69. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +2 -2
  70. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +2 -2
  71. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  72. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +2 -2
  73. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +2 -2
  74. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  75. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  76. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  77. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  78. package/web/.next/standalone/web/.next/server/app/docs.rsc +2 -2
  79. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +2 -2
  80. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  81. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +2 -2
  82. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +2 -2
  83. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  84. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  85. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  86. package/web/.next/standalone/web/.next/server/app/index.rsc +2 -2
  87. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
  88. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
  89. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +2 -2
  90. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  91. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
  92. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  93. package/web/.next/standalone/web/.next/server/app/settings.html +1 -1
  94. package/web/.next/standalone/web/.next/server/app/settings.rsc +3 -3
  95. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings/__PAGE__.segment.rsc +2 -2
  96. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings.segment.rsc +1 -1
  97. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p.segment.rsc +1 -1
  98. package/web/.next/standalone/web/.next/server/app/settings.segments/_full.segment.rsc +3 -3
  99. package/web/.next/standalone/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  100. package/web/.next/standalone/web/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  101. package/web/.next/standalone/web/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  102. package/web/.next/standalone/web/.next/server/chunks/ssr/2374f_lucide-react_dist_esm_icons_6ab1f7b7._.js +3 -0
  103. package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__f3e6443f._.js → [root-of-the-server]__4de426bd._.js} +2 -2
  104. package/web/.next/standalone/web/.next/server/chunks/ssr/web_62ca4286._.js +3 -0
  105. package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_settings_page_tsx_eb320e07._.js +3 -1
  106. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  107. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  108. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  109. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  110. package/web/.next/standalone/web/.next/static/chunks/74ae1f17d607b2fc.js +7 -0
  111. package/web/.next/standalone/web/.next/static/chunks/883ea0d08f88e366.js +1 -0
  112. package/web/.next/standalone/web/.next/static/chunks/{44c575e006ddb992.js → 91988e253d5fa420.js} +4 -4
  113. package/web/.next/standalone/web/.next/static/chunks/9b88f148788e4504.js +3 -0
  114. package/web/.next/standalone/web/.next/static/chunks/acb0fc66f5414af6.css +1 -0
  115. package/web/.next/standalone/web/.next/static/static/chunks/74ae1f17d607b2fc.js +7 -0
  116. package/web/.next/standalone/web/.next/static/static/chunks/883ea0d08f88e366.js +1 -0
  117. package/web/.next/standalone/web/.next/static/static/chunks/{44c575e006ddb992.js → 91988e253d5fa420.js} +4 -4
  118. package/web/.next/standalone/web/.next/static/static/chunks/9b88f148788e4504.js +3 -0
  119. package/web/.next/standalone/web/.next/static/static/chunks/acb0fc66f5414af6.css +1 -0
  120. package/web/.next/standalone/web/src/app/(main)/settings/page.tsx +464 -1
  121. package/web/.next/static/chunks/74ae1f17d607b2fc.js +7 -0
  122. package/web/.next/static/chunks/883ea0d08f88e366.js +1 -0
  123. package/web/.next/static/chunks/{44c575e006ddb992.js → 91988e253d5fa420.js} +4 -4
  124. package/web/.next/static/chunks/9b88f148788e4504.js +3 -0
  125. package/web/.next/static/chunks/acb0fc66f5414af6.css +1 -0
  126. package/web/.next/standalone/web/.next/server/chunks/ssr/2374f_lucide-react_dist_esm_icons_7340c8b3._.js +0 -3
  127. package/web/.next/standalone/web/.next/server/chunks/ssr/web_41927ef5._.js +0 -3
  128. package/web/.next/standalone/web/.next/static/chunks/2c3c1d478808e4e4.js +0 -1
  129. package/web/.next/standalone/web/.next/static/chunks/3b0501ec3249235f.js +0 -7
  130. package/web/.next/standalone/web/.next/static/chunks/9a3afb48c245fb2a.js +0 -1
  131. package/web/.next/standalone/web/.next/static/chunks/b3bc7244f3477729.css +0 -1
  132. package/web/.next/standalone/web/.next/static/static/chunks/2c3c1d478808e4e4.js +0 -1
  133. package/web/.next/standalone/web/.next/static/static/chunks/3b0501ec3249235f.js +0 -7
  134. package/web/.next/standalone/web/.next/static/static/chunks/9a3afb48c245fb2a.js +0 -1
  135. package/web/.next/standalone/web/.next/static/static/chunks/b3bc7244f3477729.css +0 -1
  136. package/web/.next/static/chunks/2c3c1d478808e4e4.js +0 -1
  137. package/web/.next/static/chunks/3b0501ec3249235f.js +0 -7
  138. package/web/.next/static/chunks/9a3afb48c245fb2a.js +0 -1
  139. package/web/.next/static/chunks/b3bc7244f3477729.css +0 -1
  140. /package/web/.next/standalone/web/.next/static/{static/uy1OnyxIm3QeGGgKEmxAj → BEIBC9-dP0_AWGmRy97hJ}/_buildManifest.js +0 -0
  141. /package/web/.next/standalone/web/.next/static/{static/uy1OnyxIm3QeGGgKEmxAj → BEIBC9-dP0_AWGmRy97hJ}/_clientMiddlewareManifest.json +0 -0
  142. /package/web/.next/standalone/web/.next/static/{static/uy1OnyxIm3QeGGgKEmxAj → BEIBC9-dP0_AWGmRy97hJ}/_ssgManifest.js +0 -0
  143. /package/web/.next/standalone/web/.next/static/{uy1OnyxIm3QeGGgKEmxAj → static/BEIBC9-dP0_AWGmRy97hJ}/_buildManifest.js +0 -0
  144. /package/web/.next/standalone/web/.next/static/{uy1OnyxIm3QeGGgKEmxAj → static/BEIBC9-dP0_AWGmRy97hJ}/_clientMiddlewareManifest.json +0 -0
  145. /package/web/.next/standalone/web/.next/static/{uy1OnyxIm3QeGGgKEmxAj → static/BEIBC9-dP0_AWGmRy97hJ}/_ssgManifest.js +0 -0
  146. /package/web/.next/static/{uy1OnyxIm3QeGGgKEmxAj → BEIBC9-dP0_AWGmRy97hJ}/_buildManifest.js +0 -0
  147. /package/web/.next/static/{uy1OnyxIm3QeGGgKEmxAj → BEIBC9-dP0_AWGmRy97hJ}/_clientMiddlewareManifest.json +0 -0
  148. /package/web/.next/static/{uy1OnyxIm3QeGGgKEmxAj → BEIBC9-dP0_AWGmRy97hJ}/_ssgManifest.js +0 -0
@@ -6,6 +6,7 @@ import {
6
6
  Copy, RotateCw, Trash2, Plus, Check, Eye, EyeOff, Bot, Cpu, Key,
7
7
  BookOpen, X as XIcon, ShieldCheck, Plug, List as ListIcon,
8
8
  ChevronLeft, ChevronRight, RefreshCw, Search,
9
+ Sparkles, FolderPlus, FileText, FolderOpen, Save, Pencil, Upload,
9
10
  } from 'lucide-react';
10
11
  import { Button } from '@/components/ui/button';
11
12
  import { Input } from '@/components/ui/input';
@@ -62,12 +63,13 @@ async function jsonFetch<T>(path: string, init?: RequestInit): Promise<T> {
62
63
  return res.json();
63
64
  }
64
65
 
65
- type TabId = 'general' | 'integrations' | 'mcp' | 'schedules' | 'webhooks' | 'audit' | 'models' | 'apikeys';
66
+ type TabId = 'general' | 'integrations' | 'mcp' | 'schedules' | 'webhooks' | 'audit' | 'models' | 'apikeys' | 'skills';
66
67
 
67
68
  const TABS: Array<{ id: TabId; label: string; icon: React.ComponentType<{ className?: string }> }> = [
68
69
  { id: 'general', label: 'General', icon: Bot },
69
70
  { id: 'integrations', label: 'Integrations', icon: Slack },
70
71
  { id: 'mcp', label: 'MCP', icon: Plug },
72
+ { id: 'skills', label: 'Skills', icon: Sparkles },
71
73
  { id: 'schedules', label: 'Schedules', icon: CalendarIcon },
72
74
  { id: 'webhooks', label: 'Webhooks', icon: WebhookIcon },
73
75
  { id: 'audit', label: 'Audit log', icon: ListIcon },
@@ -149,6 +151,7 @@ export default function SettingsPage() {
149
151
  {tab === 'audit' && <AuditSection />}
150
152
  {tab === 'models' && <ModelsSection />}
151
153
  {tab === 'apikeys' && <ApiKeysSection />}
154
+ {tab === 'skills' && <SkillsSection />}
152
155
  </div>
153
156
  </div>
154
157
  </div>
@@ -1421,6 +1424,466 @@ function formatEventTime(iso: string): string {
1421
1424
  return `${date} ${time}`;
1422
1425
  }
1423
1426
 
1427
+ // ────────────────────────────────────────────────────────────────────
1428
+ // Skills — markdown skill files from default + global directories
1429
+ // ────────────────────────────────────────────────────────────────────
1430
+
1431
+ interface SkillRow {
1432
+ id: string;
1433
+ name: string;
1434
+ description: string;
1435
+ filePath: string;
1436
+ fileName: string;
1437
+ sourceDir: string;
1438
+ sourceLabel: string;
1439
+ sourceType: 'builtin' | 'project' | 'additional';
1440
+ alwaysApply: boolean;
1441
+ globs: string[];
1442
+ sizeBytes: number;
1443
+ }
1444
+ interface SkillDirectory {
1445
+ path: string;
1446
+ label: string;
1447
+ source: 'builtin' | 'project' | 'additional';
1448
+ alwaysApply: boolean;
1449
+ exists: boolean;
1450
+ writable: boolean;
1451
+ }
1452
+ interface SkillsResponse {
1453
+ directories: SkillDirectory[];
1454
+ skills: SkillRow[];
1455
+ }
1456
+
1457
+ function SkillsSection() {
1458
+ const [data, setData] = useState<SkillsResponse | null>(null);
1459
+ const [loading, setLoading] = useState(true);
1460
+ const [filter, setFilter] = useState('');
1461
+ const [editing, setEditing] = useState<SkillRow | null>(null);
1462
+ const [creatingInDir, setCreatingInDir] = useState<string | null>(null);
1463
+ const [addDirOpen, setAddDirOpen] = useState(false);
1464
+ const [dragTargetDir, setDragTargetDir] = useState<string | null>(null);
1465
+
1466
+ const refresh = useCallback(async () => {
1467
+ setLoading(true);
1468
+ try { setData(await jsonFetch<SkillsResponse>('/api/skills')); }
1469
+ finally { setLoading(false); }
1470
+ }, []);
1471
+
1472
+ useEffect(() => { refresh(); }, [refresh]);
1473
+
1474
+ if (loading || !data) {
1475
+ return <div className="flex items-center justify-center py-12"><Loader2 className="size-5 animate-spin text-muted-foreground" /></div>;
1476
+ }
1477
+
1478
+ const grouped = data.directories.map((dir) => ({
1479
+ dir,
1480
+ items: data.skills.filter((s) => s.sourceDir === dir.path)
1481
+ .filter((s) => !filter || s.name.toLowerCase().includes(filter.toLowerCase()) || s.description.toLowerCase().includes(filter.toLowerCase())),
1482
+ }));
1483
+
1484
+ const handleDropFiles = async (files: FileList | null, targetDir: string) => {
1485
+ if (!files || files.length === 0) return;
1486
+ for (const file of Array.from(files)) {
1487
+ if (!/\.(md|mdc|markdown|txt)$/i.test(file.name)) continue;
1488
+ const text = await file.text();
1489
+ try {
1490
+ await jsonFetch('/api/skills', {
1491
+ method: 'POST',
1492
+ body: JSON.stringify({ directory: targetDir, fileName: file.name, content: text }),
1493
+ });
1494
+ } catch (err) {
1495
+ console.error('skill upload failed', err);
1496
+ }
1497
+ }
1498
+ refresh();
1499
+ };
1500
+
1501
+ return (
1502
+ <section>
1503
+ <div className="flex items-center justify-between mb-3">
1504
+ <SectionHeader
1505
+ title="Skills"
1506
+ description="Markdown skill files the orchestrator loads on demand. Pulled from built-in defaults plus any directories below — drop .md files in or create new ones from here."
1507
+ />
1508
+ <div className="flex gap-2">
1509
+ <Button size="sm" variant="outline" onClick={() => setAddDirOpen(true)}>
1510
+ <FolderPlus className="size-3.5 mr-1" />Add folder
1511
+ </Button>
1512
+ <Button size="sm" variant="ghost" onClick={refresh}>
1513
+ <RefreshCw className="size-3.5" />
1514
+ </Button>
1515
+ </div>
1516
+ </div>
1517
+
1518
+ <div className="mb-3">
1519
+ <div className="relative max-w-sm">
1520
+ <Search className="absolute left-2 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground" />
1521
+ <Input
1522
+ value={filter}
1523
+ onChange={(e) => setFilter(e.target.value)}
1524
+ placeholder="Search skills…"
1525
+ className="pl-7 h-8 text-xs"
1526
+ />
1527
+ </div>
1528
+ </div>
1529
+
1530
+ {addDirOpen && (
1531
+ <AddSkillDirectoryForm
1532
+ onCancel={() => setAddDirOpen(false)}
1533
+ onSaved={() => { setAddDirOpen(false); refresh(); }}
1534
+ />
1535
+ )}
1536
+
1537
+ <div className="space-y-3">
1538
+ {grouped.map(({ dir, items }) => (
1539
+ <div
1540
+ key={dir.path}
1541
+ onDragOver={(e) => {
1542
+ if (!dir.writable) return;
1543
+ e.preventDefault();
1544
+ setDragTargetDir(dir.path);
1545
+ }}
1546
+ onDragLeave={() => setDragTargetDir((d) => (d === dir.path ? null : d))}
1547
+ onDrop={(e) => {
1548
+ e.preventDefault();
1549
+ setDragTargetDir(null);
1550
+ if (!dir.writable) return;
1551
+ void handleDropFiles(e.dataTransfer.files, dir.path);
1552
+ }}
1553
+ className={cn(
1554
+ 'rounded-lg border bg-card/40 p-3 transition-colors',
1555
+ dragTargetDir === dir.path ? 'border-primary bg-primary/5' : 'border-border/60',
1556
+ )}
1557
+ >
1558
+ <div className="flex items-center gap-2 mb-2">
1559
+ <FolderOpen className={cn('size-4', dir.alwaysApply ? 'text-amber-500' : 'text-muted-foreground')} />
1560
+ <span className="text-sm font-medium">{dir.label}</span>
1561
+ <span className="text-[10px] uppercase tracking-wide text-muted-foreground">{dir.source}</span>
1562
+ {dir.alwaysApply && <span className="text-[10px] uppercase tracking-wide text-amber-500">always-on</span>}
1563
+ {!dir.exists && <span className="text-[10px] text-rose-500">missing</span>}
1564
+ <code className="ml-auto text-[10px] text-muted-foreground/70 font-mono truncate max-w-[280px]">{dir.path}</code>
1565
+ {dir.writable && (
1566
+ <Button size="sm" variant="ghost" className="h-6 px-2"
1567
+ onClick={() => setCreatingInDir(dir.path)}>
1568
+ <Plus className="size-3 mr-1" />New
1569
+ </Button>
1570
+ )}
1571
+ {dir.source === 'additional' && (
1572
+ <Button size="sm" variant="ghost" className="h-6 px-1 text-rose-500"
1573
+ onClick={async () => {
1574
+ if (!confirm(`Stop loading skills from ${dir.path}?`)) return;
1575
+ await jsonFetch(`/api/skills/directories?path=${encodeURIComponent(dir.path)}`, { method: 'DELETE' });
1576
+ refresh();
1577
+ }}>
1578
+ <Trash2 className="size-3" />
1579
+ </Button>
1580
+ )}
1581
+ </div>
1582
+
1583
+ {creatingInDir === dir.path && (
1584
+ <NewSkillForm
1585
+ directory={dir.path}
1586
+ onCancel={() => setCreatingInDir(null)}
1587
+ onSaved={() => { setCreatingInDir(null); refresh(); }}
1588
+ />
1589
+ )}
1590
+
1591
+ {items.length === 0 ? (
1592
+ <p className="text-[11px] text-muted-foreground italic px-1">
1593
+ {dir.writable ? 'No skills here yet — drop .md files or click New.' : 'No skills here.'}
1594
+ </p>
1595
+ ) : (
1596
+ <div className="divide-y divide-border/30">
1597
+ {items.map((s) => (
1598
+ <button
1599
+ key={s.id}
1600
+ type="button"
1601
+ onClick={() => setEditing(s)}
1602
+ className="w-full flex items-start gap-2 px-1 py-2 text-left hover:bg-accent/40 rounded transition-colors"
1603
+ >
1604
+ <FileText className="size-3.5 text-muted-foreground shrink-0 mt-0.5" />
1605
+ <div className="flex-1 min-w-0">
1606
+ <div className="flex items-center gap-2">
1607
+ <span className="text-xs font-medium truncate">{s.name}</span>
1608
+ <code className="text-[10px] text-muted-foreground font-mono truncate">{s.fileName}</code>
1609
+ {s.alwaysApply && <span className="text-[10px] text-amber-500 uppercase tracking-wide">always</span>}
1610
+ {s.globs.length > 0 && (
1611
+ <span className="text-[10px] text-muted-foreground">{s.globs.join(', ')}</span>
1612
+ )}
1613
+ </div>
1614
+ {s.description && (
1615
+ <p className="text-[11px] text-muted-foreground line-clamp-2 mt-0.5">{s.description}</p>
1616
+ )}
1617
+ </div>
1618
+ <Pencil className="size-3 text-muted-foreground/60 mt-1" />
1619
+ </button>
1620
+ ))}
1621
+ </div>
1622
+ )}
1623
+ </div>
1624
+ ))}
1625
+ </div>
1626
+
1627
+ {editing && (
1628
+ <SkillEditorModal
1629
+ skill={editing}
1630
+ onClose={() => setEditing(null)}
1631
+ onSaved={() => { setEditing(null); refresh(); }}
1632
+ />
1633
+ )}
1634
+ </section>
1635
+ );
1636
+ }
1637
+
1638
+ function AddSkillDirectoryForm({ onCancel, onSaved }: { onCancel: () => void; onSaved: () => void }) {
1639
+ const [path, setPath] = useState('');
1640
+ const [saving, setSaving] = useState(false);
1641
+ const [error, setError] = useState<string | null>(null);
1642
+ // Browsers can't expose absolute filesystem paths even when the user
1643
+ // picks a directory. We let them pick a folder so we can suggest the
1644
+ // folder NAME, but they still have to paste/confirm the absolute path
1645
+ // (or a path relative to the working directory) so the orchestrator
1646
+ // can read files server-side.
1647
+ const folderInputRef = useRef<HTMLInputElement | null>(null);
1648
+
1649
+ const onPickFolder = (e: React.ChangeEvent<HTMLInputElement>) => {
1650
+ const files = e.target.files;
1651
+ if (!files || files.length === 0) return;
1652
+ const first = files[0];
1653
+ // webkitRelativePath is "<folder>/file.md"; first segment = folder.
1654
+ const rel = (first as any).webkitRelativePath || first.name;
1655
+ const folderName = rel.split('/')[0];
1656
+ if (!path) setPath(folderName);
1657
+ };
1658
+
1659
+ const save = async () => {
1660
+ setError(null);
1661
+ if (!path.trim()) return;
1662
+ setSaving(true);
1663
+ try {
1664
+ const r = await jsonFetch<{ ok?: boolean; error?: string }>('/api/skills/directories', {
1665
+ method: 'POST',
1666
+ body: JSON.stringify({ path: path.trim() }),
1667
+ });
1668
+ if ((r as any).error) { setError((r as any).error); return; }
1669
+ onSaved();
1670
+ } finally { setSaving(false); }
1671
+ };
1672
+
1673
+ return (
1674
+ <div className="rounded-lg border border-border/60 bg-card/40 p-3 mb-3 space-y-2">
1675
+ <Label className="text-xs">Directory path</Label>
1676
+ <p className="text-[11px] text-muted-foreground">
1677
+ Absolute path (recommended) or a path relative to the orchestrator's working directory. All <code>.md</code> / <code>.mdc</code> files inside are picked up automatically.
1678
+ </p>
1679
+ <div className="flex gap-2">
1680
+ <Input
1681
+ value={path}
1682
+ onChange={(e) => setPath(e.target.value)}
1683
+ placeholder="/Users/you/skills or ./my-skills"
1684
+ className="text-xs font-mono"
1685
+ />
1686
+ <Button
1687
+ size="sm"
1688
+ variant="outline"
1689
+ onClick={() => folderInputRef.current?.click()}
1690
+ title="Pick a folder to suggest its name (browser security: you'll still need to paste the absolute path)"
1691
+ >
1692
+ <FolderOpen className="size-3.5 mr-1" />Browse
1693
+ </Button>
1694
+ <input
1695
+ ref={folderInputRef}
1696
+ type="file"
1697
+ // @ts-expect-error non-standard but widely supported
1698
+ webkitdirectory=""
1699
+ directory=""
1700
+ multiple
1701
+ hidden
1702
+ onChange={onPickFolder}
1703
+ />
1704
+ </div>
1705
+ {error && <p className="text-[11px] text-rose-500">{error}</p>}
1706
+ <div className="flex gap-2 pt-1">
1707
+ <Button size="sm" onClick={save} disabled={saving || !path.trim()}>
1708
+ {saving ? <Loader2 className="size-3.5 animate-spin" /> : 'Add folder'}
1709
+ </Button>
1710
+ <Button size="sm" variant="ghost" onClick={onCancel}>Cancel</Button>
1711
+ </div>
1712
+ </div>
1713
+ );
1714
+ }
1715
+
1716
+ function NewSkillForm({ directory, onCancel, onSaved }: { directory: string; onCancel: () => void; onSaved: () => void }) {
1717
+ const [fileName, setFileName] = useState('');
1718
+ const [name, setName] = useState('');
1719
+ const [description, setDescription] = useState('');
1720
+ const [body, setBody] = useState('');
1721
+ const [saving, setSaving] = useState(false);
1722
+ const [error, setError] = useState<string | null>(null);
1723
+ const fileInputRef = useRef<HTMLInputElement | null>(null);
1724
+
1725
+ const buildContent = () => {
1726
+ const fm: string[] = ['---'];
1727
+ if (name) fm.push(`name: ${name}`);
1728
+ if (description) fm.push(`description: ${description.replace(/\n/g, ' ')}`);
1729
+ fm.push('---', '');
1730
+ return fm.join('\n') + (body || `# ${name || 'New skill'}\n\nDescribe what this skill does and when to use it.`);
1731
+ };
1732
+
1733
+ const save = async () => {
1734
+ setError(null);
1735
+ const finalFileName = fileName.trim() || (name ? name.toLowerCase().replace(/[^a-z0-9]+/g, '-') + '.md' : 'untitled.md');
1736
+ setSaving(true);
1737
+ try {
1738
+ const r = await jsonFetch<{ error?: string }>('/api/skills', {
1739
+ method: 'POST',
1740
+ body: JSON.stringify({ directory, fileName: finalFileName, content: buildContent() }),
1741
+ });
1742
+ if (r.error) { setError(r.error); return; }
1743
+ onSaved();
1744
+ } finally { setSaving(false); }
1745
+ };
1746
+
1747
+ const onPickFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
1748
+ const files = e.target.files;
1749
+ if (!files || !files[0]) return;
1750
+ const file = files[0];
1751
+ setFileName(file.name);
1752
+ setBody(await file.text());
1753
+ };
1754
+
1755
+ return (
1756
+ <div className="rounded-md border border-border/60 bg-background/50 p-3 mb-2 space-y-2">
1757
+ <div className="grid grid-cols-2 gap-2">
1758
+ <div>
1759
+ <Label className="text-xs">Skill name</Label>
1760
+ <Input value={name} onChange={(e) => setName(e.target.value)} placeholder="React Performance" className="text-xs" />
1761
+ </div>
1762
+ <div>
1763
+ <Label className="text-xs">File name</Label>
1764
+ <Input value={fileName} onChange={(e) => setFileName(e.target.value)} placeholder="react-performance.md" className="text-xs font-mono" />
1765
+ </div>
1766
+ </div>
1767
+ <div>
1768
+ <Label className="text-xs">Description</Label>
1769
+ <Input value={description} onChange={(e) => setDescription(e.target.value)} placeholder="When to use this skill (one sentence)." className="text-xs" />
1770
+ </div>
1771
+ <div>
1772
+ <Label className="text-xs">Body (markdown)</Label>
1773
+ <textarea
1774
+ value={body}
1775
+ onChange={(e) => setBody(e.target.value)}
1776
+ rows={6}
1777
+ placeholder="# How to do X&#10;&#10;Step 1…"
1778
+ className="w-full rounded-md border border-input bg-background px-2 py-1.5 text-xs font-mono"
1779
+ />
1780
+ </div>
1781
+ {error && <p className="text-[11px] text-rose-500">{error}</p>}
1782
+ <div className="flex gap-2 pt-1">
1783
+ <Button size="sm" onClick={save} disabled={saving}>
1784
+ {saving ? <Loader2 className="size-3.5 animate-spin" /> : <><Save className="size-3.5 mr-1" />Save</>}
1785
+ </Button>
1786
+ <Button size="sm" variant="outline" onClick={() => fileInputRef.current?.click()}>
1787
+ <Upload className="size-3.5 mr-1" />Import .md
1788
+ </Button>
1789
+ <input ref={fileInputRef} type="file" hidden accept=".md,.mdc,.markdown,.txt" onChange={onPickFile} />
1790
+ <Button size="sm" variant="ghost" onClick={onCancel}>Cancel</Button>
1791
+ </div>
1792
+ </div>
1793
+ );
1794
+ }
1795
+
1796
+ function SkillEditorModal({ skill, onClose, onSaved }: { skill: SkillRow; onClose: () => void; onSaved: () => void }) {
1797
+ const [content, setContent] = useState<string | null>(null);
1798
+ const [writable, setWritable] = useState(false);
1799
+ const [saving, setSaving] = useState(false);
1800
+ const [savedFlash, setSavedFlash] = useState(false);
1801
+ const [original, setOriginal] = useState('');
1802
+
1803
+ useEffect(() => {
1804
+ let cancelled = false;
1805
+ (async () => {
1806
+ const r = await jsonFetch<{ content: string; writable: boolean }>(`/api/skills/${skill.id}`);
1807
+ if (cancelled) return;
1808
+ setContent(r.content);
1809
+ setOriginal(r.content);
1810
+ setWritable(r.writable);
1811
+ })();
1812
+ return () => { cancelled = true; };
1813
+ }, [skill.id]);
1814
+
1815
+ const dirty = content !== null && content !== original;
1816
+
1817
+ const save = async () => {
1818
+ if (content === null || !writable) return;
1819
+ setSaving(true);
1820
+ try {
1821
+ await jsonFetch(`/api/skills/${skill.id}`, { method: 'PUT', body: JSON.stringify({ content }) });
1822
+ setOriginal(content);
1823
+ setSavedFlash(true);
1824
+ setTimeout(() => setSavedFlash(false), 1200);
1825
+ } finally { setSaving(false); }
1826
+ };
1827
+
1828
+ const remove = async () => {
1829
+ if (!confirm(`Delete skill "${skill.name}"? This removes the file from disk.`)) return;
1830
+ await jsonFetch(`/api/skills/${skill.id}`, { method: 'DELETE' });
1831
+ onSaved();
1832
+ };
1833
+
1834
+ return (
1835
+ <div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4" onClick={onClose}>
1836
+ <div className="bg-card rounded-lg border border-border max-w-3xl w-full max-h-[85vh] flex flex-col shadow-2xl" onClick={(e) => e.stopPropagation()}>
1837
+ <div className="flex items-center justify-between px-5 py-3 border-b border-border/60">
1838
+ <div className="min-w-0">
1839
+ <h2 className="text-base font-semibold flex items-center gap-2 truncate">
1840
+ <FileText className="size-4 text-muted-foreground" />
1841
+ {skill.name}
1842
+ {!writable && <span className="text-[10px] uppercase tracking-wide text-muted-foreground">read-only</span>}
1843
+ </h2>
1844
+ <code className="text-[11px] text-muted-foreground font-mono truncate block">{skill.filePath}</code>
1845
+ </div>
1846
+ <Button size="icon" variant="ghost" onClick={onClose}><XIcon className="size-4" /></Button>
1847
+ </div>
1848
+
1849
+ <div className="flex-1 min-h-0 overflow-hidden flex flex-col p-4">
1850
+ {content === null ? (
1851
+ <div className="flex items-center justify-center flex-1"><Loader2 className="size-5 animate-spin text-muted-foreground" /></div>
1852
+ ) : (
1853
+ <textarea
1854
+ value={content}
1855
+ onChange={(e) => setContent(e.target.value)}
1856
+ readOnly={!writable}
1857
+ spellCheck={false}
1858
+ className="flex-1 w-full rounded-md border border-input bg-background px-3 py-2 text-xs font-mono resize-none focus:outline-none focus:ring-2 focus:ring-primary/40"
1859
+ />
1860
+ )}
1861
+ </div>
1862
+
1863
+ <div className="flex items-center gap-2 px-5 py-3 border-t border-border/60">
1864
+ {writable && (
1865
+ <>
1866
+ <Button size="sm" onClick={save} disabled={!dirty || saving}>
1867
+ {saving ? <Loader2 className="size-3.5 animate-spin mr-1" /> : <Save className="size-3.5 mr-1" />}
1868
+ Save
1869
+ </Button>
1870
+ {savedFlash && <span className="text-xs text-emerald-500">Saved ✓</span>}
1871
+ <Button size="sm" variant="ghost" onClick={remove} className="text-rose-500 hover:text-rose-600 ml-auto">
1872
+ <Trash2 className="size-3.5 mr-1" />Delete
1873
+ </Button>
1874
+ </>
1875
+ )}
1876
+ {!writable && (
1877
+ <p className="text-[11px] text-muted-foreground">
1878
+ Built-in skills can't be edited from the UI. Copy the contents into a custom folder to override.
1879
+ </p>
1880
+ )}
1881
+ </div>
1882
+ </div>
1883
+ </div>
1884
+ );
1885
+ }
1886
+
1424
1887
  function StatusPill({ status }: { status: WebhookEventRow['status'] }) {
1425
1888
  const colors: Record<WebhookEventRow['status'], string> = {
1426
1889
  routed: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30',