Add minimal stock listing app: actions, Postgres schema, stocks page, Claude Agent SDK harness

- Add stocks table (server/db) with list/get/add/update/delete actions, scoped
  per user, backing both the chat agent and the /stocks UI via
  useActionQuery/useActionMutation
- Add mobile-first /stocks page (shadcn table/cards, watchlist, search) and
  wire it into the sidebar, navigation state, and view-screen context
- Install the platform's Claude Agent SDK harness (server/plugins,
  server/routes/api/claude-agent) so the app agent runs on the platform's own
  Claude subscription instead of the Builder.io gateway; fix the vendored
  turn.post.ts route to resolve the session and wrap it in
  runWithRequestContext, since custom /api/ routes aren't auto-wrapped in
  request context the way /_agent-native/actions/* are
- Add shadcn table/badge/dialog/select components (Tabler icons, not lucide)
This commit is contained in:
exe.dev user
2026-07-29 12:18:14 +00:00
parent 138d195e4c
commit e859245867
25 changed files with 14804 additions and 10 deletions
+56
View File
@@ -0,0 +1,56 @@
import { defineAction } from "@agent-native/core/action";
import { and, desc, eq, or, sql } from "@agent-native/core/db/schema";
import { z } from "zod";
import { getDb } from "../server/db/index.js";
import { stocks } from "../server/db/schema.js";
function booleanParam(value: string | undefined): boolean | undefined {
if (value === undefined) return undefined;
return value === "true" || value === "1";
}
export default defineAction({
description:
"List stock listings, optionally filtered by search text (symbol or company name), exchange, or watchlisted status.",
schema: z.object({
search: z
.string()
.optional()
.describe("Filter by symbol or company name (case-insensitive substring match)"),
exchange: z.string().optional().describe("Filter by exchange (e.g. NASDAQ, NYSE)"),
watchlistedOnly: z
.string()
.optional()
.describe("Set to \"true\" to only return watchlisted stocks"),
}),
http: { method: "GET" },
readOnly: true,
run: async (args, ctx) => {
if (!ctx?.userEmail) throw new Error("Authentication required.");
const db = getDb();
const conditions = [eq(stocks.ownerEmail, ctx.userEmail)];
if (args.search) {
const term = `%${args.search.toLowerCase()}%`;
conditions.push(
or(
sql`lower(${stocks.symbol}) like ${term}`,
sql`lower(${stocks.companyName}) like ${term}`,
)!,
);
}
if (args.exchange) conditions.push(eq(stocks.exchange, args.exchange));
if (booleanParam(args.watchlistedOnly)) {
conditions.push(eq(stocks.watchlisted, true));
}
const rows = await db
.select()
.from(stocks)
.where(and(...conditions))
.orderBy(desc(stocks.watchlisted), stocks.symbol);
return { stocks: rows };
},
});