@rtrentjones/greenlight 0.2.29 → 0.3.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.
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: provider-gemini
3
+ description: How the `agent` lane works in Greenlight — an autonomous cron-triggered Cloudflare Worker backed by Google Gemini (free tier). Covers the GEMINI_API_KEY (Google AI Studio, no billing), the gemini-2.5-flash generateContent call, the wrangler deploy (cron + KV + secret + custom_domain), the /, /status, /run surface, api-mode verify, and the free-tier safety envelope. Use when building, deploying, or verifying an agent tool.
4
+ ---
5
+
6
+ # provider-gemini
7
+
8
+ The `agent` lane is an **autonomous tool**: a Cloudflare Worker that wakes on a **cron trigger**,
9
+ calls **Gemini** (Google's LLM, free tier), does low-stakes work, stores the result in KV, and
10
+ exposes a tiny HTTP surface. It's the keepalive Worker pattern promoted to a user tool — free,
11
+ always-available, immune to repo-inactivity, no OCI box, no new paid account.
12
+
13
+ `agent` → target **workers**, data **none | kv** (kv holds the last output + run metadata).
14
+
15
+ ## Token — `GEMINI_API_KEY`
16
+
17
+ Create it at **Google AI Studio** (https://aistudio.google.com/apikey) — **free tier, no billing,
18
+ no card**. `greenlight add` verifies it against `…/v1beta/models?key=…` (HTTP 200). One key serves
19
+ every agent (shared, not per-tool). It is a **Cloudflare Worker secret** (`wrangler secret put
20
+ GEMINI_API_KEY`) — never in the repo.
21
+
22
+ ## The model + call
23
+
24
+ `gemini-2.5-flash` (fast; generous free limits — ~15 RPM / 1500 req/day, so a daily cron is ~1/day).
25
+
26
+ ```
27
+ POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_API_KEY}
28
+ { "contents": [{ "parts": [{ "text": "<prompt>" }] }] }
29
+ → candidates[0].content.parts[0].text
30
+ ```
31
+
32
+ ## Deploy — wrangler (workers target)
33
+
34
+ Like the astro blog: cron + KV + secret + `custom_domain` in `wrangler.toml`; no Terraform.
35
+
36
+ ```toml
37
+ [triggers]
38
+ crons = ["0 13 * * *"] # daily; stays far under the free-tier quota
39
+ [[kv_namespaces]]
40
+ binding = "STATE"
41
+ [[routes]]
42
+ pattern = "<name>.<domain>"
43
+ custom_domain = true
44
+ ```
45
+
46
+ ## Surface
47
+
48
+ | route | purpose |
49
+ |---|---|
50
+ | `scheduled()` | the cron: prompt Gemini → `STATE.put(today, text + metadata)` |
51
+ | `GET /` | the latest output (public, read-only) |
52
+ | `GET /status` | `{ ok, lastRun, model, preview }` — the **api-mode verify** target |
53
+ | `POST /run` | force a run — **bearer-gated** (a `RUN_TOKEN` secret) so randoms can't burn the Gemini quota; lets deploy/verify seed the first output |
54
+
55
+ ## Verify — `api` mode on `/status`
56
+
57
+ `verify.config.ts` hits `/status` and asserts `ok: true` + a recent run. (Output *quality* is a
58
+ future `eval` mode — LLM-judged.) Because the first cron may not have fired at deploy time, the
59
+ deploy step `POST /run`s once to seed, then verifies.
60
+
61
+ ## Safety envelope
62
+
63
+ - **Low-stakes / read-only** first agents (generate → store → serve; no destructive external actions).
64
+ - **Bearer on `/run`**; the cron frequency stays far under the free-tier daily limit.
65
+ - Key is **secret-only** (a Worker secret), never committed or echoed.
66
+
67
+ ## No keepalive
68
+
69
+ An agent needs no keepalive — the cron *is* its heartbeat and the Worker is always-available
70
+ (Cloudflare's edge, not a reclaimable box). Don't add it to `module.keepalive.targets_json`.
package/dist/bin.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  resolveUrl,
7
7
  scanSqlFiles,
8
8
  verifyAll
9
- } from "./chunk-HMU7D7R2.js";
9
+ } from "./chunk-P6FRYOOV.js";
10
10
  import "./chunk-HX7VA25D.js";
11
11
  import "./chunk-N3IKUCSF.js";
12
12
  import "./chunk-KP3Y6WRU.js";
@@ -351,6 +351,29 @@ var PACKS = [
351
351
  skill: "provider-neon",
352
352
  tfModules: ["neon"]
353
353
  },
354
+ {
355
+ id: "gemini",
356
+ name: "Google Gemini (free tier)",
357
+ // The LLM behind the `agent` lane. (A future `llm` axis would generalize this beyond agents.)
358
+ appliesTo: (t) => t.lane === "agent",
359
+ guide: "docs/provider-tokens.md \u2014 GEMINI_API_KEY (Google AI Studio, free tier, no billing)",
360
+ setupUrl: "https://aistudio.google.com/apikey",
361
+ tokens: [
362
+ {
363
+ // The agent Worker's LLM credential. Free tier (no billing / no card); set as a Cloudflare
364
+ // Worker secret, never in the repo. One key serves every agent (shared, not per-tool).
365
+ envVar: "GEMINI_API_KEY",
366
+ label: "Google AI Studio API key (Gemini free tier)",
367
+ verify: async (t) => {
368
+ const r = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${t}`);
369
+ return { ok: okStatus(r), detail: `HTTP ${r.status}` };
370
+ }
371
+ }
372
+ ],
373
+ skill: "provider-gemini"
374
+ // No tfModules: the agent Worker (cron + KV + secret + custom_domain) deploys via wrangler,
375
+ // like the astro blog — KV/DNS are wrangler-managed for the workers target.
376
+ },
354
377
  {
355
378
  id: "hcp",
356
379
  name: "HCP Terraform (remote state)",
@@ -478,7 +501,7 @@ function tokensForTool(tool) {
478
501
  }
479
502
 
480
503
  // src/version.ts
481
- var MODULE_REF = "v0.2.29";
504
+ var MODULE_REF = "v0.3.0";
482
505
  var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
483
506
  function moduleSource(module, ref = MODULE_REF) {
484
507
  return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
@@ -488,6 +511,14 @@ function moduleSource(module, ref = MODULE_REF) {
488
511
  var hcl = (s) => s.replace(/\n{3,}/g, "\n\n").trimEnd();
489
512
  function emitToolTf(opts) {
490
513
  const { name, domain, lane, target, data, envs, ref = MODULE_REF } = opts;
514
+ if (lane === "agent") {
515
+ const suffix = data && data !== "none" ? `/${data}` : "";
516
+ return `# ${name} \u2014 agent/${target}${suffix}, emitted by \`greenlight add\`.
517
+ # Wrangler-managed: the Worker (cron + KV + custom_domain route + GEMINI_API_KEY/RUN_TOKEN
518
+ # secrets) deploys via \`wrangler deploy\` from tools/${name}/. No Terraform here \u2014 see that
519
+ # wrangler.toml + the provider-gemini skill + docs/agents-plan.md.
520
+ `;
521
+ }
491
522
  const port = opts.port ?? 8e3;
492
523
  const slug = opts.slug ?? `OWNER/${name}`;
493
524
  const useSupabase = data === "supabase";
@@ -875,10 +906,10 @@ async function agentCommand(args) {
875
906
  const { config } = await loadManifest();
876
907
  const entry = resolveEntry(config, name);
877
908
  const dir = resolve3(process.cwd(), entry.dir ?? ".");
878
- materializeAgentKit(dir, { target: entry.target, data: entry.data });
909
+ materializeAgentKit(dir, { lane: entry.lane, target: entry.target, data: entry.data });
879
910
  console.log(
880
911
  `
881
- Synced the kit for "${name}" \u2192 ${entry.dir ?? "."} (target=${entry.target}, data=${entry.data}).`
912
+ Synced the kit for "${name}" \u2192 ${entry.dir ?? "."} (lane=${entry.lane}, target=${entry.target}, data=${entry.data}).`
882
913
  );
883
914
  return;
884
915
  }
@@ -1035,7 +1066,7 @@ function setGitHubSecret(repo, env, key, value) {
1035
1066
  async function gatherSecrets(name, repo, env, prefill) {
1036
1067
  const { config } = await loadManifest();
1037
1068
  const entry = resolveEntry(config, name);
1038
- const packs = packsForTool({ target: entry.target, data: entry.data });
1069
+ const packs = packsForTool({ lane: entry.lane, target: entry.target, data: entry.data });
1039
1070
  const dest = env ? `env "${env}" of ${repo}` : repo;
1040
1071
  const existing = listGitHubSecrets(repo, env);
1041
1072
  console.log(`Gathering secrets for "${name}" \u2192 GitHub ${dest}`);
@@ -1194,7 +1225,7 @@ async function addCommand(args) {
1194
1225
  const entry = next.tools.find((t) => t.name === name);
1195
1226
  const data = entry?.data ?? "none";
1196
1227
  const envs = entry?.envs ?? ["beta", "prod"];
1197
- const toolInfo = { target, data };
1228
+ const toolInfo = { lane, target, data };
1198
1229
  const dest = resolve5(process.cwd(), "tools", name);
1199
1230
  if (existsSync5(dest)) throw new Error(`tools/${name} already exists`);
1200
1231
  const src = templateDir(lane, target);
@@ -1209,6 +1240,11 @@ async function addCommand(args) {
1209
1240
  }
1210
1241
  const shippedGitignore = join(dest, "gitignore");
1211
1242
  if (existsSync5(shippedGitignore)) renameSync(shippedGitignore, join(dest, ".gitignore"));
1243
+ const wranglerPath = join(dest, "wrangler.toml");
1244
+ if (existsSync5(wranglerPath)) {
1245
+ const wt = readFileSync3(wranglerPath, "utf8").replaceAll("agent-tool", name).replaceAll("example.dev", config.domain);
1246
+ writeFileSync2(wranglerPath, wt);
1247
+ }
1212
1248
  console.log(`\u2714 copied ${src} \u2192 tools/${name}`);
1213
1249
  if (existsSync5(pkgPath)) registerWorkspaceMember(process.cwd(), `tools/${name}`);
1214
1250
  } else {
@@ -1838,11 +1874,11 @@ async function adoptWrapper(ctx) {
1838
1874
  `verify/${name}.config.ts`
1839
1875
  );
1840
1876
  }
1841
- const providers = providersForTool({ target, data });
1877
+ const providers = providersForTool({ lane, target, data });
1842
1878
  if (existsSync6(join2(cwd, "infra/main.tf")) && providers.some((p) => p !== "cloudflare" && p !== "github")) {
1843
1879
  console.log(`\xB7 ensure infra/main.tf declares provider(s): ${providers.join(", ")}`);
1844
1880
  }
1845
- materializeAgentKit(dest, { target, data });
1881
+ materializeAgentKit(dest, { lane, target, data });
1846
1882
  addGreenlightScript(dest);
1847
1883
  if (target === "oci") {
1848
1884
  const wrapperSlug = parseRepo(safeGit(cwd, ["remote", "get-url", "origin"])) ?? "OWNER/REPO";
@@ -1940,7 +1976,7 @@ async function adoptStandalone(ctx) {
1940
1976
  ".github/workflows/greenlight-promote.yml"
1941
1977
  );
1942
1978
  writeIfAbsent(join2(repo, "verify.config.ts"), starterVerifyConfig(lane), "verify.config.ts");
1943
- materializeAgentKit(repo, { target, data });
1979
+ materializeAgentKit(repo, { lane, target, data });
1944
1980
  writeIfAbsent(join2(repo, "mise.toml"), MISE_TOML, "mise.toml");
1945
1981
  writeIfAbsent(join2(repo, ".node-version"), "24\n", ".node-version");
1946
1982
  const nextReg = addTool(reg, {
@@ -2674,6 +2710,13 @@ function defaultSpec(lane) {
2674
2710
  return { mode: "api", checks: [{ path: "/", status: 200 }] };
2675
2711
  case "mcp":
2676
2712
  return { mode: "mcp", expectTools: [] };
2713
+ case "agent":
2714
+ return {
2715
+ mode: "api",
2716
+ checks: [{ path: "/status", status: 200 }],
2717
+ settleRetries: 6,
2718
+ settleMs: 5e3
2719
+ };
2677
2720
  }
2678
2721
  }
2679
2722
  function printReport(report) {
@@ -13,7 +13,7 @@ import { createJiti } from "jiti";
13
13
 
14
14
  // ../packages/shared/src/schema.ts
15
15
  import { z } from "zod";
16
- var LaneEnum = z.enum(["astro", "next", "mcp"]);
16
+ var LaneEnum = z.enum(["astro", "next", "mcp", "agent"]);
17
17
  var TargetEnum = z.enum(["workers", "vercel", "oci"]);
18
18
  var DataEnum = z.enum(["none", "d1", "kv", "supabase", "neon"]);
19
19
  var AuthEnum = z.enum(["none", "bearer", "oauth"]);
@@ -22,7 +22,8 @@ var EnvEnum = z.enum(["preview", "beta", "prod"]);
22
22
  var MATRIX = {
23
23
  astro: { targets: ["workers"], data: ["none", "d1", "kv"] },
24
24
  next: { targets: ["vercel"], data: ["none", "supabase", "neon"] },
25
- mcp: { targets: ["workers", "oci"], data: ["none"] }
25
+ mcp: { targets: ["workers", "oci"], data: ["none"] },
26
+ agent: { targets: ["workers"], data: ["none", "kv"] }
26
27
  };
27
28
  var ToolSchema = z.object({
28
29
  name: z.string().regex(/^[a-z][a-z0-9-]*$/, "tool name must be kebab-case starting with a letter"),
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  defineConfig,
3
3
  defineVerify,
4
4
  loadConfig
5
- } from "./chunk-HMU7D7R2.js";
5
+ } from "./chunk-P6FRYOOV.js";
6
6
  import "./chunk-HX7VA25D.js";
7
7
  import "./chunk-N3IKUCSF.js";
8
8
  import "./chunk-KP3Y6WRU.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtrentjones/greenlight",
3
- "version": "0.2.29",
3
+ "version": "0.3.0",
4
4
  "description": "Greenlight CLI — setup and lifecycle for the harness.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,10 +31,10 @@
31
31
  "@anthropic-ai/sdk": "^0.69.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@rtrentjones/greenlight-adapters": "0.2.29",
35
- "@rtrentjones/greenlight-loop": "0.2.29",
36
- "@rtrentjones/greenlight-verify": "0.2.29",
37
- "@rtrentjones/greenlight-shared": "0.2.29"
34
+ "@rtrentjones/greenlight-adapters": "0.3.0",
35
+ "@rtrentjones/greenlight-loop": "0.3.0",
36
+ "@rtrentjones/greenlight-verify": "0.3.0",
37
+ "@rtrentjones/greenlight-shared": "0.3.0"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "node scripts/copy-assets.mjs && tsup",
@@ -0,0 +1,33 @@
1
+ # agent — autonomous Gemini-backed Worker
2
+
3
+ A cron-triggered Cloudflare Worker that calls **Gemini** (free tier), stores the result in **KV**,
4
+ and serves it. Scaffolded by `greenlight add <name> --lane agent --target workers --data kv`.
5
+
6
+ ## What it does
7
+
8
+ - `scheduled` (cron, daily) → prompt Gemini → store the text + metadata in KV.
9
+ - `GET /` — the latest output (plain text).
10
+ - `GET /status` — `{ ok, lastRun, model, preview }` (the verify target).
11
+ - `POST /run` — force a run; **bearer-gated** (`Authorization: Bearer $RUN_TOKEN`).
12
+
13
+ ## Setup (once)
14
+
15
+ `greenlight add` already rewrote the worker `name` + route domain in `wrangler.toml`. Then:
16
+
17
+ 1. **KV namespace** — `pnpm exec wrangler kv namespace create STATE`, and paste the id into the
18
+ three `id = "REPLACE_WITH_KV_NAMESPACE_ID"` slots in `wrangler.toml`.
19
+ 2. **Secrets** (per env — the key never goes in the repo):
20
+ - `pnpm exec wrangler secret put GEMINI_API_KEY --env prod` — your Google AI Studio key (free,
21
+ `aistudio.google.com/apikey`).
22
+ - `pnpm exec wrangler secret put RUN_TOKEN --env prod` — any random string.
23
+ 3. **Deploy → seed → verify**:
24
+ - `pnpm greenlight deploy <name>` (or `pnpm exec wrangler deploy --env prod`).
25
+ - `curl -XPOST https://<name>.<domain>/run -H "Authorization: Bearer $RUN_TOKEN"` (seed the first run).
26
+ - `pnpm greenlight verify <name> --env prod`.
27
+
28
+ ## Free-tier safety
29
+
30
+ The daily cron is ~1 Gemini call/day (the free tier allows ~1500/day). `/run` is bearer-gated so it
31
+ can't be used to burn the quota. The key lives **only** as a Worker secret.
32
+
33
+ See the `provider-gemini` skill + [docs/agents-plan.md](../../docs/agents-plan.md).
@@ -0,0 +1,5 @@
1
+ # Shipped as `gitignore` (no dot) so it survives the npm tarball; `greenlight add` renames it to
2
+ # `.gitignore` on scaffold.
3
+ node_modules/
4
+ .wrangler/
5
+ dist/
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "_template-agent",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "wrangler dev",
7
+ "deploy": "wrangler deploy"
8
+ },
9
+ "devDependencies": {
10
+ "@cloudflare/workers-types": "^4.20250601.0",
11
+ "typescript": "^5.6.0",
12
+ "wrangler": "^3.90.0"
13
+ }
14
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Agent Worker — autonomous, cron-triggered, Gemini-backed.
3
+ *
4
+ * On the cron (`scheduled`) it asks Gemini for content and stores it in KV. The `fetch` handler
5
+ * serves the latest output (`GET /`), reports last-run metadata (`GET /status` — the verify
6
+ * target), and force-runs (`POST /run`, bearer-gated so it can't be used to burn the free-tier
7
+ * quota; the deploy step uses it to seed the first output before verify).
8
+ *
9
+ * GEMINI_API_KEY + RUN_TOKEN are Worker secrets (`wrangler secret put …`), never in wrangler.toml.
10
+ */
11
+
12
+ export interface Env {
13
+ STATE: KVNamespace;
14
+ GEMINI_API_KEY: string;
15
+ RUN_TOKEN?: string;
16
+ MODEL: string;
17
+ GREENLIGHT_ENV: string;
18
+ }
19
+
20
+ interface AgentRecord {
21
+ ok: true;
22
+ text: string;
23
+ lastRun: string;
24
+ model: string;
25
+ }
26
+
27
+ const PROMPT =
28
+ 'In one or two sentences, share a single concrete, non-obvious tip about deploying software ' +
29
+ 'safely. Vary the topic each time. Plain text, no preamble, no markdown.';
30
+
31
+ async function callGemini(env: Env): Promise<string> {
32
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${env.MODEL}:generateContent?key=${env.GEMINI_API_KEY}`;
33
+ const res = await fetch(url, {
34
+ method: 'POST',
35
+ headers: { 'content-type': 'application/json' },
36
+ body: JSON.stringify({ contents: [{ parts: [{ text: PROMPT }] }] }),
37
+ });
38
+ if (!res.ok) throw new Error(`Gemini HTTP ${res.status}`);
39
+ const data = (await res.json()) as {
40
+ candidates?: { content?: { parts?: { text?: string }[] } }[];
41
+ };
42
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text?.trim();
43
+ if (!text) throw new Error('Gemini returned no text');
44
+ return text;
45
+ }
46
+
47
+ function key(env: Env): string {
48
+ return `${env.GREENLIGHT_ENV}:latest`;
49
+ }
50
+
51
+ async function runOnce(env: Env): Promise<AgentRecord> {
52
+ const rec: AgentRecord = {
53
+ ok: true,
54
+ text: await callGemini(env),
55
+ lastRun: new Date().toISOString(),
56
+ model: env.MODEL,
57
+ };
58
+ await env.STATE.put(key(env), JSON.stringify(rec));
59
+ return rec;
60
+ }
61
+
62
+ async function latest(env: Env): Promise<AgentRecord | null> {
63
+ const raw = await env.STATE.get(key(env));
64
+ return raw ? (JSON.parse(raw) as AgentRecord) : null;
65
+ }
66
+
67
+ export default {
68
+ async scheduled(_event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
69
+ ctx.waitUntil(runOnce(env));
70
+ },
71
+
72
+ async fetch(req: Request, env: Env): Promise<Response> {
73
+ const { pathname } = new URL(req.url);
74
+
75
+ if (req.method === 'POST' && pathname === '/run') {
76
+ const auth = req.headers.get('authorization');
77
+ if (!env.RUN_TOKEN || auth !== `Bearer ${env.RUN_TOKEN}`) {
78
+ return new Response('unauthorized', { status: 401 });
79
+ }
80
+ return Response.json(await runOnce(env));
81
+ }
82
+
83
+ if (pathname === '/status') {
84
+ const rec = await latest(env);
85
+ return Response.json({
86
+ ok: Boolean(rec),
87
+ lastRun: rec?.lastRun ?? null,
88
+ model: env.MODEL,
89
+ preview: rec ? rec.text.slice(0, 80) : null,
90
+ });
91
+ }
92
+
93
+ const rec = await latest(env);
94
+ if (!rec) return new Response('No run yet — POST /run to seed.\n', { status: 200 });
95
+ return new Response(`${rec.text}\n`, {
96
+ headers: { 'content-type': 'text/plain; charset=utf-8' },
97
+ });
98
+ },
99
+ };
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2022",
4
+ "module": "es2022",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["es2022"],
7
+ "types": ["@cloudflare/workers-types"],
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true
11
+ },
12
+ "include": ["src"]
13
+ }
@@ -0,0 +1,8 @@
1
+ // The agent exposes GET /status with last-run metadata. The deploy step seeds a run (POST /run)
2
+ // before verify, so /status reports ok:true — assert that. settle absorbs Cloudflare propagation.
3
+ export default {
4
+ mode: 'api',
5
+ checks: [{ path: '/status', status: 200, contains: '"ok":true' }],
6
+ settleRetries: 6,
7
+ settleMs: 5000,
8
+ };
@@ -0,0 +1,47 @@
1
+ # Agent Worker. `greenlight add` rewrites `name` (agent-tool → your tool) + the route domain
2
+ # (example.dev → your domain). You still set the KV namespace id (create it once with
3
+ # `wrangler kv namespace create STATE`) and the secrets:
4
+ # wrangler secret put GEMINI_API_KEY --env prod (Google AI Studio key — free tier)
5
+ # wrangler secret put RUN_TOKEN --env prod (any random string; bearer for POST /run)
6
+ name = "agent-tool"
7
+ main = "src/index.ts"
8
+ compatibility_date = "2025-06-01"
9
+
10
+ # Daily 13:00 UTC — far under the Gemini free-tier quota (~1500 req/day). Edit to taste.
11
+ [triggers]
12
+ crons = ["0 13 * * *"]
13
+
14
+ [vars]
15
+ MODEL = "gemini-2.5-flash"
16
+ GREENLIGHT_ENV = "prod"
17
+
18
+ # Output store. `wrangler kv namespace create STATE` → paste the id here (and per-env below).
19
+ [[kv_namespaces]]
20
+ binding = "STATE"
21
+ id = "REPLACE_WITH_KV_NAMESPACE_ID"
22
+
23
+ # Named environments (the deploy uses `wrangler deploy --env prod|beta`). Wrangler envs do NOT
24
+ # inherit top-level config, so vars + kv + routes are repeated per env.
25
+ [env.prod]
26
+ name = "agent-tool"
27
+ routes = [{ pattern = "agent-tool.example.dev", custom_domain = true }]
28
+
29
+ [env.prod.vars]
30
+ MODEL = "gemini-2.5-flash"
31
+ GREENLIGHT_ENV = "prod"
32
+
33
+ [[env.prod.kv_namespaces]]
34
+ binding = "STATE"
35
+ id = "REPLACE_WITH_KV_NAMESPACE_ID"
36
+
37
+ [env.beta]
38
+ name = "agent-tool-beta"
39
+ routes = [{ pattern = "beta.agent-tool.example.dev", custom_domain = true }]
40
+
41
+ [env.beta.vars]
42
+ MODEL = "gemini-2.5-flash"
43
+ GREENLIGHT_ENV = "beta"
44
+
45
+ [[env.beta.kv_namespaces]]
46
+ binding = "STATE"
47
+ id = "REPLACE_WITH_KV_NAMESPACE_ID"