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,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Solo Founder Use Case"
|
|
3
|
+
category: "use-case"
|
|
4
|
+
lastUpdated: "2026-03-31"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Solo Founder
|
|
8
|
+
|
|
9
|
+
You are one person running a business. You handle product, marketing, support, ops, and finance — often in the same afternoon. Stagent lets you deploy AI agents as your team, with the governance and cost controls a business requires.
|
|
10
|
+
|
|
11
|
+
## How Stagent Maps to Your Business
|
|
12
|
+
|
|
13
|
+
| Business Concept | Stagent Feature | What It Does |
|
|
14
|
+
|-----------------|----------------|--------------|
|
|
15
|
+
| Business units | **Projects** | Organize work by product line, client, or initiative. Each project scopes tasks, documents, and agent activity |
|
|
16
|
+
| Business processes | **Workflows** | Multi-step automations with 6 patterns — sequence, parallel, loop, planner-executor, checkpoint, swarm |
|
|
17
|
+
| AI team members | **Profiles** | 21 specialist agents (researcher, writer, code reviewer, DevOps, etc.) with consistent instructions and tool policies |
|
|
18
|
+
| Recurring ops | **Schedules** | Automated agent execution on intervals or cron — content publishing, report generation, monitoring sweeps |
|
|
19
|
+
| Business spend | **Cost & Usage** | Per-task, per-provider spend tracking with budget guardrails to prevent surprise bills |
|
|
20
|
+
| Oversight | **Inbox & Permissions** | Approve high-stakes actions, answer agent questions, and maintain an audit trail for every decision |
|
|
21
|
+
|
|
22
|
+
## Example Scenarios
|
|
23
|
+
|
|
24
|
+
### 1. Content Pipeline
|
|
25
|
+
|
|
26
|
+
**Goal:** Publish three blog posts per week without writing each one from scratch.
|
|
27
|
+
|
|
28
|
+
**Setup:**
|
|
29
|
+
- Create a "Content" project with your brand guidelines as a linked document
|
|
30
|
+
- Use the **document-writer** profile for drafting and the **researcher** profile for topic research
|
|
31
|
+
- Build a **sequence workflow**: Research trending topics → Draft outline → Write full post → Format for publishing
|
|
32
|
+
|
|
33
|
+
**Schedule:** Run the research step every Monday and Thursday at 9 AM. The workflow pauses at a checkpoint for your review before the agent publishes.
|
|
34
|
+
|
|
35
|
+
**Cost control:** Set a per-task budget of $2.00 to prevent runaway generation. The cost dashboard shows exactly how much each post costs to produce.
|
|
36
|
+
|
|
37
|
+
### 2. Lead Research
|
|
38
|
+
|
|
39
|
+
**Goal:** Build a qualified prospect list from public data every week.
|
|
40
|
+
|
|
41
|
+
**Setup:**
|
|
42
|
+
- Create a "Sales Pipeline" project
|
|
43
|
+
- Use the **researcher** profile with browser tools enabled for web research
|
|
44
|
+
- Build a **parallel workflow**: Fork into 3-5 industry verticals → Each branch researches companies and contacts → Join step synthesizes into a ranked list
|
|
45
|
+
|
|
46
|
+
**Schedule:** Run every Monday morning. Results land in the project as a document you can review and export.
|
|
47
|
+
|
|
48
|
+
**Governance:** The researcher profile has read-only tool permissions — it can browse and read, but cannot modify files or run shell commands. Browser automation requests route through the inbox for approval.
|
|
49
|
+
|
|
50
|
+
### 3. Support Triage
|
|
51
|
+
|
|
52
|
+
**Goal:** Classify incoming support emails and draft responses without manual sorting.
|
|
53
|
+
|
|
54
|
+
**Setup:**
|
|
55
|
+
- Create a "Customer Support" project
|
|
56
|
+
- Upload your FAQ and standard operating procedures as reference documents
|
|
57
|
+
- Use a custom **support-triage** profile that references these documents
|
|
58
|
+
- Build a **loop workflow**: The agent processes support items iteratively, classifying priority and drafting responses, until the queue is empty or it hits the iteration limit
|
|
59
|
+
|
|
60
|
+
**Schedule:** Run every 2 hours during business hours with an expiry at end of day. The agent draft responses wait in the inbox for your approval before sending.
|
|
61
|
+
|
|
62
|
+
**Audit trail:** Every classification decision and draft response is logged in the monitor, so you can review the agent's reasoning and correct patterns over time through the learned context loop.
|
|
63
|
+
|
|
64
|
+
## Getting Started
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npx stagent
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
1. Create your first project for one of the scenarios above
|
|
71
|
+
2. Browse the profile gallery and assign a specialist to your first task
|
|
72
|
+
3. Run the task and approve tool requests from the inbox
|
|
73
|
+
4. Check the cost dashboard after a few runs to establish your baseline spend
|
|
74
|
+
|
|
75
|
+
See the [Personal Use Journey](../journeys/personal-use.md) for a step-by-step walkthrough, or [Why Stagent](../why-stagent.md) for the full platform overview.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Why Stagent"
|
|
3
|
+
category: "overview"
|
|
4
|
+
lastUpdated: "2026-03-31"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Why Stagent
|
|
8
|
+
|
|
9
|
+
## The Broken AI Agent Stack
|
|
10
|
+
|
|
11
|
+
AI agents can write code, research markets, draft proposals, and triage support tickets. The raw capability is there. But the stack between "demo" and "daily business operations" is broken in five places — and every solo founder, micro-team, and AI agency hits them.
|
|
12
|
+
|
|
13
|
+
### Five Gaps
|
|
14
|
+
|
|
15
|
+
**1. Orchestration gap.** You can run one agent on one task. But real business work is multi-step: research leads, then enrich profiles, then draft outreach, then schedule follow-ups. Stitching agents into reliable sequences, parallel branches, or iterative loops requires custom glue code that nobody wants to maintain.
|
|
16
|
+
|
|
17
|
+
**2. Strategy-to-execution gap.** Business operators think in projects, processes, and outcomes. Agent tools think in prompts, tokens, and tool calls. There is no shared language between what the business needs and what the agent executes — so operators end up copy-pasting prompts and hoping for the best.
|
|
18
|
+
|
|
19
|
+
**3. Lifecycle gap.** A task does not end when the agent finishes. It needs scheduling, retry on failure, resume from checkpoints, cost tracking, and audit trails. Most agent frameworks stop at "run once and print output," leaving the entire operational lifecycle to the user.
|
|
20
|
+
|
|
21
|
+
**4. Trust and governance gap.** Agents that can read files, run shell commands, and call APIs need guardrails. Which tools are allowed? Who approves destructive actions? What happens when the agent asks a question at 2 AM? Without a governance layer, agent use stays limited to low-stakes experiments.
|
|
22
|
+
|
|
23
|
+
**5. Distribution gap.** Setting up an agent workspace should not require cloning a repo, configuring a build system, and managing a database. Operators need `npx stagent` — one command, zero config, own your data.
|
|
24
|
+
|
|
25
|
+
## Stagent: AI Business Operating System
|
|
26
|
+
|
|
27
|
+
Stagent closes all five gaps in a single local-first platform.
|
|
28
|
+
|
|
29
|
+
**Projects as business units.** Organize work into projects with scoped context and working directories. Each project is a container for tasks, documents, workflows, and agent activity — mapping directly to how you think about your business.
|
|
30
|
+
|
|
31
|
+
**Profiles as your AI team.** 21 specialist profiles ship out of the box: researcher, code reviewer, document writer, DevOps engineer, and more. Each profile packages instructions, allowed tools, runtime tuning, and MCP configs so you deploy consistent behavior instead of ad-hoc prompts. Create custom profiles or import them from GitHub.
|
|
32
|
+
|
|
33
|
+
**Workflows as business processes.** Six orchestration patterns — sequence, planner-executor, checkpoint, parallel, loop, and swarm — cover everything from simple task chains to multi-agent research pipelines. Workflow blueprints let you template common processes and spin them up with a form.
|
|
34
|
+
|
|
35
|
+
**Schedules as recurring operations.** Human-friendly intervals (`5m`, `2h`, `1d`) or cron expressions drive automated agent execution. Each firing creates a governed task through the same pipeline, with pause/resume and expiry controls.
|
|
36
|
+
|
|
37
|
+
**Governance as a business benefit.** Tool permissions, inbox approvals, audit trails, and budget guardrails are not overhead — they are what make it safe to let agents handle real work. Permission presets (read-only, git-safe, full-auto) let you dial trust up or down per profile.
|
|
38
|
+
|
|
39
|
+
**Cost visibility as financial control.** Provider-normalized metering tracks token usage and spend per task, per model, per provider. Budget guardrails prevent surprise bills. The cost dashboard gives you the same spend visibility you expect from any business tool.
|
|
40
|
+
|
|
41
|
+
**Multi-provider runtime.** Claude Code and OpenAI Codex App Server run behind one shared registry. Switch providers per task, per schedule, or per workflow step — without changing anything else.
|
|
42
|
+
|
|
43
|
+
## Who It's For
|
|
44
|
+
|
|
45
|
+
**Solo founders** who need to run content pipelines, lead research, support triage, and other business processes without hiring a team. Stagent's profiles and workflows replace the coordination overhead of managing multiple tools and prompts.
|
|
46
|
+
|
|
47
|
+
**Micro-teams (2-10 people)** who want governed AI operations without building internal tooling. Projects organize workstreams, profiles standardize agent behavior across team members, and the inbox keeps a human in the loop for high-stakes decisions.
|
|
48
|
+
|
|
49
|
+
**AI agencies** deploying agent workflows for clients. Multi-project support maps to client portfolios, profiles customize per vertical, and workflow blueprints package repeatable service offerings. Cost tracking provides per-client spend visibility.
|
|
50
|
+
|
|
51
|
+
## Get Started
|
|
52
|
+
|
|
53
|
+
Install and run your first task in under a minute:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx stagent
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
See the [Getting Started guide](./getting-started.md) for configuration details, or explore the [User Journeys](./index.md#user-journeys) for guided walkthroughs by experience level.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stagent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "AI Business Operating System — run your business with AI agents. Local-first, multi-provider, governed.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
7
7
|
"agents",
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
"openai",
|
|
12
12
|
"codex",
|
|
13
13
|
"nextjs",
|
|
14
|
-
"local-first"
|
|
14
|
+
"local-first",
|
|
15
|
+
"business",
|
|
16
|
+
"operating-system",
|
|
17
|
+
"orchestration",
|
|
18
|
+
"solo-founder",
|
|
19
|
+
"ai-business"
|
|
15
20
|
],
|
|
16
21
|
"license": "Apache-2.0",
|
|
17
22
|
"type": "module",
|
|
@@ -58,6 +63,7 @@
|
|
|
58
63
|
},
|
|
59
64
|
"dependencies": {
|
|
60
65
|
"@anthropic-ai/claude-agent-sdk": "^0.2.71",
|
|
66
|
+
"@anthropic-ai/sdk": "^0.80.0",
|
|
61
67
|
"@dnd-kit/core": "^6.3.1",
|
|
62
68
|
"@dnd-kit/sortable": "^10.0.0",
|
|
63
69
|
"@dnd-kit/utilities": "^3.2.2",
|
|
@@ -80,6 +86,7 @@
|
|
|
80
86
|
"next": "^16",
|
|
81
87
|
"next-themes": "^0.4.6",
|
|
82
88
|
"open": "^10",
|
|
89
|
+
"openai": "^6.33.0",
|
|
83
90
|
"pdf-parse": "^2.4.5",
|
|
84
91
|
"radix-ui": "^1.4.3",
|
|
85
92
|
"react": "^19",
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { channelConfigs } from "@/lib/db/schema";
|
|
4
|
+
import { eq } from "drizzle-orm";
|
|
5
|
+
|
|
6
|
+
export async function GET(
|
|
7
|
+
_req: NextRequest,
|
|
8
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
9
|
+
) {
|
|
10
|
+
const { id } = await params;
|
|
11
|
+
|
|
12
|
+
const [channel] = await db
|
|
13
|
+
.select()
|
|
14
|
+
.from(channelConfigs)
|
|
15
|
+
.where(eq(channelConfigs.id, id));
|
|
16
|
+
|
|
17
|
+
if (!channel) {
|
|
18
|
+
return NextResponse.json({ error: "Channel not found" }, { status: 404 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return NextResponse.json(channel);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function PATCH(
|
|
25
|
+
req: NextRequest,
|
|
26
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
27
|
+
) {
|
|
28
|
+
const { id } = await params;
|
|
29
|
+
const body = await req.json();
|
|
30
|
+
const { name, config, status, direction } = body as {
|
|
31
|
+
name?: string;
|
|
32
|
+
config?: Record<string, unknown>;
|
|
33
|
+
status?: "active" | "disabled";
|
|
34
|
+
direction?: "outbound" | "bidirectional";
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const [channel] = await db
|
|
38
|
+
.select()
|
|
39
|
+
.from(channelConfigs)
|
|
40
|
+
.where(eq(channelConfigs.id, id));
|
|
41
|
+
|
|
42
|
+
if (!channel) {
|
|
43
|
+
return NextResponse.json({ error: "Channel not found" }, { status: 404 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const now = new Date();
|
|
47
|
+
const updates: Record<string, unknown> = { updatedAt: now };
|
|
48
|
+
|
|
49
|
+
if (name !== undefined) {
|
|
50
|
+
if (!name.trim()) {
|
|
51
|
+
return NextResponse.json({ error: "Name cannot be empty" }, { status: 400 });
|
|
52
|
+
}
|
|
53
|
+
updates.name = name.trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (config !== undefined) {
|
|
57
|
+
updates.config = JSON.stringify(config);
|
|
58
|
+
updates.testStatus = "untested"; // Reset test status when config changes
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (status !== undefined) {
|
|
62
|
+
if (!["active", "disabled"].includes(status)) {
|
|
63
|
+
return NextResponse.json({ error: "Invalid status" }, { status: 400 });
|
|
64
|
+
}
|
|
65
|
+
updates.status = status;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (direction !== undefined) {
|
|
69
|
+
if (!["outbound", "bidirectional"].includes(direction)) {
|
|
70
|
+
return NextResponse.json({ error: "Invalid direction" }, { status: 400 });
|
|
71
|
+
}
|
|
72
|
+
updates.direction = direction;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await db.update(channelConfigs).set(updates).where(eq(channelConfigs.id, id));
|
|
76
|
+
|
|
77
|
+
const [updated] = await db
|
|
78
|
+
.select()
|
|
79
|
+
.from(channelConfigs)
|
|
80
|
+
.where(eq(channelConfigs.id, id));
|
|
81
|
+
|
|
82
|
+
return NextResponse.json(updated);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function DELETE(
|
|
86
|
+
_req: NextRequest,
|
|
87
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
88
|
+
) {
|
|
89
|
+
const { id } = await params;
|
|
90
|
+
|
|
91
|
+
const [channel] = await db
|
|
92
|
+
.select()
|
|
93
|
+
.from(channelConfigs)
|
|
94
|
+
.where(eq(channelConfigs.id, id));
|
|
95
|
+
|
|
96
|
+
if (!channel) {
|
|
97
|
+
return NextResponse.json({ error: "Channel not found" }, { status: 404 });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await db.delete(channelConfigs).where(eq(channelConfigs.id, id));
|
|
101
|
+
|
|
102
|
+
return NextResponse.json({ deleted: true });
|
|
103
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { channelConfigs } from "@/lib/db/schema";
|
|
4
|
+
import { eq } from "drizzle-orm";
|
|
5
|
+
import { getChannelAdapter } from "@/lib/channels/registry";
|
|
6
|
+
|
|
7
|
+
export async function POST(
|
|
8
|
+
_req: NextRequest,
|
|
9
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
10
|
+
) {
|
|
11
|
+
const { id } = await params;
|
|
12
|
+
|
|
13
|
+
const [channel] = await db
|
|
14
|
+
.select()
|
|
15
|
+
.from(channelConfigs)
|
|
16
|
+
.where(eq(channelConfigs.id, id));
|
|
17
|
+
|
|
18
|
+
if (!channel) {
|
|
19
|
+
return NextResponse.json({ error: "Channel not found" }, { status: 404 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let parsedConfig: Record<string, unknown>;
|
|
23
|
+
try {
|
|
24
|
+
parsedConfig = JSON.parse(channel.config) as Record<string, unknown>;
|
|
25
|
+
} catch {
|
|
26
|
+
return NextResponse.json({ error: "Invalid channel config JSON" }, { status: 500 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const adapter = getChannelAdapter(channel.channelType);
|
|
31
|
+
const result = await adapter.testConnection(parsedConfig);
|
|
32
|
+
|
|
33
|
+
const now = new Date();
|
|
34
|
+
await db
|
|
35
|
+
.update(channelConfigs)
|
|
36
|
+
.set({
|
|
37
|
+
testStatus: result.ok ? "ok" : "failed",
|
|
38
|
+
updatedAt: now,
|
|
39
|
+
})
|
|
40
|
+
.where(eq(channelConfigs.id, id));
|
|
41
|
+
|
|
42
|
+
return NextResponse.json({
|
|
43
|
+
testStatus: result.ok ? "ok" : "failed",
|
|
44
|
+
error: result.error,
|
|
45
|
+
});
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return NextResponse.json(
|
|
48
|
+
{ testStatus: "failed", error: err instanceof Error ? err.message : String(err) },
|
|
49
|
+
{ status: 500 }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { channelConfigs } from "@/lib/db/schema";
|
|
4
|
+
import { eq } from "drizzle-orm";
|
|
5
|
+
import { slackAdapter } from "@/lib/channels/slack-adapter";
|
|
6
|
+
import { handleInboundMessage } from "@/lib/channels/gateway";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* POST /api/channels/inbound/slack?configId=xxx
|
|
10
|
+
*
|
|
11
|
+
* Receives Slack Events API callbacks.
|
|
12
|
+
* Handles:
|
|
13
|
+
* - url_verification challenge (required during Slack app setup)
|
|
14
|
+
* - event_callback with message events
|
|
15
|
+
*
|
|
16
|
+
* IMPORTANT: Slack requires a 200 response within 3 seconds.
|
|
17
|
+
* We respond immediately and process the message asynchronously.
|
|
18
|
+
*/
|
|
19
|
+
export async function POST(req: NextRequest) {
|
|
20
|
+
const configId = req.nextUrl.searchParams.get("configId");
|
|
21
|
+
|
|
22
|
+
// Read raw body for signature verification
|
|
23
|
+
const rawBody = await req.text();
|
|
24
|
+
let body: SlackPayload;
|
|
25
|
+
try {
|
|
26
|
+
body = JSON.parse(rawBody) as SlackPayload;
|
|
27
|
+
} catch {
|
|
28
|
+
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Handle URL verification challenge (no configId needed)
|
|
32
|
+
if (body.type === "url_verification") {
|
|
33
|
+
return NextResponse.json({ challenge: body.challenge });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!configId) {
|
|
37
|
+
return NextResponse.json({ error: "Missing configId" }, { status: 400 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fetch channel config
|
|
41
|
+
const config = db
|
|
42
|
+
.select()
|
|
43
|
+
.from(channelConfigs)
|
|
44
|
+
.where(eq(channelConfigs.id, configId))
|
|
45
|
+
.get();
|
|
46
|
+
|
|
47
|
+
if (!config) {
|
|
48
|
+
return NextResponse.json({ error: "Channel not found" }, { status: 404 });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Verify Slack signature
|
|
52
|
+
let parsedConfig: Record<string, unknown>;
|
|
53
|
+
try {
|
|
54
|
+
parsedConfig = JSON.parse(config.config) as Record<string, unknown>;
|
|
55
|
+
} catch {
|
|
56
|
+
return NextResponse.json({ error: "Invalid channel config" }, { status: 500 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Only verify signature if signingSecret is configured
|
|
60
|
+
if (parsedConfig.signingSecret && slackAdapter.verifySignature) {
|
|
61
|
+
const headers: Record<string, string> = {};
|
|
62
|
+
req.headers.forEach((value, key) => {
|
|
63
|
+
headers[key] = value;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!slackAdapter.verifySignature(rawBody, headers, parsedConfig)) {
|
|
67
|
+
return NextResponse.json({ error: "Invalid signature" }, { status: 403 });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Only handle event_callback with message events
|
|
72
|
+
if (body.type !== "event_callback") {
|
|
73
|
+
return NextResponse.json({ ok: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const message = slackAdapter.parseInbound!(body, {});
|
|
77
|
+
if (!message) {
|
|
78
|
+
// Not a message event or bot message — acknowledge silently
|
|
79
|
+
return NextResponse.json({ ok: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Process asynchronously — respond 200 immediately (Slack 3-second requirement)
|
|
83
|
+
handleInboundMessage({
|
|
84
|
+
channelConfigId: configId,
|
|
85
|
+
message,
|
|
86
|
+
}).catch((err) => {
|
|
87
|
+
console.error(`[slack-inbound] Error handling message for config ${configId}:`, err);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return NextResponse.json({ ok: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Slack payload types ────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
interface SlackPayload {
|
|
96
|
+
type: string;
|
|
97
|
+
challenge?: string;
|
|
98
|
+
token?: string;
|
|
99
|
+
event?: {
|
|
100
|
+
type: string;
|
|
101
|
+
text?: string;
|
|
102
|
+
user?: string;
|
|
103
|
+
bot_id?: string;
|
|
104
|
+
subtype?: string;
|
|
105
|
+
ts?: string;
|
|
106
|
+
thread_ts?: string;
|
|
107
|
+
channel?: string;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { channelConfigs } from "@/lib/db/schema";
|
|
4
|
+
import { eq } from "drizzle-orm";
|
|
5
|
+
import { telegramAdapter } from "@/lib/channels/telegram-adapter";
|
|
6
|
+
import { handleInboundMessage } from "@/lib/channels/gateway";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* POST /api/channels/inbound/telegram/poll?configId=xxx
|
|
10
|
+
*
|
|
11
|
+
* Poll Telegram's getUpdates API for pending messages and process them
|
|
12
|
+
* through the gateway. Use this for local development when Telegram
|
|
13
|
+
* can't reach localhost via webhooks.
|
|
14
|
+
*
|
|
15
|
+
* Returns the number of updates processed.
|
|
16
|
+
*/
|
|
17
|
+
export async function POST(req: NextRequest) {
|
|
18
|
+
const configId = req.nextUrl.searchParams.get("configId");
|
|
19
|
+
if (!configId) {
|
|
20
|
+
return NextResponse.json({ error: "Missing configId" }, { status: 400 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const config = db
|
|
24
|
+
.select()
|
|
25
|
+
.from(channelConfigs)
|
|
26
|
+
.where(eq(channelConfigs.id, configId))
|
|
27
|
+
.get();
|
|
28
|
+
|
|
29
|
+
if (!config) {
|
|
30
|
+
return NextResponse.json({ error: "Channel not found" }, { status: 404 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let parsedConfig: Record<string, unknown>;
|
|
34
|
+
try {
|
|
35
|
+
parsedConfig = JSON.parse(config.config) as Record<string, unknown>;
|
|
36
|
+
} catch {
|
|
37
|
+
return NextResponse.json({ error: "Invalid channel config" }, { status: 500 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const botToken = parsedConfig.botToken as string;
|
|
41
|
+
if (!botToken) {
|
|
42
|
+
return NextResponse.json({ error: "Missing botToken in config" }, { status: 400 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Read optional offset from request body
|
|
46
|
+
const body = await req.json().catch(() => ({})) as { offset?: number };
|
|
47
|
+
|
|
48
|
+
// Fetch updates from Telegram
|
|
49
|
+
const url = `https://api.telegram.org/bot${botToken}/getUpdates`;
|
|
50
|
+
const params: Record<string, unknown> = { timeout: 0 };
|
|
51
|
+
if (body.offset) {
|
|
52
|
+
params.offset = body.offset;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let updates: TelegramUpdate[];
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(url, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
body: JSON.stringify(params),
|
|
61
|
+
});
|
|
62
|
+
const data = (await res.json()) as { ok: boolean; result: TelegramUpdate[] };
|
|
63
|
+
if (!data.ok) {
|
|
64
|
+
return NextResponse.json({ error: "Telegram getUpdates failed" }, { status: 502 });
|
|
65
|
+
}
|
|
66
|
+
updates = data.result;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return NextResponse.json(
|
|
69
|
+
{ error: err instanceof Error ? err.message : "Telegram API error" },
|
|
70
|
+
{ status: 502 }
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (updates.length === 0) {
|
|
75
|
+
return NextResponse.json({ processed: 0, nextOffset: body.offset ?? null });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Process each update through the gateway
|
|
79
|
+
let processed = 0;
|
|
80
|
+
let maxUpdateId = 0;
|
|
81
|
+
|
|
82
|
+
for (const update of updates) {
|
|
83
|
+
if (update.update_id > maxUpdateId) {
|
|
84
|
+
maxUpdateId = update.update_id;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const message = telegramAdapter.parseInbound!(update, {});
|
|
88
|
+
if (!message || message.isBot) continue;
|
|
89
|
+
|
|
90
|
+
// Process sequentially to respect turn locking
|
|
91
|
+
try {
|
|
92
|
+
await handleInboundMessage({ channelConfigId: configId, message });
|
|
93
|
+
processed++;
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error(`[telegram-poll] Error processing update ${update.update_id}:`, err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Acknowledge processed updates so Telegram doesn't return them again
|
|
100
|
+
if (maxUpdateId > 0) {
|
|
101
|
+
try {
|
|
102
|
+
await fetch(`https://api.telegram.org/bot${botToken}/getUpdates`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "Content-Type": "application/json" },
|
|
105
|
+
body: JSON.stringify({ offset: maxUpdateId + 1, timeout: 0 }),
|
|
106
|
+
});
|
|
107
|
+
} catch {
|
|
108
|
+
// Non-fatal — updates will be re-processed next poll
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return NextResponse.json({
|
|
113
|
+
processed,
|
|
114
|
+
total: updates.length,
|
|
115
|
+
nextOffset: maxUpdateId + 1,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface TelegramUpdate {
|
|
120
|
+
update_id: number;
|
|
121
|
+
message?: {
|
|
122
|
+
message_id: number;
|
|
123
|
+
from?: { id: number; is_bot: boolean; first_name: string };
|
|
124
|
+
chat: { id: number; type: string };
|
|
125
|
+
date: number;
|
|
126
|
+
text?: string;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { channelConfigs } from "@/lib/db/schema";
|
|
4
|
+
import { eq } from "drizzle-orm";
|
|
5
|
+
import { telegramAdapter } from "@/lib/channels/telegram-adapter";
|
|
6
|
+
import { handleInboundMessage } from "@/lib/channels/gateway";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* POST /api/channels/inbound/telegram?configId=xxx&secret=yyy
|
|
10
|
+
*
|
|
11
|
+
* Receives Telegram Bot API webhook updates.
|
|
12
|
+
* - configId: channel config ID to route to
|
|
13
|
+
* - secret: shared secret for verification (set during webhook registration)
|
|
14
|
+
*/
|
|
15
|
+
export async function POST(req: NextRequest) {
|
|
16
|
+
const configId = req.nextUrl.searchParams.get("configId");
|
|
17
|
+
const secret = req.nextUrl.searchParams.get("secret");
|
|
18
|
+
|
|
19
|
+
if (!configId) {
|
|
20
|
+
return NextResponse.json({ error: "Missing configId" }, { status: 400 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Fetch channel config
|
|
24
|
+
const config = db
|
|
25
|
+
.select()
|
|
26
|
+
.from(channelConfigs)
|
|
27
|
+
.where(eq(channelConfigs.id, configId))
|
|
28
|
+
.get();
|
|
29
|
+
|
|
30
|
+
if (!config) {
|
|
31
|
+
return NextResponse.json({ error: "Channel not found" }, { status: 404 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Verify secret token
|
|
35
|
+
let parsedConfig: Record<string, unknown>;
|
|
36
|
+
try {
|
|
37
|
+
parsedConfig = JSON.parse(config.config) as Record<string, unknown>;
|
|
38
|
+
} catch {
|
|
39
|
+
return NextResponse.json({ error: "Invalid channel config" }, { status: 500 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const expectedSecret = parsedConfig.webhookSecret as string | undefined;
|
|
43
|
+
if (expectedSecret && secret !== expectedSecret) {
|
|
44
|
+
return NextResponse.json({ error: "Invalid secret" }, { status: 403 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Parse the Telegram update
|
|
48
|
+
let body: unknown;
|
|
49
|
+
try {
|
|
50
|
+
body = await req.json();
|
|
51
|
+
} catch {
|
|
52
|
+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const message = telegramAdapter.parseInbound!(body, {});
|
|
56
|
+
if (!message) {
|
|
57
|
+
// Not a text message (e.g., edited message, photo) — acknowledge silently
|
|
58
|
+
return NextResponse.json({ ok: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Skip bot messages to prevent loops
|
|
62
|
+
if (message.isBot) {
|
|
63
|
+
return NextResponse.json({ ok: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Process asynchronously — respond 200 immediately to Telegram
|
|
67
|
+
// (Telegram retries if no response within ~60s, but faster is better)
|
|
68
|
+
handleInboundMessage({
|
|
69
|
+
channelConfigId: configId,
|
|
70
|
+
message,
|
|
71
|
+
}).catch((err) => {
|
|
72
|
+
console.error(`[telegram-inbound] Error handling message for config ${configId}:`, err);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return NextResponse.json({ ok: true });
|
|
76
|
+
}
|