stagent 0.5.0 → 0.6.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.
Files changed (252) 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 +103 -0
  38. package/src/app/api/channels/[id]/test/route.ts +52 -0
  39. package/src/app/api/channels/inbound/slack/route.ts +109 -0
  40. package/src/app/api/channels/inbound/telegram/poll/route.ts +128 -0
  41. package/src/app/api/channels/inbound/telegram/route.ts +76 -0
  42. package/src/app/api/channels/route.ts +71 -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/environment/profiles/suggest/route.ts +19 -3
  46. package/src/app/api/environment/scan/route.ts +8 -1
  47. package/src/app/api/handoffs/[id]/route.ts +76 -0
  48. package/src/app/api/handoffs/route.ts +89 -0
  49. package/src/app/api/memory/route.ts +181 -0
  50. package/src/app/api/profiles/[id]/route.ts +16 -1
  51. package/src/app/api/profiles/[id]/test/route.ts +4 -0
  52. package/src/app/api/profiles/[id]/test-results/route.ts +22 -0
  53. package/src/app/api/profiles/[id]/test-single/route.ts +64 -0
  54. package/src/app/api/profiles/assist/route.ts +35 -0
  55. package/src/app/api/profiles/import-repo/apply-updates/route.ts +123 -0
  56. package/src/app/api/profiles/import-repo/check-updates/route.ts +163 -0
  57. package/src/app/api/profiles/import-repo/confirm/route.ts +118 -0
  58. package/src/app/api/profiles/import-repo/preview/route.ts +107 -0
  59. package/src/app/api/profiles/import-repo/route.ts +29 -0
  60. package/src/app/api/profiles/import-repo/scan/route.ts +25 -0
  61. package/src/app/api/profiles/route.ts +73 -22
  62. package/src/app/api/runtimes/ollama/route.ts +86 -0
  63. package/src/app/api/runtimes/suggest/route.ts +29 -0
  64. package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
  65. package/src/app/api/schedules/[id]/route.ts +41 -3
  66. package/src/app/api/schedules/parse/route.ts +66 -0
  67. package/src/app/api/schedules/route.ts +71 -12
  68. package/src/app/api/settings/author-default/route.ts +7 -0
  69. package/src/app/api/settings/learning/route.ts +41 -0
  70. package/src/app/api/settings/ollama/route.ts +34 -0
  71. package/src/app/api/settings/providers/route.ts +57 -0
  72. package/src/app/api/settings/routing/route.ts +24 -0
  73. package/src/app/api/settings/web-search/route.ts +28 -0
  74. package/src/app/api/tasks/[id]/execute/route.ts +13 -1
  75. package/src/app/documents/page.tsx +3 -0
  76. package/src/app/environment/page.tsx +8 -1
  77. package/src/app/settings/page.tsx +10 -4
  78. package/src/app/workflows/[id]/edit/page.tsx +2 -0
  79. package/src/app/workflows/new/page.tsx +2 -0
  80. package/src/components/chat/chat-command-popover.tsx +22 -19
  81. package/src/components/chat/chat-input.tsx +5 -0
  82. package/src/components/chat/chat-model-selector.tsx +42 -1
  83. package/src/components/chat/chat-shell.tsx +2 -0
  84. package/src/components/dashboard/welcome-landing.tsx +9 -9
  85. package/src/components/environment/artifact-card.tsx +27 -1
  86. package/src/components/environment/environment-dashboard.tsx +50 -2
  87. package/src/components/environment/environment-summary-card.tsx +5 -2
  88. package/src/components/environment/suggested-profiles.tsx +117 -52
  89. package/src/components/handoffs/handoff-approval-card.tsx +159 -0
  90. package/src/components/memory/memory-browser.tsx +315 -0
  91. package/src/components/profiles/learned-context-panel.tsx +4 -4
  92. package/src/components/profiles/profile-assist-panel.tsx +512 -0
  93. package/src/components/profiles/profile-browser.tsx +109 -8
  94. package/src/components/profiles/profile-card.tsx +29 -1
  95. package/src/components/profiles/profile-detail-view.tsx +200 -28
  96. package/src/components/profiles/profile-form-view.tsx +220 -82
  97. package/src/components/profiles/repo-import-wizard.tsx +648 -0
  98. package/src/components/profiles/smoke-test-editor.tsx +106 -0
  99. package/src/components/schedules/schedule-create-sheet.tsx +9 -1
  100. package/src/components/schedules/schedule-form.tsx +348 -9
  101. package/src/components/schedules/schedule-list.tsx +15 -2
  102. package/src/components/settings/auth-method-selector.tsx +7 -1
  103. package/src/components/settings/budget-guardrails-section.tsx +111 -48
  104. package/src/components/settings/channels-section.tsx +526 -0
  105. package/src/components/settings/chat-settings-section.tsx +27 -1
  106. package/src/components/settings/data-management-section.tsx +8 -6
  107. package/src/components/settings/learning-context-section.tsx +124 -0
  108. package/src/components/settings/ollama-section.tsx +270 -0
  109. package/src/components/settings/providers-runtimes-section.tsx +499 -0
  110. package/src/components/settings/web-search-section.tsx +101 -0
  111. package/src/components/shared/tag-input.tsx +156 -0
  112. package/src/components/tasks/kanban-board.tsx +32 -0
  113. package/src/components/tasks/kanban-column.tsx +4 -2
  114. package/src/components/tasks/task-card.tsx +1 -0
  115. package/src/components/tasks/task-chip-bar.tsx +6 -1
  116. package/src/components/tasks/task-create-panel.tsx +55 -5
  117. package/src/components/workflows/workflow-form-view.tsx +38 -3
  118. package/src/hooks/use-chat-autocomplete.ts +24 -26
  119. package/src/hooks/use-project-skills.ts +66 -0
  120. package/src/hooks/use-tag-suggestions.ts +31 -0
  121. package/src/instrumentation.ts +4 -1
  122. package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
  123. package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
  124. package/src/lib/agents/agentic-loop.ts +235 -0
  125. package/src/lib/agents/browser-mcp.ts +59 -4
  126. package/src/lib/agents/claude-agent.ts +26 -199
  127. package/src/lib/agents/handoff/bus.ts +164 -0
  128. package/src/lib/agents/handoff/governance.ts +47 -0
  129. package/src/lib/agents/handoff/types.ts +16 -0
  130. package/src/lib/agents/learned-context.ts +27 -7
  131. package/src/lib/agents/memory/decay.ts +61 -0
  132. package/src/lib/agents/memory/extractor.ts +181 -0
  133. package/src/lib/agents/memory/retrieval.ts +96 -0
  134. package/src/lib/agents/memory/types.ts +6 -0
  135. package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
  136. package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
  137. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
  138. package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
  139. package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
  140. package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
  141. package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
  142. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
  143. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
  144. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
  145. package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
  146. package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
  147. package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
  148. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
  149. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
  150. package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
  151. package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
  152. package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
  153. package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
  154. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
  155. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
  156. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
  157. package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
  158. package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
  159. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
  160. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
  161. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
  162. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
  163. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
  164. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
  165. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
  166. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
  167. package/src/lib/agents/profiles/project-profiles.ts +193 -0
  168. package/src/lib/agents/profiles/registry.ts +130 -6
  169. package/src/lib/agents/profiles/types.ts +28 -0
  170. package/src/lib/agents/router.ts +174 -2
  171. package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
  172. package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
  173. package/src/lib/agents/runtime/catalog.ts +57 -2
  174. package/src/lib/agents/runtime/claude.ts +205 -1
  175. package/src/lib/agents/runtime/index.ts +22 -0
  176. package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
  177. package/src/lib/agents/runtime/openai-direct.ts +514 -0
  178. package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
  179. package/src/lib/agents/runtime/types.ts +2 -0
  180. package/src/lib/agents/tool-permissions.ts +203 -0
  181. package/src/lib/channels/gateway.ts +321 -0
  182. package/src/lib/channels/poller.ts +268 -0
  183. package/src/lib/channels/registry.ts +90 -0
  184. package/src/lib/channels/slack-adapter.ts +188 -0
  185. package/src/lib/channels/telegram-adapter.ts +218 -0
  186. package/src/lib/channels/types.ts +43 -0
  187. package/src/lib/channels/webhook-adapter.ts +74 -0
  188. package/src/lib/chat/context-builder.ts +22 -2
  189. package/src/lib/chat/engine.ts +95 -13
  190. package/src/lib/chat/ollama-engine.ts +198 -0
  191. package/src/lib/chat/stagent-tools.ts +106 -20
  192. package/src/lib/chat/tool-catalog.ts +24 -0
  193. package/src/lib/chat/tool-registry.ts +90 -0
  194. package/src/lib/chat/tools/chat-history-tools.ts +4 -4
  195. package/src/lib/chat/tools/document-tools.ts +7 -7
  196. package/src/lib/chat/tools/handoff-tools.ts +70 -0
  197. package/src/lib/chat/tools/notification-tools.ts +4 -4
  198. package/src/lib/chat/tools/profile-tools.ts +3 -3
  199. package/src/lib/chat/tools/project-tools.ts +3 -3
  200. package/src/lib/chat/tools/schedule-tools.ts +29 -13
  201. package/src/lib/chat/tools/settings-tools.ts +2 -2
  202. package/src/lib/chat/tools/task-tools.ts +66 -11
  203. package/src/lib/chat/tools/usage-tools.ts +2 -2
  204. package/src/lib/chat/tools/workflow-tools.ts +8 -8
  205. package/src/lib/chat/types.ts +11 -5
  206. package/src/lib/constants/known-tools.ts +19 -0
  207. package/src/lib/constants/prose-styles.ts +1 -1
  208. package/src/lib/constants/settings.ts +7 -0
  209. package/src/lib/data/channel-bindings.ts +85 -0
  210. package/src/lib/data/clear.ts +22 -0
  211. package/src/lib/data/profile-test-results.ts +48 -0
  212. package/src/lib/data/seed-data/conversations.ts +196 -0
  213. package/src/lib/data/seed-data/learned-context.ts +99 -0
  214. package/src/lib/data/seed-data/notifications.ts +54 -1
  215. package/src/lib/data/seed-data/profile-test-results.ts +96 -0
  216. package/src/lib/data/seed-data/repo-imports.ts +51 -0
  217. package/src/lib/data/seed-data/views.ts +60 -0
  218. package/src/lib/data/seed.ts +51 -0
  219. package/src/lib/db/bootstrap.ts +162 -0
  220. package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
  221. package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
  222. package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
  223. package/src/lib/db/schema.ts +187 -1
  224. package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
  225. package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
  226. package/src/lib/environment/auto-scan.ts +48 -0
  227. package/src/lib/environment/data.ts +25 -0
  228. package/src/lib/environment/profile-generator.ts +40 -10
  229. package/src/lib/environment/profile-linker.ts +143 -0
  230. package/src/lib/environment/profile-rules.ts +96 -0
  231. package/src/lib/import/dedup.ts +149 -0
  232. package/src/lib/import/format-adapter.ts +631 -0
  233. package/src/lib/import/github-api.ts +219 -0
  234. package/src/lib/import/repo-scanner.ts +251 -0
  235. package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
  236. package/src/lib/schedules/active-hours.ts +120 -0
  237. package/src/lib/schedules/heartbeat-parser.ts +224 -0
  238. package/src/lib/schedules/heartbeat-prompt.ts +153 -0
  239. package/src/lib/schedules/nlp-parser.ts +357 -0
  240. package/src/lib/schedules/scheduler.ts +218 -3
  241. package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
  242. package/src/lib/settings/helpers.ts +6 -0
  243. package/src/lib/settings/routing.ts +24 -0
  244. package/src/lib/settings/runtime-setup.ts +28 -1
  245. package/src/lib/usage/ledger.ts +2 -1
  246. package/src/lib/validators/__tests__/settings.test.ts +9 -0
  247. package/src/lib/validators/profile.ts +39 -0
  248. package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
  249. package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
  250. package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
  251. package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
  252. package/src/lib/workflows/blueprints/builtins/lead-research-pipeline.yaml +82 -0
@@ -0,0 +1,218 @@
1
+ import type { ChannelAdapter, ChannelMessage, ChannelDeliveryResult, InboundMessage } from "./types";
2
+
3
+ /**
4
+ * Escape special characters for Telegram MarkdownV2 format.
5
+ */
6
+ function escapeMarkdownV2(text: string): string {
7
+ return text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, "\\$&");
8
+ }
9
+
10
+ /**
11
+ * Convert basic Markdown to Telegram MarkdownV2.
12
+ * We escape special chars first, then re-apply bold/code formatting.
13
+ */
14
+ function toTelegramMarkdownV2(subject: string, body: string): string {
15
+ const escapedSubject = escapeMarkdownV2(subject);
16
+ const escapedBody = escapeMarkdownV2(body);
17
+ return `*${escapedSubject}*\n\n${escapedBody}`;
18
+ }
19
+
20
+ export const telegramAdapter: ChannelAdapter = {
21
+ channelType: "telegram",
22
+
23
+ async send(message: ChannelMessage, config: Record<string, unknown>): Promise<ChannelDeliveryResult> {
24
+ const botToken = config.botToken as string;
25
+ const chatId = config.chatId as string;
26
+
27
+ if (!botToken || !chatId) {
28
+ return { success: false, error: "Missing botToken or chatId in config" };
29
+ }
30
+
31
+ const text = message.format === "markdown"
32
+ ? toTelegramMarkdownV2(message.subject, message.body)
33
+ : `${message.subject}\n\n${message.body}`;
34
+
35
+ const parseMode = message.format === "markdown" ? "MarkdownV2" : undefined;
36
+
37
+ try {
38
+ const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
39
+ const res = await fetch(url, {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/json" },
42
+ body: JSON.stringify({
43
+ chat_id: chatId,
44
+ text,
45
+ ...(parseMode ? { parse_mode: parseMode } : {}),
46
+ }),
47
+ });
48
+
49
+ const data = await res.json() as { ok: boolean; result?: { message_id: number }; description?: string };
50
+
51
+ if (!data.ok) {
52
+ return { success: false, error: data.description ?? `Telegram API error (${res.status})` };
53
+ }
54
+
55
+ return { success: true, externalId: String(data.result?.message_id) };
56
+ } catch (err) {
57
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
58
+ }
59
+ },
60
+
61
+ async testConnection(config: Record<string, unknown>): Promise<{ ok: boolean; error?: string }> {
62
+ const botToken = config.botToken as string;
63
+ const chatId = config.chatId as string;
64
+ if (!botToken || !chatId) {
65
+ return { ok: false, error: "Missing botToken or chatId" };
66
+ }
67
+
68
+ try {
69
+ const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
70
+ const res = await fetch(url, {
71
+ method: "POST",
72
+ headers: { "Content-Type": "application/json" },
73
+ body: JSON.stringify({
74
+ chat_id: chatId,
75
+ text: "Stagent channel test \\- connection OK",
76
+ parse_mode: "MarkdownV2",
77
+ }),
78
+ });
79
+ const data = await res.json() as { ok: boolean; description?: string };
80
+
81
+ if (!data.ok) {
82
+ return { ok: false, error: data.description ?? "Telegram sendMessage failed" };
83
+ }
84
+
85
+ return { ok: true };
86
+ } catch (err) {
87
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
88
+ }
89
+ },
90
+
91
+ // ── Bidirectional support ────────────────────────────────────────────
92
+
93
+ parseInbound(rawBody: unknown): InboundMessage | null {
94
+ const update = rawBody as TelegramUpdate;
95
+ const msg = update?.message;
96
+ if (!msg?.text) return null;
97
+
98
+ return {
99
+ text: msg.text,
100
+ senderName: msg.from?.first_name
101
+ ? `${msg.from.first_name}${msg.from.last_name ? ` ${msg.from.last_name}` : ""}`
102
+ : undefined,
103
+ senderId: msg.from?.id ? String(msg.from.id) : undefined,
104
+ externalThreadId: String(msg.chat.id),
105
+ externalMessageId: String(msg.message_id),
106
+ isBot: msg.from?.is_bot ?? false,
107
+ raw: rawBody,
108
+ };
109
+ },
110
+
111
+ async sendReply(
112
+ message: ChannelMessage,
113
+ config: Record<string, unknown>,
114
+ threadId?: string
115
+ ): Promise<ChannelDeliveryResult> {
116
+ const botToken = config.botToken as string;
117
+ const chatId = threadId ?? (config.chatId as string);
118
+
119
+ if (!botToken || !chatId) {
120
+ return { success: false, error: "Missing botToken or chatId for reply" };
121
+ }
122
+
123
+ // For replies, send body directly (subject is empty in gateway context)
124
+ const text = message.body || message.subject;
125
+ if (!text) {
126
+ return { success: false, error: "Empty message body" };
127
+ }
128
+
129
+ try {
130
+ const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
131
+ const res = await fetch(url, {
132
+ method: "POST",
133
+ headers: { "Content-Type": "application/json" },
134
+ body: JSON.stringify({
135
+ chat_id: chatId,
136
+ text,
137
+ // Skip MarkdownV2 for replies — raw text avoids escape issues with agent output
138
+ }),
139
+ });
140
+
141
+ const data = await res.json() as { ok: boolean; result?: { message_id: number }; description?: string };
142
+
143
+ if (!data.ok) {
144
+ return { success: false, error: data.description ?? `Telegram API error (${res.status})` };
145
+ }
146
+
147
+ return { success: true, externalId: String(data.result?.message_id) };
148
+ } catch (err) {
149
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
150
+ }
151
+ },
152
+ };
153
+
154
+ // ── Telegram types (minimal subset) ────────────────────────────────────
155
+
156
+ interface TelegramUpdate {
157
+ update_id: number;
158
+ message?: {
159
+ message_id: number;
160
+ from?: {
161
+ id: number;
162
+ is_bot: boolean;
163
+ first_name: string;
164
+ last_name?: string;
165
+ username?: string;
166
+ };
167
+ chat: {
168
+ id: number;
169
+ type: string;
170
+ };
171
+ date: number;
172
+ text?: string;
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Register a Telegram webhook for bidirectional mode.
178
+ * Call this when a channel config is set to direction="bidirectional".
179
+ */
180
+ export async function registerTelegramWebhook(
181
+ botToken: string,
182
+ webhookUrl: string
183
+ ): Promise<{ ok: boolean; error?: string }> {
184
+ try {
185
+ const url = `https://api.telegram.org/bot${botToken}/setWebhook`;
186
+ const res = await fetch(url, {
187
+ method: "POST",
188
+ headers: { "Content-Type": "application/json" },
189
+ body: JSON.stringify({ url: webhookUrl }),
190
+ });
191
+ const data = await res.json() as { ok: boolean; description?: string };
192
+ if (!data.ok) {
193
+ return { ok: false, error: data.description ?? "setWebhook failed" };
194
+ }
195
+ return { ok: true };
196
+ } catch (err) {
197
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Remove a Telegram webhook (revert to outbound-only).
203
+ */
204
+ export async function removeTelegramWebhook(
205
+ botToken: string
206
+ ): Promise<{ ok: boolean; error?: string }> {
207
+ try {
208
+ const url = `https://api.telegram.org/bot${botToken}/deleteWebhook`;
209
+ const res = await fetch(url, { method: "POST" });
210
+ const data = await res.json() as { ok: boolean; description?: string };
211
+ if (!data.ok) {
212
+ return { ok: false, error: data.description ?? "deleteWebhook failed" };
213
+ }
214
+ return { ok: true };
215
+ } catch (err) {
216
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
217
+ }
218
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Channel adapter types for multi-channel delivery and bidirectional chat.
3
+ */
4
+
5
+ export interface ChannelAdapter {
6
+ channelType: string;
7
+ send(message: ChannelMessage, config: Record<string, unknown>): Promise<ChannelDeliveryResult>;
8
+ testConnection(config: Record<string, unknown>): Promise<{ ok: boolean; error?: string }>;
9
+
10
+ // ── Bidirectional support (optional) ────────────────────────────────
11
+ /** Parse an inbound webhook payload into a normalized message. */
12
+ parseInbound?(rawBody: unknown, headers: Record<string, string>): InboundMessage | null;
13
+ /** Verify webhook signature authenticity. */
14
+ verifySignature?(rawBody: string, headers: Record<string, string>, config: Record<string, unknown>): boolean;
15
+ /** Send a reply in-thread (distinct from fire-and-forget send). */
16
+ sendReply?(message: ChannelMessage, config: Record<string, unknown>, threadId?: string): Promise<ChannelDeliveryResult>;
17
+ }
18
+
19
+ export interface ChannelMessage {
20
+ subject: string;
21
+ body: string;
22
+ format: "text" | "markdown";
23
+ metadata?: Record<string, unknown>;
24
+ }
25
+
26
+ export interface ChannelDeliveryResult {
27
+ success: boolean;
28
+ channelId?: string;
29
+ externalId?: string;
30
+ error?: string;
31
+ }
32
+
33
+ /** Normalized inbound message from any channel. */
34
+ export interface InboundMessage {
35
+ text: string;
36
+ senderName?: string;
37
+ senderId?: string;
38
+ externalThreadId?: string;
39
+ externalMessageId?: string;
40
+ isBot?: boolean;
41
+ /** Raw channel-specific payload for adapter-level access. */
42
+ raw?: unknown;
43
+ }
@@ -0,0 +1,74 @@
1
+ import type { ChannelAdapter, ChannelMessage, ChannelDeliveryResult } from "./types";
2
+
3
+ export const webhookAdapter: ChannelAdapter = {
4
+ channelType: "webhook",
5
+
6
+ async send(message: ChannelMessage, config: Record<string, unknown>): Promise<ChannelDeliveryResult> {
7
+ const url = config.url as string;
8
+ if (!url) {
9
+ return { success: false, error: "Missing url in config" };
10
+ }
11
+
12
+ const customHeaders = (config.headers ?? {}) as Record<string, string>;
13
+
14
+ try {
15
+ const res = await fetch(url, {
16
+ method: "POST",
17
+ headers: {
18
+ "Content-Type": "application/json",
19
+ ...customHeaders,
20
+ },
21
+ body: JSON.stringify({
22
+ subject: message.subject,
23
+ body: message.body,
24
+ format: message.format,
25
+ metadata: message.metadata ?? {},
26
+ timestamp: new Date().toISOString(),
27
+ source: "stagent",
28
+ }),
29
+ });
30
+
31
+ if (!res.ok) {
32
+ const body = await res.text();
33
+ return { success: false, error: `Webhook returned ${res.status}: ${body.slice(0, 200)}` };
34
+ }
35
+
36
+ return { success: true };
37
+ } catch (err) {
38
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
39
+ }
40
+ },
41
+
42
+ async testConnection(config: Record<string, unknown>): Promise<{ ok: boolean; error?: string }> {
43
+ const url = config.url as string;
44
+ if (!url) {
45
+ return { ok: false, error: "Missing url" };
46
+ }
47
+
48
+ const customHeaders = (config.headers ?? {}) as Record<string, string>;
49
+
50
+ try {
51
+ const res = await fetch(url, {
52
+ method: "POST",
53
+ headers: {
54
+ "Content-Type": "application/json",
55
+ ...customHeaders,
56
+ },
57
+ body: JSON.stringify({
58
+ test: true,
59
+ source: "stagent",
60
+ timestamp: new Date().toISOString(),
61
+ }),
62
+ });
63
+
64
+ if (!res.ok) {
65
+ const body = await res.text();
66
+ return { ok: false, error: `Webhook returned ${res.status}: ${body.slice(0, 200)}` };
67
+ }
68
+
69
+ return { ok: true };
70
+ } catch (err) {
71
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
72
+ }
73
+ },
74
+ };
@@ -2,6 +2,7 @@ import { db } from "@/lib/db";
2
2
  import { projects, tasks, workflows, documents, schedules } from "@/lib/db/schema";
3
3
  import { eq, desc, and } from "drizzle-orm";
4
4
  import { getMessages } from "@/lib/data/chat";
5
+ import { getProfile } from "@/lib/agents/profiles/registry";
5
6
  import { STAGENT_SYSTEM_PROMPT } from "./system-prompt";
6
7
  import type { WorkspaceContext } from "@/lib/environment/workspace-context";
7
8
 
@@ -254,8 +255,27 @@ async function buildTier3(mentions: MentionReference[]): Promise<string> {
254
255
  break;
255
256
  }
256
257
  case "profile": {
257
- parts.push(`\n### Profile: ${mention.label}`);
258
- parts.push(`Profile ID: ${mention.entityId}`);
258
+ const profile = getProfile(mention.entityId);
259
+ if (profile) {
260
+ parts.push(`\n### Agent Profile: ${profile.name}`);
261
+ parts.push(`Domain: ${profile.domain}`);
262
+ if (profile.description) parts.push(`Description: ${profile.description}`);
263
+ if (profile.tags?.length) parts.push(`Tags: ${profile.tags.join(", ")}`);
264
+ if (profile.allowedTools?.length) parts.push(`Allowed Tools: ${profile.allowedTools.join(", ")}`);
265
+ if (profile.maxTurns) parts.push(`Max Turns: ${profile.maxTurns}`);
266
+ if (profile.outputFormat) parts.push(`Output Format: ${profile.outputFormat}`);
267
+ if (profile.skillMd) {
268
+ if (profile.skillMd.length <= 4000) {
269
+ parts.push(`\nProfile Instructions (SKILL.md):\n${profile.skillMd}`);
270
+ } else {
271
+ parts.push(`\nProfile Instructions (SKILL.md, preview):\n${profile.skillMd.slice(0, 3000)}`);
272
+ parts.push(`\n...(truncated — use the get_profile tool with profileId "${profile.id}" for full content)`);
273
+ }
274
+ }
275
+ } else {
276
+ parts.push(`\n### Profile: ${mention.label}`);
277
+ parts.push(`Profile ID: ${mention.entityId} (not found in registry)`);
278
+ }
259
279
  break;
260
280
  }
261
281
  }
@@ -43,8 +43,12 @@ import { createStagentMcpServer } from "./stagent-tools";
43
43
  import {
44
44
  getBrowserMcpServers,
45
45
  getBrowserAllowedToolPatterns,
46
+ getExternalMcpServers,
47
+ getExternalAllowedToolPatterns,
46
48
  isBrowserTool,
47
49
  isBrowserReadOnly,
50
+ isExaTool,
51
+ isExaReadOnly,
48
52
  } from "@/lib/agents/browser-mcp";
49
53
 
50
54
  // ── Streaming input wrapper (required for MCP tools) ─────────────────
@@ -58,6 +62,46 @@ async function* generatePrompt(text: string) {
58
62
  };
59
63
  }
60
64
 
65
+ // ── Error diagnostics ──────────────────────────────────────────────────
66
+
67
+ /**
68
+ * Translate a raw SDK / process error + stderr into an actionable message.
69
+ * The Claude Code subprocess can exit with code 1 for many reasons;
70
+ * stderr output usually reveals the real cause.
71
+ */
72
+ function diagnoseProcessError(rawMessage: string, stderr: string): string {
73
+ const combined = `${rawMessage}\n${stderr}`.toLowerCase();
74
+
75
+ if (combined.includes("authentication") || combined.includes("not logged in") || combined.includes("oauth") || combined.includes("token expired")) {
76
+ return "Authentication failed — please check your API key or run `claude login` to refresh OAuth tokens. (Settings → Authentication)";
77
+ }
78
+ if (combined.includes("rate limit") || combined.includes("rate_limit") || combined.includes("429")) {
79
+ return "Rate limit reached — please wait a moment before sending another message.";
80
+ }
81
+ if (combined.includes("billing") || combined.includes("insufficient") || combined.includes("payment")) {
82
+ return "Billing issue — your account may need a payment method or has exceeded its budget.";
83
+ }
84
+ if (combined.includes("enoent") || combined.includes("not found") || combined.includes("command not found")) {
85
+ return "Claude Code CLI not found — please install it with `npm install -g @anthropic-ai/claude-code`.";
86
+ }
87
+ if (combined.includes("model") && (combined.includes("not available") || combined.includes("invalid"))) {
88
+ return "The selected model is not available for your account. Try switching to a different model.";
89
+ }
90
+
91
+ // Generic process exit — append stderr hint if available
92
+ if (/process exited with code \d+/i.test(rawMessage)) {
93
+ if (stderr) {
94
+ // Extract last meaningful line from stderr
95
+ const lines = stderr.split("\n").filter((l) => l.trim());
96
+ const lastLine = lines[lines.length - 1] ?? "";
97
+ return `${rawMessage}${lastLine ? ` — ${lastLine}` : ""}. Check Settings → Authentication if this persists.`;
98
+ }
99
+ return `${rawMessage}. This usually means an authentication or configuration issue — check Settings → Authentication.`;
100
+ }
101
+
102
+ return rawMessage;
103
+ }
104
+
61
105
  // ── Public API ─────────────────────────────────────────────────────────
62
106
 
63
107
  /**
@@ -87,6 +131,13 @@ export async function* sendMessage(
87
131
  return;
88
132
  }
89
133
 
134
+ // Route to Ollama for local models
135
+ if (conversation.runtimeId === "ollama") {
136
+ const { sendOllamaMessage } = await import("./ollama-engine");
137
+ yield* sendOllamaMessage(conversationId, userContent, signal);
138
+ return;
139
+ }
140
+
90
141
  const runtimeId = conversation.runtimeId;
91
142
  const providerId = getProviderForRuntime(runtimeId);
92
143
 
@@ -181,6 +232,8 @@ export async function* sendMessage(
181
232
  const startedAt = new Date();
182
233
  let usage: UsageSnapshot = {};
183
234
  let fullText = "";
235
+ // Capture stderr for diagnostics when the Claude Code process fails
236
+ const stderrChunks: string[] = [];
184
237
 
185
238
  try {
186
239
  const authEnv = await getAuthEnv();
@@ -206,9 +259,14 @@ export async function* sendMessage(
206
259
  const maxTurnsSetting = await getSetting(SETTINGS_KEYS.MAX_TURNS);
207
260
  const maxTurns = maxTurnsSetting ? parseInt(maxTurnsSetting, 10) || 30 : 30;
208
261
 
209
- // Merge browser MCP servers when enabled in settings
210
- const browserServers = await getBrowserMcpServers();
211
- const browserToolPatterns = await getBrowserAllowedToolPatterns();
262
+ // Merge browser + external MCP servers when enabled in settings
263
+ const [browserServers, browserToolPatterns, externalServers, externalToolPatterns] =
264
+ await Promise.all([
265
+ getBrowserMcpServers(),
266
+ getBrowserAllowedToolPatterns(),
267
+ getExternalMcpServers(),
268
+ getExternalAllowedToolPatterns(),
269
+ ]);
212
270
 
213
271
  const response = query({
214
272
  prompt: generatePrompt(fullPrompt),
@@ -219,8 +277,13 @@ export async function* sendMessage(
219
277
  includePartialMessages: true,
220
278
  cwd: workspace.cwd,
221
279
  env: buildClaudeSdkEnv(authEnv),
222
- mcpServers: { stagent: stagentServer, ...browserServers },
223
- allowedTools: ["mcp__stagent__*", ...browserToolPatterns],
280
+ stderr: (data: string) => {
281
+ stderrChunks.push(data);
282
+ // Keep only last 50 chunks to avoid unbounded memory
283
+ if (stderrChunks.length > 50) stderrChunks.shift();
284
+ },
285
+ mcpServers: { stagent: stagentServer, ...browserServers, ...externalServers },
286
+ allowedTools: ["mcp__stagent__*", ...browserToolPatterns, ...externalToolPatterns],
224
287
  // @ts-expect-error Agent SDK canUseTool types are incomplete — our async handler is compatible at runtime
225
288
  canUseTool: async (
226
289
  toolName: string,
@@ -248,6 +311,17 @@ export async function* sendMessage(
248
311
  return { behavior: "allow", updatedInput: input };
249
312
  }
250
313
 
314
+ // Exa tools: auto-allow read-only (all Exa tools are read-only)
315
+ if (isExaTool(toolName) && isExaReadOnly(toolName)) {
316
+ const shortName = toolName.replace("mcp__exa__", "").replace(/_/g, " ");
317
+ emitSideChannelEvent(conversationId, {
318
+ type: "status",
319
+ phase: "tool_use",
320
+ message: `Exa: ${shortName}...`,
321
+ });
322
+ return { behavior: "allow", updatedInput: input };
323
+ }
324
+
251
325
  // Browser tools: auto-allow read-only, gate mutations
252
326
  if (isBrowserTool(toolName)) {
253
327
  if (isBrowserReadOnly(toolName)) {
@@ -419,19 +493,23 @@ export async function* sendMessage(
419
493
  }
420
494
  }
421
495
  }
422
- } else if (raw.type === "result" && "result" in raw) {
496
+ } else if (raw.type === "result") {
423
497
  if (raw.is_error && raw.subtype !== "error_max_turns") {
424
- throw new Error(
425
- typeof raw.result === "string"
426
- ? raw.result
427
- : "Agent SDK returned an error"
428
- );
498
+ // SDKResultError has `errors: string[]`; SDKResultSuccess has `result: string`
499
+ const errors = (raw as Record<string, unknown>).errors as string[] | undefined;
500
+ const result = (raw as Record<string, unknown>).result as string | undefined;
501
+ const errorDetail = errors?.length
502
+ ? errors.join("; ")
503
+ : typeof result === "string"
504
+ ? result
505
+ : "Agent SDK returned an error";
506
+ throw new Error(errorDetail);
429
507
  }
430
508
  // Only emit result text as fallback when streaming didn't deliver content.
431
509
  // When deltas were active, fullText is already complete — re-emitting
432
510
  // the result would duplicate the entire response.
433
511
  if (!hasStreamedDeltas || !fullText) {
434
- const result = raw.result;
512
+ const result = (raw as Record<string, unknown>).result;
435
513
  if (typeof result === "string" && result.length > 0) {
436
514
  if (result !== fullText) {
437
515
  const remainder = result.startsWith(fullText)
@@ -502,9 +580,13 @@ export async function* sendMessage(
502
580
  quickAccess,
503
581
  };
504
582
  } catch (error) {
505
- const errorMessage =
583
+ const rawMessage =
506
584
  error instanceof Error ? error.message : "Unknown error";
507
585
 
586
+ // Enrich the error with stderr diagnostics when available
587
+ const stderrTail = stderrChunks.join("").trim();
588
+ const errorMessage = diagnoseProcessError(rawMessage, stderrTail);
589
+
508
590
  if (fullText && fullText.length > 50) {
509
591
  // Substantial content was already streamed — complete gracefully with warning
510
592
  const warning = `\n\n---\n\n*Response may be incomplete: ${errorMessage}*`;