stagent 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/README.md +8 -8
  2. package/dist/cli.js +146 -2
  3. package/docs/.coverage-gaps.json +21 -0
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +36 -14
  6. package/docs/features/chat.md +33 -56
  7. package/docs/features/cost-usage.md +14 -10
  8. package/docs/features/dashboard-kanban.md +30 -13
  9. package/docs/features/delivery-channels.md +198 -0
  10. package/docs/features/design-system.md +10 -10
  11. package/docs/features/documents.md +8 -8
  12. package/docs/features/home-workspace.md +20 -15
  13. package/docs/features/inbox-notifications.md +22 -10
  14. package/docs/features/keyboard-navigation.md +11 -11
  15. package/docs/features/monitoring.md +1 -1
  16. package/docs/features/playbook.md +30 -32
  17. package/docs/features/profiles.md +33 -11
  18. package/docs/features/projects.md +2 -2
  19. package/docs/features/provider-runtimes.md +58 -14
  20. package/docs/features/schedules.md +70 -40
  21. package/docs/features/settings.md +74 -46
  22. package/docs/features/shared-components.md +7 -15
  23. package/docs/features/tool-permissions.md +9 -9
  24. package/docs/features/workflows.md +32 -21
  25. package/docs/getting-started.md +33 -9
  26. package/docs/index.md +25 -16
  27. package/docs/journeys/developer.md +124 -207
  28. package/docs/journeys/personal-use.md +70 -79
  29. package/docs/journeys/power-user.md +107 -151
  30. package/docs/journeys/work-use.md +81 -113
  31. package/docs/manifest.json +77 -45
  32. package/docs/superpowers/plans/2026-03-30-finish-in-progress-features.md +547 -0
  33. package/docs/use-cases/agency-operator.md +84 -0
  34. package/docs/use-cases/solo-founder.md +75 -0
  35. package/docs/why-stagent.md +59 -0
  36. package/package.json +10 -3
  37. package/src/app/api/channels/[id]/route.ts +104 -0
  38. package/src/app/api/channels/[id]/test/route.ts +52 -0
  39. package/src/app/api/channels/inbound/slack/route.ts +116 -0
  40. package/src/app/api/channels/inbound/telegram/poll/route.ts +140 -0
  41. package/src/app/api/channels/inbound/telegram/route.ts +87 -0
  42. package/src/app/api/channels/route.ts +72 -0
  43. package/src/app/api/chat/conversations/route.ts +15 -0
  44. package/src/app/api/chat/entities/search/route.ts +46 -31
  45. package/src/app/api/data/clear/route.ts +4 -0
  46. package/src/app/api/data/seed/route.ts +4 -0
  47. package/src/app/api/documents/route.ts +36 -6
  48. package/src/app/api/environment/profiles/suggest/route.ts +19 -3
  49. package/src/app/api/environment/scan/route.ts +8 -1
  50. package/src/app/api/handoffs/[id]/route.ts +76 -0
  51. package/src/app/api/handoffs/route.ts +89 -0
  52. package/src/app/api/memory/route.ts +181 -0
  53. package/src/app/api/profiles/[id]/route.ts +16 -1
  54. package/src/app/api/profiles/[id]/test/route.ts +4 -0
  55. package/src/app/api/profiles/[id]/test-results/route.ts +22 -0
  56. package/src/app/api/profiles/[id]/test-single/route.ts +64 -0
  57. package/src/app/api/profiles/assist/route.ts +35 -0
  58. package/src/app/api/profiles/import-repo/apply-updates/route.ts +123 -0
  59. package/src/app/api/profiles/import-repo/check-updates/route.ts +163 -0
  60. package/src/app/api/profiles/import-repo/confirm/route.ts +118 -0
  61. package/src/app/api/profiles/import-repo/preview/route.ts +107 -0
  62. package/src/app/api/profiles/import-repo/route.ts +29 -0
  63. package/src/app/api/profiles/import-repo/scan/route.ts +25 -0
  64. package/src/app/api/profiles/route.ts +73 -22
  65. package/src/app/api/runtimes/ollama/route.ts +86 -0
  66. package/src/app/api/runtimes/suggest/route.ts +29 -0
  67. package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
  68. package/src/app/api/schedules/[id]/route.ts +41 -3
  69. package/src/app/api/schedules/parse/route.ts +66 -0
  70. package/src/app/api/schedules/route.ts +71 -12
  71. package/src/app/api/settings/author-default/route.ts +7 -0
  72. package/src/app/api/settings/learning/route.ts +41 -0
  73. package/src/app/api/settings/ollama/route.ts +34 -0
  74. package/src/app/api/settings/providers/route.ts +57 -0
  75. package/src/app/api/settings/routing/route.ts +24 -0
  76. package/src/app/api/settings/web-search/route.ts +28 -0
  77. package/src/app/api/tasks/[id]/execute/route.ts +13 -1
  78. package/src/app/api/tasks/[id]/respond/route.ts +23 -1
  79. package/src/app/documents/page.tsx +3 -0
  80. package/src/app/environment/page.tsx +8 -1
  81. package/src/app/settings/page.tsx +10 -4
  82. package/src/app/workflows/[id]/edit/page.tsx +2 -0
  83. package/src/app/workflows/new/page.tsx +2 -0
  84. package/src/components/chat/chat-command-popover.tsx +22 -19
  85. package/src/components/chat/chat-input.tsx +5 -0
  86. package/src/components/chat/chat-model-selector.tsx +42 -1
  87. package/src/components/chat/chat-shell.tsx +2 -0
  88. package/src/components/dashboard/welcome-landing.tsx +9 -9
  89. package/src/components/environment/artifact-card.tsx +27 -1
  90. package/src/components/environment/environment-dashboard.tsx +50 -2
  91. package/src/components/environment/environment-summary-card.tsx +5 -2
  92. package/src/components/environment/suggested-profiles.tsx +117 -52
  93. package/src/components/handoffs/handoff-approval-card.tsx +159 -0
  94. package/src/components/memory/memory-browser.tsx +315 -0
  95. package/src/components/profiles/learned-context-panel.tsx +4 -4
  96. package/src/components/profiles/profile-assist-panel.tsx +512 -0
  97. package/src/components/profiles/profile-browser.tsx +109 -8
  98. package/src/components/profiles/profile-card.tsx +29 -1
  99. package/src/components/profiles/profile-detail-view.tsx +200 -28
  100. package/src/components/profiles/profile-form-view.tsx +220 -82
  101. package/src/components/profiles/repo-import-wizard.tsx +648 -0
  102. package/src/components/profiles/smoke-test-editor.tsx +106 -0
  103. package/src/components/schedules/schedule-create-sheet.tsx +9 -1
  104. package/src/components/schedules/schedule-form.tsx +348 -9
  105. package/src/components/schedules/schedule-list.tsx +15 -2
  106. package/src/components/settings/auth-method-selector.tsx +7 -1
  107. package/src/components/settings/budget-guardrails-section.tsx +111 -48
  108. package/src/components/settings/channels-section.tsx +526 -0
  109. package/src/components/settings/chat-settings-section.tsx +27 -1
  110. package/src/components/settings/data-management-section.tsx +8 -6
  111. package/src/components/settings/learning-context-section.tsx +124 -0
  112. package/src/components/settings/ollama-section.tsx +270 -0
  113. package/src/components/settings/providers-runtimes-section.tsx +499 -0
  114. package/src/components/settings/web-search-section.tsx +101 -0
  115. package/src/components/shared/tag-input.tsx +156 -0
  116. package/src/components/tasks/kanban-board.tsx +32 -0
  117. package/src/components/tasks/kanban-column.tsx +4 -2
  118. package/src/components/tasks/task-card.tsx +1 -0
  119. package/src/components/tasks/task-chip-bar.tsx +6 -1
  120. package/src/components/tasks/task-create-panel.tsx +55 -5
  121. package/src/components/workflows/workflow-form-view.tsx +38 -3
  122. package/src/hooks/use-chat-autocomplete.ts +24 -26
  123. package/src/hooks/use-project-skills.ts +66 -0
  124. package/src/hooks/use-tag-suggestions.ts +31 -0
  125. package/src/instrumentation.ts +4 -1
  126. package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
  127. package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
  128. package/src/lib/agents/agentic-loop.ts +235 -0
  129. package/src/lib/agents/browser-mcp.ts +59 -4
  130. package/src/lib/agents/claude-agent.ts +27 -200
  131. package/src/lib/agents/handoff/bus.ts +164 -0
  132. package/src/lib/agents/handoff/governance.ts +47 -0
  133. package/src/lib/agents/handoff/types.ts +16 -0
  134. package/src/lib/agents/learned-context.ts +27 -7
  135. package/src/lib/agents/memory/decay.ts +61 -0
  136. package/src/lib/agents/memory/extractor.ts +181 -0
  137. package/src/lib/agents/memory/retrieval.ts +96 -0
  138. package/src/lib/agents/memory/types.ts +6 -0
  139. package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
  140. package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
  141. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
  142. package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
  143. package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
  144. package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
  145. package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
  146. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
  147. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
  148. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
  149. package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
  150. package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
  151. package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
  152. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
  153. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
  154. package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
  155. package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
  156. package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
  157. package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
  158. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
  159. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
  160. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
  161. package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
  162. package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
  163. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
  164. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
  165. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
  166. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
  167. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
  168. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
  169. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
  170. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
  171. package/src/lib/agents/profiles/project-profiles.ts +193 -0
  172. package/src/lib/agents/profiles/registry.ts +130 -6
  173. package/src/lib/agents/profiles/types.ts +28 -0
  174. package/src/lib/agents/router.ts +174 -2
  175. package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
  176. package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
  177. package/src/lib/agents/runtime/catalog.ts +57 -2
  178. package/src/lib/agents/runtime/claude.ts +205 -1
  179. package/src/lib/agents/runtime/index.ts +22 -0
  180. package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
  181. package/src/lib/agents/runtime/openai-direct.ts +514 -0
  182. package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
  183. package/src/lib/agents/runtime/types.ts +2 -0
  184. package/src/lib/agents/tool-permissions.ts +203 -0
  185. package/src/lib/channels/gateway.ts +321 -0
  186. package/src/lib/channels/poller.ts +268 -0
  187. package/src/lib/channels/registry.ts +90 -0
  188. package/src/lib/channels/slack-adapter.ts +188 -0
  189. package/src/lib/channels/telegram-adapter.ts +218 -0
  190. package/src/lib/channels/types.ts +75 -0
  191. package/src/lib/channels/webhook-adapter.ts +74 -0
  192. package/src/lib/chat/context-builder.ts +22 -2
  193. package/src/lib/chat/engine.ts +95 -13
  194. package/src/lib/chat/ollama-engine.ts +198 -0
  195. package/src/lib/chat/stagent-tools.ts +106 -20
  196. package/src/lib/chat/tool-catalog.ts +24 -0
  197. package/src/lib/chat/tool-registry.ts +90 -0
  198. package/src/lib/chat/tools/chat-history-tools.ts +4 -4
  199. package/src/lib/chat/tools/document-tools.ts +7 -7
  200. package/src/lib/chat/tools/handoff-tools.ts +70 -0
  201. package/src/lib/chat/tools/notification-tools.ts +4 -4
  202. package/src/lib/chat/tools/profile-tools.ts +3 -3
  203. package/src/lib/chat/tools/project-tools.ts +3 -3
  204. package/src/lib/chat/tools/schedule-tools.ts +29 -13
  205. package/src/lib/chat/tools/settings-tools.ts +2 -2
  206. package/src/lib/chat/tools/task-tools.ts +66 -11
  207. package/src/lib/chat/tools/usage-tools.ts +2 -2
  208. package/src/lib/chat/tools/workflow-tools.ts +8 -8
  209. package/src/lib/chat/types.ts +11 -5
  210. package/src/lib/constants/known-tools.ts +19 -0
  211. package/src/lib/constants/prose-styles.ts +1 -1
  212. package/src/lib/constants/settings.ts +7 -0
  213. package/src/lib/data/channel-bindings.ts +85 -0
  214. package/src/lib/data/clear.ts +22 -0
  215. package/src/lib/data/profile-test-results.ts +48 -0
  216. package/src/lib/data/seed-data/conversations.ts +196 -0
  217. package/src/lib/data/seed-data/learned-context.ts +99 -0
  218. package/src/lib/data/seed-data/notifications.ts +54 -1
  219. package/src/lib/data/seed-data/profile-test-results.ts +96 -0
  220. package/src/lib/data/seed-data/repo-imports.ts +51 -0
  221. package/src/lib/data/seed-data/views.ts +60 -0
  222. package/src/lib/data/seed.ts +51 -0
  223. package/src/lib/db/bootstrap.ts +162 -0
  224. package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
  225. package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
  226. package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
  227. package/src/lib/db/schema.ts +190 -1
  228. package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
  229. package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
  230. package/src/lib/environment/auto-scan.ts +48 -0
  231. package/src/lib/environment/data.ts +25 -0
  232. package/src/lib/environment/profile-generator.ts +40 -10
  233. package/src/lib/environment/profile-linker.ts +143 -0
  234. package/src/lib/environment/profile-rules.ts +96 -0
  235. package/src/lib/import/dedup.ts +149 -0
  236. package/src/lib/import/format-adapter.ts +631 -0
  237. package/src/lib/import/github-api.ts +219 -0
  238. package/src/lib/import/repo-scanner.ts +251 -0
  239. package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
  240. package/src/lib/schedules/active-hours.ts +120 -0
  241. package/src/lib/schedules/heartbeat-parser.ts +224 -0
  242. package/src/lib/schedules/heartbeat-prompt.ts +153 -0
  243. package/src/lib/schedules/nlp-parser.ts +357 -0
  244. package/src/lib/schedules/scheduler.ts +218 -3
  245. package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
  246. package/src/lib/settings/helpers.ts +6 -0
  247. package/src/lib/settings/routing.ts +24 -0
  248. package/src/lib/settings/runtime-setup.ts +28 -1
  249. package/src/lib/usage/ledger.ts +2 -1
  250. package/src/lib/validators/__tests__/settings.test.ts +9 -0
  251. package/src/lib/validators/profile.ts +39 -0
  252. package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
  253. package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
  254. package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
  255. package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
  256. package/src/lib/workflows/blueprints/builtins/lead-research-pipeline.yaml +82 -0
@@ -0,0 +1,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") ?? "10", 10), 20);
24
+ const limit = Math.min(parseInt(url.searchParams.get("limit") ?? "20", 10), 30);
25
25
 
26
- if (!query.trim()) {
27
- return NextResponse.json({ results: [] });
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
- db
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
- db
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
- db
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
- db
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
- db
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 lowerQuery = query.toLowerCase();
88
- const profileMatches = listProfiles()
89
- .filter((p) => p.name.toLowerCase().includes(lowerQuery) || p.id.toLowerCase().includes(lowerQuery))
90
- .slice(0, perType);
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({ entityType: "profile", entityId: p.id, label: p.name });
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, sql } from "drizzle-orm";
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(body.file_path);
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(body.file_path);
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(body.file_path);
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(body.file_path, storagePath);
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({ suggestions: [], message: "No scan found" });
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
- const suggestions = suggestProfiles(resolvedScanId);
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
+ }