@meffecta/agent 1.0.2 → 1.0.3

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/README.md CHANGED
@@ -23,7 +23,7 @@ deployment is, so a command can never quietly act on the wrong one.
23
23
  npx @meffecta/agent create-project # a GCP project
24
24
  npx @meffecta/agent link-billing # attach billing
25
25
  npx @meffecta/agent setup-infra # registry, buckets, service account, service
26
- npx @meffecta/agent set-secret AGENT_WEBHOOK_SECRET --random
26
+ npx @meffecta/agent set-secret AGENT_API_SECRET --random
27
27
  npx @meffecta/agent set-env GIT_REPO_URL=https://github.com/you/your-content-repo.git
28
28
  npx @meffecta/agent deploy # roll out the engine
29
29
  npx @meffecta/agent setup-scheduler # give it its triggers
@@ -32,10 +32,10 @@ const STEPS = [
32
32
  ],
33
33
  [
34
34
  "init",
35
- "Create your content repo — jobs/, worlds/, SYSTEM.md — and a deployment.env in it.\n Run everything from there; it is what says which deployment you mean.",
35
+ "Create your content repo — jobs/, worlds/, SYSTEM.md, ENVIRONMENT.md — and a deployment.env\n in it. Run everything from there; it is what says which deployment you mean.",
36
36
  ],
37
37
  ["setup-infra", "Provision GCP and the Cloud Run service shell."],
38
- ["set-secret", "AGENT_WEBHOOK_SECRET --random, CLAUDE_CODE_OAUTH_TOKEN, GITHUB_TOKEN."],
38
+ ["set-secret", "AGENT_API_SECRET --random, CLAUDE_CODE_OAUTH_TOKEN, GITHUB_TOKEN."],
39
39
  ["set-env", "GIT_REPO_URL=… — the service will not boot without it."],
40
40
  ["deploy", "Roll out the engine image."],
41
41
  ["setup-scheduler", "Give it its triggers. Scaled to zero, it fires nothing without them."],
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # Then operate from the content repo, running the engine's scripts by path:
12
12
  # cd /path/to/your-content-repo
13
- # /path/to/agent/scripts/set-secret.sh AGENT_WEBHOOK_SECRET --random
13
+ # /path/to/agent/scripts/set-secret.sh AGENT_API_SECRET --random
14
14
  # /path/to/agent/scripts/deploy.sh
15
15
 
16
16
  # The GCP *project ID* — check it with `gcloud projects list`, as the console may have
package/engine.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "engineTag": "1.0.2",
3
- "builtFrom": "6a4ebd7751d2eaeaca8f2f28d699a171d162208b"
2
+ "engineTag": "1.0.3",
3
+ "builtFrom": "f784051bc092c57aa37b4e9c44e06ee8f368f8f7"
4
4
  }
package/lib/commands.js CHANGED
@@ -245,7 +245,7 @@ async function envList() {
245
245
  );
246
246
  const entries = svc.spec.template.spec.containers[0].env ?? [];
247
247
  if (entries.length === 0) {
248
- console.log("Nothing set. At minimum this service needs GIT_REPO_URL and AGENT_WEBHOOK_SECRET.");
248
+ console.log("Nothing set. At minimum this service needs GIT_REPO_URL and AGENT_API_SECRET.");
249
249
  return 0;
250
250
  }
251
251
  const width = Math.max(...entries.map((e) => e.name.length));
@@ -281,7 +281,7 @@ async function secretsList() {
281
281
  );
282
282
  if (secrets.length === 0) {
283
283
  console.log("No secrets in this project yet. Start with:");
284
- console.log(" meffecta-agent set-secret AGENT_WEBHOOK_SECRET --random");
284
+ console.log(" meffecta-agent set-secret AGENT_API_SECRET --random");
285
285
  return 0;
286
286
  }
287
287
  const width = Math.max(...secrets.map((name) => name.length));
@@ -364,8 +364,31 @@ async function logs(args) {
364
364
  }
365
365
 
366
366
  /** Start a deployment.env in the current directory, from the template in this package. */
367
+ // Scaffolded by `init` into a new content repo. Deliberately short: an empty table with the
368
+ // columns that matter is a thing an operator fills in, where a long example is a thing they
369
+ // delete.
370
+ const REGISTER_TEMPLATE = `# Deployment environment
371
+
372
+ The register of systems this agent can reach. The agent reads this file **at run time** —
373
+ its skills are shared across deployments and name no deployment's variables, so a skill
374
+ says what kind of credential it needs and comes here for the name.
375
+
376
+ A system that is not listed here is one the agent will report as unavailable.
377
+
378
+ | System | Skill | Variables | Access |
379
+ | --- | --- | --- | --- |
380
+ | | | | |
381
+
382
+ Fill the Access column honestly — "read-only", "may send mail", "may change DNS". It is
383
+ what a person reads when deciding whether a job is safe to let run unattended.
384
+
385
+ Set the values themselves with \`meffecta-agent set-secret <NAME>\` (credentials) or
386
+ \`set-env <NAME>=<value>\` (ids, addresses, URLs); \`meffecta-agent connect\` lists the
387
+ systems the engine already has skills for and walks through each.
388
+ `;
389
+
367
390
  async function init(args) {
368
- const { copyFileSync, existsSync: exists } = await import("node:fs");
391
+ const { copyFileSync, existsSync: exists, writeFileSync } = await import("node:fs");
369
392
  const target = resolve(process.cwd(), "deployment.env");
370
393
  if (exists(target) && !args.includes("--force")) {
371
394
  console.log(`deployment.env already exists here. Leaving it alone (--force to replace).`);
@@ -383,6 +406,20 @@ async function init(args) {
383
406
  console.log("Fill in PROJECT and SERVICE — those two are never guessed — then commit it.");
384
407
  console.log("It belongs in your content repo, beside jobs/, because which GCP project a");
385
408
  console.log("set of jobs runs as is a fact about the deployment and none of it is secret.\n");
409
+
410
+ // The register is read by the agent itself at run time, not just by people: the skills
411
+ // are shared across deployments and name no deployment's variables, so without this file
412
+ // a run knows how a system works and not which variable holds its credential.
413
+ const register = resolve(process.cwd(), "ENVIRONMENT.md");
414
+ if (exists(register)) {
415
+ console.log("ENVIRONMENT.md already exists here. Leaving it alone.\n");
416
+ } else {
417
+ writeFileSync(register, REGISTER_TEMPLATE);
418
+ console.log("✅ Wrote ENVIRONMENT.md here — the register of systems this agent can reach.\n");
419
+ console.log("Add a row per system as you connect one. The agent reads this file at run");
420
+ console.log("time to find out which variable holds which credential, so a system that is");
421
+ console.log("not listed is a system it will report as unavailable.\n");
422
+ }
386
423
  console.log("Next: meffecta-agent steps");
387
424
  return 0;
388
425
  }
@@ -410,7 +447,7 @@ export const GROUPS = [
410
447
  "Provision GCP: registry, buckets, service account, service shell",
411
448
  script("setup-infrastructure.sh"),
412
449
  ],
413
- ["set-secret", "AGENT_WEBHOOK_SECRET --random, CLAUDE_CODE_OAUTH_TOKEN, GITHUB_TOKEN", script("set-secret.sh")],
450
+ ["set-secret", "AGENT_API_SECRET --random, CLAUDE_CODE_OAUTH_TOKEN, GITHUB_TOKEN", script("set-secret.sh")],
414
451
  ["set-env", "GIT_REPO_URL — the one setting the service will not boot without", script("set-env.sh")],
415
452
  ["deploy", "Roll out an engine image and assert the runtime shape", script("deploy.sh")],
416
453
  ["setup-scheduler", "Create the external triggers — Cloud Scheduler + Cloud Tasks", script("setup-scheduler.sh")],
package/lib/doctor.js CHANGED
@@ -188,7 +188,22 @@ export async function doctor() {
188
188
  .filter(Boolean)
189
189
  .map((n) => n.split("/").pop()),
190
190
  );
191
- for (const name of ["AGENT_WEBHOOK_SECRET", "CLAUDE_CODE_OAUTH_TOKEN", "GITHUB_TOKEN"]) {
191
+ // AGENT_WEBHOOK_SECRET is the old name for AGENT_API_SECRET and the engine still reads
192
+ // it, so a deployment on the old one is working, not broken — check the pair, not the
193
+ // preferred name, or doctor fails a healthy service.
194
+ const apiSecretName = env.AGENT_API_SECRET
195
+ ? "AGENT_API_SECRET"
196
+ : env.AGENT_WEBHOOK_SECRET
197
+ ? "AGENT_WEBHOOK_SECRET"
198
+ : "AGENT_API_SECRET";
199
+ if (apiSecretName === "AGENT_WEBHOOK_SECRET") {
200
+ r.warn(
201
+ "AGENT_WEBHOOK_SECRET is the old name",
202
+ "It guards the whole API, not just webhooks, and is now AGENT_API_SECRET. The engine reads both.",
203
+ "meffecta-agent set-secret AGENT_API_SECRET (paste the same value, deploy, then delete the old one)",
204
+ );
205
+ }
206
+ for (const name of [apiSecretName, "CLAUDE_CODE_OAUTH_TOKEN", "GITHUB_TOKEN"]) {
192
207
  const ref = env[name]?.valueFrom?.secretKeyRef?.name;
193
208
  if (!env[name]) {
194
209
  r.fail(`${name} not set`, "Required by the engine.", `meffecta-agent set-secret ${name}`);
package/lib/gcloud.js CHANGED
@@ -69,27 +69,31 @@ export function serviceUrl(deployment) {
69
69
  }
70
70
 
71
71
  /**
72
- * The deployment's webhook secret, straight from Secret Manager into memory. It is never
72
+ * The deployment's API secret, straight from Secret Manager into memory. It is never
73
73
  * printed, never passed as a process argument, and only ever leaves here as a header.
74
+ *
75
+ * AGENT_WEBHOOK_SECRET is the old name — it guarded webhooks once, then everything else —
76
+ * and the engine still reads it, so this tries both rather than telling a deployment that
77
+ * works fine that its secret is missing.
74
78
  */
75
- export function webhookSecret(deployment) {
76
- try {
77
- return capture(
78
- "gcloud",
79
- gcloudArgs(deployment, ["secrets", "versions", "access", "latest", "--secret=AGENT_WEBHOOK_SECRET"]),
80
- );
81
- } catch {
82
- throw new UserError(
83
- "Could not read AGENT_WEBHOOK_SECRET from Secret Manager.\n" +
84
- "Create it first: meffecta-agent set-secret AGENT_WEBHOOK_SECRET --random",
85
- );
79
+ export function apiSecret(deployment) {
80
+ for (const name of ["AGENT_API_SECRET", "AGENT_WEBHOOK_SECRET"]) {
81
+ try {
82
+ return capture("gcloud", gcloudArgs(deployment, ["secrets", "versions", "access", "latest", `--secret=${name}`]));
83
+ } catch {
84
+ // Try the other name before deciding there is no secret at all.
85
+ }
86
86
  }
87
+ throw new UserError(
88
+ "Could not read AGENT_API_SECRET from Secret Manager (nor the old AGENT_WEBHOOK_SECRET).\n" +
89
+ "Create it first: meffecta-agent set-secret AGENT_API_SECRET --random",
90
+ );
87
91
  }
88
92
 
89
93
  /** An authenticated request to the deployment's own API. */
90
94
  export async function api(deployment, path, { method = "GET", body, accept } = {}) {
91
95
  const url = `${serviceUrl(deployment)}${path}`;
92
- const headers = { Authorization: `Bearer ${webhookSecret(deployment)}` };
96
+ const headers = { Authorization: `Bearer ${apiSecret(deployment)}` };
93
97
  if (accept) {
94
98
  headers.Accept = accept;
95
99
  }
@@ -132,6 +132,57 @@ const INTEGRATIONS = {
132
132
  ],
133
133
  },
134
134
 
135
+ hubspot: {
136
+ title: "HubSpot CRM",
137
+ gives: "query-hubspot — pipelines, contacts, deals, activity. manage-hubspot writes, and only when a job says to",
138
+ needs: [
139
+ "A private app in the HubSpot portal: Settings → Integrations → Private Apps → Create.",
140
+ "Grant only the scopes the jobs need — crm.objects.*.read for reporting; add the",
141
+ "matching .write scopes only if a job is meant to change records.",
142
+ ],
143
+ steps: [["meffecta-agent set-secret HUBSPOT_TOKEN", "the private app's access token"]],
144
+ more: [
145
+ "A second portal is HUBSPOT_<NAME>_TOKEN, and the job says which portal it may touch.",
146
+ "Reading and writing are two skills on purpose: a reporting job loads query-hubspot and",
147
+ "then cannot alter the CRM at all, because the ability to is not in its context.",
148
+ "Rotate the token from the same screen; HubSpot can expire the old one after 7 days.",
149
+ ],
150
+ },
151
+
152
+ kleer: {
153
+ title: "Kleer accounting and payroll",
154
+ gives:
155
+ "query-kleer — invoices out and in, payment status, vouchers, the SIE4E export, payroll runs, bank transactions. Read-only",
156
+ needs: [
157
+ "A Kleer API token, requested through your accounting consultant — Kleer issues it by",
158
+ "SMS, there is no self-service. It inherits the permissions of the Kleer user it is",
159
+ "bound to, so ask for it on a READ-ONLY user: that, not the skill, is what stops a",
160
+ "write. (Kleer was PE Accounting before the rebrand; the docs still say PE in places.)",
161
+ ],
162
+ steps: [["meffecta-agent set-secret KLEER_API_TOKEN", "the token Kleer sent"]],
163
+ more: [
164
+ "Company ids are discovered at run time from the token, so nothing else needs setting.",
165
+ "There is a test environment on the same token, mirroring production a day behind —",
166
+ "worth pointing a new job at first, since live books are not a place to explore.",
167
+ ],
168
+ },
169
+
170
+ mongodb: {
171
+ title: "A MongoDB database",
172
+ gives: "query-mongodb — counts, filters and aggregations over a product database. Read-only",
173
+ needs: [
174
+ "A MongoDB user with the `read` role on the database and nothing more, and a connection",
175
+ "URI for it. The role is the real boundary — the skill refuses to write, but a",
176
+ "read-only user makes a mistake impossible rather than merely forbidden.",
177
+ ],
178
+ steps: [["meffecta-agent set-secret ACME_MONGODB_URI_READONLY", "one per database, named for the project"]],
179
+ more: [
180
+ "Connection strings are per-database, so there is no single shared variable: name each",
181
+ "one for its project and record it in the deployment's ENVIRONMENT.md, which is where",
182
+ "the skill looks for the name. The driver already ships in the engine image.",
183
+ ],
184
+ },
185
+
135
186
  cloudflare: {
136
187
  title: "Cloudflare",
137
188
  gives: "cloudflare — DNS, cache purge, Pages deployments, certificate checks",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meffecta/agent",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Set up and operate a Meffecta Agent deployment — a self-hosted Claude Code job runner.",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/deploy.sh CHANGED
@@ -165,5 +165,5 @@ URL=$(gcloud run services describe "${SERVICE}" \
165
165
  if [ -n "${URL}" ]; then
166
166
  echo "🌍 ${SERVICE} → ${URL}"
167
167
  echo " Liveness: curl -fsS ${URL}/health"
168
- echo " Ask it: ${URL}/ask (any username, AGENT_WEBHOOK_SECRET as the password)"
168
+ echo " Ask it: ${URL}/ask (any username, AGENT_API_SECRET as the password)"
169
169
  fi
@@ -8,7 +8,7 @@ set -euo pipefail
8
8
  # The value never appears in your shell history, in the process list, or on screen.
9
9
  #
10
10
  # Usage:
11
- # scripts/set-secret.sh AGENT_WEBHOOK_SECRET --random # generate one, 32 random bytes
11
+ # scripts/set-secret.sh AGENT_API_SECRET --random # generate one, 32 random bytes
12
12
  # scripts/set-secret.sh GITHUB_TOKEN # prompt (hidden), or read stdin
13
13
  # scripts/set-secret.sh GMAIL_REFRESH_TOKEN --from-file token.txt
14
14
  # cat token.txt | scripts/set-secret.sh GMAIL_REFRESH_TOKEN
@@ -233,7 +233,7 @@ echo " 3. Create + wire the secrets:"
233
233
  echo " printf '%s' '<value>' | gcloud secrets create <NAME> --data-file=- --project ${PROJECT}"
234
234
  echo " gcloud run services update ${SERVICE} --region ${REGION} --project ${PROJECT} \\"
235
235
  echo " --update-secrets '<NAME>=<NAME>:latest'"
236
- echo " Required: AGENT_WEBHOOK_SECRET, CLAUDE_CODE_OAUTH_TOKEN (claude setup-token), GITHUB_TOKEN"
236
+ echo " Required: AGENT_API_SECRET, CLAUDE_CODE_OAUTH_TOKEN (claude setup-token), GITHUB_TOKEN"
237
237
  echo " Gmail: GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKEN — and one"
238
238
  echo " GMAIL_<NAME>_REFRESH_TOKEN per extra account (e.g. GMAIL_MEVISIO_REFRESH_TOKEN)"
239
239
  echo " Optional: GRAFANA_LOGS_HOST/USERNAME + GRAFANA_API_KEY"
@@ -70,8 +70,8 @@ GC="gcloud --project=${PROJECT}"
70
70
  # arguments of the calls that create them — and gcloud echoes the arguments back when it
71
71
  # rejects one. Everything this script prints goes through here first.
72
72
  redact() {
73
- if [ -n "${WEBHOOK_SECRET:-}" ]; then
74
- printf '%s\n' "${1//${WEBHOOK_SECRET}/<secret>}"
73
+ if [ -n "${API_SECRET:-}" ]; then
74
+ printf '%s\n' "${1//${API_SECRET}/<secret>}"
75
75
  else
76
76
  printf '%s\n' "$1"
77
77
  fi
@@ -119,18 +119,24 @@ fi
119
119
 
120
120
  # The triggers authenticate with the same bearer token as everything else on this API, so
121
121
  # the secret has to be resolved — it is normally a Secret Manager reference on the service.
122
- SECRET_REF=$(printf '%s' "${SERVICE_JSON}" | svc_field AGENT_WEBHOOK_SECRET)
123
- IFS=$'\t' read -r SECRET_KIND SECRET_A SECRET_B <<<"${SECRET_REF}"
124
- if [ "${SECRET_KIND}" = "value" ]; then
125
- WEBHOOK_SECRET="${SECRET_A}"
126
- elif [ "${SECRET_KIND}" = "secret" ] && [ -n "${SECRET_A}" ]; then
127
- WEBHOOK_SECRET=$(${GC} secrets versions access "${SECRET_B}" --secret="${SECRET_A}")
128
- else
129
- echo "Could not resolve AGENT_WEBHOOK_SECRET from ${SERVICE} is it set on the service?" >&2
130
- echo " scripts/set-secret.sh AGENT_WEBHOOK_SECRET --random" >&2
122
+ # AGENT_WEBHOOK_SECRET is the old name, still read by the engine and still looked for here,
123
+ # so a deployment part-way through the rename gets working triggers either way.
124
+ API_SECRET=""
125
+ for SECRET_VAR in AGENT_API_SECRET AGENT_WEBHOOK_SECRET; do
126
+ IFS=$'\t' read -r SECRET_KIND SECRET_A SECRET_B <<<"$(printf '%s' "${SERVICE_JSON}" | svc_field "${SECRET_VAR}")"
127
+ if [ "${SECRET_KIND}" = "value" ]; then
128
+ API_SECRET="${SECRET_A}"
129
+ elif [ "${SECRET_KIND}" = "secret" ] && [ -n "${SECRET_A}" ]; then
130
+ API_SECRET=$(${GC} secrets versions access "${SECRET_B}" --secret="${SECRET_A}")
131
+ fi
132
+ [ -n "${API_SECRET}" ] && break
133
+ done
134
+ if [ -z "${API_SECRET}" ]; then
135
+ echo "Could not resolve AGENT_API_SECRET (or the old AGENT_WEBHOOK_SECRET) from ${SERVICE}." >&2
136
+ echo " scripts/set-secret.sh AGENT_API_SECRET --random" >&2
137
+ echo "Refusing to create triggers that would 401 on every firing." >&2
131
138
  exit 1
132
139
  fi
133
- [ -n "${WEBHOOK_SECRET}" ] || { echo "AGENT_WEBHOOK_SECRET resolved empty — refusing to create triggers that would 401." >&2; exit 1; }
134
140
 
135
141
  echo " url: ${AGENT_URL}"
136
142
  echo " runtime: ${RUNTIME_SA}"
@@ -171,7 +177,7 @@ if [ "${CURRENT_QUEUE}" != "${QUEUE_PATH}" ] || [ "${CURRENT_URL}" != "${AGENT_U
171
177
  fi
172
178
 
173
179
  echo "📋 Reading the job list from ${AGENT_URL}/jobs..."
174
- JOBS_JSON=$(curl -fsS --max-time 300 -H "Authorization: Bearer ${WEBHOOK_SECRET}" "${AGENT_URL}/jobs") || {
180
+ JOBS_JSON=$(curl -fsS --max-time 300 -H "Authorization: Bearer ${API_SECRET}" "${AGENT_URL}/jobs") || {
175
181
  cat >&2 <<EOF
176
182
  Could not read ${AGENT_URL}/jobs.
177
183
 
@@ -193,7 +199,7 @@ EOF
193
199
  CRON_JOBS=$(printf '%s' "${JOBS_JSON}" | node "${READ_JSON}" crons)
194
200
  WATCHES_INBOX=$(printf '%s' "${JOBS_JSON}" | node "${READ_JSON}" watches-inbox)
195
201
 
196
- HEADERS="Content-Type=application/json,Authorization=Bearer ${WEBHOOK_SECRET}"
202
+ HEADERS="Content-Type=application/json,Authorization=Bearer ${API_SECRET}"
197
203
 
198
204
  # `create http` takes --headers; `update http` insists on --update-headers for the same
199
205
  # thing. Everything else about the two calls is identical.
@@ -164,6 +164,48 @@ async function checkSerper() {
164
164
  return fail("SERPER_API_KEY", json?.message ? `${json.message} (${status})` : `search → ${status}`);
165
165
  }
166
166
 
167
+ async function checkHubspot() {
168
+ const token = env.HUBSPOT_TOKEN;
169
+ if (!token) {
170
+ return skip("HUBSPOT_TOKEN", "unset");
171
+ }
172
+ // limit=1 with no properties: the cheapest call that still proves the token and a scope.
173
+ const { status, json } = await http("https://api.hubapi.com/crm/v3/objects/contacts?limit=1", {
174
+ headers: { Authorization: `Bearer ${token}` },
175
+ });
176
+ if (status === 200) {
177
+ return ok("HUBSPOT_TOKEN", "CRM readable");
178
+ }
179
+ // 403 is a granted-scopes problem, not a bad token — different fix, so say which.
180
+ return fail(
181
+ "HUBSPOT_TOKEN",
182
+ status === 403 ? "token valid but missing CRM read scope (403)" : `contacts → ${status}`,
183
+ );
184
+ }
185
+
186
+ async function checkKleer() {
187
+ const token = env.KLEER_API_TOKEN;
188
+ if (!token) {
189
+ return skip("KLEER_API_TOKEN", "unset");
190
+ }
191
+ // The only POST in the read-only skill: it lists the companies this token can reach, and
192
+ // is the documented way to discover a company id. It changes nothing.
193
+ const { status, json } = await http("https://api.kleer.se/v1/access/authenticate", {
194
+ method: "POST",
195
+ headers: { "X-Token": token, "content-type": "application/json" },
196
+ });
197
+ if (status === 200) {
198
+ const companies = json?.["accessible-companies"];
199
+ const count = Array.isArray(companies)
200
+ ? companies.length
201
+ : Array.isArray(companies?.["accessible-companies"])
202
+ ? companies["accessible-companies"].length
203
+ : "?";
204
+ return ok("KLEER_API_TOKEN", `${count} companies reachable`);
205
+ }
206
+ return fail("KLEER_API_TOKEN", json?.message ? `${json.message} (${status})` : `authenticate → ${status}`);
207
+ }
208
+
167
209
  async function checkCloudflare() {
168
210
  if (!env.CLOUDFLARE_API_KEY || !env.CLOUDFLARE_ACCOUNT_ID) {
169
211
  return skip("CLOUDFLARE", "CLOUDFLARE_API_KEY/ACCOUNT_ID not set");
@@ -513,9 +555,17 @@ async function checkDeploymentsContext() {
513
555
  }
514
556
 
515
557
  function checkPresence() {
516
- for (const k of ["CLAUDE_CODE_OAUTH_TOKEN", "AGENT_WEBHOOK_SECRET"]) {
558
+ for (const k of ["CLAUDE_CODE_OAUTH_TOKEN"]) {
517
559
  env[k] ? ok(k, "present (not remotely testable)") : skip(k, "unset");
518
560
  }
561
+ // Either name satisfies the engine; say which one is actually in play.
562
+ if (env.AGENT_API_SECRET) {
563
+ ok("AGENT_API_SECRET", "present (not remotely testable)");
564
+ } else if (env.AGENT_WEBHOOK_SECRET) {
565
+ ok("AGENT_API_SECRET", "present under the old name AGENT_WEBHOOK_SECRET");
566
+ } else {
567
+ skip("AGENT_API_SECRET", "unset");
568
+ }
519
569
  }
520
570
 
521
571
  // --- Run ---------------------------------------------------------------------
@@ -540,6 +590,8 @@ const CHECKS = [
540
590
  ["POSTHOG", checkPosthog],
541
591
  ["AHREFS_API_KEY", checkAhrefs],
542
592
  ["SERPER_API_KEY", checkSerper],
593
+ ["HUBSPOT_TOKEN", checkHubspot],
594
+ ["KLEER_API_TOKEN", checkKleer],
543
595
  ["CLOUDFLARE", checkCloudflare],
544
596
  ["ELEVENLABS_API_KEY", checkElevenlabs],
545
597
  ["GOOGLE_API_KEY (Places)", checkPlaces],