@getnoan/wizard 0.1.2 → 0.1.4
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 +23 -7
- package/package.json +1 -1
- package/src/agents.mjs +62 -4
- package/src/report.mjs +1 -1
- package/src/run.mjs +48 -8
- package/src/telemetry.mjs +33 -5
package/README.md
CHANGED
|
@@ -30,11 +30,17 @@ Run it again any time; every write is a merge, and nothing you have is overwritt
|
|
|
30
30
|
4. **Your fact layer.** If the workspace holds fewer than ten facts, the wizard does not guess
|
|
31
31
|
facts for you. It hands the seeding to your assistant, which has the procedure from the skill:
|
|
32
32
|
read your website, repo and docs, propose a structure, and write only after you say yes.
|
|
33
|
-
5. **The agent pack** (optional, `--agents`).
|
|
34
|
-
|
|
33
|
+
5. **The agent pack** (optional, `--agents`). Asks what to call your agent — Verity by default, the
|
|
34
|
+
name she answers to in NOAN — then forks [getnoan/agent-pack](https://github.com/getnoan/agent-pack)
|
|
35
|
+
to your account, verifies and stores your model and Resend keys (and optionally a Postgres URL
|
|
35
36
|
and a Firecrawl key) as repository secrets, runs the pack's seed scripts so each agent's starting
|
|
36
|
-
instructions are in your workspace, records the block slugs as repository variables,
|
|
37
|
-
one dry run. Safe mode stays on: the wizard never sets `DRY_RUN` to 0. That switch is yours.
|
|
37
|
+
instructions are in your workspace, records the name and the block slugs as repository variables,
|
|
38
|
+
and triggers one dry run. Safe mode stays on: the wizard never sets `DRY_RUN` to 0. That switch is yours.
|
|
39
|
+
|
|
40
|
+
The model does not have to be Anthropic's. Set `ANTHROPIC_BASE_URL` in the environment before
|
|
41
|
+
running — `https://openrouter.ai/api`, say, or your own gateway — and the wizard verifies your key
|
|
42
|
+
against *that* endpoint, stores it as `LLM_API_KEY`, and records the endpoint as a repository
|
|
43
|
+
variable so the fork actually uses it. Unset, nothing changes: it asks for an Anthropic key.
|
|
38
44
|
6. **The report.** What happened, and the one thing to do next.
|
|
39
45
|
|
|
40
46
|
## For a coding assistant
|
|
@@ -65,12 +71,22 @@ Exit codes: 0 done, 2 the key did not work, 3 cancelled, 1 anything else.
|
|
|
65
71
|
| `--agents` / `--no-agents` | include or skip the agent pack step |
|
|
66
72
|
| `--no-mcp`, `--no-skill` | skip a step |
|
|
67
73
|
| `--dry-run` | show every write without making it |
|
|
68
|
-
| `--no-telemetry` | send nothing about this run (also `NOAN_WIZARD_NO_TELEMETRY
|
|
74
|
+
| `--no-telemetry` | send nothing about this run (also `NOAN_WIZARD_NO_TELEMETRY`, `DO_NOT_TRACK`, or any `CI`) |
|
|
69
75
|
|
|
70
76
|
## Telemetry
|
|
71
77
|
|
|
72
|
-
Three
|
|
73
|
-
platform
|
|
78
|
+
Three events — a run started, and then completed or cancelled — carrying the Node version, the
|
|
79
|
+
platform, the exit code, how long the run took, whether it was non-interactive (`--yes`), whether
|
|
80
|
+
the report was JSON, and whether the workspace was empty. That is the whole payload. Never a key,
|
|
81
|
+
never a path, never a workspace name, never anything read out of your fact layer.
|
|
82
|
+
|
|
83
|
+
A run is identified by an id made up for that process and thrown away with it, so the events of one
|
|
84
|
+
run line up and nothing links one run to the next. The events ask PostHog for no person profile and
|
|
85
|
+
no geolocation. Like any HTTP request they arrive from your IP address.
|
|
86
|
+
|
|
87
|
+
Nothing is sent when any of these is true: `--no-telemetry`, `NOAN_WIZARD_NO_TELEMETRY` set to
|
|
88
|
+
anything but `0` or `false`, `DO_NOT_TRACK` likewise, or `CI` set — a pipeline running the wizard
|
|
89
|
+
is not a person trying it.
|
|
74
90
|
|
|
75
91
|
## Why a wizard and not a prompt
|
|
76
92
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getnoan/wizard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "One command from nothing to a grounded agent: sign in to NOAN, wire your coding assistant, install the skill, and set up the agent pack.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/agents.mjs
CHANGED
|
@@ -31,12 +31,70 @@ export function parseSeedOutput(text) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/** The secrets and variables the pack documents, and which are required for which agent. */
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
/** LLM_API_KEY is an ALTERNATIVE to ANTHROPIC_API_KEY, not an addition: the pack accepts either
|
|
35
|
+
* name for the model key and one of the two is required. It is listed so a caller enumerating
|
|
36
|
+
* the pack's secrets knows the name exists, not so anyone sets both. */
|
|
37
|
+
export const PACK_SECRETS = ["NOAN_PERSONAL_API_KEY", "RESEND_API_KEY", "ANTHROPIC_API_KEY", "LLM_API_KEY", "DATABASE_URL", "FIRECRAWL_API_KEY", "NEWSLETTER_UNSUB_SECRET"];
|
|
38
|
+
export const PACK_VARS = ["MAIL_FROM", "REPLY_TO", "ESCALATE_TO", "STATE_BACKEND", "ANTHROPIC_BASE_URL", "AGENT_NAME", "COMPANY_NAME", "AGENT_IDENTITY_IDS", "COMMANDERS", "REPORT_RECIPIENT_TAG"];
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
/** The pack's model endpoint, from the same variable the pack itself reads. Bare base, no
|
|
41
|
+
* trailing slash; empty string means the default vendor.
|
|
42
|
+
*
|
|
43
|
+
* A bad value THROWS rather than falling back, and both rules are the pack's own
|
|
44
|
+
* (resolveBaseUrl in agents/anthropic.mjs), deliberately down to the wording.
|
|
45
|
+
*
|
|
46
|
+
* This returned "" on anything URL() rejected until review caught it. "" means "default
|
|
47
|
+
* vendor", so `ANTHROPIC_BASE_URL=openrouter.ai/api` — a missing scheme, the likeliest typo
|
|
48
|
+
* there is — silently became "no endpoint": the key went in under the vendor's name, was
|
|
49
|
+
* verified against the vendor's host, came back 401 and was DISCARDED. That is precisely the
|
|
50
|
+
* sequence this whole change exists to stop, reached by a typo instead of by design.
|
|
51
|
+
*
|
|
52
|
+
* The scheme rule matters for the opposite reason: URL() happily parses `ftp://host` and
|
|
53
|
+
* `javascript:...`, and accepting one writes a repository variable the agents refuse at
|
|
54
|
+
* runtime — on a schedule, where nobody is watching. Better to refuse it here, in front of
|
|
55
|
+
* someone who can retype it. */
|
|
56
|
+
export function modelBase(env = process.env) {
|
|
57
|
+
const raw = (env.ANTHROPIC_BASE_URL || "").trim().replace(/\/+$/, "");
|
|
58
|
+
if (!raw) return "";
|
|
59
|
+
let url;
|
|
60
|
+
try { url = new URL(raw); } catch { throw new Error(`ANTHROPIC_BASE_URL is not a valid URL: ${raw}`); }
|
|
61
|
+
if (!/^https?:$/.test(url.protocol)) throw new Error(`ANTHROPIC_BASE_URL must be http(s): ${raw}`);
|
|
62
|
+
return raw;
|
|
63
|
+
}
|
|
64
|
+
export const DEFAULT_MODEL_BASE = "https://api.anthropic.com";
|
|
65
|
+
/** Which secret name the model key is STORED under: the neutral one once an endpoint is set,
|
|
66
|
+
* because "ANTHROPIC_API_KEY" holding an OpenRouter key is a lie the next reader has to unpick. */
|
|
67
|
+
export const modelKeyName = (env = process.env) => (modelBase(env) ? "LLM_API_KEY" : "ANTHROPIC_API_KEY");
|
|
68
|
+
|
|
69
|
+
/** What the agents call themselves, and how their own prose refers to them.
|
|
70
|
+
* Verity is the default because that is the agent people meet in NOAN; anyone who
|
|
71
|
+
* wants their own name says so and gets it. Unset, the pack signs as "Agent", which
|
|
72
|
+
* is nobody, so the wizard always sends a name.
|
|
73
|
+
* Pronouns default to she/her only for Verity: assuming them for a name someone
|
|
74
|
+
* else chose is worse than the pack's own neutral fallback (they/them). */
|
|
75
|
+
export function agentIdentity({ name, pronouns } = {}) {
|
|
76
|
+
const n = String(name ?? "").trim() || "Verity";
|
|
77
|
+
const p = String(pronouns ?? "").trim() || (n.toLowerCase() === "verity" ? "she/her" : "");
|
|
78
|
+
return { AGENT_NAME: n, ...(p && { AGENT_PRONOUNS: p }) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Verify the model key against the endpoint that will actually be called.
|
|
82
|
+
*
|
|
83
|
+
* Against the default vendor this is a straight yes/no. Against a configured endpoint it is
|
|
84
|
+
* advisory: `/v1/models` is not part of the Messages contract, so a gateway may not implement
|
|
85
|
+
* it, and a 404 there says nothing about the key. Returning "unknown" rather than false is the
|
|
86
|
+
* point — the previous behaviour verified every key against the vendor's own host, so a valid
|
|
87
|
+
* gateway key came back 401 and was DISCARDED with "not accepted by its service". */
|
|
88
|
+
export async function verifyModelKey(key, base = "") {
|
|
89
|
+
const root = base || DEFAULT_MODEL_BASE;
|
|
90
|
+
try {
|
|
91
|
+
const r = await fetch(`${root}/v1/models`, { headers: { "x-api-key": key, "anthropic-version": "2023-06-01" } });
|
|
92
|
+
if (r.status === 200) return true;
|
|
93
|
+
// Only the vendor's own host is trusted to mean "this key is bad".
|
|
94
|
+
return base ? "unknown" : false;
|
|
95
|
+
} catch { return base ? "unknown" : false; }
|
|
39
96
|
}
|
|
97
|
+
export const verifyAnthropic = (key) => verifyModelKey(key, "");
|
|
40
98
|
export async function verifyResend(key) {
|
|
41
99
|
try { const r = await fetch("https://api.resend.com/domains", { headers: { Authorization: `Bearer ${key}` } }); return r.status === 200; } catch { return false; }
|
|
42
100
|
}
|
package/src/report.mjs
CHANGED
|
@@ -14,7 +14,7 @@ export function renderReport(report, { json = false } = {}) {
|
|
|
14
14
|
}
|
|
15
15
|
if (s.skill?.rows) L.push(`skill: ${s.skill.rows.map(r => `${r.id} (${r.written} written)`).join(" · ")}; pointers: ${s.skill.pointers.map(p => p.action).join(", ")}`);
|
|
16
16
|
if (s.workspace) L.push(`facts: ${s.workspace.count ?? "?"} (${s.workspace.state})`);
|
|
17
|
-
if (s.agents && !s.agents.skipped) L.push(s.agents.ok ? `agents: ${s.agents.repo || s.agents.dest} — ${s.agents.secrets.length} secrets, ${s.agents.variables.length} variables, safe mode on` : `agents: not set up (${s.agents.reason})`);
|
|
17
|
+
if (s.agents && !s.agents.skipped) L.push(s.agents.ok ? `agents: ${s.agents.repo || s.agents.dest} — ${s.agents.agentName || "your agent"}, ${s.agents.secrets.length} secrets, ${s.agents.variables.length} variables, safe mode on` : `agents: not set up (${s.agents.reason})`);
|
|
18
18
|
if (report.next?.length) { L.push("", "──────── next ────────"); for (const n of report.next) L.push(`• ${n.say}${n.why ? ` (${n.why})` : ""}`); }
|
|
19
19
|
L.push("");
|
|
20
20
|
return L.join("\n");
|
package/src/run.mjs
CHANGED
|
@@ -105,28 +105,67 @@ async function setupAgents({ args, ui, dir, key, me, report }) {
|
|
|
105
105
|
const out = { ok: true, dest: fork.dest, secrets: [], variables: [], seeds: [], skipped: [] };
|
|
106
106
|
|
|
107
107
|
// Keys: from the environment (an agent's run) or a prompt (a person's). Verified before they are stored.
|
|
108
|
-
const need = async (name, verify, prompt, { optional = false } = {}) => {
|
|
109
|
-
|
|
108
|
+
const need = async (name, verify, prompt, { optional = false, from = [] } = {}) => {
|
|
109
|
+
// `from` is other env names that may carry the same value. The model key has two accepted
|
|
110
|
+
// spellings and a user who set the one we are not storing under has still configured it.
|
|
111
|
+
let v = "";
|
|
112
|
+
for (const n of [name, ...from]) { v = (process.env[n] || "").trim(); if (v) break; }
|
|
110
113
|
if (!v && ui.interactive) v = await ui.ask(prompt, { secret: true });
|
|
111
114
|
if (!v) { out.skipped.push({ name, why: optional ? "not provided" : "required, not provided" }); return null; }
|
|
112
|
-
if (verify
|
|
115
|
+
if (verify) {
|
|
116
|
+
// A verdict of "unknown" means the endpoint could not answer, not that the key is bad —
|
|
117
|
+
// see verifyModelKey. Discarding on that would throw away a working key.
|
|
118
|
+
const verdict = await verify(v);
|
|
119
|
+
if (verdict === "unknown") ui.warn(`${name} could not be confirmed by ${prompt.replace(/^.*for /, "").replace(/ \(.*$/, "")}; storing it anyway`);
|
|
120
|
+
else if (!verdict) { ui.warn(`${name} was not accepted by its service; not stored`); out.skipped.push({ name, why: "rejected" }); return null; }
|
|
121
|
+
}
|
|
113
122
|
return v;
|
|
114
123
|
};
|
|
115
|
-
|
|
124
|
+
/* The model endpoint is configuration in the pack (ANTHROPIC_BASE_URL), so it is here too.
|
|
125
|
+
* With one set, the key is verified against THAT endpoint and stored under the neutral name —
|
|
126
|
+
* verifying a gateway key against the vendor's own host returns 401, and the wizard used to
|
|
127
|
+
* discard a perfectly good key on the strength of it. Either spelling is accepted as a source. */
|
|
128
|
+
/* A bad ANTHROPIC_BASE_URL stops the agents step and says why, rather than being treated as
|
|
129
|
+
* "unset". Falling back would put the key in under the vendor's name and verify it against the
|
|
130
|
+
* vendor's host — the discarded-key failure this change exists to fix, reached by a typo.
|
|
131
|
+
*
|
|
132
|
+
* The fork and clone have already happened by here, so the message says what is and is not
|
|
133
|
+
* true: no secret, variable or .env has been written, and re-running is safe because
|
|
134
|
+
* forkAndClone reuses an existing clone. */
|
|
135
|
+
let mBase;
|
|
136
|
+
try { mBase = pack.modelBase(); }
|
|
137
|
+
catch (e) {
|
|
138
|
+
ui.warn(`${e.message}\n Fix it (or unset it to use the default) and run again — no secret or variable was written, and the clone is reused.`);
|
|
139
|
+
return { ok: false, reason: e.message };
|
|
140
|
+
}
|
|
141
|
+
const mName = pack.modelKeyName();
|
|
142
|
+
const mHost = mBase ? new URL(mBase).host : "Anthropic";
|
|
143
|
+
const anthropic = await need(mName, (k) => pack.verifyModelKey(k, mBase),
|
|
144
|
+
`API key for ${mHost} (the agents' model)`,
|
|
145
|
+
{ from: [mName === "LLM_API_KEY" ? "ANTHROPIC_API_KEY" : "LLM_API_KEY"] });
|
|
116
146
|
const resend = await need("RESEND_API_KEY", pack.verifyResend, "Resend API key (the agents' email)");
|
|
117
147
|
const db = await need("DATABASE_URL", null, "Postgres URL for the agents' state ledger (blank to skip; then only market research runs)", { optional: true });
|
|
118
148
|
const firecrawl = await need("FIRECRAWL_API_KEY", pack.verifyFirecrawl, "Firecrawl API key (market research; blank to skip)", { optional: true });
|
|
119
149
|
const mailFrom = process.env.MAIL_FROM || (ui.interactive ? await ui.ask("Sender for the agents' mail, e.g. Agent <agent@yourdomain.com> (a domain verified in Resend)") : "");
|
|
120
150
|
const replyTo = process.env.REPLY_TO || (ui.interactive ? await ui.ask("Reply-to address (a mailbox someone reads)", { dflt: mailFrom.replace(/^.*<|>.*$/g, "") }) : "");
|
|
121
151
|
const escalateTo = process.env.ESCALATE_TO || (ui.interactive ? await ui.ask("Where the support agent escalates when it cannot answer") : "");
|
|
152
|
+
const askedName = process.env.AGENT_NAME || (ui.interactive ? await ui.ask("What should your agent be called?", { dflt: "Verity" }) : "");
|
|
153
|
+
const identity = pack.agentIdentity({ name: askedName, pronouns: process.env.AGENT_PRONOUNS
|
|
154
|
+
|| (ui.interactive && askedName && askedName.toLowerCase() !== "verity"
|
|
155
|
+
? await ui.ask(`Pronouns for ${askedName}, in ${askedName}'s own prose (blank for they/them)`)
|
|
156
|
+
: "") });
|
|
122
157
|
|
|
123
|
-
const secrets = { NOAN_PERSONAL_API_KEY: key, ...(anthropic && {
|
|
158
|
+
const secrets = { NOAN_PERSONAL_API_KEY: key, ...(anthropic && { [mName]: anthropic }), ...(resend && { RESEND_API_KEY: resend }),
|
|
124
159
|
...(db && { DATABASE_URL: db }), ...(firecrawl && { FIRECRAWL_API_KEY: firecrawl }), NEWSLETTER_UNSUB_SECRET: pack.randomSecret() };
|
|
125
160
|
for (const [n, v] of Object.entries(secrets)) { const r = pack.setSecret(fork.dest, n, v, args.dryRun); out.secrets.push(r); ui.ok(`secret ${n}: ${r.action}${r.error ? ` — ${r.error}` : ""}`); }
|
|
126
|
-
|
|
161
|
+
// The endpoint is a repository VARIABLE, not a secret: without it the fork holds a gateway key
|
|
162
|
+
// and still calls the default vendor, which is the same "documented is not delivered" trap the
|
|
163
|
+
// pack's own workflows had.
|
|
164
|
+
const vars = { DRY_RUN: "1", STATE_BACKEND: db ? "postgres" : "local", ...(mBase && { ANTHROPIC_BASE_URL: mBase }), ...(mailFrom && { MAIL_FROM: mailFrom }), ...(replyTo && { REPLY_TO: replyTo }), ...(escalateTo && { ESCALATE_TO: escalateTo }),
|
|
165
|
+
...identity,
|
|
127
166
|
COMPANY_NAME: me.project?.name || "", ...(me.identity?.id && { AGENT_IDENTITY_IDS: me.identity.id }), ...(me.identity?.email && { COMMANDERS: me.identity.email }) };
|
|
128
167
|
// The seeds need the key in the clone's .env; the pack's own .env.example documents the rest.
|
|
129
|
-
pack.writePackEnv(fork.dest, { NOAN_PERSONAL_API_KEY: key, ...(anthropic && {
|
|
168
|
+
pack.writePackEnv(fork.dest, { NOAN_PERSONAL_API_KEY: key, ...(anthropic && { [mName]: anthropic }), ...(mBase && { ANTHROPIC_BASE_URL: mBase }), ...(resend && { RESEND_API_KEY: resend }) }, args.dryRun);
|
|
130
169
|
const seeds = pack.runSeeds(fork.dest, { NOAN_PERSONAL_API_KEY: key }, { dryRun: args.dryRun });
|
|
131
170
|
out.seeds = seeds.rows;
|
|
132
171
|
for (const r of seeds.rows) ui.ok(`${r.seed}: ${r.action}${r.slugs != null ? ` (${r.slugs} slug(s))` : ""}${r.error ? ` — ${r.error}` : ""}`);
|
|
@@ -145,7 +184,7 @@ async function setupAgents({ args, ui, dir, key, me, report }) {
|
|
|
145
184
|
ui.ok(`${out.variables.length} repository variable(s) ${args.dryRun ? "to set" : "set"} (DRY_RUN=1: every agent stays in safe mode)`);
|
|
146
185
|
// No point dispatching a run that will only fail for a missing key: say what is missing instead.
|
|
147
186
|
if (!anthropic || !resend) {
|
|
148
|
-
const missing = [!anthropic &&
|
|
187
|
+
const missing = [!anthropic && mName, !resend && "RESEND_API_KEY"].filter(Boolean);
|
|
149
188
|
out.incomplete = missing;
|
|
150
189
|
ui.warn(`not dispatching a run: ${missing.join(" and ")} still missing. Add them as repository secrets (or re-run with them in the environment), then run any agent workflow with dry_run=1.`);
|
|
151
190
|
} else {
|
|
@@ -155,5 +194,6 @@ async function setupAgents({ args, ui, dir, key, me, report }) {
|
|
|
155
194
|
ui.ok(`${wf}: ${d.action}${d.error ? ` — ${d.error}` : ""} — read it under Actions, then set DRY_RUN=0 for the agents you trust`);
|
|
156
195
|
}
|
|
157
196
|
out.repo = pack.repoSlug(fork.dest);
|
|
197
|
+
out.agentName = identity.AGENT_NAME;
|
|
158
198
|
return out;
|
|
159
199
|
}
|
package/src/telemetry.mjs
CHANGED
|
@@ -1,17 +1,45 @@
|
|
|
1
1
|
/** Runs, completions and cancels — the three counts that say whether the wizard works for strangers.
|
|
2
|
-
* Off
|
|
3
|
-
|
|
2
|
+
* Off whenever the user says so, and off in CI. Never a key, never a path, never a workspace name. */
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
// The public, write-only PostHog project key. Embedding one is what it is for; it can send
|
|
6
|
+
// events and read nothing. Empty = telemetry off.
|
|
7
|
+
export const POSTHOG_TOKEN = "phc_7O3g17L9CuhzTtpKkrklUZpFGJymLxrO3iMMH1krn6s";
|
|
4
8
|
const HOST = "https://us.i.posthog.com";
|
|
9
|
+
const TIMEOUT_MS = 2000; // a slow or blackholed endpoint must never hold up the wizard or its report
|
|
10
|
+
const RUN_ID = randomUUID(); // this process only, never stored: pairs started with completed/cancelled
|
|
11
|
+
|
|
12
|
+
/** Everything an event may carry, and the only thing it can. README and SECURITY.md list exactly
|
|
13
|
+
* this set; a property from any caller that is not here is dropped rather than sent. The promise
|
|
14
|
+
* is "never a key, never a path, never a workspace name", and a promise one call site can break
|
|
15
|
+
* by passing an extra key is not a promise — so the guard lives here, not in the callers. */
|
|
16
|
+
export const ALLOWED_PROPERTIES = new Set([
|
|
17
|
+
"$process_person_profile", "$geoip_disable",
|
|
18
|
+
"node", "platform", "yes", "json", "exit", "ms", "empty",
|
|
19
|
+
]);
|
|
20
|
+
const onlyAllowed = (props) => Object.fromEntries(Object.entries(props).filter(([k]) => ALLOWED_PROPERTIES.has(k)));
|
|
21
|
+
|
|
22
|
+
/** An opt-out env var is set unless it is absent, empty, "0" or "false" — because someone who
|
|
23
|
+
* writes NO_TELEMETRY=true means it, and the old exact match on "1" silently ignored them. */
|
|
24
|
+
const optedOut = (v) => {
|
|
25
|
+
const s = String(v ?? "").trim().toLowerCase();
|
|
26
|
+
return s !== "" && s !== "0" && s !== "false";
|
|
27
|
+
};
|
|
5
28
|
|
|
6
29
|
export function telemetryEnabled(args) {
|
|
7
|
-
|
|
30
|
+
if (!POSTHOG_TOKEN || args.telemetry === false) return false;
|
|
31
|
+
if (optedOut(process.env.NOAN_WIZARD_NO_TELEMETRY) || optedOut(process.env.DO_NOT_TRACK)) return false;
|
|
32
|
+
if (optedOut(process.env.CI)) return false; // a pipeline running the wizard is not a person trying it
|
|
33
|
+
return true;
|
|
8
34
|
}
|
|
9
35
|
export async function capture(args, event, props = {}) {
|
|
10
36
|
if (!telemetryEnabled(args)) return false;
|
|
11
37
|
try {
|
|
12
38
|
await fetch(`${HOST}/capture/`, { method: "POST", headers: { "Content-Type": "application/json" },
|
|
13
|
-
|
|
14
|
-
|
|
39
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
40
|
+
body: JSON.stringify({ api_key: POSTHOG_TOKEN, event: `wizard_${event}`, distinct_id: RUN_ID,
|
|
41
|
+
properties: onlyAllowed({ $process_person_profile: false, $geoip_disable: true,
|
|
42
|
+
node: process.version, platform: process.platform, yes: !!args.yes, json: !!args.json, ...props }) }) });
|
|
15
43
|
return true;
|
|
16
44
|
} catch { return false; }
|
|
17
45
|
}
|