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.
- package/README.md +8 -8
- package/dist/cli.js +146 -2
- package/docs/.coverage-gaps.json +21 -0
- package/docs/.last-generated +1 -1
- package/docs/features/agent-intelligence.md +36 -14
- package/docs/features/chat.md +33 -56
- package/docs/features/cost-usage.md +14 -10
- package/docs/features/dashboard-kanban.md +30 -13
- package/docs/features/delivery-channels.md +198 -0
- package/docs/features/design-system.md +10 -10
- package/docs/features/documents.md +8 -8
- package/docs/features/home-workspace.md +20 -15
- package/docs/features/inbox-notifications.md +22 -10
- package/docs/features/keyboard-navigation.md +11 -11
- package/docs/features/monitoring.md +1 -1
- package/docs/features/playbook.md +30 -32
- package/docs/features/profiles.md +33 -11
- package/docs/features/projects.md +2 -2
- package/docs/features/provider-runtimes.md +58 -14
- package/docs/features/schedules.md +70 -40
- package/docs/features/settings.md +74 -46
- package/docs/features/shared-components.md +7 -15
- package/docs/features/tool-permissions.md +9 -9
- package/docs/features/workflows.md +32 -21
- package/docs/getting-started.md +33 -9
- package/docs/index.md +25 -16
- package/docs/journeys/developer.md +124 -207
- package/docs/journeys/personal-use.md +70 -79
- package/docs/journeys/power-user.md +107 -151
- package/docs/journeys/work-use.md +81 -113
- package/docs/manifest.json +77 -45
- package/docs/superpowers/plans/2026-03-30-finish-in-progress-features.md +547 -0
- package/docs/use-cases/agency-operator.md +84 -0
- package/docs/use-cases/solo-founder.md +75 -0
- package/docs/why-stagent.md +59 -0
- package/package.json +10 -3
- package/src/app/api/channels/[id]/route.ts +103 -0
- package/src/app/api/channels/[id]/test/route.ts +52 -0
- package/src/app/api/channels/inbound/slack/route.ts +109 -0
- package/src/app/api/channels/inbound/telegram/poll/route.ts +128 -0
- package/src/app/api/channels/inbound/telegram/route.ts +76 -0
- package/src/app/api/channels/route.ts +71 -0
- package/src/app/api/chat/conversations/route.ts +15 -0
- package/src/app/api/chat/entities/search/route.ts +46 -31
- package/src/app/api/environment/profiles/suggest/route.ts +19 -3
- package/src/app/api/environment/scan/route.ts +8 -1
- package/src/app/api/handoffs/[id]/route.ts +76 -0
- package/src/app/api/handoffs/route.ts +89 -0
- package/src/app/api/memory/route.ts +181 -0
- package/src/app/api/profiles/[id]/route.ts +16 -1
- package/src/app/api/profiles/[id]/test/route.ts +4 -0
- package/src/app/api/profiles/[id]/test-results/route.ts +22 -0
- package/src/app/api/profiles/[id]/test-single/route.ts +64 -0
- package/src/app/api/profiles/assist/route.ts +35 -0
- package/src/app/api/profiles/import-repo/apply-updates/route.ts +123 -0
- package/src/app/api/profiles/import-repo/check-updates/route.ts +163 -0
- package/src/app/api/profiles/import-repo/confirm/route.ts +118 -0
- package/src/app/api/profiles/import-repo/preview/route.ts +107 -0
- package/src/app/api/profiles/import-repo/route.ts +29 -0
- package/src/app/api/profiles/import-repo/scan/route.ts +25 -0
- package/src/app/api/profiles/route.ts +73 -22
- package/src/app/api/runtimes/ollama/route.ts +86 -0
- package/src/app/api/runtimes/suggest/route.ts +29 -0
- package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
- package/src/app/api/schedules/[id]/route.ts +41 -3
- package/src/app/api/schedules/parse/route.ts +66 -0
- package/src/app/api/schedules/route.ts +71 -12
- package/src/app/api/settings/author-default/route.ts +7 -0
- package/src/app/api/settings/learning/route.ts +41 -0
- package/src/app/api/settings/ollama/route.ts +34 -0
- package/src/app/api/settings/providers/route.ts +57 -0
- package/src/app/api/settings/routing/route.ts +24 -0
- package/src/app/api/settings/web-search/route.ts +28 -0
- package/src/app/api/tasks/[id]/execute/route.ts +13 -1
- package/src/app/documents/page.tsx +3 -0
- package/src/app/environment/page.tsx +8 -1
- package/src/app/settings/page.tsx +10 -4
- package/src/app/workflows/[id]/edit/page.tsx +2 -0
- package/src/app/workflows/new/page.tsx +2 -0
- package/src/components/chat/chat-command-popover.tsx +22 -19
- package/src/components/chat/chat-input.tsx +5 -0
- package/src/components/chat/chat-model-selector.tsx +42 -1
- package/src/components/chat/chat-shell.tsx +2 -0
- package/src/components/dashboard/welcome-landing.tsx +9 -9
- package/src/components/environment/artifact-card.tsx +27 -1
- package/src/components/environment/environment-dashboard.tsx +50 -2
- package/src/components/environment/environment-summary-card.tsx +5 -2
- package/src/components/environment/suggested-profiles.tsx +117 -52
- package/src/components/handoffs/handoff-approval-card.tsx +159 -0
- package/src/components/memory/memory-browser.tsx +315 -0
- package/src/components/profiles/learned-context-panel.tsx +4 -4
- package/src/components/profiles/profile-assist-panel.tsx +512 -0
- package/src/components/profiles/profile-browser.tsx +109 -8
- package/src/components/profiles/profile-card.tsx +29 -1
- package/src/components/profiles/profile-detail-view.tsx +200 -28
- package/src/components/profiles/profile-form-view.tsx +220 -82
- package/src/components/profiles/repo-import-wizard.tsx +648 -0
- package/src/components/profiles/smoke-test-editor.tsx +106 -0
- package/src/components/schedules/schedule-create-sheet.tsx +9 -1
- package/src/components/schedules/schedule-form.tsx +348 -9
- package/src/components/schedules/schedule-list.tsx +15 -2
- package/src/components/settings/auth-method-selector.tsx +7 -1
- package/src/components/settings/budget-guardrails-section.tsx +111 -48
- package/src/components/settings/channels-section.tsx +526 -0
- package/src/components/settings/chat-settings-section.tsx +27 -1
- package/src/components/settings/data-management-section.tsx +8 -6
- package/src/components/settings/learning-context-section.tsx +124 -0
- package/src/components/settings/ollama-section.tsx +270 -0
- package/src/components/settings/providers-runtimes-section.tsx +499 -0
- package/src/components/settings/web-search-section.tsx +101 -0
- package/src/components/shared/tag-input.tsx +156 -0
- package/src/components/tasks/kanban-board.tsx +32 -0
- package/src/components/tasks/kanban-column.tsx +4 -2
- package/src/components/tasks/task-card.tsx +1 -0
- package/src/components/tasks/task-chip-bar.tsx +6 -1
- package/src/components/tasks/task-create-panel.tsx +55 -5
- package/src/components/workflows/workflow-form-view.tsx +38 -3
- package/src/hooks/use-chat-autocomplete.ts +24 -26
- package/src/hooks/use-project-skills.ts +66 -0
- package/src/hooks/use-tag-suggestions.ts +31 -0
- package/src/instrumentation.ts +4 -1
- package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
- package/src/lib/agents/agentic-loop.ts +235 -0
- package/src/lib/agents/browser-mcp.ts +59 -4
- package/src/lib/agents/claude-agent.ts +26 -199
- package/src/lib/agents/handoff/bus.ts +164 -0
- package/src/lib/agents/handoff/governance.ts +47 -0
- package/src/lib/agents/handoff/types.ts +16 -0
- package/src/lib/agents/learned-context.ts +27 -7
- package/src/lib/agents/memory/decay.ts +61 -0
- package/src/lib/agents/memory/extractor.ts +181 -0
- package/src/lib/agents/memory/retrieval.ts +96 -0
- package/src/lib/agents/memory/types.ts +6 -0
- package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
- package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
- package/src/lib/agents/profiles/project-profiles.ts +193 -0
- package/src/lib/agents/profiles/registry.ts +130 -6
- package/src/lib/agents/profiles/types.ts +28 -0
- package/src/lib/agents/router.ts +174 -2
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
- package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
- package/src/lib/agents/runtime/catalog.ts +57 -2
- package/src/lib/agents/runtime/claude.ts +205 -1
- package/src/lib/agents/runtime/index.ts +22 -0
- package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
- package/src/lib/agents/runtime/openai-direct.ts +514 -0
- package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
- package/src/lib/agents/runtime/types.ts +2 -0
- package/src/lib/agents/tool-permissions.ts +203 -0
- package/src/lib/channels/gateway.ts +321 -0
- package/src/lib/channels/poller.ts +268 -0
- package/src/lib/channels/registry.ts +90 -0
- package/src/lib/channels/slack-adapter.ts +188 -0
- package/src/lib/channels/telegram-adapter.ts +218 -0
- package/src/lib/channels/types.ts +43 -0
- package/src/lib/channels/webhook-adapter.ts +74 -0
- package/src/lib/chat/context-builder.ts +22 -2
- package/src/lib/chat/engine.ts +95 -13
- package/src/lib/chat/ollama-engine.ts +198 -0
- package/src/lib/chat/stagent-tools.ts +106 -20
- package/src/lib/chat/tool-catalog.ts +24 -0
- package/src/lib/chat/tool-registry.ts +90 -0
- package/src/lib/chat/tools/chat-history-tools.ts +4 -4
- package/src/lib/chat/tools/document-tools.ts +7 -7
- package/src/lib/chat/tools/handoff-tools.ts +70 -0
- package/src/lib/chat/tools/notification-tools.ts +4 -4
- package/src/lib/chat/tools/profile-tools.ts +3 -3
- package/src/lib/chat/tools/project-tools.ts +3 -3
- package/src/lib/chat/tools/schedule-tools.ts +29 -13
- package/src/lib/chat/tools/settings-tools.ts +2 -2
- package/src/lib/chat/tools/task-tools.ts +66 -11
- package/src/lib/chat/tools/usage-tools.ts +2 -2
- package/src/lib/chat/tools/workflow-tools.ts +8 -8
- package/src/lib/chat/types.ts +11 -5
- package/src/lib/constants/known-tools.ts +19 -0
- package/src/lib/constants/prose-styles.ts +1 -1
- package/src/lib/constants/settings.ts +7 -0
- package/src/lib/data/channel-bindings.ts +85 -0
- package/src/lib/data/clear.ts +22 -0
- package/src/lib/data/profile-test-results.ts +48 -0
- package/src/lib/data/seed-data/conversations.ts +196 -0
- package/src/lib/data/seed-data/learned-context.ts +99 -0
- package/src/lib/data/seed-data/notifications.ts +54 -1
- package/src/lib/data/seed-data/profile-test-results.ts +96 -0
- package/src/lib/data/seed-data/repo-imports.ts +51 -0
- package/src/lib/data/seed-data/views.ts +60 -0
- package/src/lib/data/seed.ts +51 -0
- package/src/lib/db/bootstrap.ts +162 -0
- package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
- package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
- package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
- package/src/lib/db/schema.ts +187 -1
- package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
- package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
- package/src/lib/environment/auto-scan.ts +48 -0
- package/src/lib/environment/data.ts +25 -0
- package/src/lib/environment/profile-generator.ts +40 -10
- package/src/lib/environment/profile-linker.ts +143 -0
- package/src/lib/environment/profile-rules.ts +96 -0
- package/src/lib/import/dedup.ts +149 -0
- package/src/lib/import/format-adapter.ts +631 -0
- package/src/lib/import/github-api.ts +219 -0
- package/src/lib/import/repo-scanner.ts +251 -0
- package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
- package/src/lib/schedules/active-hours.ts +120 -0
- package/src/lib/schedules/heartbeat-parser.ts +224 -0
- package/src/lib/schedules/heartbeat-prompt.ts +153 -0
- package/src/lib/schedules/nlp-parser.ts +357 -0
- package/src/lib/schedules/scheduler.ts +218 -3
- package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
- package/src/lib/settings/helpers.ts +6 -0
- package/src/lib/settings/routing.ts +24 -0
- package/src/lib/settings/runtime-setup.ts +28 -1
- package/src/lib/usage/ledger.ts +2 -1
- package/src/lib/validators/__tests__/settings.test.ts +9 -0
- package/src/lib/validators/profile.ts +39 -0
- package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
- package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
- package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
- package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
- 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
|
-
|
|
258
|
-
|
|
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
|
}
|
package/src/lib/chat/engine.ts
CHANGED
|
@@ -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
|
|
211
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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"
|
|
496
|
+
} else if (raw.type === "result") {
|
|
423
497
|
if (raw.is_error && raw.subtype !== "error_max_turns") {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
|
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}*`;
|