@camelai/create-run-app 0.0.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 qaml-ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,13 @@
1
1
  # @camelai/create-run-app
2
2
 
3
- Placeholder. The first release is 0.11.0, from https://github.com/qaml-ai/run.
3
+ Create a Next.js app with a streaming chat to an agent on camelRun:
4
+
5
+ ```sh
6
+ npm create @camelai/run-app my-app -- --api-key $CAMELAI_API_KEY
7
+ cd my-app && npm run dev
8
+ ```
9
+
10
+ Options: `--api-key` (default: `CAMELAI_API_KEY`), `--base-url` (a runtime other than
11
+ https://agents.camelai.dev), `--no-install`. The app has a chat (`<AgentChat>`), its route
12
+ (`createAgentHandler`, which keeps your key on the server), a tool drawn with its own component, and a
13
+ demo sign-in to replace with yours.
package/index.js ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * npm create @camelai/run-app [directory] [--api-key <key>] [--base-url <url>] [--no-install]
4
+ *
5
+ * Copies the Next.js starter into `directory` (default: agent-app), writes .env.local with the API key
6
+ * (from --api-key or CAMELAI_API_KEY) and installs its dependencies with the package manager it was run with.
7
+ */
8
+ import { spawnSync } from "node:child_process";
9
+ import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
10
+ import { basename, join, resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const here = fileURLToPath(new URL(".", import.meta.url));
14
+ const { version } = JSON.parse(readFileSync(join(here, "package.json"), "utf8"));
15
+
16
+ function parse(argv) {
17
+ const options = { directory: undefined, apiKey: process.env.CAMELAI_API_KEY, baseUrl: process.env.CAMELAI_BASE_URL, install: true, help: false };
18
+ for (let at = 0; at < argv.length; at++) {
19
+ const arg = argv[at];
20
+ const value = () => { const next = argv[++at]; if (next === undefined) fail(`${arg} needs a value`); return next; };
21
+ if (arg === "--api-key") options.apiKey = value();
22
+ else if (arg.startsWith("--api-key=")) options.apiKey = arg.slice(10);
23
+ else if (arg === "--base-url") options.baseUrl = value();
24
+ else if (arg.startsWith("--base-url=")) options.baseUrl = arg.slice(11);
25
+ else if (arg === "--no-install") options.install = false;
26
+ else if (arg === "-h" || arg === "--help") options.help = true;
27
+ else if (arg.startsWith("-")) fail(`Unknown option ${arg}`);
28
+ else if (options.directory === undefined) options.directory = arg;
29
+ else fail(`Unexpected argument ${arg}`);
30
+ }
31
+ return options;
32
+ }
33
+ function fail(message) { console.error(`create-run-app: ${message}`); process.exit(1); }
34
+
35
+ const options = parse(process.argv.slice(2));
36
+ if (options.help) {
37
+ console.log("Usage: npm create @camelai/run-app [directory] [--api-key <key>] [--base-url <url>] [--no-install]");
38
+ process.exit(0);
39
+ }
40
+ const target = resolve(options.directory ?? "agent-app");
41
+ if (existsSync(target) && readdirSync(target).length) fail(`${target} is not empty`);
42
+ const name = basename(target).toLowerCase().replace(/[^a-z0-9-_.]/g, "-").replace(/^[-_.]+/, "") || "agent-app";
43
+
44
+ cpSync(join(here, "template"), target, { recursive: true });
45
+ renameSync(join(target, "_gitignore"), join(target, ".gitignore"));
46
+ const manifest = join(target, "package.json");
47
+ writeFileSync(manifest, readFileSync(manifest, "utf8").replaceAll("{{version}}", version).replace('"name": "agent-app"', `"name": "${name}"`));
48
+ const env = [`CAMELAI_API_KEY=${options.apiKey ?? ""}`, ...(options.baseUrl ? [`CAMELAI_BASE_URL=${options.baseUrl}`] : [])].join("\n");
49
+ writeFileSync(join(target, ".env.local"), `${env}\n`, { mode: 0o600 });
50
+
51
+ const agent = process.env.npm_config_user_agent ?? "";
52
+ const manager = agent.startsWith("pnpm") ? "pnpm" : agent.startsWith("yarn") ? "yarn" : agent.startsWith("bun") ? "bun" : "npm";
53
+ if (options.install) {
54
+ console.log(`Installing dependencies with ${manager}…`);
55
+ const result = spawnSync(manager, ["install"], { cwd: target, stdio: "inherit", shell: process.platform === "win32" });
56
+ if (result.status !== 0) console.warn(`${manager} install failed; run it yourself in ${target}`);
57
+ }
58
+ const relative = options.directory ?? "agent-app";
59
+ console.log(`
60
+ Created ${relative}.
61
+
62
+ cd ${relative}${options.install ? "" : `\n ${manager} install`}${options.apiKey ? "" : "\n # add your API key to .env.local (https://agents.camelai.dev/console)"}
63
+ ${manager === "npm" ? "npm run" : manager} dev
64
+
65
+ Then open http://localhost:3000.`);
package/package.json CHANGED
@@ -1 +1,29 @@
1
- { "name": "@camelai/create-run-app", "version": "0.0.0", "description": "Placeholder: the first release (0.11.0) comes from github.com/qaml-ai/run.", "license": "MIT", "publishConfig": { "access": "public" } }
1
+ {
2
+ "name": "@camelai/create-run-app",
3
+ "version": "0.11.0",
4
+ "description": "Create a Next.js app with a streaming chat to an agent on camelRun: npm create @camelai/run-app",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/qaml-ai/run.git",
10
+ "directory": "packages/create-run-app"
11
+ },
12
+ "homepage": "https://agents.camelai.dev",
13
+ "publishConfig": {
14
+ "registry": "https://registry.npmjs.org",
15
+ "access": "public"
16
+ },
17
+ "bin": {
18
+ "create-run-app": "index.js"
19
+ },
20
+ "files": [
21
+ "index.js",
22
+ "template",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "engines": {
27
+ "node": ">=20"
28
+ }
29
+ }
@@ -0,0 +1,8 @@
1
+ # Your API key from https://agents.camelai.dev/console (keep it server-side: never NEXT_PUBLIC_).
2
+ CAMELAI_API_KEY=
3
+ # The agent's model, if not the one in app/api/agent/route.ts (any of GET /v1/models?available=true).
4
+ # AGENT_MODEL=openrouter/anthropic/claude-sonnet-5.5
5
+ # Only to use a runtime other than https://agents.camelai.dev (e.g. one you run locally).
6
+ # CAMELAI_BASE_URL=http://127.0.0.1:8790
7
+ # Allow anonymous demo users in production (only for a throwaway demo: see the README).
8
+ # DEMO_AUTH=1
@@ -0,0 +1,30 @@
1
+ # Agent app
2
+
3
+ > **⚠ Add your own sign-in before you deploy.** This starter's sign-in is a demo: every browser gets
4
+ > an anonymous user id, so anyone who can reach the app can use your agents (and your API key's
5
+ > credit). In production (`next build && next start`, or any host) the route refuses everyone until
6
+ > you replace `authorize` in `app/api/agent/route.ts` (and `proxy.ts`) with your auth, or opt in to
7
+ > anonymous demo users with `DEMO_AUTH=1`.
8
+
9
+ A Next.js app with a streaming chat to an agent on [camelRun](https://agents.camelai.dev).
10
+
11
+ ```sh
12
+ cp .env.example .env.local # add your CAMELAI_API_KEY
13
+ npm install
14
+ npm run dev # http://localhost:3000
15
+ ```
16
+
17
+ The agent uses `openrouter/anthropic/claude-sonnet-5.5` (set in `route.ts`, or with `AGENT_MODEL`).
18
+ Your account needs access to it: prepaid credit, or your own OpenRouter key in the console's
19
+ **Models & keys**. `GET /v1/models?available=true` lists the models your key can use.
20
+
21
+ - `app/api/agent/route.ts`: the route the chat talks to (`createAgentHandler`). It keeps your API key on
22
+ the server, makes each user their own agent, and defines its instructions and tools. Replace the demo
23
+ sign-in in `authorize` (and `proxy.ts`) with yours.
24
+ - `app/components/chat.tsx`: the chat (`<AgentChat>`), with `get_weather` drawn by your own component.
25
+ Theme it with CSS variables (`--agent-accent`, `--agent-radius`, …), or build your own UI with the hooks
26
+ in `@camelai/run-react` (`useMessages`, `useSend`, `useAgentStatus`, `useInputs`).
27
+
28
+ Tools in `route.ts` run inside this server, so they need a long-running process (`next dev`,
29
+ `next start`, a container). On serverless hosting, serve them with `serveTools` (with your `tenant`) from
30
+ `@camelai/run/server` and name them in a definition (`agent: { definition: "def_…" }`).
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ .next
3
+ out
4
+ .env*.local
5
+ .env
6
+ next-env.d.ts
7
+ *.tsbuildinfo
@@ -0,0 +1,39 @@
1
+ import { cookies } from "next/headers";
2
+ import { createAgentHandler } from "@camelai/run/server";
3
+ import { schema, tool } from "@camelai/run";
4
+
5
+ /**
6
+ * The route the chat talks to. Your API key (CAMELAI_API_KEY) stays on this server: browsers get
7
+ * short-lived read-only tokens for their own agent, and everything they send goes through here, as
8
+ * the user `authorize` returns.
9
+ */
10
+ export const POST = createAgentHandler({
11
+ authorize: async () => {
12
+ // ⚠ A DEMO SIGN-IN: anyone with a browser is a user. Replace this with your own (Clerk, Auth.js,
13
+ // your session) and return { userId, name } for a signed-in user, or null.
14
+ // Deployed (production), it refuses everyone unless you opt in with DEMO_AUTH=1.
15
+ if (process.env.NODE_ENV === "production" && process.env.DEMO_AUTH !== "1") {
16
+ throw Response.json({ error: { code: "unauthorized", message: "This app has no sign-in yet: add your own in app/api/agent/route.ts (or set DEMO_AUTH=1 to allow anonymous demo users)." } }, { status: 401 });
17
+ }
18
+ const userId = (await cookies()).get("demo_user")?.value;
19
+ return userId && /^[0-9a-f-]{36}$/.test(userId) ? { userId } : null;
20
+ },
21
+ agent: {
22
+ // Any model in GET /v1/models your account can use (?available=true): prepaid credit, or your own key for its provider.
23
+ model: process.env.AGENT_MODEL ?? "openrouter/anthropic/claude-sonnet-5.5",
24
+ instructions: "You are a friendly assistant in a demo app. Keep answers short. Use get_weather when asked about the weather.",
25
+ // Tools run here, in this server, while `next dev` or `next start` runs. For serverless hosting,
26
+ // serve them with serveTools and a definition instead (see the README).
27
+ tools: {
28
+ get_weather: tool({
29
+ description: "The current weather in a city",
30
+ input: schema.Object({ city: schema.String({ maxLength: 100 }) }),
31
+ execute: async ({ city }) => {
32
+ // Swap in a real weather API. This one is made up, so the demo needs no other key.
33
+ const seed = [...city].reduce((sum, char) => sum + char.charCodeAt(0), 0);
34
+ return { city, temperatureC: 8 + (seed % 22), conditions: ["Sunny", "Cloudy", "Rain", "Windy"][seed % 4] };
35
+ },
36
+ }),
37
+ },
38
+ },
39
+ });
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import { AgentChat } from "@camelai/run-react/ui";
4
+ import type { ToolRenderProps } from "@camelai/run-react";
5
+
6
+ /** A tool call rendered with your own component ("generative UI"): here, get_weather as a card. */
7
+ function WeatherCard({ args, state, result }: ToolRenderProps) {
8
+ const data = result?.data as { city: string; temperatureC: number; conditions: string } | undefined;
9
+ return (
10
+ <div style={{ border: "1px solid var(--agent-border)", borderRadius: 12, padding: "12px 16px", maxWidth: 280 }}>
11
+ <div style={{ fontSize: 13, color: "var(--agent-muted-fg)" }}>Weather · {String(args.city ?? "…")}</div>
12
+ {data ? (
13
+ <div style={{ fontSize: 28, fontWeight: 600 }}>{data.temperatureC}°C <span style={{ fontSize: 15, fontWeight: 400 }}>{data.conditions}</span></div>
14
+ ) : (
15
+ <div style={{ color: "var(--agent-muted-fg)" }}>{state === "error" ? "Could not get the weather." : "Checking…"}</div>
16
+ )}
17
+ </div>
18
+ );
19
+ }
20
+
21
+ export function Chat() {
22
+ return (
23
+ <AgentChat
24
+ endpoint="/api/agent"
25
+ tools={{ get_weather: WeatherCard }}
26
+ suggestions={["What's the weather in Lisbon?", "Write a haiku about deploys", "Explain SSE in two sentences"]}
27
+ style={{ flex: 1, minHeight: 0 }}
28
+ />
29
+ );
30
+ }
@@ -0,0 +1,5 @@
1
+ html, body { height: 100%; margin: 0; }
2
+ body { background: var(--page-bg, #fff); }
3
+ @media (prefers-color-scheme: dark) { body { --page-bg: #0a0a0a; } }
4
+ main { height: 100dvh; display: flex; flex-direction: column; }
5
+ header { padding: 12px 16px; font: 600 15px/1.4 ui-sans-serif, system-ui, sans-serif; color: #71717a; }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="8" fill="#18181b"/><path d="M9 11h14v8H14l-5 4z" fill="#fafafa"/></svg>
@@ -0,0 +1,13 @@
1
+ import type { ReactNode } from "react";
2
+ import "@camelai/run-react/styles.css";
3
+ import "./globals.css";
4
+
5
+ export const metadata = { title: "Agent app", description: "A chat with an agent on camelRun" };
6
+
7
+ export default function RootLayout({ children }: { children: ReactNode }) {
8
+ return (
9
+ <html lang="en">
10
+ <body>{children}</body>
11
+ </html>
12
+ );
13
+ }
@@ -0,0 +1,10 @@
1
+ import { Chat } from "./components/chat";
2
+
3
+ export default function Page() {
4
+ return (
5
+ <main>
6
+ <header>Agent app</header>
7
+ <Chat />
8
+ </main>
9
+ );
10
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "agent-app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "next dev",
8
+ "build": "next build",
9
+ "start": "next start"
10
+ },
11
+ "dependencies": {
12
+ "@camelai/run": "^{{version}}",
13
+ "@camelai/run-react": "^{{version}}",
14
+ "next": "^16.0.0",
15
+ "react": "^19.0.0",
16
+ "react-dom": "^19.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22",
20
+ "@types/react": "^19",
21
+ "@types/react-dom": "^19",
22
+ "typescript": "^5"
23
+ }
24
+ }
@@ -0,0 +1,17 @@
1
+ import { NextResponse, type NextRequest } from "next/server";
2
+
3
+ /**
4
+ * A stand-in for your sign-in: each browser gets a random, http-only user id, so every visitor has
5
+ * their own agent. Replace it with your auth (Clerk, Auth.js, your session) and read that in
6
+ * app/api/agent/route.ts instead.
7
+ */
8
+ export function proxy(request: NextRequest) {
9
+ // Deployed, there are no demo users unless you opt in (DEMO_AUTH=1): see app/api/agent/route.ts.
10
+ if (process.env.NODE_ENV === "production" && process.env.DEMO_AUTH !== "1") return NextResponse.next();
11
+ if (request.cookies.get("demo_user")) return NextResponse.next();
12
+ const response = NextResponse.next();
13
+ response.cookies.set("demo_user", crypto.randomUUID(), { httpOnly: true, sameSite: "lax", secure: request.nextUrl.protocol === "https:", path: "/", maxAge: 60 * 60 * 24 * 365 });
14
+ return response;
15
+ }
16
+
17
+ export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": false,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": { "@/*": ["./*"] }
18
+ },
19
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
20
+ "exclude": ["node_modules"]
21
+ }