@rudderhq/agent-runtime-opencode-local 0.6.6-canary.8 → 0.7.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.
Files changed (51) hide show
  1. package/package.json +2 -2
  2. package/skills/app-builder/SKILL.md +104 -0
  3. package/skills/app-builder/agents/openai.yaml +14 -0
  4. package/skills/app-builder/assets/scaffold/.env.example +3 -0
  5. package/skills/app-builder/assets/scaffold/app/api/%5F%5Frudder/health/route.ts +20 -0
  6. package/skills/app-builder/assets/scaffold/app/api/contacts/[id]/route.ts +39 -0
  7. package/skills/app-builder/assets/scaffold/app/api/contacts/route.ts +31 -0
  8. package/skills/app-builder/assets/scaffold/app/api/data/export/route.ts +19 -0
  9. package/skills/app-builder/assets/scaffold/app/api/data/import/route.ts +40 -0
  10. package/skills/app-builder/assets/scaffold/app/globals.css +32 -0
  11. package/skills/app-builder/assets/scaffold/app/layout.tsx +15 -0
  12. package/skills/app-builder/assets/scaffold/app/page.tsx +9 -0
  13. package/skills/app-builder/assets/scaffold/components/contacts-workspace.tsx +292 -0
  14. package/skills/app-builder/assets/scaffold/components/ui/button.tsx +42 -0
  15. package/skills/app-builder/assets/scaffold/components/ui/card.tsx +19 -0
  16. package/skills/app-builder/assets/scaffold/components/ui/input.tsx +17 -0
  17. package/skills/app-builder/assets/scaffold/components/ui/label.tsx +6 -0
  18. package/skills/app-builder/assets/scaffold/data/.gitkeep +1 -0
  19. package/skills/app-builder/assets/scaffold/drizzle.config.ts +18 -0
  20. package/skills/app-builder/assets/scaffold/instrumentation.ts +5 -0
  21. package/skills/app-builder/assets/scaffold/lib/data-transfer.ts +44 -0
  22. package/skills/app-builder/assets/scaffold/lib/db/client.ts +78 -0
  23. package/skills/app-builder/assets/scaffold/lib/db/schema.ts +35 -0
  24. package/skills/app-builder/assets/scaffold/lib/domain.ts +18 -0
  25. package/skills/app-builder/assets/scaffold/lib/jobs/runner.ts +56 -0
  26. package/skills/app-builder/assets/scaffold/lib/utils.ts +6 -0
  27. package/skills/app-builder/assets/scaffold/migrations/0000_app_builder_foundation.sql +27 -0
  28. package/skills/app-builder/assets/scaffold/migrations/meta/_journal.json +13 -0
  29. package/skills/app-builder/assets/scaffold/next-env.d.ts +6 -0
  30. package/skills/app-builder/assets/scaffold/next.config.ts +8 -0
  31. package/skills/app-builder/assets/scaffold/package.json +47 -0
  32. package/skills/app-builder/assets/scaffold/playwright.config.ts +30 -0
  33. package/skills/app-builder/assets/scaffold/pnpm-lock.yaml +2687 -0
  34. package/skills/app-builder/assets/scaffold/postcss.config.mjs +5 -0
  35. package/skills/app-builder/assets/scaffold/rudder.app.json +32 -0
  36. package/skills/app-builder/assets/scaffold/scripts/migrate.ts +21 -0
  37. package/skills/app-builder/assets/scaffold/scripts/seed.ts +31 -0
  38. package/skills/app-builder/assets/scaffold/scripts/snapshot.ts +17 -0
  39. package/skills/app-builder/assets/scaffold/tests/e2e/app.spec.ts +50 -0
  40. package/skills/app-builder/assets/scaffold/tests/unit/data-transfer.test.ts +28 -0
  41. package/skills/app-builder/assets/scaffold/tests/unit/domain.test.ts +23 -0
  42. package/skills/app-builder/assets/scaffold/tsconfig.json +41 -0
  43. package/skills/app-builder/assets/scaffold/vitest.config.ts +14 -0
  44. package/skills/app-builder/evals/evals.json +65 -0
  45. package/skills/app-builder/references/data-safety.md +38 -0
  46. package/skills/app-builder/references/design-guidelines.md +20 -0
  47. package/skills/app-builder/references/migrations-and-promotion.md +36 -0
  48. package/skills/app-builder/references/scaffold-contract.md +77 -0
  49. package/skills/app-builder/references/verification.md +23 -0
  50. package/skills/app-builder/scripts/scaffold.mjs +58 -0
  51. package/skills/app-builder/scripts/validate-manifest.mjs +63 -0
@@ -0,0 +1,42 @@
1
+ import { cn } from "@/lib/utils";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import * as React from "react";
5
+
6
+ const buttonVariants = cva(
7
+ "inline-flex min-h-10 items-center justify-center gap-2 rounded-lg px-4 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] disabled:pointer-events-none disabled:opacity-50",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-[var(--primary)] text-[var(--primary-foreground)] hover:brightness-95",
12
+ outline: "border bg-[var(--card)] hover:bg-[var(--muted)]",
13
+ ghost: "hover:bg-[var(--muted)]",
14
+ destructive: "bg-[var(--destructive)] text-white hover:brightness-95",
15
+ },
16
+ size: {
17
+ default: "h-10",
18
+ sm: "h-9 px-3",
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ variant: "default",
23
+ size: "default",
24
+ },
25
+ },
26
+ );
27
+
28
+ export interface ButtonProps
29
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
30
+ VariantProps<typeof buttonVariants> {
31
+ asChild?: boolean;
32
+ }
33
+
34
+ export function Button({ className, variant, size, asChild, ...props }: ButtonProps) {
35
+ const Component = asChild ? Slot : "button";
36
+ return (
37
+ <Component
38
+ className={cn(buttonVariants({ variant, size, className }))}
39
+ {...props}
40
+ />
41
+ );
42
+ }
@@ -0,0 +1,19 @@
1
+ import { cn } from "@/lib/utils";
2
+ import * as React from "react";
3
+
4
+ export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
5
+ return (
6
+ <section
7
+ className={cn("rounded-xl border bg-[var(--card)] text-[var(--card-foreground)] shadow-sm", className)}
8
+ {...props}
9
+ />
10
+ );
11
+ }
12
+
13
+ export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
14
+ return <div className={cn("border-b px-5 py-4", className)} {...props} />;
15
+ }
16
+
17
+ export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
18
+ return <div className={cn("p-5", className)} {...props} />;
19
+ }
@@ -0,0 +1,17 @@
1
+ import { cn } from "@/lib/utils";
2
+ import * as React from "react";
3
+
4
+ export const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
5
+ ({ className, type, ...props }, ref) => (
6
+ <input
7
+ ref={ref}
8
+ type={type}
9
+ className={cn(
10
+ "flex h-10 w-full rounded-lg border bg-white px-3 py-2 text-sm outline-none placeholder:text-[var(--muted-foreground)] focus:ring-2 focus:ring-[var(--ring)] disabled:opacity-50",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ ),
16
+ );
17
+ Input.displayName = "Input";
@@ -0,0 +1,6 @@
1
+ import { cn } from "@/lib/utils";
2
+ import * as React from "react";
3
+
4
+ export function Label({ className, ...props }: React.LabelHTMLAttributes<HTMLLabelElement>) {
5
+ return <label className={cn("text-sm font-medium", className)} {...props} />;
6
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+ import path from "node:path";
3
+
4
+ const dataDir = process.env.RUDDER_APP_DATA_DIR
5
+ ? path.resolve(process.env.RUDDER_APP_DATA_DIR)
6
+ : path.resolve("data");
7
+ const mode = process.env.RUDDER_APP_DATA_MODE === "production"
8
+ ? "app.sqlite"
9
+ : "dev.sqlite";
10
+
11
+ export default defineConfig({
12
+ dialect: "sqlite",
13
+ schema: "./lib/db/schema.ts",
14
+ out: "./migrations",
15
+ dbCredentials: {
16
+ url: path.join(dataDir, mode),
17
+ },
18
+ });
@@ -0,0 +1,5 @@
1
+ export async function register() {
2
+ if (process.env.NEXT_RUNTIME !== "nodejs") return;
3
+ const { startJobRunner } = await import("@/lib/jobs/runner");
4
+ startJobRunner();
5
+ }
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ import { contactStatusSchema } from "./domain";
3
+
4
+ export const exportedContactSchema = z.object({
5
+ id: z.string().uuid(),
6
+ name: z.string().min(1).max(120),
7
+ email: z.email().max(320),
8
+ company: z.string().max(160),
9
+ status: contactStatusSchema,
10
+ createdAt: z.iso.datetime(),
11
+ updatedAt: z.iso.datetime(),
12
+ });
13
+
14
+ export const importEnvelopeSchema = z.object({
15
+ format: z.literal("rudder-app-data/v1"),
16
+ exportedAt: z.iso.datetime(),
17
+ data: z.object({
18
+ contacts: z.array(exportedContactSchema).max(100_000),
19
+ }),
20
+ }).strict();
21
+
22
+ type ContactRow = {
23
+ id: string;
24
+ name: string;
25
+ email: string;
26
+ company: string;
27
+ status: "new" | "contacted" | "replied" | "paused";
28
+ createdAt: Date;
29
+ updatedAt: Date;
30
+ };
31
+
32
+ export function buildExportEnvelope(rows: ContactRow[]) {
33
+ return {
34
+ format: "rudder-app-data/v1" as const,
35
+ exportedAt: new Date().toISOString(),
36
+ data: {
37
+ contacts: rows.map((row) => ({
38
+ ...row,
39
+ createdAt: row.createdAt.toISOString(),
40
+ updatedAt: row.updatedAt.toISOString(),
41
+ })),
42
+ },
43
+ };
44
+ }
@@ -0,0 +1,78 @@
1
+ import {
2
+ drizzle,
3
+ type SqliteRemoteDatabase,
4
+ } from "drizzle-orm/sqlite-proxy";
5
+ import { mkdirSync } from "node:fs";
6
+ import path from "node:path";
7
+ import { DatabaseSync, type SQLInputValue } from "node:sqlite";
8
+ import * as schema from "./schema";
9
+
10
+ type DatabaseHandle = {
11
+ sqlite: DatabaseSync;
12
+ db: SqliteRemoteDatabase<typeof schema>;
13
+ filePath: string;
14
+ };
15
+
16
+ const handles = new Map<string, DatabaseHandle>();
17
+
18
+ export function dataMode() {
19
+ return process.env.RUDDER_APP_DATA_MODE === "production"
20
+ ? "production"
21
+ : "development";
22
+ }
23
+
24
+ export function dataDirectory() {
25
+ return process.env.RUDDER_APP_DATA_DIR
26
+ ? path.resolve(process.env.RUDDER_APP_DATA_DIR)
27
+ : path.resolve("data");
28
+ }
29
+
30
+ export function databasePath() {
31
+ if (process.env.RUDDER_APP_DATA_DIR) {
32
+ return path.join(
33
+ dataDirectory(),
34
+ dataMode() === "production" ? "app.sqlite" : "dev.sqlite",
35
+ );
36
+ }
37
+ return path.join(
38
+ dataDirectory(),
39
+ dataMode() === "production" ? "production/app.sqlite" : "development/dev.sqlite",
40
+ );
41
+ }
42
+
43
+ export function getDatabase(): DatabaseHandle {
44
+ const filePath = databasePath();
45
+ const cached = handles.get(filePath);
46
+ if (cached) return cached;
47
+
48
+ mkdirSync(path.dirname(filePath), { recursive: true });
49
+ const sqlite = new DatabaseSync(filePath);
50
+ sqlite.exec("PRAGMA journal_mode = WAL");
51
+ sqlite.exec("PRAGMA foreign_keys = ON");
52
+ sqlite.exec("PRAGMA busy_timeout = 5000");
53
+ const db = drizzle(async (query, parameters, method): Promise<{ rows: unknown[] }> => {
54
+ const statement = sqlite.prepare(query);
55
+ statement.setReturnArrays(true);
56
+ const values = parameters as SQLInputValue[];
57
+ if (method === "run") {
58
+ statement.run(...values);
59
+ return { rows: [] };
60
+ }
61
+ if (method === "get") {
62
+ return { rows: statement.get(...values) as unknown as unknown[] };
63
+ }
64
+ return { rows: statement.all(...values) as unknown as unknown[][] };
65
+ }, { schema });
66
+ const handle: DatabaseHandle = {
67
+ sqlite,
68
+ db,
69
+ filePath,
70
+ };
71
+ handles.set(filePath, handle);
72
+ return handle;
73
+ }
74
+
75
+ export function closeDatabases() {
76
+ for (const handle of handles.values()) handle.sqlite.close();
77
+ handles.clear();
78
+ }
@@ -0,0 +1,35 @@
1
+ import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
2
+
3
+ export const contacts = sqliteTable("contacts", {
4
+ id: text("id").primaryKey(),
5
+ name: text("name").notNull(),
6
+ email: text("email").notNull(),
7
+ company: text("company").notNull().default(""),
8
+ status: text("status", { enum: ["new", "contacted", "replied", "paused"] })
9
+ .notNull()
10
+ .default("new"),
11
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
12
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
13
+ }, (table) => [
14
+ uniqueIndex("contacts_email_uq").on(table.email),
15
+ ]);
16
+
17
+ export const jobs = sqliteTable("jobs", {
18
+ id: text("id").primaryKey(),
19
+ kind: text("kind").notNull(),
20
+ idempotencyKey: text("idempotency_key").notNull(),
21
+ payloadJson: text("payload_json").notNull().default("{}"),
22
+ status: text("status", { enum: ["pending", "running", "completed", "failed", "missed"] })
23
+ .notNull()
24
+ .default("pending"),
25
+ catchUpPolicy: text("catch_up_policy", { enum: ["run", "skip", "prompt"] })
26
+ .notNull()
27
+ .default("prompt"),
28
+ scheduledFor: integer("scheduled_for", { mode: "timestamp_ms" }).notNull(),
29
+ attempts: integer("attempts").notNull().default(0),
30
+ lastError: text("last_error"),
31
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
32
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
33
+ }, (table) => [
34
+ uniqueIndex("jobs_idempotency_key_uq").on(table.idempotencyKey),
35
+ ]);
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+
3
+ export const contactStatusSchema = z.enum(["new", "contacted", "replied", "paused"]);
4
+
5
+ export const contactCreateSchema = z.object({
6
+ name: z.string().trim().min(1).max(120),
7
+ email: z.email().max(320),
8
+ company: z.string().trim().max(160).default(""),
9
+ status: contactStatusSchema.default("new"),
10
+ });
11
+
12
+ export const contactUpdateSchema = contactCreateSchema.partial().refine(
13
+ (value) => Object.keys(value).length > 0,
14
+ "At least one contact field is required",
15
+ );
16
+
17
+ export type ContactInput = z.infer<typeof contactCreateSchema>;
18
+ export type ContactStatus = z.infer<typeof contactStatusSchema>;
@@ -0,0 +1,56 @@
1
+ import { getDatabase } from "@/lib/db/client";
2
+
3
+ type RunnerState = {
4
+ timer: NodeJS.Timeout | null;
5
+ ticking: boolean;
6
+ };
7
+
8
+ const globalState = globalThis as typeof globalThis & {
9
+ __rudderAppJobRunner?: RunnerState;
10
+ };
11
+
12
+ function state(): RunnerState {
13
+ globalState.__rudderAppJobRunner ??= { timer: null, ticking: false };
14
+ return globalState.__rudderAppJobRunner;
15
+ }
16
+
17
+ async function tick() {
18
+ const current = state();
19
+ if (current.ticking) return;
20
+ current.ticking = true;
21
+ try {
22
+ const { sqlite } = getDatabase();
23
+ const now = Date.now();
24
+ sqlite.prepare(`
25
+ update jobs
26
+ set status = case catch_up_policy
27
+ when 'skip' then 'missed'
28
+ when 'run' then 'pending'
29
+ else 'missed'
30
+ end,
31
+ updated_at = ?
32
+ where status = 'pending' and scheduled_for < ?
33
+ `).run(now, now - 60_000);
34
+ // Domain-specific handlers should claim one pending job transactionally,
35
+ // execute it with the persisted idempotency key, and then complete/fail it.
36
+ } catch {
37
+ // The health endpoint remains authoritative. A missing pre-migration table
38
+ // must not create an unhandled background rejection during startup.
39
+ } finally {
40
+ current.ticking = false;
41
+ }
42
+ }
43
+
44
+ export function startJobRunner() {
45
+ const current = state();
46
+ if (current.timer) return;
47
+ current.timer = setInterval(() => void tick(), 15_000);
48
+ current.timer.unref();
49
+ void tick();
50
+ }
51
+
52
+ export function stopJobRunner() {
53
+ const current = state();
54
+ if (current.timer) clearInterval(current.timer);
55
+ current.timer = null;
56
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,27 @@
1
+ CREATE TABLE `contacts` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `name` text NOT NULL,
4
+ `email` text NOT NULL,
5
+ `company` text DEFAULT '' NOT NULL,
6
+ `status` text DEFAULT 'new' NOT NULL,
7
+ `created_at` integer NOT NULL,
8
+ `updated_at` integer NOT NULL
9
+ );
10
+ --> statement-breakpoint
11
+ CREATE UNIQUE INDEX `contacts_email_uq` ON `contacts` (`email`);
12
+ --> statement-breakpoint
13
+ CREATE TABLE `jobs` (
14
+ `id` text PRIMARY KEY NOT NULL,
15
+ `kind` text NOT NULL,
16
+ `idempotency_key` text NOT NULL,
17
+ `payload_json` text DEFAULT '{}' NOT NULL,
18
+ `status` text DEFAULT 'pending' NOT NULL,
19
+ `catch_up_policy` text DEFAULT 'prompt' NOT NULL,
20
+ `scheduled_for` integer NOT NULL,
21
+ `attempts` integer DEFAULT 0 NOT NULL,
22
+ `last_error` text,
23
+ `created_at` integer NOT NULL,
24
+ `updated_at` integer NOT NULL
25
+ );
26
+ --> statement-breakpoint
27
+ CREATE UNIQUE INDEX `jobs_idempotency_key_uq` ON `jobs` (`idempotency_key`);
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "6",
8
+ "when": 1785312000000,
9
+ "tag": "0000_app_builder_foundation",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,6 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ import "./.next/dev/types/routes.d.ts";
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,8 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ output: "standalone",
5
+ poweredByHeader: false,
6
+ };
7
+
8
+ export default nextConfig;
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "rudder-app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.5.0"
8
+ },
9
+ "scripts": {
10
+ "dev": "next dev --hostname 127.0.0.1",
11
+ "build": "next build",
12
+ "start": "next start --hostname 127.0.0.1",
13
+ "typecheck": "tsc --noEmit",
14
+ "test": "vitest run",
15
+ "test:e2e": "playwright test",
16
+ "db:generate": "drizzle-kit generate",
17
+ "db:migrate": "tsx scripts/migrate.ts",
18
+ "db:seed": "tsx scripts/seed.ts",
19
+ "data:snapshot": "tsx scripts/snapshot.ts",
20
+ "verify": "tsc --noEmit && vitest run && next build",
21
+ "predev": "tsx scripts/migrate.ts && tsx scripts/seed.ts"
22
+ },
23
+ "dependencies": {
24
+ "@radix-ui/react-slot": "1.2.4",
25
+ "class-variance-authority": "0.7.1",
26
+ "clsx": "2.1.1",
27
+ "drizzle-orm": "0.45.2",
28
+ "lucide-react": "1.27.0",
29
+ "next": "16.2.12",
30
+ "react": "19.2.8",
31
+ "react-dom": "19.2.8",
32
+ "tailwind-merge": "3.6.0",
33
+ "zod": "4.4.3"
34
+ },
35
+ "devDependencies": {
36
+ "@playwright/test": "1.58.2",
37
+ "@tailwindcss/postcss": "4.3.3",
38
+ "@types/node": "24.12.0",
39
+ "@types/react": "19.2.7",
40
+ "@types/react-dom": "19.2.3",
41
+ "drizzle-kit": "0.31.10",
42
+ "tailwindcss": "4.3.3",
43
+ "tsx": "4.23.1",
44
+ "typescript": "5.9.3",
45
+ "vitest": "3.2.7"
46
+ }
47
+ }
@@ -0,0 +1,30 @@
1
+ import { defineConfig, devices } from "@playwright/test";
2
+
3
+ export default defineConfig({
4
+ testDir: "./tests/e2e",
5
+ fullyParallel: false,
6
+ retries: 0,
7
+ reporter: "list",
8
+ use: {
9
+ baseURL: process.env.RUDDER_APP_BASE_URL ?? "http://127.0.0.1:3000",
10
+ trace: "retain-on-failure",
11
+ },
12
+ webServer: process.env.RUDDER_APP_BASE_URL
13
+ ? undefined
14
+ : {
15
+ command: "pnpm exec next dev --hostname 127.0.0.1 --port 3000",
16
+ url: "http://127.0.0.1:3000/api/__rudder/health",
17
+ reuseExistingServer: true,
18
+ timeout: 120_000,
19
+ },
20
+ projects: [
21
+ { name: "desktop", use: { ...devices["Desktop Chrome"] } },
22
+ {
23
+ name: "mobile",
24
+ use: {
25
+ viewport: { width: 390, height: 844 },
26
+ isMobile: true,
27
+ },
28
+ },
29
+ ],
30
+ });