stagent 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +104 -0
- package/src/app/api/channels/[id]/test/route.ts +52 -0
- package/src/app/api/channels/inbound/slack/route.ts +116 -0
- package/src/app/api/channels/inbound/telegram/poll/route.ts +140 -0
- package/src/app/api/channels/inbound/telegram/route.ts +87 -0
- package/src/app/api/channels/route.ts +72 -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/data/clear/route.ts +4 -0
- package/src/app/api/data/seed/route.ts +4 -0
- package/src/app/api/documents/route.ts +36 -6
- 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/api/tasks/[id]/respond/route.ts +23 -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 +27 -200
- 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 +75 -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 +190 -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,72 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { channelConfigs } from "@/lib/db/schema";
|
|
4
|
+
import { desc, eq } from "drizzle-orm";
|
|
5
|
+
import { maskChannelRow } from "@/lib/channels/types";
|
|
6
|
+
|
|
7
|
+
const VALID_CHANNEL_TYPES = ["slack", "telegram", "webhook"] as const;
|
|
8
|
+
|
|
9
|
+
export async function GET() {
|
|
10
|
+
const result = await db
|
|
11
|
+
.select()
|
|
12
|
+
.from(channelConfigs)
|
|
13
|
+
.orderBy(desc(channelConfigs.createdAt));
|
|
14
|
+
|
|
15
|
+
return NextResponse.json(result.map(maskChannelRow));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function POST(req: NextRequest) {
|
|
19
|
+
const body = await req.json();
|
|
20
|
+
const { channelType, name, config } = body as {
|
|
21
|
+
channelType?: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
config?: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (!name?.trim()) {
|
|
27
|
+
return NextResponse.json({ error: "Name is required" }, { status: 400 });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!channelType || !VALID_CHANNEL_TYPES.includes(channelType as typeof VALID_CHANNEL_TYPES[number])) {
|
|
31
|
+
return NextResponse.json(
|
|
32
|
+
{ error: `Invalid channel type. Must be one of: ${VALID_CHANNEL_TYPES.join(", ")}` },
|
|
33
|
+
{ status: 400 }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!config || typeof config !== "object") {
|
|
38
|
+
return NextResponse.json({ error: "Config object is required" }, { status: 400 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Validate required fields per type
|
|
42
|
+
if (channelType === "slack" && !config.webhookUrl) {
|
|
43
|
+
return NextResponse.json({ error: "Slack channels require a webhookUrl" }, { status: 400 });
|
|
44
|
+
}
|
|
45
|
+
if (channelType === "telegram" && (!config.botToken || !config.chatId)) {
|
|
46
|
+
return NextResponse.json({ error: "Telegram channels require botToken and chatId" }, { status: 400 });
|
|
47
|
+
}
|
|
48
|
+
if (channelType === "webhook" && !config.url) {
|
|
49
|
+
return NextResponse.json({ error: "Webhook channels require a url" }, { status: 400 });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const id = crypto.randomUUID();
|
|
53
|
+
const now = new Date();
|
|
54
|
+
|
|
55
|
+
await db.insert(channelConfigs).values({
|
|
56
|
+
id,
|
|
57
|
+
channelType: channelType as typeof VALID_CHANNEL_TYPES[number],
|
|
58
|
+
name: name.trim(),
|
|
59
|
+
config: JSON.stringify(config),
|
|
60
|
+
status: "active",
|
|
61
|
+
testStatus: "untested",
|
|
62
|
+
createdAt: now,
|
|
63
|
+
updatedAt: now,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const [created] = await db
|
|
67
|
+
.select()
|
|
68
|
+
.from(channelConfigs)
|
|
69
|
+
.where(eq(channelConfigs.id, id));
|
|
70
|
+
|
|
71
|
+
return NextResponse.json(maskChannelRow(created), { status: 201 });
|
|
72
|
+
}
|
|
@@ -3,6 +3,10 @@ import {
|
|
|
3
3
|
createConversation,
|
|
4
4
|
listConversations,
|
|
5
5
|
} from "@/lib/data/chat";
|
|
6
|
+
import { db } from "@/lib/db";
|
|
7
|
+
import { projects } from "@/lib/db/schema";
|
|
8
|
+
import { eq } from "drizzle-orm";
|
|
9
|
+
import { ensureFreshScan } from "@/lib/environment/auto-scan";
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* GET /api/chat/conversations?status=active&projectId=xxx&limit=50
|
|
@@ -48,6 +52,17 @@ export async function POST(req: NextRequest) {
|
|
|
48
52
|
);
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
// Auto-scan environment when starting a conversation for a project
|
|
56
|
+
if (projectId) {
|
|
57
|
+
const [project] = await db
|
|
58
|
+
.select()
|
|
59
|
+
.from(projects)
|
|
60
|
+
.where(eq(projects.id, projectId));
|
|
61
|
+
if (project?.workingDirectory) {
|
|
62
|
+
ensureFreshScan(project.workingDirectory, projectId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
51
66
|
const conversation = await createConversation({
|
|
52
67
|
projectId: projectId ?? null,
|
|
53
68
|
title: title ?? null,
|
|
@@ -21,48 +21,47 @@ interface EntityResult {
|
|
|
21
21
|
export async function GET(request: Request) {
|
|
22
22
|
const url = new URL(request.url);
|
|
23
23
|
const query = url.searchParams.get("q") ?? "";
|
|
24
|
-
const limit = Math.min(parseInt(url.searchParams.get("limit") ?? "
|
|
24
|
+
const limit = Math.min(parseInt(url.searchParams.get("limit") ?? "20", 10), 30);
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const pattern = `%${query}%`;
|
|
26
|
+
const hasQuery = query.trim().length > 0;
|
|
27
|
+
const pattern = hasQuery ? `%${query}%` : "";
|
|
31
28
|
const perType = Math.max(2, Math.floor(limit / 5));
|
|
32
29
|
|
|
33
30
|
const results: EntityResult[] = [];
|
|
34
31
|
|
|
32
|
+
// Build queries — apply LIKE filter only when query is non-empty
|
|
33
|
+
const projectQuery = db
|
|
34
|
+
.select({ id: projects.id, name: projects.name, status: projects.status, description: projects.description })
|
|
35
|
+
.from(projects);
|
|
36
|
+
const taskQuery = db
|
|
37
|
+
.select({ id: tasks.id, title: tasks.title, status: tasks.status, description: tasks.description })
|
|
38
|
+
.from(tasks);
|
|
39
|
+
const workflowQuery = db
|
|
40
|
+
.select({ id: workflows.id, name: workflows.name, status: workflows.status })
|
|
41
|
+
.from(workflows);
|
|
42
|
+
const documentQuery = db
|
|
43
|
+
.select({ id: documents.id, name: documents.originalName, status: documents.status, mimeType: documents.mimeType, size: documents.size })
|
|
44
|
+
.from(documents);
|
|
45
|
+
const scheduleQuery = db
|
|
46
|
+
.select({ id: schedules.id, name: schedules.name, status: schedules.status })
|
|
47
|
+
.from(schedules);
|
|
48
|
+
|
|
35
49
|
// Search in parallel across all entity types
|
|
36
50
|
const [projectRows, taskRows, workflowRows, documentRows, scheduleRows] =
|
|
37
51
|
await Promise.all([
|
|
38
|
-
|
|
39
|
-
.select({ id: projects.id, name: projects.name, status: projects.status, description: projects.description })
|
|
40
|
-
.from(projects)
|
|
41
|
-
.where(like(projects.name, pattern))
|
|
52
|
+
(hasQuery ? projectQuery.where(like(projects.name, pattern)) : projectQuery)
|
|
42
53
|
.orderBy(desc(projects.updatedAt))
|
|
43
54
|
.limit(perType),
|
|
44
|
-
|
|
45
|
-
.select({ id: tasks.id, title: tasks.title, status: tasks.status, description: tasks.description })
|
|
46
|
-
.from(tasks)
|
|
47
|
-
.where(like(tasks.title, pattern))
|
|
55
|
+
(hasQuery ? taskQuery.where(like(tasks.title, pattern)) : taskQuery)
|
|
48
56
|
.orderBy(desc(tasks.updatedAt))
|
|
49
57
|
.limit(perType),
|
|
50
|
-
|
|
51
|
-
.select({ id: workflows.id, name: workflows.name, status: workflows.status })
|
|
52
|
-
.from(workflows)
|
|
53
|
-
.where(like(workflows.name, pattern))
|
|
58
|
+
(hasQuery ? workflowQuery.where(like(workflows.name, pattern)) : workflowQuery)
|
|
54
59
|
.orderBy(desc(workflows.updatedAt))
|
|
55
60
|
.limit(perType),
|
|
56
|
-
|
|
57
|
-
.select({ id: documents.id, name: documents.originalName, status: documents.status, mimeType: documents.mimeType, size: documents.size })
|
|
58
|
-
.from(documents)
|
|
59
|
-
.where(like(documents.originalName, pattern))
|
|
61
|
+
(hasQuery ? documentQuery.where(like(documents.originalName, pattern)) : documentQuery)
|
|
60
62
|
.orderBy(desc(documents.createdAt))
|
|
61
63
|
.limit(perType),
|
|
62
|
-
|
|
63
|
-
.select({ id: schedules.id, name: schedules.name, status: schedules.status })
|
|
64
|
-
.from(schedules)
|
|
65
|
-
.where(like(schedules.name, pattern))
|
|
64
|
+
(hasQuery ? scheduleQuery.where(like(schedules.name, pattern)) : scheduleQuery)
|
|
66
65
|
.orderBy(desc(schedules.updatedAt))
|
|
67
66
|
.limit(perType),
|
|
68
67
|
]);
|
|
@@ -84,13 +83,29 @@ export async function GET(request: Request) {
|
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
// Search profiles in-memory (file-based registry)
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
const allProfiles = listProfiles();
|
|
87
|
+
const q = query.toLowerCase();
|
|
88
|
+
const profileMatches = hasQuery
|
|
89
|
+
? allProfiles
|
|
90
|
+
.filter((p) =>
|
|
91
|
+
p.name.toLowerCase().includes(q) ||
|
|
92
|
+
p.id.toLowerCase().includes(q) ||
|
|
93
|
+
p.description?.toLowerCase().includes(q) ||
|
|
94
|
+
p.tags?.some((t) => t.toLowerCase().includes(q))
|
|
95
|
+
)
|
|
96
|
+
.slice(0, perType)
|
|
97
|
+
: allProfiles.slice(0, perType);
|
|
91
98
|
|
|
92
99
|
for (const p of profileMatches) {
|
|
93
|
-
results.push({
|
|
100
|
+
results.push({
|
|
101
|
+
entityType: "profile",
|
|
102
|
+
entityId: p.id,
|
|
103
|
+
label: p.name,
|
|
104
|
+
description: p.description
|
|
105
|
+
? `${p.domain} · ${p.description.slice(0, 100)}`
|
|
106
|
+
: p.domain,
|
|
107
|
+
status: p.domain,
|
|
108
|
+
});
|
|
94
109
|
}
|
|
95
110
|
|
|
96
111
|
return NextResponse.json({ results: results.slice(0, limit) });
|
|
@@ -2,6 +2,10 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { clearAllData } from "@/lib/data/clear";
|
|
3
3
|
|
|
4
4
|
export async function POST() {
|
|
5
|
+
if (process.env.NODE_ENV === "production") {
|
|
6
|
+
return NextResponse.json(null, { status: 404 });
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
try {
|
|
6
10
|
const deleted = clearAllData();
|
|
7
11
|
return NextResponse.json({ success: true, deleted });
|
|
@@ -2,6 +2,10 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { seedSampleData } from "@/lib/data/seed";
|
|
3
3
|
|
|
4
4
|
export async function POST() {
|
|
5
|
+
if (process.env.NODE_ENV === "production") {
|
|
6
|
+
return NextResponse.json(null, { status: 404 });
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
try {
|
|
6
10
|
const seeded = await seedSampleData();
|
|
7
11
|
return NextResponse.json({ success: true, seeded });
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { db } from "@/lib/db";
|
|
3
3
|
import { documents, tasks, projects } from "@/lib/db/schema";
|
|
4
|
-
import { eq, and, like, or, desc
|
|
4
|
+
import { eq, and, like, or, desc } from "drizzle-orm";
|
|
5
5
|
import { access, stat, copyFile, mkdir } from "fs/promises";
|
|
6
|
-
import { basename, extname, join } from "path";
|
|
6
|
+
import path, { basename, extname, join } from "path";
|
|
7
|
+
import { homedir } from "os";
|
|
7
8
|
import crypto from "crypto";
|
|
8
9
|
import { getStagentUploadsDir } from "@/lib/utils/stagent-paths";
|
|
9
10
|
import { processDocument } from "@/lib/documents/processor";
|
|
@@ -120,18 +121,47 @@ export async function POST(req: NextRequest) {
|
|
|
120
121
|
|
|
121
122
|
const body = parsed.data;
|
|
122
123
|
|
|
124
|
+
// Path traversal protection: resolve and validate the file path
|
|
125
|
+
const resolvedPath = path.resolve(body.file_path);
|
|
126
|
+
const home = homedir();
|
|
127
|
+
const SENSITIVE_PREFIXES = ["/etc", "/var", "/proc", "/sys", "/dev", "/root"];
|
|
128
|
+
const SENSITIVE_HOME_DIRS = [".ssh", ".gnupg", ".aws", ".config", ".env"];
|
|
129
|
+
|
|
130
|
+
if (SENSITIVE_PREFIXES.some((prefix) => resolvedPath.startsWith(prefix))) {
|
|
131
|
+
return NextResponse.json(
|
|
132
|
+
{ error: "Access denied: path points to a restricted system directory" },
|
|
133
|
+
{ status: 403 }
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (resolvedPath.startsWith(home)) {
|
|
138
|
+
const relativeToHome = resolvedPath.slice(home.length + 1);
|
|
139
|
+
if (SENSITIVE_HOME_DIRS.some((dir) => relativeToHome.startsWith(dir))) {
|
|
140
|
+
return NextResponse.json(
|
|
141
|
+
{ error: "Access denied: path points to a sensitive home directory" },
|
|
142
|
+
{ status: 403 }
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
} else if (!resolvedPath.startsWith("/tmp")) {
|
|
146
|
+
// Outside home and not /tmp — reject
|
|
147
|
+
return NextResponse.json(
|
|
148
|
+
{ error: "Access denied: path must be under the user's home directory or /tmp" },
|
|
149
|
+
{ status: 403 }
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
123
153
|
try {
|
|
124
|
-
await access(
|
|
154
|
+
await access(resolvedPath);
|
|
125
155
|
} catch {
|
|
126
156
|
return NextResponse.json({ error: `File not found: ${body.file_path}` }, { status: 400 });
|
|
127
157
|
}
|
|
128
158
|
|
|
129
|
-
const stats = await stat(
|
|
159
|
+
const stats = await stat(resolvedPath);
|
|
130
160
|
if (!stats.isFile()) {
|
|
131
161
|
return NextResponse.json({ error: `Not a file: ${body.file_path}` }, { status: 400 });
|
|
132
162
|
}
|
|
133
163
|
|
|
134
|
-
const originalName = basename(
|
|
164
|
+
const originalName = basename(resolvedPath);
|
|
135
165
|
const ext = extname(originalName).toLowerCase();
|
|
136
166
|
const mimeType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
137
167
|
const id = crypto.randomUUID();
|
|
@@ -140,7 +170,7 @@ export async function POST(req: NextRequest) {
|
|
|
140
170
|
const uploadsDir = getStagentUploadsDir();
|
|
141
171
|
await mkdir(uploadsDir, { recursive: true });
|
|
142
172
|
const storagePath = join(uploadsDir, filename);
|
|
143
|
-
await copyFile(
|
|
173
|
+
await copyFile(resolvedPath, storagePath);
|
|
144
174
|
|
|
145
175
|
const now = new Date();
|
|
146
176
|
await db.insert(documents).values({
|
|
@@ -1,23 +1,39 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { getLatestScan } from "@/lib/environment/data";
|
|
3
|
-
import { suggestProfiles } from "@/lib/environment/profile-generator";
|
|
3
|
+
import { suggestProfiles, suggestProfilesTiered } from "@/lib/environment/profile-generator";
|
|
4
4
|
|
|
5
5
|
/** GET: Suggest profiles based on latest (or specified) scan. */
|
|
6
6
|
export async function GET(req: NextRequest) {
|
|
7
7
|
const url = new URL(req.url);
|
|
8
8
|
const scanId = url.searchParams.get("scanId");
|
|
9
|
+
const tiered = url.searchParams.get("tiered") === "true";
|
|
9
10
|
|
|
10
11
|
let resolvedScanId = scanId;
|
|
11
12
|
if (!resolvedScanId) {
|
|
12
13
|
const latest = getLatestScan();
|
|
13
14
|
if (!latest) {
|
|
14
|
-
return NextResponse.json({
|
|
15
|
+
return NextResponse.json({
|
|
16
|
+
suggestions: [],
|
|
17
|
+
curated: [],
|
|
18
|
+
discovered: [],
|
|
19
|
+
message: "No scan found",
|
|
20
|
+
});
|
|
15
21
|
}
|
|
16
22
|
resolvedScanId = latest.id;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
if (tiered) {
|
|
26
|
+
const { curated, discovered } = suggestProfilesTiered(resolvedScanId);
|
|
27
|
+
return NextResponse.json({
|
|
28
|
+
curated,
|
|
29
|
+
discovered,
|
|
30
|
+
curatedCount: curated.length,
|
|
31
|
+
discoveredCount: discovered.length,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
20
34
|
|
|
35
|
+
// Legacy flat response (backward-compatible)
|
|
36
|
+
const suggestions = suggestProfiles(resolvedScanId);
|
|
21
37
|
return NextResponse.json({
|
|
22
38
|
suggestions,
|
|
23
39
|
count: suggestions.length,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
getArtifactCounts,
|
|
9
9
|
getToolCounts,
|
|
10
10
|
} from "@/lib/environment/data";
|
|
11
|
+
import { ensureFreshScan } from "@/lib/environment/auto-scan";
|
|
11
12
|
|
|
12
13
|
/** POST: Trigger a new environment scan. */
|
|
13
14
|
export async function POST(req: NextRequest) {
|
|
@@ -33,11 +34,17 @@ export async function POST(req: NextRequest) {
|
|
|
33
34
|
});
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
/** GET: Return the latest scan result from cache. */
|
|
37
|
+
/** GET: Return the latest scan result from cache. Auto-scans if stale and projectDir is provided. */
|
|
37
38
|
export async function GET(req: NextRequest) {
|
|
38
39
|
const url = new URL(req.url);
|
|
39
40
|
const projectId = url.searchParams.get("projectId");
|
|
40
41
|
const scanId = url.searchParams.get("scanId");
|
|
42
|
+
const projectDir = url.searchParams.get("projectDir");
|
|
43
|
+
|
|
44
|
+
// Auto-scan if a projectDir was provided and the latest scan is stale
|
|
45
|
+
if (projectDir && !scanId) {
|
|
46
|
+
ensureFreshScan(projectDir, projectId || undefined);
|
|
47
|
+
}
|
|
41
48
|
|
|
42
49
|
let scan;
|
|
43
50
|
if (scanId) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { agentMessages } 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 [message] = await db
|
|
13
|
+
.select()
|
|
14
|
+
.from(agentMessages)
|
|
15
|
+
.where(eq(agentMessages.id, id));
|
|
16
|
+
|
|
17
|
+
if (!message) {
|
|
18
|
+
return NextResponse.json({ error: "Handoff not found" }, { status: 404 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return NextResponse.json(message);
|
|
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 { action, approvedBy } = body as {
|
|
31
|
+
action?: "approve" | "reject";
|
|
32
|
+
approvedBy?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const [message] = await db
|
|
36
|
+
.select()
|
|
37
|
+
.from(agentMessages)
|
|
38
|
+
.where(eq(agentMessages.id, id));
|
|
39
|
+
|
|
40
|
+
if (!message) {
|
|
41
|
+
return NextResponse.json({ error: "Handoff not found" }, { status: 404 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!action || !["approve", "reject"].includes(action)) {
|
|
45
|
+
return NextResponse.json(
|
|
46
|
+
{ error: "action must be 'approve' or 'reject'" },
|
|
47
|
+
{ status: 400 }
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (message.status !== "pending") {
|
|
52
|
+
return NextResponse.json(
|
|
53
|
+
{ error: `Cannot ${action} a handoff with status: ${message.status}` },
|
|
54
|
+
{ status: 409 }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const now = new Date();
|
|
59
|
+
const newStatus = action === "approve" ? "accepted" : "rejected";
|
|
60
|
+
|
|
61
|
+
await db
|
|
62
|
+
.update(agentMessages)
|
|
63
|
+
.set({
|
|
64
|
+
status: newStatus,
|
|
65
|
+
approvedBy: approvedBy ?? "user",
|
|
66
|
+
respondedAt: now,
|
|
67
|
+
})
|
|
68
|
+
.where(eq(agentMessages.id, id));
|
|
69
|
+
|
|
70
|
+
const [updated] = await db
|
|
71
|
+
.select()
|
|
72
|
+
.from(agentMessages)
|
|
73
|
+
.where(eq(agentMessages.id, id));
|
|
74
|
+
|
|
75
|
+
return NextResponse.json(updated);
|
|
76
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { agentMessages } from "@/lib/db/schema";
|
|
4
|
+
import { desc, eq, and } from "drizzle-orm";
|
|
5
|
+
import { sendHandoff } from "@/lib/agents/handoff/bus";
|
|
6
|
+
|
|
7
|
+
export async function GET(req: NextRequest) {
|
|
8
|
+
const { searchParams } = new URL(req.url);
|
|
9
|
+
const status = searchParams.get("status");
|
|
10
|
+
const profileId = searchParams.get("profileId");
|
|
11
|
+
|
|
12
|
+
const conditions = [];
|
|
13
|
+
if (status) {
|
|
14
|
+
conditions.push(eq(agentMessages.status, status as "pending" | "accepted" | "in_progress" | "completed" | "rejected" | "expired"));
|
|
15
|
+
}
|
|
16
|
+
if (profileId) {
|
|
17
|
+
conditions.push(eq(agentMessages.toProfileId, profileId));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const result = await db
|
|
21
|
+
.select()
|
|
22
|
+
.from(agentMessages)
|
|
23
|
+
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
|
24
|
+
.orderBy(desc(agentMessages.createdAt))
|
|
25
|
+
.limit(100);
|
|
26
|
+
|
|
27
|
+
return NextResponse.json(result);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function POST(req: NextRequest) {
|
|
31
|
+
const body = await req.json();
|
|
32
|
+
const {
|
|
33
|
+
fromProfileId,
|
|
34
|
+
toProfileId,
|
|
35
|
+
sourceTaskId,
|
|
36
|
+
subject,
|
|
37
|
+
body: messageBody,
|
|
38
|
+
priority,
|
|
39
|
+
requiresApproval,
|
|
40
|
+
parentMessageId,
|
|
41
|
+
} = body as {
|
|
42
|
+
fromProfileId?: string;
|
|
43
|
+
toProfileId?: string;
|
|
44
|
+
sourceTaskId?: string;
|
|
45
|
+
subject?: string;
|
|
46
|
+
body?: string;
|
|
47
|
+
priority?: number;
|
|
48
|
+
requiresApproval?: boolean;
|
|
49
|
+
parentMessageId?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (!fromProfileId?.trim()) {
|
|
53
|
+
return NextResponse.json({ error: "fromProfileId is required" }, { status: 400 });
|
|
54
|
+
}
|
|
55
|
+
if (!toProfileId?.trim()) {
|
|
56
|
+
return NextResponse.json({ error: "toProfileId is required" }, { status: 400 });
|
|
57
|
+
}
|
|
58
|
+
if (!subject?.trim()) {
|
|
59
|
+
return NextResponse.json({ error: "subject is required" }, { status: 400 });
|
|
60
|
+
}
|
|
61
|
+
if (!messageBody?.trim()) {
|
|
62
|
+
return NextResponse.json({ error: "body is required" }, { status: 400 });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const messageId = await sendHandoff({
|
|
67
|
+
fromProfileId: fromProfileId.trim(),
|
|
68
|
+
toProfileId: toProfileId.trim(),
|
|
69
|
+
sourceTaskId: sourceTaskId ?? "",
|
|
70
|
+
subject: subject.trim(),
|
|
71
|
+
body: messageBody.trim(),
|
|
72
|
+
priority,
|
|
73
|
+
requiresApproval,
|
|
74
|
+
parentMessageId,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const [created] = await db
|
|
78
|
+
.select()
|
|
79
|
+
.from(agentMessages)
|
|
80
|
+
.where(eq(agentMessages.id, messageId));
|
|
81
|
+
|
|
82
|
+
return NextResponse.json(created, { status: 201 });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return NextResponse.json(
|
|
85
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
86
|
+
{ status: 400 }
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|