@meffecta/agent 1.0.3 → 1.0.7

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
@@ -26,7 +26,7 @@ npx @meffecta/agent setup-infra # registry, buckets, service account,
26
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
- npx @meffecta/agent setup-scheduler # give it its triggers
29
+ npx @meffecta/agent sync-triggers # give it its triggers
30
30
  ```
31
31
 
32
32
  Giving it access to something — Gmail, GA4, Grafana, PostHog, Cloudflare, an inbox
@@ -71,6 +71,54 @@ together, so a script can never provision the wrong shape for the engine it is a
71
71
  reads it; `env` names secret-backed settings without resolving them. Setting one takes the
72
72
  value from a pipe, a file, or a hidden prompt — never from your shell history.
73
73
 
74
+ ## Writing a job
75
+
76
+ ```bash
77
+ meffecta-agent create-job "a marketing report every Wednesday at 2pm"
78
+ ```
79
+
80
+ The model runs **on your deployment**, not on your laptop — it already holds the Claude
81
+ token, and more importantly its run has your content repo cloned and the engine's skills
82
+ mounted, so it can read your existing jobs for house style, your worlds, and
83
+ `systems/` for which systems this deployment can actually reach. It writes a file to
84
+ `jobs/<name>.md` and stops: nothing is committed, pushed or deployed, because that prompt
85
+ is something your agent will run and you should read it first.
86
+
87
+ What comes back is checked before it is written — a valid five-field cron in your
88
+ deployment's timezone, a usable job name, no `inbox:` without `allowFrom:` (the engine
89
+ refuses those, deliberately), an `effort:` the engine accepts, and no silent overwrite of
90
+ an existing job. `--dry-run` prints without writing; `--name` overrides the chosen name.
91
+
92
+ After you edit it by hand:
93
+
94
+ ```bash
95
+ meffecta-agent check-jobs # the same checks, on every job file here
96
+ ```
97
+
98
+ ## Anonymous analytics
99
+
100
+ On by default, and one command to stop:
101
+
102
+ ```bash
103
+ meffecta-agent analytics # exactly what is sent, and the current state
104
+ meffecta-agent analytics off # or set DO_NOT_TRACK=1
105
+ ```
106
+
107
+ **Sent:** which command ran — matched against the built-in list, never a word you typed —
108
+ whether it worked, how long it took, the class of any error, this CLI's version and the
109
+ engine tag it deploys, Node version, OS and architecture, how you installed it, and whether
110
+ a `deployment.env` was found. Plus a random id generated on your machine, so repeat runs
111
+ count as one install.
112
+
113
+ **Never sent:** your GCP project or service name, region, repo URLs, account or email
114
+ address, file paths, command arguments, secret names or values, job names, prompts — or any
115
+ error *message*. Only the error's class, because messages routinely quote a project id, a
116
+ path, or a line of `gcloud` output.
117
+
118
+ Off automatically in CI. Nothing is written to your machine at all if you have opted out.
119
+ It is one bounded request after the command's own work is finished, and a failure to send
120
+ is silent — analytics can never delay or fail a deploy.
121
+
74
122
  ## Requirements
75
123
 
76
124
  `gcloud` (authenticated), `docker` (to mirror engine images into your registry), `git`, and
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import { open as openAnalytics } from "../lib/analytics.js";
2
3
  import { COMMANDS, cliVersion, engineTag, GROUPS } from "../lib/commands.js";
3
4
  import { loadDeployment, UserError } from "../lib/config.js";
5
+ import { installMethod } from "../lib/version.js";
4
6
 
5
7
  /**
6
8
  * One entry point for everything an operator does to a Meffecta Agent deployment.
@@ -32,13 +34,15 @@ const STEPS = [
32
34
  ],
33
35
  [
34
36
  "init",
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.",
37
+ "Create your content repo — jobs/, SYSTEM.md, systems/, and a deployment.env in it.\n Run everything from there; it is what says which deployment you mean.",
36
38
  ],
37
- ["setup-infra", "Provision GCP and the Cloud Run service shell."],
39
+ ["setup-infra", "Provision GCP, the run queue, and the Cloud Run service shell.\n --dry-run shows it first."],
38
40
  ["set-secret", "AGENT_API_SECRET --random, CLAUDE_CODE_OAUTH_TOKEN, GITHUB_TOKEN."],
39
41
  ["set-env", "GIT_REPO_URL=… — the service will not boot without it."],
40
- ["deploy", "Roll out the engine image."],
41
- ["setup-scheduler", "Give it its triggers. Scaled to zero, it fires nothing without them."],
42
+ [
43
+ "deploy",
44
+ "Roll out the engine image, then create its triggers — scaled to zero it fires\n nothing without them, and only the running engine knows which jobs exist.",
45
+ ],
42
46
  ["doctor", 'Check it. Then: jobs, run <job>, ask "…".'],
43
47
  ];
44
48
 
@@ -60,9 +64,10 @@ function help() {
60
64
  console.log("");
61
65
  }
62
66
  console.log(" steps The whole set-up, in order, with the command for each");
63
- console.log(" version This CLI and the engine build it deploys\n");
64
- console.log("Every command takes --help, and passes its options through to the underlying");
65
- console.log("script so `meffecta-agent deploy --dry-run` and `--tag <sha>` work as documented.\n");
67
+ console.log(" version This CLI and the engine build it deploys");
68
+ console.log(" upgrade Whether a newer one is out, and what to run\n");
69
+ console.log("Every command takes --help, and every option it documents — so");
70
+ console.log("`meffecta-agent deploy --dry-run` and `--tag <sha>` work as described there.\n");
66
71
  console.log(`Engine: ghcr.io/meffecta/agent:${tag}${pinned ? " (pinned to this release)" : ""}`);
67
72
  }
68
73
 
@@ -94,17 +99,60 @@ function version() {
94
99
 
95
100
  const [name, ...args] = process.argv.slice(2);
96
101
 
102
+ const { analytics, firstRun } = openAnalytics({ version: cliVersion() });
103
+
104
+ // The name is matched against the known commands before it is ever recorded. Whatever an
105
+ // operator actually typed is theirs — a mistyped command could be anything, including
106
+ // something they would not want leaving the machine.
107
+ const BUILT_IN = new Set(["help", "steps", "version"]);
108
+ function recorded(candidate) {
109
+ if (!candidate) {
110
+ return "help";
111
+ }
112
+ return COMMANDS.has(candidate) || BUILT_IN.has(candidate) ? candidate : "unknown";
113
+ }
114
+
115
+ const started = Date.now();
116
+
117
+ /** Record the run, flush, and leave. The only exit in this file. */
118
+ async function finish(code, outcome, extra = {}) {
119
+ analytics.capture("cli_command", {
120
+ command: recorded(name),
121
+ outcome,
122
+ exit_code: code,
123
+ duration_ms: Date.now() - started,
124
+ cli_version: cliVersion(),
125
+ engine_tag: engineTag().tag,
126
+ install: installMethod().kind,
127
+ node_version: process.versions.node,
128
+ platform: process.platform,
129
+ arch: process.arch,
130
+ has_deployment: Boolean(loadDeployment().PROJECT),
131
+ ...extra,
132
+ });
133
+ await analytics.flush();
134
+ process.exit(code);
135
+ }
136
+
137
+ // Said once, on the run that mints the id, and never again. A tool that phones home should
138
+ // say so on the machine it phones home from — not only in documentation the operator would
139
+ // have to go looking for. stderr, so it cannot corrupt anything reading stdout.
140
+ if (firstRun) {
141
+ console.error("(Anonymous usage analytics are on: which commands run, and whether they worked.");
142
+ console.error(" No project names, arguments or secrets. `meffecta-agent analytics` to see or stop it.)\n");
143
+ }
144
+
97
145
  if (!name || name === "help" || name === "--help" || name === "-h") {
98
146
  help();
99
- process.exit(0);
147
+ await finish(0, "ok");
100
148
  }
101
149
  if (name === "steps") {
102
150
  steps();
103
- process.exit(0);
151
+ await finish(0, "ok");
104
152
  }
105
153
  if (name === "version" || name === "--version" || name === "-v") {
106
154
  version();
107
- process.exit(0);
155
+ await finish(0, "ok");
108
156
  }
109
157
 
110
158
  const command = COMMANDS.get(name);
@@ -115,7 +163,7 @@ if (!command) {
115
163
  console.error(`Did you mean: ${near.join(", ")}?`);
116
164
  }
117
165
  console.error("Run `meffecta-agent help` for the list.");
118
- process.exit(2);
166
+ await finish(2, "unknown-command");
119
167
  }
120
168
 
121
169
  try {
@@ -128,11 +176,15 @@ try {
128
176
  args.push("--tag", tag);
129
177
  }
130
178
  }
131
- process.exit((await command.handler(args)) ?? 0);
179
+ const code = (await command.handler(args)) ?? 0;
180
+ await finish(code, code === 0 ? "ok" : "failed");
132
181
  } catch (err) {
133
182
  if (err instanceof UserError) {
134
183
  console.error(err.message);
135
- process.exit(1);
184
+ await finish(1, "user-error");
136
185
  }
186
+ // The CLASS, never the message: messages quote project ids, paths and gcloud output.
187
+ analytics.capture("cli_crash", { command: recorded(name), error_kind: err?.constructor?.name ?? "Error" });
188
+ await analytics.flush();
137
189
  throw err;
138
190
  }
@@ -5,13 +5,13 @@
5
5
  # in Secret Manager), and a fresh clone of the content repo is then everything an
6
6
  # operator needs.
7
7
  #
8
- # Copy it there once:
9
- # cp deployment.env.example /path/to/your-content-repo/deployment.env
8
+ # Start one there:
9
+ # cd /path/to/your-content-repo && meffecta-agent init
10
10
  #
11
- # Then operate from the content repo, running the engine's scripts by path:
12
- # cd /path/to/your-content-repo
13
- # /path/to/agent/scripts/set-secret.sh AGENT_API_SECRET --random
14
- # /path/to/agent/scripts/deploy.sh
11
+ # Then run everything from that directory each command reads this file to know which
12
+ # deployment you mean:
13
+ # meffecta-agent set-secret AGENT_API_SECRET --random
14
+ # meffecta-agent deploy
15
15
 
16
16
  # The GCP *project ID* — check it with `gcloud projects list`, as the console may have
17
17
  # suffixed it and it need not match the project's display name.
@@ -23,6 +23,17 @@ SERVICE=acme-agent
23
23
  # Docker repo in that project, holding images mirrored from GHCR.
24
24
  ARTIFACT_REPO=acme-agent-images
25
25
 
26
+ # How the engine image reaches Cloud Run, which can only pull from Artifact Registry.
27
+ # proxy (default) ARTIFACT_REPO is a remote repository in front of ghcr.io. AR fetches
28
+ # the public engine image on demand — nothing is copied, and no docker is needed
29
+ # on your machine at any point.
30
+ # mirror the older path: pull from ghcr.io here, retag, push. Needs docker running and
31
+ # `gcloud auth configure-docker <region>-docker.pkg.dev`, and pushes several GB
32
+ # per engine version. Use it only if your policy forbids AR reaching the internet.
33
+ # A repository's mode is fixed when it is created, so changing this means a new
34
+ # ARTIFACT_REPO name and another setup-infrastructure run.
35
+ REGISTRY_MODE=proxy
36
+
26
37
  REGION=europe-west1
27
38
 
28
39
  # Optional. Unset = whichever account gcloud is logged in as. Set it when you work across
@@ -35,14 +46,16 @@ REGION=europe-west1
35
46
  # Optional. Where published engine images come from.
36
47
  # GHCR_IMAGE=ghcr.io/meffecta/agent
37
48
 
38
- # Optional. The runtime shape deploy.sh asserts on every rollout, so the deployed service
49
+ # Optional. The runtime shape asserted on every rollout, so the deployed service
39
50
  # matches this file rather than whatever was last changed by hand. The defaults below are
40
- # what setup-infrastructure.sh provisions, so leaving them out is the same as setting them.
51
+ # what `meffecta-agent setup-infra` provisions, so leaving them out is the same as setting
52
+ # them.
41
53
  #
42
54
  # SCALING picks one of two coherent pairs, and the choice is not cosmetic:
43
- # scale-to-zero min-instances=0 + CPU billed per request. Costs nothing between jobs,
44
- # and requires setup-scheduler.sh — the service has no CPU of its own to
45
- # run a timer with, so everything must arrive as a request.
55
+ # scale-to-zero min-instances=0 + CPU billed per request. Costs nothing between jobs.
56
+ # The service has no CPU of its own to run a timer with, so everything
57
+ # arrives as a request: `setup-infra` creates the queue those arrive on,
58
+ # and `deploy` creates the schedules that send them.
46
59
  # always-on min-instances=1 + CPU always allocated, for a deployment that drives
47
60
  # itself with in-process timers and has no Cloud Scheduler triggers.
48
61
  # Running scale-to-zero without triggers is a service that does nothing; running always-on
package/engine.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "engineTag": "1.0.3",
3
- "builtFrom": "f784051bc092c57aa37b4e9c44e06ee8f368f8f7"
2
+ "engineTag": "1.0.7",
3
+ "builtFrom": "26157a11d57ec9821672a644bf2ea5b3da3e23f5"
4
4
  }
@@ -0,0 +1,262 @@
1
+ // Anonymous usage analytics, so the set-up can be improved by evidence rather than guess.
2
+ //
3
+ // This CLI runs on other people's machines, in their cloud accounts, holding their
4
+ // credentials. That earns a stricter standard than a web app's:
5
+ //
6
+ // - it never blocks — one bounded POST at exit, and the command's own work is done
7
+ // - it never throws — every path swallows its errors; analytics cannot fail a deploy
8
+ // - it never fires off — disabled, in CI, or with no key, capture() is a no-op
9
+ // - it never carries content — see WHAT IS SENT below; the list is exhaustive on purpose
10
+ //
11
+ // Dependency-free by necessity and by choice: the package has no runtime dependencies, and
12
+ // a tool holding someone's cloud credentials is a poor place for a supply chain. That means
13
+ // speaking PostHog's capture API directly rather than pulling in posthog-node.
14
+ //
15
+ // WHAT IS SENT, in full:
16
+ // which command ran (from the fixed list — never a word the operator typed), whether it
17
+ // succeeded, how long it took, this CLI's version and the engine tag it deploys, Node
18
+ // version, OS and architecture, how the CLI was installed, and whether a deployment.env
19
+ // was found. Plus a random id, minted here, so repeat runs group as one install.
20
+ //
21
+ // WHAT IS NEVER SENT:
22
+ // the GCP project or service name, the region, repo URLs, email or account names, file
23
+ // paths, command arguments, secret names or values, job names, prompts, or any error
24
+ // MESSAGE — only the error's class. Messages are the leak: they routinely quote a
25
+ // project id, a path, or a line of gcloud output.
26
+ //
27
+ // Not even a hashed project id. It would be genuinely useful — it would let one deployment
28
+ // be followed across machines — but a hash of a short, guessable string is pseudonymous at
29
+ // best, and the anonymous install id already answers most of the same questions. If that
30
+ // correlation is ever wanted it should be a deliberate decision, disclosed on its own, not
31
+ // something that arrived quietly inside a telemetry patch.
32
+
33
+ import { randomUUID } from "node:crypto";
34
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
35
+ import { homedir } from "node:os";
36
+ import { dirname, join } from "node:path";
37
+
38
+ /**
39
+ * PostHog *project* key — write-only, designed to ship inside a client, and public by
40
+ * nature: this package is on npm, so treat it as published. It grants event ingestion and
41
+ * nothing else. Empty means nothing is ever sent.
42
+ */
43
+ const DEFAULT_KEY = "phc_pyUMjGAJSMJtStPULeiiutucEiN6qkRhuKr5RJVuEXqX";
44
+
45
+ /** A project key belongs to one region; this one is EU cloud. */
46
+ const DEFAULT_HOST = "https://eu.i.posthog.com";
47
+
48
+ const LIB = "meffecta-agent-cli";
49
+
50
+ /** Analytics is never worth a hang, and never worth a visible pause. */
51
+ const FLUSH_TIMEOUT_MS = 1500;
52
+
53
+ /** An env var means "on" unless it is empty, "0", or "false". */
54
+ function truthy(value) {
55
+ const v = (value ?? "").trim().toLowerCase();
56
+ return v !== "" && v !== "0" && v !== "false";
57
+ }
58
+
59
+ /** Exported so tests can neutralise every one of them, rather than the two they remember. */
60
+ export const CI_VARS = [
61
+ "CI",
62
+ "CONTINUOUS_INTEGRATION",
63
+ "GITHUB_ACTIONS",
64
+ "GITLAB_CI",
65
+ "CIRCLECI",
66
+ "TRAVIS",
67
+ "BUILDKITE",
68
+ "JENKINS_URL",
69
+ "TEAMCITY_VERSION",
70
+ "BITBUCKET_BUILD_NUMBER",
71
+ ];
72
+
73
+ export function isCI(env = process.env) {
74
+ return CI_VARS.some((key) => truthy(env[key]));
75
+ }
76
+
77
+ /**
78
+ * Where the choice and the anonymous id live. Machine-global, NOT beside deployment.env:
79
+ * the opt-out is a decision by the person at the keyboard and should cover every
80
+ * deployment they operate, rather than being made again per content repo — and it must
81
+ * never end up committed to one.
82
+ */
83
+ export function configDir(env = process.env) {
84
+ const base = env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config");
85
+ return join(base, "meffecta-agent");
86
+ }
87
+
88
+ export function configPath(env = process.env) {
89
+ return join(configDir(env), "config.json");
90
+ }
91
+
92
+ /** Missing or corrupt reads as empty. Never throws — this is on the startup path. */
93
+ export function readConfig(env = process.env) {
94
+ try {
95
+ const parsed = JSON.parse(readFileSync(configPath(env), "utf8"));
96
+ return parsed && typeof parsed === "object" ? parsed : {};
97
+ } catch {
98
+ return {};
99
+ }
100
+ }
101
+
102
+ /** Merge and persist atomically, so a crash mid-write cannot corrupt the choice. */
103
+ export function writeConfig(patch, env = process.env) {
104
+ const path = configPath(env);
105
+ const next = { ...readConfig(env), ...patch };
106
+ try {
107
+ mkdirSync(dirname(path), { recursive: true });
108
+ const tmp = `${path}.${process.pid}.tmp`;
109
+ writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`);
110
+ renameSync(tmp, path);
111
+ } catch {
112
+ // A read-only home directory is not a reason to fail a deploy. The consequence is a
113
+ // fresh id next run, which is the harmless direction to fail in.
114
+ }
115
+ return next;
116
+ }
117
+
118
+ /**
119
+ * On or off, and why. Opt-out by default, with several ways to say no and one explicit
120
+ * way to say yes:
121
+ *
122
+ * 1. DO_NOT_TRACK=1 the cross-tool standard — always wins
123
+ * 2. MEFFECTA_ANALYTICS_DISABLED=1 this tool's own kill switch
124
+ * 3. config "disabled" `meffecta-agent analytics disable`
125
+ * 4. config "enabled" explicit yes — beats CI detection
126
+ * 5. a CI environment off: a pipeline is nobody's choice to make
127
+ * 6. otherwise on
128
+ *
129
+ * Pure, so every rule is testable without touching a disk or a network.
130
+ */
131
+ export function decideAnalytics({ config = {}, env = process.env } = {}) {
132
+ if (truthy(env.DO_NOT_TRACK)) {
133
+ return { enabled: false, reason: "do-not-track" };
134
+ }
135
+ if (truthy(env.MEFFECTA_ANALYTICS_DISABLED)) {
136
+ return { enabled: false, reason: "env-disabled" };
137
+ }
138
+ if (config.analytics === "disabled") {
139
+ return { enabled: false, reason: "config-disabled" };
140
+ }
141
+ if (config.analytics === "enabled") {
142
+ return { enabled: true, reason: "config-enabled" };
143
+ }
144
+ if (isCI(env)) {
145
+ return { enabled: false, reason: "ci" };
146
+ }
147
+ return { enabled: true, reason: "default" };
148
+ }
149
+
150
+ /** Endpoint and key, with env overrides for anyone who wants to point it elsewhere. */
151
+ export function resolvePosthog(env = process.env) {
152
+ return {
153
+ host: (env.MEFFECTA_POSTHOG_HOST?.trim() || DEFAULT_HOST).replace(/\/+$/, ""),
154
+ key: env.MEFFECTA_POSTHOG_KEY?.trim() ?? DEFAULT_KEY,
155
+ };
156
+ }
157
+
158
+ /** The default transport: one fetch, aborted rather than allowed to hang. */
159
+ const defaultSender = async (url, body, timeoutMs) => {
160
+ const controller = new AbortController();
161
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
162
+ try {
163
+ await fetch(url, {
164
+ method: "POST",
165
+ headers: { "content-type": "application/json" },
166
+ body: JSON.stringify(body),
167
+ signal: controller.signal,
168
+ });
169
+ } finally {
170
+ clearTimeout(timer);
171
+ }
172
+ };
173
+
174
+ export class Analytics {
175
+ #enabled;
176
+ #id;
177
+ #key;
178
+ #host;
179
+ #version;
180
+ #send;
181
+ #queue = [];
182
+
183
+ constructor({ enabled = false, anonymousId = "", key = "", host = "", version = "", send } = {}) {
184
+ this.#enabled = enabled;
185
+ this.#id = anonymousId;
186
+ this.#key = key;
187
+ this.#host = host;
188
+ this.#version = version;
189
+ this.#send = send ?? defaultSender;
190
+ }
191
+
192
+ /** Guaranteed to do nothing — the fallback whenever anything is unclear. */
193
+ static off() {
194
+ return new Analytics();
195
+ }
196
+
197
+ /** Only true when a real, sendable pipeline exists. */
198
+ get active() {
199
+ return this.#enabled && this.#key !== "" && this.#id !== "";
200
+ }
201
+
202
+ /** Queue an event. A no-op when off, and never throws. */
203
+ capture(event, properties = {}) {
204
+ if (!this.active) {
205
+ return;
206
+ }
207
+ this.#queue.push({ event, properties, timestamp: new Date().toISOString() });
208
+ }
209
+
210
+ /** Send what is queued. Bounded, and swallows everything. */
211
+ async flush() {
212
+ if (!this.active || this.#queue.length === 0) {
213
+ return;
214
+ }
215
+ const batch = this.#queue.splice(0, this.#queue.length);
216
+ try {
217
+ await this.#send(
218
+ `${this.#host}/batch/`,
219
+ {
220
+ api_key: this.#key,
221
+ batch: batch.map((e) => ({
222
+ event: e.event,
223
+ distinct_id: this.#id,
224
+ timestamp: e.timestamp,
225
+ properties: { ...e.properties, $lib: LIB, $lib_version: this.#version },
226
+ })),
227
+ },
228
+ FLUSH_TIMEOUT_MS,
229
+ );
230
+ } catch {
231
+ // Analytics must never surface an error to the operator.
232
+ }
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Resolve everything and hand back a client ready to use.
238
+ *
239
+ * The id is minted on the first run where analytics is on, and only then: a machine that
240
+ * has opted out never gets an identifier written to it at all. `firstRun` is true for the
241
+ * run that mints it, which is the one run that says so out loud.
242
+ */
243
+ export function open({ env = process.env, version = "unknown", send } = {}) {
244
+ const config = readConfig(env);
245
+ const decision = decideAnalytics({ config, env });
246
+ if (!decision.enabled) {
247
+ return { analytics: Analytics.off(), decision, config, firstRun: false };
248
+ }
249
+ const { host, key } = resolvePosthog(env);
250
+ let anonymousId = config.anonymousId;
251
+ const firstRun = !anonymousId;
252
+ if (firstRun) {
253
+ anonymousId = randomUUID();
254
+ writeConfig({ anonymousId }, env);
255
+ }
256
+ return {
257
+ analytics: new Analytics({ enabled: true, anonymousId, key, host, version, send }),
258
+ decision,
259
+ config: { ...config, anonymousId },
260
+ firstRun,
261
+ };
262
+ }