@getnoan/wizard 0.1.2 → 0.1.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
@@ -30,11 +30,12 @@ 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`). Forks [getnoan/agent-pack](https://github.com/getnoan/agent-pack)
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)
34
35
  to your account, verifies and stores your Anthropic 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, and triggers
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.
38
39
  6. **The report.** What happened, and the one thing to do next.
39
40
 
40
41
  ## For a coding assistant
@@ -65,12 +66,22 @@ Exit codes: 0 done, 2 the key did not work, 3 cancelled, 1 anything else.
65
66
  | `--agents` / `--no-agents` | include or skip the agent pack step |
66
67
  | `--no-mcp`, `--no-skill` | skip a step |
67
68
  | `--dry-run` | show every write without making it |
68
- | `--no-telemetry` | send nothing about this run (also `NOAN_WIZARD_NO_TELEMETRY=1`) |
69
+ | `--no-telemetry` | send nothing about this run (also `NOAN_WIZARD_NO_TELEMETRY`, `DO_NOT_TRACK`, or any `CI`) |
69
70
 
70
71
  ## Telemetry
71
72
 
72
- Three counts and nothing else: runs started, completed, cancelled, with the Node version and
73
- platform. Never a key, never a path, never a workspace name. Off with `--no-telemetry`.
73
+ Three events — a run started, and then completed or cancelled — carrying the Node version, the
74
+ platform, the exit code, how long the run took, whether it was non-interactive (`--yes`), whether
75
+ the report was JSON, and whether the workspace was empty. That is the whole payload. Never a key,
76
+ never a path, never a workspace name, never anything read out of your fact layer.
77
+
78
+ A run is identified by an id made up for that process and thrown away with it, so the events of one
79
+ run line up and nothing links one run to the next. The events ask PostHog for no person profile and
80
+ no geolocation. Like any HTTP request they arrive from your IP address.
81
+
82
+ Nothing is sent when any of these is true: `--no-telemetry`, `NOAN_WIZARD_NO_TELEMETRY` set to
83
+ anything but `0` or `false`, `DO_NOT_TRACK` likewise, or `CI` set — a pipeline running the wizard
84
+ is not a person trying it.
74
85
 
75
86
  ## Why a wizard and not a prompt
76
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getnoan/wizard",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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
@@ -34,6 +34,18 @@ export function parseSeedOutput(text) {
34
34
  export const PACK_SECRETS = ["NOAN_PERSONAL_API_KEY", "RESEND_API_KEY", "ANTHROPIC_API_KEY", "DATABASE_URL", "FIRECRAWL_API_KEY", "NEWSLETTER_UNSUB_SECRET"];
35
35
  export const PACK_VARS = ["MAIL_FROM", "REPLY_TO", "ESCALATE_TO", "STATE_BACKEND", "AGENT_NAME", "COMPANY_NAME", "AGENT_IDENTITY_IDS", "COMMANDERS", "REPORT_RECIPIENT_TAG"];
36
36
 
37
+ /** What the agents call themselves, and how their own prose refers to them.
38
+ * Verity is the default because that is the agent people meet in NOAN; anyone who
39
+ * wants their own name says so and gets it. Unset, the pack signs as "Agent", which
40
+ * is nobody, so the wizard always sends a name.
41
+ * Pronouns default to she/her only for Verity: assuming them for a name someone
42
+ * else chose is worse than the pack's own neutral fallback (they/them). */
43
+ export function agentIdentity({ name, pronouns } = {}) {
44
+ const n = String(name ?? "").trim() || "Verity";
45
+ const p = String(pronouns ?? "").trim() || (n.toLowerCase() === "verity" ? "she/her" : "");
46
+ return { AGENT_NAME: n, ...(p && { AGENT_PRONOUNS: p }) };
47
+ }
48
+
37
49
  export async function verifyAnthropic(key) {
38
50
  try { const r = await fetch("https://api.anthropic.com/v1/models", { headers: { "x-api-key": key, "anthropic-version": "2023-06-01" } }); return r.status === 200; } catch { return false; }
39
51
  }
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
@@ -119,11 +119,17 @@ async function setupAgents({ args, ui, dir, key, me, report }) {
119
119
  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
120
  const replyTo = process.env.REPLY_TO || (ui.interactive ? await ui.ask("Reply-to address (a mailbox someone reads)", { dflt: mailFrom.replace(/^.*<|>.*$/g, "") }) : "");
121
121
  const escalateTo = process.env.ESCALATE_TO || (ui.interactive ? await ui.ask("Where the support agent escalates when it cannot answer") : "");
122
+ const askedName = process.env.AGENT_NAME || (ui.interactive ? await ui.ask("What should your agent be called?", { dflt: "Verity" }) : "");
123
+ const identity = pack.agentIdentity({ name: askedName, pronouns: process.env.AGENT_PRONOUNS
124
+ || (ui.interactive && askedName && askedName.toLowerCase() !== "verity"
125
+ ? await ui.ask(`Pronouns for ${askedName}, in ${askedName}'s own prose (blank for they/them)`)
126
+ : "") });
122
127
 
123
128
  const secrets = { NOAN_PERSONAL_API_KEY: key, ...(anthropic && { ANTHROPIC_API_KEY: anthropic }), ...(resend && { RESEND_API_KEY: resend }),
124
129
  ...(db && { DATABASE_URL: db }), ...(firecrawl && { FIRECRAWL_API_KEY: firecrawl }), NEWSLETTER_UNSUB_SECRET: pack.randomSecret() };
125
130
  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
131
  const vars = { DRY_RUN: "1", STATE_BACKEND: db ? "postgres" : "local", ...(mailFrom && { MAIL_FROM: mailFrom }), ...(replyTo && { REPLY_TO: replyTo }), ...(escalateTo && { ESCALATE_TO: escalateTo }),
132
+ ...identity,
127
133
  COMPANY_NAME: me.project?.name || "", ...(me.identity?.id && { AGENT_IDENTITY_IDS: me.identity.id }), ...(me.identity?.email && { COMMANDERS: me.identity.email }) };
128
134
  // The seeds need the key in the clone's .env; the pack's own .env.example documents the rest.
129
135
  pack.writePackEnv(fork.dest, { NOAN_PERSONAL_API_KEY: key, ...(anthropic && { ANTHROPIC_API_KEY: anthropic }), ...(resend && { RESEND_API_KEY: resend }) }, args.dryRun);
@@ -155,5 +161,6 @@ async function setupAgents({ args, ui, dir, key, me, report }) {
155
161
  ui.ok(`${wf}: ${d.action}${d.error ? ` — ${d.error}` : ""} — read it under Actions, then set DRY_RUN=0 for the agents you trust`);
156
162
  }
157
163
  out.repo = pack.repoSlug(fork.dest);
164
+ out.agentName = identity.AGENT_NAME;
158
165
  return out;
159
166
  }
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 until a public project token is set here; off whenever the user says so. Never a key, never a path. */
3
- export const POSTHOG_TOKEN = ""; // public (write-only) PostHog project token; empty = telemetry off
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
- return !!POSTHOG_TOKEN && args.telemetry !== false && process.env.NOAN_WIZARD_NO_TELEMETRY !== "1";
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
- body: JSON.stringify({ api_key: POSTHOG_TOKEN, event: `wizard_${event}`, distinct_id: "anonymous",
14
- properties: { $process_person_profile: false, node: process.version, platform: process.platform, yes: !!args.yes, json: !!args.json, ...props } }) });
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
  }