stagent 0.5.0 → 0.6.1

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 (256) hide show
  1. package/README.md +8 -8
  2. package/dist/cli.js +146 -2
  3. package/docs/.coverage-gaps.json +21 -0
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +36 -14
  6. package/docs/features/chat.md +33 -56
  7. package/docs/features/cost-usage.md +14 -10
  8. package/docs/features/dashboard-kanban.md +30 -13
  9. package/docs/features/delivery-channels.md +198 -0
  10. package/docs/features/design-system.md +10 -10
  11. package/docs/features/documents.md +8 -8
  12. package/docs/features/home-workspace.md +20 -15
  13. package/docs/features/inbox-notifications.md +22 -10
  14. package/docs/features/keyboard-navigation.md +11 -11
  15. package/docs/features/monitoring.md +1 -1
  16. package/docs/features/playbook.md +30 -32
  17. package/docs/features/profiles.md +33 -11
  18. package/docs/features/projects.md +2 -2
  19. package/docs/features/provider-runtimes.md +58 -14
  20. package/docs/features/schedules.md +70 -40
  21. package/docs/features/settings.md +74 -46
  22. package/docs/features/shared-components.md +7 -15
  23. package/docs/features/tool-permissions.md +9 -9
  24. package/docs/features/workflows.md +32 -21
  25. package/docs/getting-started.md +33 -9
  26. package/docs/index.md +25 -16
  27. package/docs/journeys/developer.md +124 -207
  28. package/docs/journeys/personal-use.md +70 -79
  29. package/docs/journeys/power-user.md +107 -151
  30. package/docs/journeys/work-use.md +81 -113
  31. package/docs/manifest.json +77 -45
  32. package/docs/superpowers/plans/2026-03-30-finish-in-progress-features.md +547 -0
  33. package/docs/use-cases/agency-operator.md +84 -0
  34. package/docs/use-cases/solo-founder.md +75 -0
  35. package/docs/why-stagent.md +59 -0
  36. package/package.json +10 -3
  37. package/src/app/api/channels/[id]/route.ts +104 -0
  38. package/src/app/api/channels/[id]/test/route.ts +52 -0
  39. package/src/app/api/channels/inbound/slack/route.ts +116 -0
  40. package/src/app/api/channels/inbound/telegram/poll/route.ts +140 -0
  41. package/src/app/api/channels/inbound/telegram/route.ts +87 -0
  42. package/src/app/api/channels/route.ts +72 -0
  43. package/src/app/api/chat/conversations/route.ts +15 -0
  44. package/src/app/api/chat/entities/search/route.ts +46 -31
  45. package/src/app/api/data/clear/route.ts +4 -0
  46. package/src/app/api/data/seed/route.ts +4 -0
  47. package/src/app/api/documents/route.ts +36 -6
  48. package/src/app/api/environment/profiles/suggest/route.ts +19 -3
  49. package/src/app/api/environment/scan/route.ts +8 -1
  50. package/src/app/api/handoffs/[id]/route.ts +76 -0
  51. package/src/app/api/handoffs/route.ts +89 -0
  52. package/src/app/api/memory/route.ts +181 -0
  53. package/src/app/api/profiles/[id]/route.ts +16 -1
  54. package/src/app/api/profiles/[id]/test/route.ts +4 -0
  55. package/src/app/api/profiles/[id]/test-results/route.ts +22 -0
  56. package/src/app/api/profiles/[id]/test-single/route.ts +64 -0
  57. package/src/app/api/profiles/assist/route.ts +35 -0
  58. package/src/app/api/profiles/import-repo/apply-updates/route.ts +123 -0
  59. package/src/app/api/profiles/import-repo/check-updates/route.ts +163 -0
  60. package/src/app/api/profiles/import-repo/confirm/route.ts +118 -0
  61. package/src/app/api/profiles/import-repo/preview/route.ts +107 -0
  62. package/src/app/api/profiles/import-repo/route.ts +29 -0
  63. package/src/app/api/profiles/import-repo/scan/route.ts +25 -0
  64. package/src/app/api/profiles/route.ts +73 -22
  65. package/src/app/api/runtimes/ollama/route.ts +86 -0
  66. package/src/app/api/runtimes/suggest/route.ts +29 -0
  67. package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
  68. package/src/app/api/schedules/[id]/route.ts +41 -3
  69. package/src/app/api/schedules/parse/route.ts +66 -0
  70. package/src/app/api/schedules/route.ts +71 -12
  71. package/src/app/api/settings/author-default/route.ts +7 -0
  72. package/src/app/api/settings/learning/route.ts +41 -0
  73. package/src/app/api/settings/ollama/route.ts +34 -0
  74. package/src/app/api/settings/providers/route.ts +57 -0
  75. package/src/app/api/settings/routing/route.ts +24 -0
  76. package/src/app/api/settings/web-search/route.ts +28 -0
  77. package/src/app/api/tasks/[id]/execute/route.ts +13 -1
  78. package/src/app/api/tasks/[id]/respond/route.ts +23 -1
  79. package/src/app/documents/page.tsx +3 -0
  80. package/src/app/environment/page.tsx +8 -1
  81. package/src/app/settings/page.tsx +10 -4
  82. package/src/app/workflows/[id]/edit/page.tsx +2 -0
  83. package/src/app/workflows/new/page.tsx +2 -0
  84. package/src/components/chat/chat-command-popover.tsx +22 -19
  85. package/src/components/chat/chat-input.tsx +5 -0
  86. package/src/components/chat/chat-model-selector.tsx +42 -1
  87. package/src/components/chat/chat-shell.tsx +2 -0
  88. package/src/components/dashboard/welcome-landing.tsx +9 -9
  89. package/src/components/environment/artifact-card.tsx +27 -1
  90. package/src/components/environment/environment-dashboard.tsx +50 -2
  91. package/src/components/environment/environment-summary-card.tsx +5 -2
  92. package/src/components/environment/suggested-profiles.tsx +117 -52
  93. package/src/components/handoffs/handoff-approval-card.tsx +159 -0
  94. package/src/components/memory/memory-browser.tsx +315 -0
  95. package/src/components/profiles/learned-context-panel.tsx +4 -4
  96. package/src/components/profiles/profile-assist-panel.tsx +512 -0
  97. package/src/components/profiles/profile-browser.tsx +109 -8
  98. package/src/components/profiles/profile-card.tsx +29 -1
  99. package/src/components/profiles/profile-detail-view.tsx +200 -28
  100. package/src/components/profiles/profile-form-view.tsx +220 -82
  101. package/src/components/profiles/repo-import-wizard.tsx +648 -0
  102. package/src/components/profiles/smoke-test-editor.tsx +106 -0
  103. package/src/components/schedules/schedule-create-sheet.tsx +9 -1
  104. package/src/components/schedules/schedule-form.tsx +348 -9
  105. package/src/components/schedules/schedule-list.tsx +15 -2
  106. package/src/components/settings/auth-method-selector.tsx +7 -1
  107. package/src/components/settings/budget-guardrails-section.tsx +111 -48
  108. package/src/components/settings/channels-section.tsx +526 -0
  109. package/src/components/settings/chat-settings-section.tsx +27 -1
  110. package/src/components/settings/data-management-section.tsx +8 -6
  111. package/src/components/settings/learning-context-section.tsx +124 -0
  112. package/src/components/settings/ollama-section.tsx +270 -0
  113. package/src/components/settings/providers-runtimes-section.tsx +499 -0
  114. package/src/components/settings/web-search-section.tsx +101 -0
  115. package/src/components/shared/tag-input.tsx +156 -0
  116. package/src/components/tasks/kanban-board.tsx +32 -0
  117. package/src/components/tasks/kanban-column.tsx +4 -2
  118. package/src/components/tasks/task-card.tsx +1 -0
  119. package/src/components/tasks/task-chip-bar.tsx +6 -1
  120. package/src/components/tasks/task-create-panel.tsx +55 -5
  121. package/src/components/workflows/workflow-form-view.tsx +38 -3
  122. package/src/hooks/use-chat-autocomplete.ts +24 -26
  123. package/src/hooks/use-project-skills.ts +66 -0
  124. package/src/hooks/use-tag-suggestions.ts +31 -0
  125. package/src/instrumentation.ts +4 -1
  126. package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
  127. package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
  128. package/src/lib/agents/agentic-loop.ts +235 -0
  129. package/src/lib/agents/browser-mcp.ts +59 -4
  130. package/src/lib/agents/claude-agent.ts +27 -200
  131. package/src/lib/agents/handoff/bus.ts +164 -0
  132. package/src/lib/agents/handoff/governance.ts +47 -0
  133. package/src/lib/agents/handoff/types.ts +16 -0
  134. package/src/lib/agents/learned-context.ts +27 -7
  135. package/src/lib/agents/memory/decay.ts +61 -0
  136. package/src/lib/agents/memory/extractor.ts +181 -0
  137. package/src/lib/agents/memory/retrieval.ts +96 -0
  138. package/src/lib/agents/memory/types.ts +6 -0
  139. package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
  140. package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
  141. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
  142. package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
  143. package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
  144. package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
  145. package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
  146. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
  147. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
  148. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
  149. package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
  150. package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
  151. package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
  152. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
  153. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
  154. package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
  155. package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
  156. package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
  157. package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
  158. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
  159. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
  160. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
  161. package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
  162. package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
  163. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
  164. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
  165. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
  166. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
  167. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
  168. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
  169. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
  170. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
  171. package/src/lib/agents/profiles/project-profiles.ts +193 -0
  172. package/src/lib/agents/profiles/registry.ts +130 -6
  173. package/src/lib/agents/profiles/types.ts +28 -0
  174. package/src/lib/agents/router.ts +174 -2
  175. package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
  176. package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
  177. package/src/lib/agents/runtime/catalog.ts +57 -2
  178. package/src/lib/agents/runtime/claude.ts +205 -1
  179. package/src/lib/agents/runtime/index.ts +22 -0
  180. package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
  181. package/src/lib/agents/runtime/openai-direct.ts +514 -0
  182. package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
  183. package/src/lib/agents/runtime/types.ts +2 -0
  184. package/src/lib/agents/tool-permissions.ts +203 -0
  185. package/src/lib/channels/gateway.ts +321 -0
  186. package/src/lib/channels/poller.ts +268 -0
  187. package/src/lib/channels/registry.ts +90 -0
  188. package/src/lib/channels/slack-adapter.ts +188 -0
  189. package/src/lib/channels/telegram-adapter.ts +218 -0
  190. package/src/lib/channels/types.ts +75 -0
  191. package/src/lib/channels/webhook-adapter.ts +74 -0
  192. package/src/lib/chat/context-builder.ts +22 -2
  193. package/src/lib/chat/engine.ts +95 -13
  194. package/src/lib/chat/ollama-engine.ts +198 -0
  195. package/src/lib/chat/stagent-tools.ts +106 -20
  196. package/src/lib/chat/tool-catalog.ts +24 -0
  197. package/src/lib/chat/tool-registry.ts +90 -0
  198. package/src/lib/chat/tools/chat-history-tools.ts +4 -4
  199. package/src/lib/chat/tools/document-tools.ts +7 -7
  200. package/src/lib/chat/tools/handoff-tools.ts +70 -0
  201. package/src/lib/chat/tools/notification-tools.ts +4 -4
  202. package/src/lib/chat/tools/profile-tools.ts +3 -3
  203. package/src/lib/chat/tools/project-tools.ts +3 -3
  204. package/src/lib/chat/tools/schedule-tools.ts +29 -13
  205. package/src/lib/chat/tools/settings-tools.ts +2 -2
  206. package/src/lib/chat/tools/task-tools.ts +66 -11
  207. package/src/lib/chat/tools/usage-tools.ts +2 -2
  208. package/src/lib/chat/tools/workflow-tools.ts +8 -8
  209. package/src/lib/chat/types.ts +11 -5
  210. package/src/lib/constants/known-tools.ts +19 -0
  211. package/src/lib/constants/prose-styles.ts +1 -1
  212. package/src/lib/constants/settings.ts +7 -0
  213. package/src/lib/data/channel-bindings.ts +85 -0
  214. package/src/lib/data/clear.ts +22 -0
  215. package/src/lib/data/profile-test-results.ts +48 -0
  216. package/src/lib/data/seed-data/conversations.ts +196 -0
  217. package/src/lib/data/seed-data/learned-context.ts +99 -0
  218. package/src/lib/data/seed-data/notifications.ts +54 -1
  219. package/src/lib/data/seed-data/profile-test-results.ts +96 -0
  220. package/src/lib/data/seed-data/repo-imports.ts +51 -0
  221. package/src/lib/data/seed-data/views.ts +60 -0
  222. package/src/lib/data/seed.ts +51 -0
  223. package/src/lib/db/bootstrap.ts +162 -0
  224. package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
  225. package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
  226. package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
  227. package/src/lib/db/schema.ts +190 -1
  228. package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
  229. package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
  230. package/src/lib/environment/auto-scan.ts +48 -0
  231. package/src/lib/environment/data.ts +25 -0
  232. package/src/lib/environment/profile-generator.ts +40 -10
  233. package/src/lib/environment/profile-linker.ts +143 -0
  234. package/src/lib/environment/profile-rules.ts +96 -0
  235. package/src/lib/import/dedup.ts +149 -0
  236. package/src/lib/import/format-adapter.ts +631 -0
  237. package/src/lib/import/github-api.ts +219 -0
  238. package/src/lib/import/repo-scanner.ts +251 -0
  239. package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
  240. package/src/lib/schedules/active-hours.ts +120 -0
  241. package/src/lib/schedules/heartbeat-parser.ts +224 -0
  242. package/src/lib/schedules/heartbeat-prompt.ts +153 -0
  243. package/src/lib/schedules/nlp-parser.ts +357 -0
  244. package/src/lib/schedules/scheduler.ts +218 -3
  245. package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
  246. package/src/lib/settings/helpers.ts +6 -0
  247. package/src/lib/settings/routing.ts +24 -0
  248. package/src/lib/settings/runtime-setup.ts +28 -1
  249. package/src/lib/usage/ledger.ts +2 -1
  250. package/src/lib/validators/__tests__/settings.test.ts +9 -0
  251. package/src/lib/validators/profile.ts +39 -0
  252. package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
  253. package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
  254. package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
  255. package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
  256. package/src/lib/workflows/blueprints/builtins/lead-research-pipeline.yaml +82 -0
@@ -0,0 +1,648 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import {
5
+ Github,
6
+ Loader2,
7
+ Search,
8
+ Check,
9
+ AlertTriangle,
10
+ ChevronRight,
11
+ ChevronLeft,
12
+ Download,
13
+ Package,
14
+ ExternalLink,
15
+ } from "lucide-react";
16
+ import { Button } from "@/components/ui/button";
17
+ import { Input } from "@/components/ui/input";
18
+ import { Label } from "@/components/ui/label";
19
+ import { Badge } from "@/components/ui/badge";
20
+ import { Checkbox } from "@/components/ui/checkbox";
21
+ import {
22
+ Dialog,
23
+ DialogContent,
24
+ DialogDescription,
25
+ DialogFooter,
26
+ DialogHeader,
27
+ DialogTitle,
28
+ } from "@/components/ui/dialog";
29
+ import {
30
+ Select,
31
+ SelectContent,
32
+ SelectItem,
33
+ SelectTrigger,
34
+ SelectValue,
35
+ } from "@/components/ui/select";
36
+ import { toast } from "sonner";
37
+
38
+ interface DiscoveredSkill {
39
+ name: string;
40
+ path: string;
41
+ format: "stagent" | "skillmd-only" | "unknown";
42
+ hasProfileYaml: boolean;
43
+ hasSkillMd: boolean;
44
+ hasSkillMdTmpl: boolean;
45
+ hasReadme: boolean;
46
+ description: string;
47
+ frontmatter: Record<string, string>;
48
+ }
49
+
50
+ interface ScanResult {
51
+ owner: string;
52
+ repo: string;
53
+ branch: string;
54
+ commitSha: string;
55
+ repoReadme: string;
56
+ discoveredSkills: DiscoveredSkill[];
57
+ scanDurationMs: number;
58
+ }
59
+
60
+ interface DedupInfo {
61
+ status: "new" | "exact-match" | "near-match";
62
+ matchReason?: string;
63
+ similarity?: number;
64
+ matchedProfileId?: string;
65
+ matchedProfileName?: string;
66
+ }
67
+
68
+ interface PreviewItem {
69
+ skill: DiscoveredSkill;
70
+ config: {
71
+ id: string;
72
+ name: string;
73
+ version: string;
74
+ domain: string;
75
+ tags: string[];
76
+ author?: string;
77
+ allowedTools?: string[];
78
+ importMeta?: Record<string, string>;
79
+ } | null;
80
+ skillMd: string | null;
81
+ dedup: DedupInfo | null;
82
+ error: string | null;
83
+ }
84
+
85
+ type ImportAction = "import" | "replace" | "copy" | "skip";
86
+
87
+ interface RepoImportWizardProps {
88
+ open: boolean;
89
+ onOpenChange: (open: boolean) => void;
90
+ onImported: () => void;
91
+ }
92
+
93
+ export function RepoImportWizard({
94
+ open,
95
+ onOpenChange,
96
+ onImported,
97
+ }: RepoImportWizardProps) {
98
+ const [step, setStep] = useState(1);
99
+ const [url, setUrl] = useState("");
100
+ const [loading, setLoading] = useState(false);
101
+ const [error, setError] = useState<string | null>(null);
102
+
103
+ // Step 1 result
104
+ const [scanResult, setScanResult] = useState<ScanResult | null>(null);
105
+
106
+ // Step 2 state
107
+ const [selectedPaths, setSelectedPaths] = useState<Set<string>>(new Set());
108
+
109
+ // Step 3 state
110
+ const [previews, setPreviews] = useState<PreviewItem[]>([]);
111
+ const [actions, setActions] = useState<Map<string, ImportAction>>(new Map());
112
+
113
+ // Step 4 state
114
+ const [importResult, setImportResult] = useState<{
115
+ imported: number;
116
+ replaced: number;
117
+ skipped: number;
118
+ profileIds: string[];
119
+ errors?: string[];
120
+ } | null>(null);
121
+
122
+ function reset() {
123
+ setStep(1);
124
+ setUrl("");
125
+ setLoading(false);
126
+ setError(null);
127
+ setScanResult(null);
128
+ setSelectedPaths(new Set());
129
+ setPreviews([]);
130
+ setActions(new Map());
131
+ setImportResult(null);
132
+ }
133
+
134
+ // Step 1: Scan repo
135
+ async function handleScan() {
136
+ if (!url.trim()) return;
137
+ setLoading(true);
138
+ setError(null);
139
+
140
+ try {
141
+ const res = await fetch("/api/profiles/import-repo/scan", {
142
+ method: "POST",
143
+ headers: { "Content-Type": "application/json" },
144
+ body: JSON.stringify({ url: url.trim() }),
145
+ });
146
+ const data = await res.json();
147
+
148
+ if (!res.ok) {
149
+ setError(data.error || "Scan failed");
150
+ return;
151
+ }
152
+
153
+ setScanResult(data);
154
+ // Auto-select all skills
155
+ setSelectedPaths(new Set(data.discoveredSkills.map((s: DiscoveredSkill) => s.path)));
156
+ setStep(2);
157
+ } catch {
158
+ setError("Failed to connect to scan API");
159
+ } finally {
160
+ setLoading(false);
161
+ }
162
+ }
163
+
164
+ // Step 2 → 3: Preview selected skills
165
+ async function handlePreview() {
166
+ if (!scanResult || selectedPaths.size === 0) return;
167
+ setLoading(true);
168
+ setError(null);
169
+
170
+ try {
171
+ const selectedSkills = scanResult.discoveredSkills.filter((s) =>
172
+ selectedPaths.has(s.path)
173
+ );
174
+
175
+ const res = await fetch("/api/profiles/import-repo/preview", {
176
+ method: "POST",
177
+ headers: { "Content-Type": "application/json" },
178
+ body: JSON.stringify({
179
+ owner: scanResult.owner,
180
+ repo: scanResult.repo,
181
+ branch: scanResult.branch,
182
+ commitSha: scanResult.commitSha,
183
+ repoUrl: url.trim(),
184
+ repoReadme: scanResult.repoReadme,
185
+ skills: selectedSkills,
186
+ }),
187
+ });
188
+ const data = await res.json();
189
+
190
+ if (!res.ok) {
191
+ setError(data.error || "Preview failed");
192
+ return;
193
+ }
194
+
195
+ setPreviews(data.previews);
196
+ // Set default actions based on dedup status
197
+ const newActions = new Map<string, ImportAction>();
198
+ for (const preview of data.previews as PreviewItem[]) {
199
+ if (!preview.config) continue;
200
+ const id = preview.config.id;
201
+ if (preview.dedup?.status === "exact-match") {
202
+ newActions.set(id, "skip");
203
+ } else if (preview.dedup?.status === "near-match") {
204
+ newActions.set(id, "import");
205
+ } else {
206
+ newActions.set(id, "import");
207
+ }
208
+ }
209
+ setActions(newActions);
210
+ setStep(3);
211
+ } catch {
212
+ setError("Failed to fetch preview data");
213
+ } finally {
214
+ setLoading(false);
215
+ }
216
+ }
217
+
218
+ // Step 3 → 4: Confirm import
219
+ async function handleConfirm() {
220
+ if (!scanResult || previews.length === 0) return;
221
+ setLoading(true);
222
+ setError(null);
223
+
224
+ try {
225
+ const imports = previews
226
+ .filter((p) => p.config && p.skillMd)
227
+ .map((p) => {
228
+ const action = actions.get(p.config!.id) ?? "skip";
229
+ const config = { ...p.config! };
230
+
231
+ // For "copy" action, append suffix to avoid ID collision
232
+ if (action === "copy") {
233
+ config.id = `${config.id}-imported`;
234
+ config.name = `${config.name} (Imported)`;
235
+ }
236
+
237
+ return {
238
+ config,
239
+ skillMd: p.skillMd!,
240
+ action: action === "copy" ? "import" : action,
241
+ };
242
+ });
243
+
244
+ const res = await fetch("/api/profiles/import-repo/confirm", {
245
+ method: "POST",
246
+ headers: { "Content-Type": "application/json" },
247
+ body: JSON.stringify({
248
+ repoUrl: url.trim(),
249
+ owner: scanResult.owner,
250
+ repo: scanResult.repo,
251
+ branch: scanResult.branch,
252
+ commitSha: scanResult.commitSha,
253
+ imports,
254
+ }),
255
+ });
256
+ const data = await res.json();
257
+
258
+ if (!res.ok) {
259
+ setError(data.error || "Import failed");
260
+ return;
261
+ }
262
+
263
+ setImportResult(data);
264
+ setStep(4);
265
+ toast.success(`Imported ${data.imported + data.replaced} profiles`);
266
+ } catch {
267
+ setError("Failed to execute import");
268
+ } finally {
269
+ setLoading(false);
270
+ }
271
+ }
272
+
273
+ function toggleAll(checked: boolean) {
274
+ if (!scanResult) return;
275
+ if (checked) {
276
+ setSelectedPaths(new Set(scanResult.discoveredSkills.map((s) => s.path)));
277
+ } else {
278
+ setSelectedPaths(new Set());
279
+ }
280
+ }
281
+
282
+ function toggleSkill(path: string) {
283
+ setSelectedPaths((prev) => {
284
+ const next = new Set(prev);
285
+ if (next.has(path)) next.delete(path);
286
+ else next.add(path);
287
+ return next;
288
+ });
289
+ }
290
+
291
+ const actionCounts = (() => {
292
+ let imp = 0, rep = 0, skip = 0;
293
+ for (const [, action] of actions) {
294
+ if (action === "import" || action === "copy") imp++;
295
+ else if (action === "replace") rep++;
296
+ else skip++;
297
+ }
298
+ return { import: imp, replace: rep, skip };
299
+ })();
300
+
301
+ return (
302
+ <Dialog
303
+ open={open}
304
+ onOpenChange={(v) => {
305
+ if (!v) reset();
306
+ onOpenChange(v);
307
+ }}
308
+ >
309
+ <DialogContent className="sm:max-w-2xl max-h-[85vh] flex flex-col">
310
+ <DialogHeader>
311
+ <DialogTitle className="flex items-center gap-2">
312
+ <Package className="h-5 w-5" />
313
+ Import from Repository
314
+ </DialogTitle>
315
+ <DialogDescription>
316
+ {step === 1 && "Enter a GitHub repository URL to discover importable skills."}
317
+ {step === 2 && `Found ${scanResult?.discoveredSkills.length ?? 0} skills. Select which to import.`}
318
+ {step === 3 && "Review each skill and choose an import action."}
319
+ {step === 4 && "Import complete."}
320
+ </DialogDescription>
321
+ </DialogHeader>
322
+
323
+ {/* Step indicator */}
324
+ <div className="flex items-center gap-1 px-1">
325
+ {[1, 2, 3, 4].map((s) => (
326
+ <div
327
+ key={s}
328
+ className={`h-1 flex-1 rounded-full transition-colors ${
329
+ s <= step ? "bg-primary" : "bg-muted"
330
+ }`}
331
+ />
332
+ ))}
333
+ </div>
334
+
335
+ <div className="flex-1 overflow-y-auto space-y-4 py-2 min-h-0">
336
+ {/* Step 1: Enter URL */}
337
+ {step === 1 && (
338
+ <div className="space-y-4">
339
+ <div className="space-y-2">
340
+ <Label htmlFor="repo-url" className="flex items-center gap-1.5">
341
+ <Github className="h-3.5 w-3.5 text-muted-foreground" />
342
+ GitHub Repository URL
343
+ </Label>
344
+ <Input
345
+ id="repo-url"
346
+ placeholder="https://github.com/owner/repo"
347
+ value={url}
348
+ onChange={(e) => setUrl(e.target.value)}
349
+ onKeyDown={(e) => {
350
+ if (e.key === "Enter" && !loading) handleScan();
351
+ }}
352
+ />
353
+ <p className="text-xs text-muted-foreground">
354
+ Stagent will scan for all directories containing SKILL.md files.
355
+ </p>
356
+ </div>
357
+ </div>
358
+ )}
359
+
360
+ {/* Step 2: Select skills */}
361
+ {step === 2 && scanResult && (
362
+ <div className="space-y-3">
363
+ <div className="flex items-center justify-between">
364
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
365
+ <Github className="h-4 w-4" />
366
+ <span className="font-medium text-foreground">
367
+ {scanResult.owner}/{scanResult.repo}
368
+ </span>
369
+ <Badge variant="outline" className="text-xs">
370
+ {scanResult.branch}
371
+ </Badge>
372
+ </div>
373
+ <div className="flex items-center gap-2">
374
+ <Checkbox
375
+ id="select-all"
376
+ checked={selectedPaths.size === scanResult.discoveredSkills.length}
377
+ onCheckedChange={(checked) => toggleAll(!!checked)}
378
+ />
379
+ <Label htmlFor="select-all" className="text-xs">
380
+ Select all ({scanResult.discoveredSkills.length})
381
+ </Label>
382
+ </div>
383
+ </div>
384
+
385
+ <div className="space-y-1.5 max-h-[40vh] overflow-y-auto">
386
+ {scanResult.discoveredSkills.map((skill) => (
387
+ <label
388
+ key={skill.path}
389
+ className="flex items-start gap-3 surface-card-muted rounded-lg border border-border/60 p-3 cursor-pointer hover:border-primary/40 transition-colors"
390
+ >
391
+ <Checkbox
392
+ checked={selectedPaths.has(skill.path)}
393
+ onCheckedChange={() => toggleSkill(skill.path)}
394
+ className="mt-0.5"
395
+ />
396
+ <div className="flex-1 min-w-0">
397
+ <div className="flex items-center gap-2">
398
+ <span className="text-sm font-medium">{skill.name}</span>
399
+ <Badge
400
+ variant={skill.format === "stagent" ? "default" : "secondary"}
401
+ className="text-[10px] px-1.5 py-0"
402
+ >
403
+ {skill.format === "stagent" ? "Stagent" : "SKILL.md"}
404
+ </Badge>
405
+ </div>
406
+ {skill.description && (
407
+ <p className="text-xs text-muted-foreground line-clamp-1 mt-0.5">
408
+ {skill.description}
409
+ </p>
410
+ )}
411
+ <p className="text-[10px] text-muted-foreground/70 mt-0.5">
412
+ {skill.path || "(root)"}
413
+ </p>
414
+ </div>
415
+ </label>
416
+ ))}
417
+ </div>
418
+ </div>
419
+ )}
420
+
421
+ {/* Step 3: Preview & Dedup */}
422
+ {step === 3 && (
423
+ <div className="space-y-2 max-h-[40vh] overflow-y-auto">
424
+ {previews.map((preview) => {
425
+ if (!preview.config) {
426
+ return (
427
+ <div
428
+ key={preview.skill.path}
429
+ className="surface-card-muted rounded-lg border border-destructive/30 p-3"
430
+ >
431
+ <span className="text-sm text-destructive">
432
+ {preview.skill.name}: {preview.error}
433
+ </span>
434
+ </div>
435
+ );
436
+ }
437
+
438
+ const id = preview.config.id;
439
+ const action = actions.get(id) ?? "skip";
440
+ const dedup = preview.dedup;
441
+
442
+ return (
443
+ <div
444
+ key={id}
445
+ className="surface-card-muted rounded-lg border border-border/60 p-3 space-y-2"
446
+ >
447
+ <div className="flex items-center justify-between gap-2">
448
+ <div className="flex items-center gap-2 min-w-0">
449
+ <span className="text-sm font-medium truncate">
450
+ {preview.config.name}
451
+ </span>
452
+ {dedup && (
453
+ <Badge
454
+ variant={
455
+ dedup.status === "new"
456
+ ? "default"
457
+ : dedup.status === "near-match"
458
+ ? "secondary"
459
+ : "destructive"
460
+ }
461
+ className="text-[10px] px-1.5 py-0 shrink-0"
462
+ >
463
+ {dedup.status === "new" && (
464
+ <>
465
+ <Check className="h-2.5 w-2.5 mr-0.5" />
466
+ New
467
+ </>
468
+ )}
469
+ {dedup.status === "near-match" && (
470
+ <>
471
+ <AlertTriangle className="h-2.5 w-2.5 mr-0.5" />
472
+ Similar
473
+ </>
474
+ )}
475
+ {dedup.status === "exact-match" && "Exists"}
476
+ </Badge>
477
+ )}
478
+ </div>
479
+ <Select
480
+ value={action}
481
+ onValueChange={(v) =>
482
+ setActions((prev) => new Map(prev).set(id, v as ImportAction))
483
+ }
484
+ >
485
+ <SelectTrigger className="h-7 w-[130px] text-xs">
486
+ <SelectValue />
487
+ </SelectTrigger>
488
+ <SelectContent>
489
+ <SelectItem value="import">Import</SelectItem>
490
+ <SelectItem value="replace">Replace</SelectItem>
491
+ <SelectItem value="copy">Import as copy</SelectItem>
492
+ <SelectItem value="skip">Skip</SelectItem>
493
+ </SelectContent>
494
+ </Select>
495
+ </div>
496
+
497
+ {dedup?.matchReason && (
498
+ <p className="text-xs text-muted-foreground">
499
+ {dedup.matchReason}
500
+ </p>
501
+ )}
502
+
503
+ <div className="flex flex-wrap gap-1">
504
+ <Badge variant="outline" className="text-[10px]">
505
+ {preview.config.domain}
506
+ </Badge>
507
+ {preview.config.tags?.slice(0, 4).map((tag) => (
508
+ <Badge key={tag} variant="outline" className="text-[10px]">
509
+ {tag}
510
+ </Badge>
511
+ ))}
512
+ {preview.config.allowedTools && (
513
+ <Badge variant="secondary" className="text-[10px]">
514
+ {preview.config.allowedTools.length} tools
515
+ </Badge>
516
+ )}
517
+ </div>
518
+ </div>
519
+ );
520
+ })}
521
+ </div>
522
+ )}
523
+
524
+ {/* Step 4: Results */}
525
+ {step === 4 && importResult && (
526
+ <div className="space-y-4">
527
+ <div className="surface-card rounded-lg border border-border/60 p-4 space-y-3">
528
+ <div className="grid grid-cols-3 gap-4 text-center">
529
+ <div>
530
+ <p className="text-2xl font-bold text-primary">{importResult.imported}</p>
531
+ <p className="text-xs text-muted-foreground">Imported</p>
532
+ </div>
533
+ <div>
534
+ <p className="text-2xl font-bold text-amber-500">{importResult.replaced}</p>
535
+ <p className="text-xs text-muted-foreground">Replaced</p>
536
+ </div>
537
+ <div>
538
+ <p className="text-2xl font-bold text-muted-foreground">{importResult.skipped}</p>
539
+ <p className="text-xs text-muted-foreground">Skipped</p>
540
+ </div>
541
+ </div>
542
+
543
+ {scanResult && (
544
+ <div className="flex items-center gap-2 pt-2 border-t border-border/60 text-xs text-muted-foreground">
545
+ <Github className="h-3.5 w-3.5" />
546
+ <span>
547
+ Imported from{" "}
548
+ <a
549
+ href={url}
550
+ target="_blank"
551
+ rel="noopener noreferrer"
552
+ className="text-primary hover:underline inline-flex items-center gap-0.5"
553
+ >
554
+ {scanResult.owner}/{scanResult.repo}
555
+ <ExternalLink className="h-2.5 w-2.5" />
556
+ </a>
557
+ {" "}at {scanResult.commitSha.slice(0, 7)}
558
+ </span>
559
+ </div>
560
+ )}
561
+ </div>
562
+
563
+ {importResult.errors && importResult.errors.length > 0 && (
564
+ <div className="surface-card-muted rounded-lg border border-destructive/30 p-3 space-y-1">
565
+ <p className="text-xs font-medium text-destructive">Errors:</p>
566
+ {importResult.errors.map((err, i) => (
567
+ <p key={i} className="text-xs text-muted-foreground">{err}</p>
568
+ ))}
569
+ </div>
570
+ )}
571
+ </div>
572
+ )}
573
+
574
+ {error && (
575
+ <p className="text-sm text-destructive">{error}</p>
576
+ )}
577
+ </div>
578
+
579
+ <DialogFooter className="gap-2">
580
+ {step > 1 && step < 4 && (
581
+ <Button
582
+ variant="outline"
583
+ onClick={() => setStep(step - 1)}
584
+ disabled={loading}
585
+ >
586
+ <ChevronLeft className="h-4 w-4 mr-1" />
587
+ Back
588
+ </Button>
589
+ )}
590
+
591
+ <div className="flex-1" />
592
+
593
+ {step === 1 && (
594
+ <>
595
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
596
+ Cancel
597
+ </Button>
598
+ <Button onClick={handleScan} disabled={loading || !url.trim()}>
599
+ {loading ? (
600
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
601
+ ) : (
602
+ <Search className="mr-2 h-4 w-4" />
603
+ )}
604
+ Scan Repository
605
+ </Button>
606
+ </>
607
+ )}
608
+
609
+ {step === 2 && (
610
+ <Button onClick={handlePreview} disabled={loading || selectedPaths.size === 0}>
611
+ {loading ? (
612
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
613
+ ) : (
614
+ <ChevronRight className="mr-2 h-4 w-4" />
615
+ )}
616
+ Preview ({selectedPaths.size})
617
+ </Button>
618
+ )}
619
+
620
+ {step === 3 && (
621
+ <Button onClick={handleConfirm} disabled={loading || actionCounts.import + actionCounts.replace === 0}>
622
+ {loading ? (
623
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
624
+ ) : (
625
+ <Download className="mr-2 h-4 w-4" />
626
+ )}
627
+ Import {actionCounts.import + actionCounts.replace} profile
628
+ {actionCounts.import + actionCounts.replace !== 1 ? "s" : ""}
629
+ {actionCounts.skip > 0 && ` (${actionCounts.skip} skipped)`}
630
+ </Button>
631
+ )}
632
+
633
+ {step === 4 && (
634
+ <Button
635
+ onClick={() => {
636
+ reset();
637
+ onOpenChange(false);
638
+ onImported();
639
+ }}
640
+ >
641
+ Done
642
+ </Button>
643
+ )}
644
+ </DialogFooter>
645
+ </DialogContent>
646
+ </Dialog>
647
+ );
648
+ }