tracwell 0.5.0 → 0.6.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.
package/README.md CHANGED
@@ -212,10 +212,21 @@ src/
212
212
  types.ts Public configuration, client, delivery, and error types
213
213
  client.test.ts Deterministic SDK behavior and failure tests
214
214
  index.test.ts SSR import and browser-initialization boundary
215
+ cli/ The `tracwell` command (Node only, own tsconfig.cli.json)
216
+ main.ts Commands, help and output
217
+ args.ts Flag parsing
218
+ config.ts Saved login and default website (0600 file)
219
+ oauth.ts Browser login, refresh and revoke
220
+ mcp.ts JSON-RPC client for `/mcp`
221
+ tool-arguments.ts Flags to tool arguments, --site/--days/requestId
222
+ install.ts Skill and AGENTS.md install into the current repo
223
+ definitions.ts `goals add` / `funnels add` step parsing
224
+ skills/tracwell/
225
+ SKILL.md Agent skill, shipped in the package and served at /skill.md
215
226
  scripts/
216
227
  build-cdn.mjs Minified browser build and gzip-size enforcement
217
- build-npm.mjs Self-contained ESM package build
218
- smoke-npm.mjs Packed runtime and TypeScript consumer smoke test
228
+ build-npm.mjs Self-contained ESM package build, including dist/npm/cli.js
229
+ smoke-npm.mjs Packed runtime, TypeScript, CLI and skill consumer smoke test
219
230
  static/
220
231
  _headers Cross-origin browser asset headers
221
232
  ```
@@ -273,3 +284,55 @@ handling because Workers does not support the fetch redirect error mode. Redirec
273
284
  responses are rejected without forwarding the server key.
274
285
 
275
286
  Version 0.4.0 expands server crawler recognition to 23 tokens across 12 providers. Existing server integrations must upgrade and redeploy to send newly supported crawlers; keys and browser installation are unchanged. The collector determines the purpose, including other for mixed/general crawling.
287
+
288
+ ## CLI
289
+
290
+ The package also installs a `tracwell` command for terminals and coding agents:
291
+
292
+ ```sh
293
+ npx tracwell init --url https://acme.com # sign in, find or add the site, snippet, skill
294
+ npx tracwell login # browser sign-in; pick websites on the consent screen
295
+ npx tracwell sites add Acme https://acme.com
296
+ npx tracwell use <website-id>
297
+ npx tracwell snippet # install snippet for the website
298
+ npx tracwell verify # waiting / receiving
299
+ npx tracwell overview --days 30
300
+ npx tracwell goals add "Signed up" --event signup_completed --category signup
301
+ npx tracwell funnels add Checkout --steps /pricing,event:checkout_started
302
+ npx tracwell search funnel # find tools by keyword
303
+ npx tracwell call funnel_create --name Checkout --dry-run
304
+ npx tracwell help overview # arguments, read live from Tracwell
305
+ npx tracwell skill install # .agents/skills/tracwell (and .claude/skills if present)
306
+ npx tracwell agents-md install
307
+ ```
308
+
309
+ `init` signs in if there is no saved login, then picks the website: `--site`,
310
+ else the one whose origin matches `--url` (it reads each website's install
311
+ record, since the list has no URLs), else creates it with `website_create`
312
+ (OAuth with "Add websites" only), else the saved or only website. It saves the
313
+ website as the default, prints the snippet, and runs `skill install` and
314
+ `agents-md install`. `skill install` leaves a copy that differs alone unless
315
+ `--force`; `agents-md install` replaces only the marked `tracwell:start/end`
316
+ block. `--dry-run` prints the tool, its arguments and whether it writes, without
317
+ calling it; it still reads the tool list, so it needs a login.
318
+
319
+ It is a thin client for the existing `/mcp` endpoint: each command is one
320
+ stateless `tools/call`, so the CLI has the same tools, permissions and plan
321
+ checks as MCP and needs no API of its own. Short commands cover the common
322
+ tools; `tracwell call <tool> --arg value` runs any other, and `tracwell tools`
323
+ lists what the current login can use. Output is JSON (`--compact` for one
324
+ line). Exit codes: 0 ok, 1 request failed, 2 bad input, 3 sign-in needed.
325
+
326
+ `login` registers a public OAuth client for a `http://127.0.0.1:<port>/callback`
327
+ redirect, opens the consent page with S256 PKCE and the `/mcp` resource, and
328
+ stores the access and refresh tokens. `login --key`, `--key` on any command, or
329
+ `TRACWELL_API_KEY` use an API key instead; keys cannot add websites. Credentials
330
+ live in `$TRACWELL_CONFIG_DIR`, else `$XDG_CONFIG_HOME/tracwell` or
331
+ `~/.config/tracwell` (`%APPDATA%\tracwell` on Windows), mode 0600. `TRACWELL_URL`
332
+ points the CLI at another Tracwell origin. `logout` revokes the OAuth grant and
333
+ deletes the file.
334
+
335
+ The CLI is built as a separate Node 20 entry (`dist/npm/cli.js`); the browser
336
+ and `tracwell/server` entries never import it. `skills/tracwell/SKILL.md` is the
337
+ agent skill: sign in, add the site, install the snippet, verify, read reports.
338
+ The web build serves the same file at `https://tracwell.app/skill.md`.
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import{randomUUID as ge}from"node:crypto";var S="0.6.0";function M(e){let t=[],r={};for(let i=0;i<e.length;i+=1){let a=e[i];if(a==="--"){t.push(...e.slice(i+1));break}if(a==="-h"){r.help=!0;continue}if(!a.startsWith("--")){t.push(a);continue}let c=a.indexOf("=");if(c!==-1){r[a.slice(2,c)]=a.slice(c+1);continue}let l=e[i+1];l!==void 0&&!l.startsWith("--")?(r[a.slice(2)]=l,i+=1):r[a.slice(2)]=!0}let[n,...s]=t;return{command:n,positional:s,flags:r}}function q(e){return e.replace(/-([a-z0-9])/gu,(t,r)=>r.toUpperCase())}import{chmod as he,mkdir as ye,readFile as ke,rm as be,writeFile as ve}from"node:fs/promises";import{homedir as Se}from"node:os";import{join as _}from"node:path";var xe="https://tracwell.app";function W(e=process.env){return e.TRACWELL_CONFIG_DIR?e.TRACWELL_CONFIG_DIR:process.platform==="win32"&&e.APPDATA?_(e.APPDATA,"tracwell"):_(e.XDG_CONFIG_HOME||_(Se(),".config"),"tracwell")}function I(){return _(W(),"config.json")}async function m(){try{return JSON.parse(await ke(I(),"utf8"))}catch(e){if(e.code==="ENOENT")return{};throw new Error(`Could not read ${I()}. Delete it and log in again.`,{cause:e})}}async function y(e){await ye(W(),{recursive:!0,mode:448});let t=I();await ve(t,`${JSON.stringify(e,null,2)}
3
+ `,{mode:384}),await he(t,384)}async function K(){await be(I(),{force:!0})}function P(e,t){return(e.TRACWELL_URL||t.login?.baseUrl||xe).replace(/\/+$/u,"")}var o=class extends Error{constructor(r,n=1){super(r);this.exitCode=n}exitCode};function B(e,t){try{return JSON.parse(e).error?.message??t}catch{return t}}async function H(e,t,r,n){let s;try{s=await fetch(`${e}/mcp`,{method:"POST",headers:{accept:"application/json, text/event-stream",authorization:`Bearer ${t}`,"content-type":"application/json","user-agent":`tracwell-cli/${S}`},body:JSON.stringify({jsonrpc:"2.0",id:1,method:r,params:n})})}catch{throw new o(`Could not reach ${e}. Check your connection and try again.`)}let i=await s.text();if(s.status===401)throw new o(`${B(i,"Not signed in.")} Run \`tracwell login\`.`,3);if(!s.ok)throw new o(B(i,`Tracwell returned HTTP ${s.status}.`));let a=i.split(`
4
+ `).find(l=>l.startsWith("data: "))?.slice(6)??i,c=JSON.parse(a);if(c.error)throw new o(c.error.message??"The request failed.");return c.result}var Te=e=>`${e.name.replaceAll("_"," ")} ${e.title??""} ${e.description??""}`.toLowerCase();function G(e,t){let r=t.toLowerCase().split(/\s+/u).filter(Boolean);if(!r.length)return[...e];let n=s=>r.filter(i=>s.name.toLowerCase().includes(i)).length;return e.filter(s=>r.every(i=>Te(s).includes(i))).toSorted((s,i)=>n(i)-n(s))}async function k(e,t){return(await H(e,t,"tools/list",{})).tools??[]}async function Y(e,t,r,n){return await H(e,t,"tools/call",{name:r,arguments:n})}function _e(e){let t=e.trim();if(!t)throw new o("A step is empty. Use a page like /pricing or an event like event:signup_completed.",2);if(t.startsWith("/"))return{type:"pageview",path:t};let r=t.startsWith("event:")?t.slice(6).trim():t;if(!r)throw new o("`event:` needs an event name, like event:signup_completed.",2);return{type:"event",name:r}}function z(e){let t=typeof e.page=="string"?e.page:void 0,r=typeof e.event=="string"?e.event:void 0;if(t&&r)throw new o("Pass --page or --event, not both.",2);if(t){if(!t.startsWith("/"))throw new o("--page must start with /, like /pricing.",2);return{type:"pageview",path:t}}if(r)return{type:"event",name:r};throw new o("Usage: tracwell goals add <name> --page /pricing (or --event signup_completed)",2)}function Z(e){if(typeof e!="string"||!e.trim())throw new o("Usage: tracwell funnels add <name> --steps /,/pricing,event:checkout_started",2);if(e.trim().startsWith("["))try{return JSON.parse(e)}catch{throw new o("--steps must be a comma list or a JSON array.",2)}let t=e.split(",").map(_e);if(t.length<2)throw new o("A funnel needs at least 2 steps.",2);return t}function L(e,t){return Object.fromEntries(Object.entries(e).filter(([r])=>!t.includes(r)))}import{access as Ie,mkdir as V,readFile as C,writeFile as X}from"node:fs/promises";import{dirname as Q,join as $,relative as Pe}from"node:path";var $e=new URL("../../skills/tracwell/SKILL.md",import.meta.url);function U(){return C($e,"utf8")}var Re=e=>Ie(e).then(()=>!0,()=>!1);async function Ae(e,t,r){let n;try{n=await C(e,"utf8")}catch{}return n===t?"unchanged":n!==void 0&&!r?"skipped":(await V(Q(e),{recursive:!0}),await X(e,t),n===void 0?"created":"updated")}async function N(e,t){let r=await U(),n=[$(e,".agents/skills/tracwell/SKILL.md")];return await Re($(e,".claude"))&&n.push($(e,".claude/skills/tracwell/SKILL.md")),Promise.all(n.map(async s=>({path:Pe(e,s),status:await Ae(s,r,t)})))}var ee="<!-- tracwell:start -->",O="<!-- tracwell:end -->",j=`${ee}
5
+ ## Analytics (Tracwell)
6
+
7
+ This project uses Tracwell for analytics. Use the \`tracwell\` CLI (\`npx tracwell\`)
8
+ to check the install and read reports: \`npx tracwell verify\`, \`npx tracwell overview\`,
9
+ and \`npx tracwell help\` for everything else. Before analytics work, read
10
+ \`.agents/skills/tracwell/SKILL.md\` (or run \`npx tracwell skill\`).
11
+ ${O}`;function Le(e){let t=e.indexOf(ee),r=e.indexOf(O,t);return t!==-1&&r!==-1?`${e.slice(0,t)}${j}${e.slice(r+O.length)}`:e.trim()?`${e.replace(/\s*$/u,"")}
12
+
13
+ ${j}
14
+ `:`${j}
15
+ `}async function E(e,t="AGENTS.md"){let r=$(e,t),n="";try{n=await C(r,"utf8")}catch{}let s=Le(n);return s===n?{path:t,status:"unchanged"}:(await V(Q(r),{recursive:!0}),await X(r,s),{path:t,status:n?"updated":"created"})}import{spawn as je}from"node:child_process";import{createHash as Oe,randomBytes as te}from"node:crypto";import{createServer as Ce}from"node:http";var ne="analytics:read definitions:write websites:write offline_access",Ue=300*1e3,Ne=60*1e3,F=e=>e.toString("base64url");async function re(e,t){let r=await fetch(e,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams(t).toString()}),n=await r.text();if(!r.ok){let s=`Sign-in failed (HTTP ${r.status}).`;try{let i=JSON.parse(n);s=i.error_description??i.error??s}catch{}throw new o(`${s} Run \`tracwell login\` again.`,3)}return JSON.parse(n)}function se(e,t,r,n){let s=r.refresh_token??n;return{type:"oauth",baseUrl:e,clientId:t,accessToken:r.access_token,...s?{refreshToken:s}:{},expiresAt:Date.now()+(r.expires_in??3600)*1e3}}function Ee(e){let[t,r]=process.platform==="darwin"?["open",[e]]:process.platform==="win32"?["rundll32",["url.dll,FileProtocolHandler",e]]:["xdg-open",[e]];try{let n=je(t,r,{detached:!0,stdio:"ignore"});n.on("error",()=>{}),n.unref()}catch{}}function Fe(){return new Promise((e,t)=>{let r=Ce();r.once("error",t),r.listen(0,"127.0.0.1",()=>{let n=r.address();n&&typeof n=="object"?e({server:r,port:n.port}):t(new o("Could not start the local sign-in callback."))})})}var De='<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>',Je='<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M12 7.5v5.5M12 16.5v.01"/></svg>',Me=(e,t,r=!1)=>`<!doctype html>
16
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${e} \xB7 Tracwell</title>
17
+ <style>
18
+ :root{--bg:#fff;--fg:#111;--muted:#737373;--shell:#f5f5f5;--card:#fff;--line:#e5e5e5;--chip:#f0f0f0;--blue:#357dff}
19
+ @media (prefers-color-scheme:dark){:root{--bg:#0a0a0a;--fg:#fafafa;--muted:#a3a3a3;--shell:#171717;--card:#0f0f0f;--line:#262626;--chip:#262626}}
20
+ *{box-sizing:border-box}html,body{margin:0;height:100%}
21
+ body{background:var(--bg);color:var(--fg);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;display:grid;place-items:center;padding:24px}
22
+ .shell{width:100%;max-width:360px;background:var(--shell);border-radius:24px;padding:6px}
23
+ .card{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:28px 24px;text-align:center}
24
+ .icon{width:44px;height:44px;margin:0 auto;border-radius:999px;display:grid;place-items:center;background:var(--chip);color:var(--muted)}
25
+ .ok .icon{background:var(--blue);color:#fff}
26
+ h1{margin:16px 0 0;font-size:16px;font-weight:600;letter-spacing:-.01em}
27
+ p{margin:6px 0 0;color:var(--muted)}
28
+ .foot{padding:10px 0 6px;text-align:center;font-size:12px;color:var(--muted)}
29
+ code{font:12px ui-monospace,SFMono-Regular,Menlo,monospace;background:var(--chip);padding:2px 6px;border-radius:6px;color:var(--fg);white-space:nowrap}
30
+ @media (prefers-reduced-motion:no-preference){.card{animation:in .3s cubic-bezier(.16,1,.3,1) both}@keyframes in{from{opacity:0;transform:translateY(4px)}}}
31
+ </style></head>
32
+ <body><main class="shell${r?" ok":""}"><div class="card"><div class="icon">${r?De:Je}</div><h1>${e}</h1><p>${t}</p></div>
33
+ <p class="foot">Tracwell CLI</p></main></body></html>`;function qe(e,t){return new Promise((r,n)=>{let s=setTimeout(()=>n(new o("Sign-in timed out. Run `tracwell login` again.",3)),Ue);e.on("request",(i,a)=>{let c=new URL(i.url??"/","http://127.0.0.1");if(c.pathname!=="/callback"){a.writeHead(404).end();return}let l=c.searchParams.get("error"),d=c.searchParams.get("code"),p=(T,v,h,w=!1)=>a.writeHead(T,{"content-type":"text/html; charset=utf-8"}).end(Me(v,h,w));if(c.searchParams.get("state")!==t){p(400,"This sign-in link is out of date","Go back to your terminal and run <code>tracwell login</code> again.");return}if(clearTimeout(s),l||!d){p(400,"Sign-in was cancelled","Nothing was connected. Run <code>tracwell login</code> to try again."),n(new o(c.searchParams.get("error_description")??"Sign-in was cancelled.",3));return}p(200,"You're signed in","The Tracwell CLI is connected. You can close this tab and go back to your terminal.",!0),r(d)})})}async function ie(e,t){let{server:r,port:n}=await Fe();try{let s=`http://127.0.0.1:${n}/callback`,i=await fetch(`${e}/api/auth/oauth2/register`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({client_name:"Tracwell CLI",redirect_uris:[s],token_endpoint_auth_method:"none",grant_types:["authorization_code","refresh_token"],response_types:["code"],scope:ne})}).catch(()=>{throw new o(`Could not reach ${e}. Check your connection and try again.`)});if(!i.ok)throw new o(`Could not start sign-in (HTTP ${i.status}).`,3);let{client_id:a}=await i.json(),c=F(te(32)),l=F(te(16)),d=new URL("/api/auth/oauth2/authorize",e);for(let[v,h]of Object.entries({client_id:a,redirect_uri:s,response_type:"code",scope:ne,state:l,code_challenge:F(Oe("sha256").update(c).digest()),code_challenge_method:"S256",resource:`${e}/mcp`}))d.searchParams.set(v,h);t.print("Sign in and pick your websites in the browser:"),t.print(d.href),t.open&&Ee(d.href);let p=await qe(r,l),T=await re(`${e}/api/auth/oauth2/token`,{grant_type:"authorization_code",code:p,client_id:a,redirect_uri:s,code_verifier:c,resource:`${e}/mcp`});return se(e,a,T)}finally{r.close()}}function oe(e,t=Date.now()){return e.expiresAt-Ne<=t}async function ae(e){if(!e.refreshToken)throw new o("Your sign-in expired. Run `tracwell login`.",3);let t=await re(`${e.baseUrl}/api/auth/oauth2/token`,{grant_type:"refresh_token",refresh_token:e.refreshToken,client_id:e.clientId,resource:`${e.baseUrl}/mcp`});return se(e.baseUrl,e.clientId,t,e.refreshToken)}async function ce(e){let t=e.refreshToken??e.accessToken;await fetch(`${e.baseUrl}/api/auth/oauth2/revoke`,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({token:t,client_id:e.clientId}).toString()}).catch(()=>{})}var We=new Set(["json","site","days","compact","key","help","no-browser","dry-run","force"]),R=7,le=e=>e.toISOString().slice(0,10);function Ke(e,t){let r=new Date(Date.UTC(t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate()-(e-1)));return{from:le(r),to:le(t)}}function Be(e,t,r,n,s){let i="from"in e&&"to"in e,a=!i&&"date"in e;if(!i&&!a)return{};if(r.from!==void 0||r.to!==void 0||r.date!==void 0)return{};if(!(t.has("from")||t.has("date")||n.days!==void 0))return{};let c=n.days===void 0?R:Number(n.days);if(!Number.isInteger(c)||c<1)throw new o("--days must be a whole number of 1 or more.",2);let{from:l,to:d}=Ke(c,s);if(a)return{date:l,..."to"in e?{to:d}:{}};if(e.from?.format==="date-time"){let p=new Date(`${d}T00:00:00.000Z`);return p.setUTCDate(p.getUTCDate()+1),{from:`${l}T00:00:00.000Z`,to:p.toISOString()}}return{from:l,to:d}}function He(e){return e?.type?Array.isArray(e.type)?e.type:[e.type]:[]}function Ge(e,t,r){let n=He(r);if(t===!0){if(n.length&&!n.includes("boolean"))throw new o(`--${e} needs a value.`,2);return!0}if(n.includes("number")||n.includes("integer")){let s=Number(t);if(Number.isNaN(s))throw new o(`--${e} must be a number.`,2);return s}if(n.includes("boolean"))return t==="true";if(!n.length&&/^\s*[[{]/u.test(t))try{return JSON.parse(t)}catch{throw new o(`--${e} must be JSON.`,2)}if(n.includes("object")||n.includes("array"))try{return JSON.parse(t)}catch{throw new o(`--${e} must be JSON.`,2)}return t}function de(e,t,r){let n=e.inputSchema?.properties??{},s=new Set(e.inputSchema?.required??[]),i={};if(typeof t.json=="string")try{i=JSON.parse(t.json)}catch{throw new o("--json must be a JSON object.",2)}for(let[c,l]of Object.entries(t)){if(We.has(c))continue;let d=q(c);i[d]=Ge(c,l,n[d])}let a=typeof t.site=="string"?t.site:r.site;return"projectId"in n&&i.projectId===void 0&&a&&(i.projectId=a),Object.assign(i,Be(n,s,i,t,r.today)),"requestId"in n&&i.requestId===void 0&&(i.requestId=r.requestId()),i}var J={sites:{tool:"analytics_projects",summary:"List the websites you can read"},snippet:{tool:"install_snippet",summary:"Get the install snippet for a website"},verify:{tool:"install_verify",summary:"Check whether a website is receiving events"},overview:{tool:"analytics_overview",summary:"Traffic overview"},acquisition:{tool:"analytics_acquisition",summary:"Sources, referrers and campaigns"},revenue:{tool:"analytics_revenue",summary:"Revenue and where it came from"},events:{tool:"analytics_events",summary:"Event names and counts"},journeys:{tool:"analytics_journeys",summary:"Recorded sessions"},goals:{tool:"analytics_goals",summary:"Saved goals"},goal:{tool:"analytics_goal",summary:"One goal's report (--goal-id)"},funnels:{tool:"analytics_funnels",summary:"Saved funnels"},funnel:{tool:"analytics_funnel",summary:"One funnel's report (--funnel-id)"},notes:{tool:"notes_list",summary:"Chart notes"}},ue=`tracwell ${S} - Tracwell analytics from your terminal or coding agent
34
+
35
+ Start here
36
+ tracwell init [--url <url>] [--name <name>]
37
+ Sign in, pick or add the website, print the snippet,
38
+ and install the agent skill in this repo
39
+
40
+ Setup
41
+ tracwell login Sign in in the browser and pick websites
42
+ tracwell login --key <key> Use an API key from Settings \u2192 API & MCP instead
43
+ tracwell sites add <name> <url> Add a website (browser login only)
44
+ tracwell use <website-id> Default website for later commands
45
+ tracwell snippet Install snippet for the default website
46
+ tracwell verify Check that events are arriving
47
+ tracwell whoami Who you are signed in as and which websites
48
+ tracwell logout
49
+
50
+ Reports (default: last ${R} days, UTC)
51
+ ${Object.entries(J).filter(([e])=>!["sites","snippet","verify"].includes(e)).map(([e,{summary:t}])=>` tracwell ${e.padEnd(22)} ${t}`).join(`
52
+ `)}
53
+
54
+ Goals and funnels (need "Manage goals and funnels")
55
+ tracwell goals add <name> --page /pricing or --event signup_completed
56
+ [--category signup|revenue|engagement|other]
57
+ tracwell funnels add <name> --steps /,/pricing,event:checkout_started
58
+ [--window-hours 24]
59
+
60
+ Any tool
61
+ tracwell search <words> Find tools by keyword
62
+ tracwell tools List every tool this login can use
63
+ tracwell call <tool> [--arg value ...] [--json '{...}']
64
+ tracwell help <command|tool> Arguments for a command, read live from Tracwell
65
+
66
+ Agent setup
67
+ tracwell skill Print the agent skill
68
+ tracwell skill install [--force] Write it to .agents/skills (and .claude/skills)
69
+ tracwell agents-md install [--file AGENTS.md] Add a Tracwell block to AGENTS.md
70
+
71
+ Common flags
72
+ --site <website-id> Website for this command
73
+ --days <n> Report the last n days; or pass --from and --to (YYYY-MM-DD)
74
+ --dry-run Print the tool and arguments without sending anything
75
+ --compact One-line JSON output
76
+ --key <key> API key for this command (or set TRACWELL_API_KEY)
77
+
78
+ Output is JSON. Exit codes: 0 ok, 1 request failed, 2 bad input, 3 sign in needed.`,b=e=>{process.stdout.write(`${e}
79
+ `)},A=e=>{process.stderr.write(`${e}
80
+ `)};function f(e,t){b(typeof e=="string"?e:JSON.stringify(e,null,t.compact?void 0:2))}function Ye(e){if(e.structuredContent!==void 0)return e.structuredContent;let t=(e.content??[]).filter(r=>r.type==="text").map(r=>r.text??"").join(`
81
+ `);try{return JSON.parse(t)}catch{return t}}async function u(e){let t=await m(),r=P(process.env,t),n=typeof e.key=="string"?e.key:process.env.TRACWELL_API_KEY;if(n)return{baseUrl:r,token:n,settings:t,viaKey:!0};let s=t.login;if(!s||s.baseUrl!==r)throw new o("Not signed in. Run `tracwell login`, or set TRACWELL_API_KEY.",3);if(s.type==="key")return{baseUrl:r,token:s.key,settings:t,viaKey:!0};if(!oe(s))return{baseUrl:r,token:s.accessToken,settings:t,viaKey:!1};let i=await ae(s),a={...t,login:i};return await y(a),{baseUrl:r,token:i.accessToken,settings:a,viaKey:!1}}async function pe(e){let t=await m(),r=await ie(P(process.env,t),{open:!e["no-browser"],print:A});await y({...t,login:r})}async function fe(e,t){let r=(await k(e.baseUrl,e.token)).find(n=>n.name===t);if(!r)throw new o(`This login cannot use \`${t}\`. Run \`tracwell tools\` to see what it can use.`,2);return r}async function x(e,t,r){let n=await Y(e.baseUrl,e.token,t,r),s=Ye(n);if(n.isError)throw new o(typeof s=="string"?s:JSON.stringify(s));return s}async function g(e,t,r,n={}){let s=await fe(e,t),i={...de(s,r,{...e.settings.site?{site:e.settings.site}:{},today:new Date,requestId:ge}),...n};if(r["dry-run"])return f({dryRun:!0,tool:t,writes:s.annotations?.readOnlyHint===!1,arguments:i},r);f(await x(e,t,i),r)}function ze(e,t){let r=e.inputSchema?.properties??{},n=new Set(e.inputSchema?.required??[]),s=Object.entries(r).map(([i,a])=>{let c=Array.isArray(a.type)?a.type.join("|"):a.type??"value",l=i==="projectId"?" (or --site, or `tracwell use`)":i==="requestId"?" (filled in for you)":i==="from"||i==="to"||i==="date"?` (or --days; default last ${R} days)`:"";return` --${i.padEnd(18)} ${c}${n.has(i)?", required":""}${l}${a.description?`
82
+ ${a.description}`:""}`});return[`tracwell ${t} (${e.name})`,"",e.description??"","","Arguments",...s].join(`
83
+ `)}async function Ze(e,t){let r=async l=>await x(e,"install_snippet",{projectId:l});if(typeof t.site=="string")return{installation:await r(t.site),created:!1};let{projects:n}=await x(e,"analytics_projects",{limit:100}),s=typeof t.url=="string"?t.url:void 0;if(s){let l;try{l=new URL(s).origin}catch{throw new o("--url must be a full address, like https://acme.com.",2)}let p=(await Promise.all(n.map(w=>r(w.id)))).find(w=>w.origins.some(we=>new URL(we).origin===l));if(p)return{installation:p,created:!1};if(!(await k(e.baseUrl,e.token)).some(w=>w.name==="website_create"))throw new o(e.viaKey?'API keys cannot add websites. Run `tracwell login` and tick "Add websites", or add it in the dashboard and pass --site.':'This login cannot add websites. Run `tracwell login` again and tick "Add websites".',3);let v=typeof t.name=="string"?t.name:new URL(s).hostname.replace(/^www\./u,""),h=await x(e,"website_create",{requestId:ge(),name:v,websiteUrl:s});return{installation:h.website,created:h.created}}let i=n.find(l=>l.id===e.settings.site),a=n.length===1?n[0]:void 0,c=i??a;if(c)return{installation:await r(c.id),created:!1};throw n.length?new o(`Several websites: ${n.map(l=>`${l.name} (${l.id})`).join(", ")}. Pass --site <id> or --url <url>.`,2):new o("No websites yet. Run `tracwell init --url https://your-site.com` to add one.",2)}function D(e){return e.map(t=>`${t.path}: ${t.status==="skipped"?"exists and differs; rerun with --force to replace":t.status}`)}async function Ve(e){let{command:t,positional:r,flags:n}=M(e);if(n.version||t==="version")return b(S);if(!t||t==="help"&&!r[0])return b(ue);if(t==="help"||n.help&&!["skill","agents-md","init","search"].includes(t)){let s=t==="help"?r:[t,...r],i=s[0],a={goals:"goal_create",funnels:"funnel_create",sites:"website_create"},c=(s[1]==="add"?a[i]:void 0)??J[i]?.tool??(i==="call"?s[1]:i);if(!c)return b(ue);let l=await u(n),d=s[1]==="add"&&a[i]?`${i} add`:i;return b(ze(await fe(l,c),d))}switch(t){case"login":{if(typeof n.key=="string"){let s=await m(),i=P(process.env,s);await k(i,n.key),await y({...s,login:{type:"key",baseUrl:i,key:n.key}})}else await pe(n);A("Signed in. Run `tracwell sites` to see your websites.");return}case"logout":{let s=await m();s.login?.type==="oauth"&&await ce(s.login),await K(),A("Signed out.");return}case"use":{let s=r[0];if(!s)throw new o("Usage: tracwell use <website-id>. Run `tracwell sites` for IDs.",2);let i=await m();await y({...i,site:s}),A(`Default website set to ${s}.`);return}case"whoami":{let s=await u(n);return f({baseUrl:s.baseUrl,login:s.viaKey?"api key":"browser",defaultSite:s.settings.site??null,websites:await x(s,"analytics_projects",{})},n)}case"tools":{let s=await u(n),i=await k(s.baseUrl,s.token);return f(i.map(a=>({name:a.name,description:a.description,writes:a.annotations?.readOnlyHint===!1})),n)}case"search":{let s=r.join(" ");if(!s)throw new o("Usage: tracwell search <words>",2);let i=await u(n),a=G(await k(i.baseUrl,i.token),s);return f(a.map(c=>({name:c.name,description:c.description,writes:c.annotations?.readOnlyHint===!1})),n)}case"call":{let s=r[0];if(!s)throw new o("Usage: tracwell call <tool> [--arg value ...]. Run `tracwell tools` for names.",2);return g(await u(n),s,n)}case"goals":{if(r[0]!=="add")return g(await u(n),"analytics_goals",n);let s=r[1];if(!s)throw new o("Usage: tracwell goals add <name> --page /pricing (or --event signup_completed)",2);let i=z(n);return g(await u(n),"goal_create",L(n,["page","event"]),{name:s,match:i,...n.category===void 0?{category:"other"}:{}})}case"funnels":{if(r[0]!=="add")return g(await u(n),"analytics_funnels",n);let s=r[1];if(!s)throw new o("Usage: tracwell funnels add <name> --steps /,/pricing,event:checkout_started",2);let i=Z(n.steps),a=n["window-hours"]!==void 0||n.windowHours!==void 0;return g(await u(n),"funnel_create",L(n,["steps"]),{name:s,steps:i,...a?{}:{windowHours:24}})}case"sites":{if(r[0]==="add"){let[,s,i]=r;if(!s||!i)throw new o("Usage: tracwell sites add <name> <url>",2);return g(await u(n),"website_create",n,{name:s,websiteUrl:i})}return g(await u(n),"analytics_projects",n)}case"skill":{if(!r[0])return b(await U());if(r[0]!=="install")throw new o("Usage: tracwell skill [install [--force]]",2);return f({installed:D(await N(process.cwd(),!!n.force))},n)}case"agents-md":{if(r[0]!=="install")throw new o("Usage: tracwell agents-md install [--file AGENTS.md]",2);let s=typeof n.file=="string"?n.file:"AGENTS.md";return f({installed:D([await E(process.cwd(),s)])},n)}case"init":{let s;try{s=await u(n)}catch(l){if(!(l instanceof o)||l.exitCode!==3)throw l;await pe(n),s=await u(n)}let{installation:i,created:a}=await Ze(s,n);await y({...await m(),site:i.projectId});let c=[...await N(process.cwd(),!!n.force),await E(process.cwd())];return f({website:{id:i.projectId,name:i.name,origins:i.origins,created:a},snippet:{script:i.script,npm:i.npm,placement:i.placement},installed:D(c),next:"Add the script tag (or the npm setup) to the site, load a page, then run `tracwell verify`."},n)}default:{let s=J[t];if(!s)throw new o(`Unknown command \`${t}\`. Run \`tracwell help\`.`,2);return g(await u(n),s.tool,n)}}}Ve(process.argv.slice(2)).catch(e=>{let t=e instanceof o;process.stderr.write(`tracwell: ${t||e instanceof Error?e.message:String(e)}
84
+ `),process.exitCode=t?e.exitCode:1});
package/dist/npm/index.js CHANGED
@@ -1 +1 @@
1
- var O="https://collect.tracwell.app/v1/events",H=25,B=1e4,X=Number.POSITIVE_INFINITY,Y=2e3,G=300*1e3,A=100,K=1800*1e3,b="0.2.0";function E(t,e){throw e?.({code:"CONFIG_INVALID",message:t}),new TypeError(t)}function oe(t,e){let n=t.trim();return n.length<20||n.length>128||!/^[A-Za-z0-9_-]+$/u.test(n)?E("projectKey does not match the public project-key contract.",e):n}function se(t,e){let n;try{n=new URL(t)}catch{return E("endpoint must be an absolute HTTPS URL.",e)}return ae(n,e)}function ae(t,e){let n=t.hostname==="localhost"||t.hostname==="127.0.0.1"||t.hostname==="[::1]";return t.protocol!=="https:"&&!n?E("endpoint must use HTTPS unless it uses a loopback host.",e):t.toString()}function y(t,e,n,r,i){return!Number.isSafeInteger(e)||e<n||e>r?E(`${t} must be an integer between ${n} and ${r}.`,i):e}function z(t){let e=oe(t.projectKey,t.onError),n=t.collectionMode??"private",r=se(t.endpoint??O,t.onError);return n==="private"&&t.persistence!==void 0&&t.persistence!=="none"?E('Private collection requires persistence: "none".',t.onError):{autoTrackPageViews:t.autoTrackPageViews??!0,batchSize:y("batchSize",t.batchSize??H,1,50,t.onError),consent:t.consent??"granted",collectionMode:n,endpoint:r,flushIntervalMs:y("flushIntervalMs",t.flushIntervalMs??B,1e3,6e4,t.onError),maxRetries:t.maxRetries===void 0?X:y("maxRetries",t.maxRetries,0,5,t.onError),persistence:n==="private"?"none":t.persistence??"localStorage",projectKey:e,respectDoNotTrack:t.respectDoNotTrack??!0,sessionTimeoutMs:y("sessionTimeoutMs",t.sessionTimeoutMs??K,6e4,1440*60*1e3,t.onError),...t.onDelivery?{onDelivery:t.onDelivery}:{},...t.onError?{onError:t.onError}:{}}}var j=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"];function de(t){let e=t?.trim();return e||void 0}function ce(t){if(t)try{return new URL(t).toString()}catch{return}}function R(t){let e=new URL(t.href),n={},r=ce(t.referrer);r&&(n.referrer=r);for(let i of j){let o=de(e.searchParams.get(i));o&&(n[i]=o)}return n}function N(t){let e=j.map(n=>t[n]??"").join("");return e.replaceAll("","")?e:void 0}var L=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu,Ie=/^[A-Za-z][A-Za-z0-9_.:-]*$/u,ge=/[\s\x00-\x1f\x7f]/u,Ae=/[\x00-\x1f\x7f]/u;function P(t){return new TextEncoder().encode(JSON.stringify(t)).byteLength}function w(t){return t.length>0&&t.length<=256&&!ge.test(t)}function F(t,e=2048){if(t.length>e)return!1;try{return new URL(t).origin.length>0}catch{return!1}}function C(t,e,n){return e>4?!1:typeof t=="string"?t.length<=1024:typeof t=="number"?Number.isFinite(t):t===null||typeof t=="boolean"?!0:Array.isArray(t)?t.length<=50&&t.every(r=>C(r,e+1,n)):Object.entries(t).every(([r,i])=>(n.count+=1,n.count<=100&&r.length>0&&r.length<=128&&!Ae.test(r)&&C(i,e+1,n)))}function J(t){let{context:e}=t;try{if(P(t)>16384)return!1}catch{return!1}return L.test(t.event_id)&&w(t.anonymous_id)&&w(t.session_id)&&(t.user_id===void 0||w(t.user_id))&&(t.event_type!=="event"||t.event_name.length<=100&&Ie.test(t.event_name)&&t.event_name!=="identify"&&t.event_name!=="page_view")&&F(e.url)&&e.path.startsWith("/")&&e.path.length<=1024&&(e.title===void 0||e.title.length<=512)&&(e.referrer===void 0||F(e.referrer))&&[e.utm_source,e.utm_medium,e.utm_campaign,e.utm_term,e.utm_content].every(n=>n===void 0||n.length>0&&n.length<=256)&&(t.properties===void 0||C(t.properties,0,{count:0}))}function $(t){if(!t||typeof t!="object")return;let e=t;if(!(e.receipt_version!==1||typeof e.receipt_id!="string"||!L.test(e.receipt_id)||typeof e.batch_id!="string"||!L.test(e.batch_id)||!Number.isInteger(e.accepted_events)||(e.accepted_events??0)<1||(e.accepted_events??0)>50||typeof e.accepted_at!="string"||Number.isNaN(Date.parse(e.accepted_at))))return e}function be(t,e){let n=new URL(t.href);return n.hash="",{path:n.pathname,url:n.toString(),...t.title?{title:t.title}:{},...e}}function q(t,e,n,r){let i={schema_version:1,event_id:t.randomUUID(),event_name:n.eventName,event_type:n.eventType,timestamp:new Date(t.now()).toISOString(),anonymous_id:e.anonymousId,session_id:e.sessionId,context:be(t.page(),e.attribution),...n.properties?{properties:n.properties}:{},...n.userId?{user_id:n.userId}:{}};if(!J(i)){r?.({code:"EVENT_INVALID",message:"Event was not queued because it failed the v1 contract."});return}return i}var Q=1;function Re(t){if(!t||typeof t!="object")return!1;let e=t;return e.version===Q&&typeof e.anonymousId=="string"&&typeof e.sessionId=="string"&&typeof e.lastActivityAt=="number"&&typeof e.attribution=="object"&&e.attribution!==null&&(e.userId===void 0||typeof e.userId=="string")}var v=class{#e;#i;#o;#t;#n;#s;constructor(e,n){this.#o=e,this.#i=n.onError,this.#t=n.sessionTimeoutMs,this.#e=`tracwell:${n.projectKey}:identity`,this.#n=n.persistence==="localStorage"?this.#h(e):void 0}identify(e,n){let r=this.#r(n);return r.userId&&r.userId!==e&&(r.anonymousId=this.#o.randomUUID(),r.sessionId=this.#o.randomUUID()),r.userId=e,this.#c(r),this.#l(r)}reset(){if(this.#s=void 0,this.#n)try{this.#n.removeItem(this.#e)}catch(e){this.#d(e)}}snapshot(e){return this.#l(this.#r(e))}#a(e){let n=this.#o.now();return{anonymousId:this.#o.randomUUID(),attribution:e,lastActivityAt:n,sessionId:this.#o.randomUUID(),version:Q}}#r(e){let n=this.#o.now(),r=this.#u();if(!r){let a=this.#a(e);return this.#c(a),a}let i=N(r.attribution),o=N(e);return(n-r.lastActivityAt>=this.#t||o!==void 0&&o!==i)&&(r.sessionId=this.#o.randomUUID(),r.attribution=e),r.lastActivityAt=n,this.#c(r),r}#u(){if(this.#s)return this.#s;if(this.#n)try{let e=this.#n.getItem(this.#e);if(!e)return;let n=JSON.parse(e);if(!Re(n)){this.#n.removeItem(this.#e);return}return this.#s=n,n}catch(e){this.#d(e);return}}#h(e){try{return e.storage()}catch(n){this.#d(n);return}}#d(e){this.#i?.({cause:e,code:"STORAGE_UNAVAILABLE",message:"Persistent identity storage is unavailable; using in-memory identity."})}#l(e){return{anonymousId:e.anonymousId,attribution:e.attribution,sessionId:e.sessionId,...e.userId?{userId:e.userId}:{}}}#c(e){if(this.#s=e,!!this.#n)try{this.#n.setItem(this.#e,JSON.stringify(e))}catch(n){this.#d(n)}}};var W=8634e4;function f(t,e,n,r){return{...r===void 0?{}:{cause:r},code:t,eventIds:n.batch.events.map(i=>i.event_id),message:e}}function Le(t){let e=JSON.parse(t??"{}");return e.version===1&&Array.isArray(e.events)?e.events.filter(n=>typeof n?.event_id=="string"&&typeof n.timestamp=="string"):[]}var S=class{#e;#i;#o;#t;#n;#s=!1;#a;#r=[];constructor(e,n){this.#i=e,this.#e=n,this.#o=n.collectionMode==="product"&&n.persistence==="localStorage"?`tracwell:${n.projectKey}:pending`:void 0}enqueue(e){if(this.#r.push(e),this.#s){this.#E(),this.#m();return}this.#r.length>=this.#e.batchSize&&this.flush()}restore(){let e=[];this.#f((r,i)=>{try{e=Le(r.getItem(i))}finally{r.removeItem(i)}});let n=this.#i.now();this.#r.unshift(...e.filter(r=>n-Date.parse(r.timestamp)<W)),this.#r.length&&this.flush()}clear(){this.#n?.(),this.#n=void 0,this.#t=void 0,this.#s=!1,this.#r=[],this.#f((e,n)=>e.removeItem(n))}flush(){return this.#a?this.#a:(this.#a=this.#u().finally(()=>{this.#a=void 0}),this.#a)}flushDue(){return this.#n?Promise.resolve():this.flush()}flushWithBeacon(){for(this.#n?.(),this.#n=void 0;this.#t||this.#r.length>0;){let e=this.#t??this.#h();if(!e)return;let n=JSON.stringify(e.batch);if(!this.#i.sendBeacon(this.#e.endpoint,n)){this.#e.onError?.(f("DELIVERY_FAILED","Browser lifecycle handoff was rejected by sendBeacon.",e));return}this.#t=void 0,this.#d({batchId:e.batch.batch_id,eventIds:e.batch.events.map(r=>r.event_id),status:"handed_off"})}}async#u(){this.#n?.(),this.#n=void 0;let e=this.#t??this.#h();if(!e)return;let n=await this.#c(e);n&&this.#r.length>0?await this.#u():n&&this.#_()}#h(){if(this.#r.length===0)return;let e=this.#r.slice(0,this.#e.batchSize),n;for(;e.length>0;){let r={batch_version:1,batch_id:this.#i.randomUUID(),project_key:this.#e.projectKey,sent_at:new Date(this.#i.now()).toISOString(),sdk:{name:"tracwell-browser",version:b},events:e};if(P(r)<=49152){n=r;break}e.pop()}if(!n){let r=this.#r.shift();r&&this.#e.onError?.({code:"EVENT_INVALID",eventIds:[r.event_id],message:"Event cannot fit in a valid browser batch."});return}return this.#r.splice(0,n.events.length),this.#t={attempt:0,batch:n},this.#t}#d(e){this.#e.onDelivery?.(e)}#l(e){let n=Math.min(Y*2**(e.attempt-1),G);this.#n=this.#i.setTimeout(()=>{this.#n=void 0,this.flush()},n)}async#c(e){let n;try{n=await this.#i.fetch(this.#e.endpoint,JSON.stringify(e.batch),"text/plain;charset=UTF-8")}catch(o){return this.#t!==e?!1:this.#p(e,o)}if(n.status!==202)return n.status===408||n.status===429||n.status>=500?this.#p(e,new Error(`Collector returned HTTP ${n.status}.`)):(this.#t===e&&(this.#t=void 0),this.#e.onError?.(f("DELIVERY_REJECTED",`Collector rejected the batch with non-retryable HTTP ${n.status}.`,e)),!1);let r;try{r=await n.json()}catch(o){return this.#e.onError?.(f("RECEIPT_INVALID","Collector returned an unreadable acceptance receipt.",e,o)),this.#p(e,o)}let i=$(r);return!i||i.batch_id!==e.batch.batch_id||i.accepted_events!==e.batch.events.length?(this.#e.onError?.(f("RECEIPT_INVALID","Collector receipt does not match the submitted batch.",e)),this.#p(e,new Error("Acceptance receipt mismatch."))):(this.#t===e&&(this.#t=void 0),this.#d({batchId:e.batch.batch_id,eventIds:e.batch.events.map(o=>o.event_id),receiptId:i.receipt_id,status:"accepted"}),!0)}#p(e,n){if(this.#t!==e)return!1;e.attempt+=1;let r=this.#i.now()-Date.parse(e.batch.events[0]?.timestamp??"")<W;return e.attempt<=this.#e.maxRetries&&r?(this.#s=!0,this.#e.onError?.(f("DELIVERY_FAILED","Event batch delivery failed; retrying.",e,n)),this.#E(),this.#m(),this.#l(e),!1):(this.#t=void 0,this.#e.onError?.(f("DELIVERY_EXHAUSTED","Event batch delivery exhausted its retries.",e,n)),this.#m(),!1)}#E(){let e=(this.#t?.batch.events.length??0)+this.#r.length,n=Math.min(e-A,this.#r.length);if(n<=0)return;let r=this.#r.splice(0,n);this.#e.onError?.({code:"DELIVERY_EXHAUSTED",eventIds:r.map(i=>i.event_id),message:"Retry buffer full; oldest events dropped."})}#_(){this.#s&&(this.#s=!1,this.#f((e,n)=>e.removeItem(n)))}#m(){let e=[...this.#t?.batch.events??[],...this.#r].slice(-A);this.#f((n,r)=>e.length?n.setItem(r,JSON.stringify({version:1,events:e})):n.removeItem(r))}#f(e){if(this.#o)try{let n=this.#i.storage();n&&e(n,this.#o)}catch{}}};function Z(t,e){let n=new v(t,e),r=new S(t,e),i=e.consent,o,h=!1,c=!1,a=[];function _(){return i==="granted"&&!(e.respectDoNotTrack&&t.doNotTrackEnabled())}function T(s,u,d,l){if(c||!_())return;let V=R(t.page()),U=n.snapshot(V),p=l??U.userId,ne=s==="identify"&&p?{eventName:"identify",eventType:"identify",...d?{properties:d}:{},userId:p}:s==="pageview"?{eventName:"page_view",eventType:"pageview",...d?{properties:d}:{},...p?{userId:p}:{}}:{eventName:u,eventType:"event",...d?{properties:d}:{},...p?{userId:p}:{}},m=q(t,U,ne,e.onError);if(m){if(s==="identify"&&l){let k=n.identify(l,V);m.anonymous_id=k.anonymousId,m.session_id=k.sessionId}return r.enqueue(m),m.event_id}}function I(s){if(!_())return;let u=new URL(t.page().href);u.hash="";let d=u.toString();if(d===o)return;let l=T("pageview","page_view",s);return l&&(o=d),l}function x(){a.forEach(s=>s()),a=[],h=!1}function D(){h||c||!_()||(h=!0,a=[t.addPageHideListener(()=>r.flushWithBeacon()),t.addVisibilityHiddenListener(()=>r.flushWithBeacon()),t.setInterval(()=>{r.flushDue()},e.flushIntervalMs)],r.restore(),e.autoTrackPageViews&&(a.push(t.subscribeToNavigation(()=>{I()})),I()))}return D(),{flush(){return r.flush()},getSession(){if(c||e.collectionMode!=="product"||!_())return;let s=n.snapshot(R(t.page()));return{anonymousId:s.anonymousId,sessionId:s.sessionId,...s.userId?{userId:s.userId}:{}}},identify(s,u){if(e.collectionMode==="private"){e.onError?.({code:"IDENTIFY_UNAVAILABLE",message:"identify() is unavailable in Private collection mode."});return}return T("identify","identify",u,s)},page:I,reset(){n.reset(),o=void 0},setConsent(s){if(i=s,s==="denied"){x(),r.clear(),n.reset(),o=void 0;return}D()},async shutdown(){c||(x(),await r.flush(),c=!0)},track(s,u){return T("event",s,u)}}}function ee(t,e){return window.addEventListener(t,e),()=>window.removeEventListener(t,e)}function te(){return{addPageHideListener(t){return ee("pagehide",t)},addVisibilityHiddenListener(t){let e=()=>{document.visibilityState==="hidden"&&t()};return document.addEventListener("visibilitychange",e),()=>document.removeEventListener("visibilitychange",e)},doNotTrackEnabled(){return navigator.doNotTrack==="1"},async fetch(t,e,n){return fetch(t,{body:e,credentials:"omit",headers:{"content-type":n},keepalive:!0,method:"POST"})},now(){return Date.now()},page(){return{href:window.location.href,referrer:document.referrer,title:document.title}},randomUUID(){return crypto.randomUUID()},sendBeacon(t,e){return navigator.sendBeacon(t,e)},setInterval(t,e){let n=window.setInterval(t,e);return()=>window.clearInterval(n)},setTimeout(t,e){let n=window.setTimeout(t,e);return()=>window.clearTimeout(n)},storage(){return window.localStorage},subscribeToNavigation(t){let e=!0,n=history.pushState,r=history.replaceState,i=()=>{e&&queueMicrotask(t)},o=function(...a){n.apply(history,a),i()},h=function(...a){r.apply(history,a),i()};history.pushState=o,history.replaceState=h;let c=ee("popstate",t);return()=>{e=!1,c(),history.pushState===o&&(history.pushState=n),history.replaceState===h&&(history.replaceState=r)}}}}function et(t){if(typeof window>"u"||typeof document>"u"||typeof navigator>"u")throw new Error("createTracwell must be called in a browser after the document is available.");return Z(te(),z(t))}export{H as DEFAULT_BATCH_SIZE,O as DEFAULT_COLLECTOR_ENDPOINT,B as DEFAULT_FLUSH_INTERVAL_MS,X as DEFAULT_MAX_RETRIES,K as DEFAULT_SESSION_TIMEOUT_MS,b as SDK_VERSION,et as createTracwell};
1
+ var O="https://collect.tracwell.app/v1/events",H=25,B=1e4,X=Number.POSITIVE_INFINITY,Y=2e3,G=300*1e3,A=100,K=1800*1e3,b="0.2.0";function E(t,e){throw e?.({code:"CONFIG_INVALID",message:t}),new TypeError(t)}function oe(t,e){let n=t.trim();return n.length<20||n.length>128||!/^[A-Za-z0-9_-]+$/u.test(n)?E("projectKey does not match the public project-key contract.",e):n}function se(t,e){let n;try{n=new URL(t)}catch{return E("endpoint must be an absolute HTTPS URL.",e)}return ae(n,e)}function ae(t,e){let n=t.hostname==="localhost"||t.hostname==="127.0.0.1"||t.hostname==="[::1]";return t.protocol!=="https:"&&!n?E("endpoint must use HTTPS unless it uses a loopback host.",e):t.toString()}function y(t,e,n,r,i){return!Number.isSafeInteger(e)||e<n||e>r?E(`${t} must be an integer between ${n} and ${r}.`,i):e}function z(t){let e=oe(t.projectKey,t.onError),n=t.collectionMode??"private",r=se(t.endpoint??O,t.onError);return n==="private"&&t.persistence!==void 0&&t.persistence!=="none"?E('Private collection requires persistence: "none".',t.onError):{autoTrackPageViews:t.autoTrackPageViews??!0,batchSize:y("batchSize",t.batchSize??H,1,50,t.onError),consent:t.consent??"granted",collectionMode:n,endpoint:r,flushIntervalMs:y("flushIntervalMs",t.flushIntervalMs??B,1e3,6e4,t.onError),maxRetries:t.maxRetries===void 0?X:y("maxRetries",t.maxRetries,0,5,t.onError),persistence:n==="private"?"none":t.persistence??"localStorage",projectKey:e,respectDoNotTrack:t.respectDoNotTrack??!0,sessionTimeoutMs:y("sessionTimeoutMs",t.sessionTimeoutMs??K,6e4,1440*60*1e3,t.onError),...t.onDelivery?{onDelivery:t.onDelivery}:{},...t.onError?{onError:t.onError}:{}}}var j=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"];function de(t){let e=t?.trim();return e||void 0}function ce(t){if(t)try{return new URL(t).toString()}catch{return}}function R(t){let e=new URL(t.href),n={},r=ce(t.referrer);r&&(n.referrer=r);for(let i of j){let o=de(e.searchParams.get(i));o&&(n[i]=o)}return n}function N(t){let e=j.map(n=>t[n]??"").join("");return e.replaceAll("","")?e:void 0}var L=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu,Ie=/^[A-Za-z][A-Za-z0-9_.:-]*$/u,ge=/[\s\x00-\x1f\x7f]/u,Ae=/[\x00-\x1f\x7f]/u;function P(t){return new TextEncoder().encode(JSON.stringify(t)).byteLength}function w(t){return t.length>0&&t.length<=256&&!ge.test(t)}function F(t,e=2048){if(t.length>e)return!1;try{return new URL(t).origin.length>0}catch{return!1}}function C(t,e,n){return e>4?!1:typeof t=="string"?t.length<=1024:typeof t=="number"?Number.isFinite(t):t===null||typeof t=="boolean"?!0:Array.isArray(t)?t.length<=50&&t.every(r=>C(r,e+1,n)):Object.entries(t).every(([r,i])=>(n.count+=1,n.count<=100&&r.length>0&&r.length<=128&&!Ae.test(r)&&C(i,e+1,n)))}function J(t){let{context:e}=t;try{if(P(t)>16384)return!1}catch{return!1}return L.test(t.event_id)&&w(t.anonymous_id)&&w(t.session_id)&&(t.user_id===void 0||w(t.user_id))&&(t.event_type!=="event"||t.event_name.length<=100&&Ie.test(t.event_name)&&t.event_name!=="identify"&&t.event_name!=="page_view")&&F(e.url)&&e.path.startsWith("/")&&e.path.length<=1024&&(e.title===void 0||e.title.length<=512)&&(e.referrer===void 0||F(e.referrer))&&[e.utm_source,e.utm_medium,e.utm_campaign,e.utm_term,e.utm_content].every(n=>n===void 0||n.length>0&&n.length<=256)&&(t.properties===void 0||C(t.properties,0,{count:0}))}function $(t){if(!t||typeof t!="object")return;let e=t;if(!(e.receipt_version!==1||typeof e.receipt_id!="string"||!L.test(e.receipt_id)||typeof e.batch_id!="string"||!L.test(e.batch_id)||!Number.isInteger(e.accepted_events)||(e.accepted_events??0)<1||(e.accepted_events??0)>50||typeof e.accepted_at!="string"||Number.isNaN(Date.parse(e.accepted_at))))return e}function be(t,e){let n=new URL(t.href);return n.hash="",{path:n.pathname,url:n.toString(),...t.title?{title:t.title}:{},...e}}function q(t,e,n,r){let i={schema_version:1,event_id:t.randomUUID(),event_name:n.eventName,event_type:n.eventType,timestamp:new Date(t.now()).toISOString(),anonymous_id:e.anonymousId,session_id:e.sessionId,context:be(t.page(),e.attribution),...n.properties?{properties:n.properties}:{},...n.userId?{user_id:n.userId}:{}};if(!J(i)){r?.({code:"EVENT_INVALID",message:"Event was not queued because it failed the v1 contract."});return}return i}var Q=1;function Re(t){if(!t||typeof t!="object")return!1;let e=t;return e.version===Q&&typeof e.anonymousId=="string"&&typeof e.sessionId=="string"&&typeof e.lastActivityAt=="number"&&typeof e.attribution=="object"&&e.attribution!==null&&(e.userId===void 0||typeof e.userId=="string")}var S=class{#e;#i;#o;#t;#n;#s;constructor(e,n){this.#o=e,this.#i=n.onError,this.#t=n.sessionTimeoutMs,this.#e=`tracwell:${n.projectKey}:identity`,this.#n=n.persistence==="localStorage"?this.#h(e):void 0}identify(e,n){let r=this.#r(n);return r.userId&&r.userId!==e&&(r.anonymousId=this.#o.randomUUID(),r.sessionId=this.#o.randomUUID()),r.userId=e,this.#c(r),this.#l(r)}reset(){if(this.#s=void 0,this.#n)try{this.#n.removeItem(this.#e)}catch(e){this.#d(e)}}snapshot(e){return this.#l(this.#r(e))}#a(e){let n=this.#o.now();return{anonymousId:this.#o.randomUUID(),attribution:e,lastActivityAt:n,sessionId:this.#o.randomUUID(),version:Q}}#r(e){let n=this.#o.now(),r=this.#u();if(!r){let a=this.#a(e);return this.#c(a),a}let i=N(r.attribution),o=N(e);return(n-r.lastActivityAt>=this.#t||o!==void 0&&o!==i)&&(r.sessionId=this.#o.randomUUID(),r.attribution=e),r.lastActivityAt=n,this.#c(r),r}#u(){if(this.#s)return this.#s;if(this.#n)try{let e=this.#n.getItem(this.#e);if(!e)return;let n=JSON.parse(e);if(!Re(n)){this.#n.removeItem(this.#e);return}return this.#s=n,n}catch(e){this.#d(e);return}}#h(e){try{return e.storage()}catch(n){this.#d(n);return}}#d(e){this.#i?.({cause:e,code:"STORAGE_UNAVAILABLE",message:"Persistent identity storage is unavailable; using in-memory identity."})}#l(e){return{anonymousId:e.anonymousId,attribution:e.attribution,sessionId:e.sessionId,...e.userId?{userId:e.userId}:{}}}#c(e){if(this.#s=e,!!this.#n)try{this.#n.setItem(this.#e,JSON.stringify(e))}catch(n){this.#d(n)}}};var W=8634e4;function f(t,e,n,r){return{...r===void 0?{}:{cause:r},code:t,eventIds:n.batch.events.map(i=>i.event_id),message:e}}function Le(t){let e=JSON.parse(t??"{}");return e.version===1&&Array.isArray(e.events)?e.events.filter(n=>typeof n?.event_id=="string"&&typeof n.timestamp=="string"):[]}var v=class{#e;#i;#o;#t;#n;#s=!1;#a;#r=[];constructor(e,n){this.#i=e,this.#e=n,this.#o=n.collectionMode==="product"&&n.persistence==="localStorage"?`tracwell:${n.projectKey}:pending`:void 0}enqueue(e){if(this.#r.push(e),this.#s){this.#E(),this.#m();return}this.#r.length>=this.#e.batchSize&&this.flush()}restore(){let e=[];this.#f((r,i)=>{try{e=Le(r.getItem(i))}finally{r.removeItem(i)}});let n=this.#i.now();this.#r.unshift(...e.filter(r=>n-Date.parse(r.timestamp)<W)),this.#r.length&&this.flush()}clear(){this.#n?.(),this.#n=void 0,this.#t=void 0,this.#s=!1,this.#r=[],this.#f((e,n)=>e.removeItem(n))}flush(){return this.#a?this.#a:(this.#a=this.#u().finally(()=>{this.#a=void 0}),this.#a)}flushDue(){return this.#n?Promise.resolve():this.flush()}flushWithBeacon(){for(this.#n?.(),this.#n=void 0;this.#t||this.#r.length>0;){let e=this.#t??this.#h();if(!e)return;let n=JSON.stringify(e.batch);if(!this.#i.sendBeacon(this.#e.endpoint,n)){this.#e.onError?.(f("DELIVERY_FAILED","Browser lifecycle handoff was rejected by sendBeacon.",e));return}this.#t=void 0,this.#d({batchId:e.batch.batch_id,eventIds:e.batch.events.map(r=>r.event_id),status:"handed_off"})}}async#u(){this.#n?.(),this.#n=void 0;let e=this.#t??this.#h();if(!e)return;let n=await this.#c(e);n&&this.#r.length>0?await this.#u():n&&this.#_()}#h(){if(this.#r.length===0)return;let e=this.#r.slice(0,this.#e.batchSize),n;for(;e.length>0;){let r={batch_version:1,batch_id:this.#i.randomUUID(),project_key:this.#e.projectKey,sent_at:new Date(this.#i.now()).toISOString(),sdk:{name:"tracwell-browser",version:b},events:e};if(P(r)<=49152){n=r;break}e.pop()}if(!n){let r=this.#r.shift();r&&this.#e.onError?.({code:"EVENT_INVALID",eventIds:[r.event_id],message:"Event cannot fit in a valid browser batch."});return}return this.#r.splice(0,n.events.length),this.#t={attempt:0,batch:n},this.#t}#d(e){this.#e.onDelivery?.(e)}#l(e){let n=Math.min(Y*2**(e.attempt-1),G);this.#n=this.#i.setTimeout(()=>{this.#n=void 0,this.flush()},n)}async#c(e){let n;try{n=await this.#i.fetch(this.#e.endpoint,JSON.stringify(e.batch),"text/plain;charset=UTF-8")}catch(o){return this.#t!==e?!1:this.#p(e,o)}if(n.status!==202)return n.status===408||n.status===429||n.status>=500?this.#p(e,new Error(`Collector returned HTTP ${n.status}.`)):(this.#t===e&&(this.#t=void 0),this.#e.onError?.(f("DELIVERY_REJECTED",`Collector rejected the batch with non-retryable HTTP ${n.status}.`,e)),!1);let r;try{r=await n.json()}catch(o){return this.#e.onError?.(f("RECEIPT_INVALID","Collector returned an unreadable acceptance receipt.",e,o)),this.#p(e,o)}let i=$(r);return!i||i.batch_id!==e.batch.batch_id||i.accepted_events!==e.batch.events.length?(this.#e.onError?.(f("RECEIPT_INVALID","Collector receipt does not match the submitted batch.",e)),this.#p(e,new Error("Acceptance receipt mismatch."))):(this.#t===e&&(this.#t=void 0),this.#d({batchId:e.batch.batch_id,eventIds:e.batch.events.map(o=>o.event_id),receiptId:i.receipt_id,status:"accepted"}),!0)}#p(e,n){if(this.#t!==e)return!1;e.attempt+=1;let r=this.#i.now()-Date.parse(e.batch.events[0]?.timestamp??"")<W;return e.attempt<=this.#e.maxRetries&&r?(this.#s=!0,this.#e.onError?.(f("DELIVERY_FAILED","Event batch delivery failed; retrying.",e,n)),this.#E(),this.#m(),this.#l(e),!1):(this.#t=void 0,this.#e.onError?.(f("DELIVERY_EXHAUSTED","Event batch delivery exhausted its retries.",e,n)),this.#m(),!1)}#E(){let e=(this.#t?.batch.events.length??0)+this.#r.length,n=Math.min(e-A,this.#r.length);if(n<=0)return;let r=this.#r.splice(0,n);this.#e.onError?.({code:"DELIVERY_EXHAUSTED",eventIds:r.map(i=>i.event_id),message:"Retry buffer full; oldest events dropped."})}#_(){this.#s&&(this.#s=!1,this.#f((e,n)=>e.removeItem(n)))}#m(){let e=[...this.#t?.batch.events??[],...this.#r].slice(-A);this.#f((n,r)=>e.length?n.setItem(r,JSON.stringify({version:1,events:e})):n.removeItem(r))}#f(e){if(this.#o)try{let n=this.#i.storage();n&&e(n,this.#o)}catch{}}};function Z(t,e){let n=new S(t,e),r=new v(t,e),i=e.consent,o,h=!1,c=!1,a=[];function _(){return i==="granted"&&!(e.respectDoNotTrack&&t.doNotTrackEnabled())}function T(s,u,d,l){if(c||!_())return;let V=R(t.page()),U=n.snapshot(V),p=l??U.userId,ne=s==="identify"&&p?{eventName:"identify",eventType:"identify",...d?{properties:d}:{},userId:p}:s==="pageview"?{eventName:"page_view",eventType:"pageview",...d?{properties:d}:{},...p?{userId:p}:{}}:{eventName:u,eventType:"event",...d?{properties:d}:{},...p?{userId:p}:{}},m=q(t,U,ne,e.onError);if(m){if(s==="identify"&&l){let k=n.identify(l,V);m.anonymous_id=k.anonymousId,m.session_id=k.sessionId}return r.enqueue(m),m.event_id}}function I(s){if(!_())return;let u=new URL(t.page().href);u.hash="";let d=u.toString();if(d===o)return;let l=T("pageview","page_view",s);return l&&(o=d),l}function x(){a.forEach(s=>s()),a=[],h=!1}function D(){h||c||!_()||(h=!0,a=[t.addPageHideListener(()=>r.flushWithBeacon()),t.addVisibilityHiddenListener(()=>r.flushWithBeacon()),t.setInterval(()=>{r.flushDue()},e.flushIntervalMs)],r.restore(),e.autoTrackPageViews&&(a.push(t.subscribeToNavigation(()=>{I()})),I()))}return D(),{flush(){return r.flush()},getSession(){if(c||e.collectionMode!=="product"||!_())return;let s=n.snapshot(R(t.page()));return{anonymousId:s.anonymousId,sessionId:s.sessionId,...s.userId?{userId:s.userId}:{}}},identify(s,u){if(e.collectionMode==="private"){e.onError?.({code:"IDENTIFY_UNAVAILABLE",message:"identify() is unavailable in Private collection mode."});return}return T("identify","identify",u,s)},page:I,reset(){n.reset(),o=void 0},setConsent(s){if(i=s,s==="denied"){x(),r.clear(),n.reset(),o=void 0;return}D()},async shutdown(){c||(x(),await r.flush(),c=!0)},track(s,u){return T("event",s,u)}}}function ee(t,e){return window.addEventListener(t,e),()=>window.removeEventListener(t,e)}function te(){return{addPageHideListener(t){return ee("pagehide",t)},addVisibilityHiddenListener(t){let e=()=>{document.visibilityState==="hidden"&&t()};return document.addEventListener("visibilitychange",e),()=>document.removeEventListener("visibilitychange",e)},doNotTrackEnabled(){return navigator.doNotTrack==="1"},async fetch(t,e,n){return fetch(t,{body:e,credentials:"omit",headers:{"content-type":n},keepalive:!0,method:"POST"})},now(){return Date.now()},page(){return{href:window.location.href,referrer:document.referrer,title:document.title}},randomUUID(){return crypto.randomUUID()},sendBeacon(t,e){return navigator.sendBeacon(t,e)},setInterval(t,e){let n=window.setInterval(t,e);return()=>window.clearInterval(n)},setTimeout(t,e){let n=window.setTimeout(t,e);return()=>window.clearTimeout(n)},storage(){return window.localStorage},subscribeToNavigation(t){let e=!0,n=history.pushState,r=history.replaceState,i=()=>{e&&queueMicrotask(t)},o=function(...a){n.apply(history,a),i()},h=function(...a){r.apply(history,a),i()};history.pushState=o,history.replaceState=h;let c=ee("popstate",t);return()=>{e=!1,c(),history.pushState===o&&(history.pushState=n),history.replaceState===h&&(history.replaceState=r)}}}}function et(t){if(typeof window>"u"||typeof document>"u"||typeof navigator>"u")throw new Error("createTracwell must be called in a browser after the document is available.");return Z(te(),z(t))}export{H as DEFAULT_BATCH_SIZE,O as DEFAULT_COLLECTOR_ENDPOINT,B as DEFAULT_FLUSH_INTERVAL_MS,X as DEFAULT_MAX_RETRIES,K as DEFAULT_SESSION_TIMEOUT_MS,b as SDK_VERSION,et as createTracwell};
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "tracwell",
3
- "version": "0.5.0",
4
- "description": "Privacy-aware browser analytics and optional server-side AI crawler tracking for Tracwell",
3
+ "version": "0.6.0",
4
+ "description": "Privacy-aware browser analytics, optional server-side AI crawler tracking, and the tracwell CLI for coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": false,
8
+ "bin": {
9
+ "tracwell": "./dist/npm/cli.js"
10
+ },
8
11
  "main": "./dist/npm/index.js",
9
12
  "module": "./dist/npm/index.js",
10
13
  "types": "./dist/npm/index.d.ts",
@@ -28,7 +31,9 @@
28
31
  "LICENSE",
29
32
  "README.md",
30
33
  "dist/npm/server.js",
31
- "dist/npm/server.d.ts"
34
+ "dist/npm/server.d.ts",
35
+ "dist/npm/cli.js",
36
+ "skills/tracwell/SKILL.md"
32
37
  ],
33
38
  "keywords": [
34
39
  "analytics",
@@ -41,6 +46,7 @@
41
46
  "access": "public"
42
47
  },
43
48
  "devDependencies": {
49
+ "@types/node": "26.1.1",
44
50
  "esbuild": "0.28.1",
45
51
  "publint": "0.3.24",
46
52
  "vitest": "4.1.10",
@@ -52,6 +58,6 @@
52
58
  "build:npm": "node scripts/build-npm.mjs && tsc -p tsconfig.build.json",
53
59
  "check:package": "pnpm build:npm && publint && node scripts/smoke-npm.mjs",
54
60
  "test": "vitest run",
55
- "typecheck": "tsc -p tsconfig.json"
61
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.cli.json"
56
62
  }
57
63
  }
@@ -0,0 +1,104 @@
1
+ ---
2
+ name: tracwell
3
+ description: Add Tracwell analytics to a website or app and read its reports with the tracwell CLI. Use when the user wants analytics, page views, signups, funnels or revenue tracking set up, or asks which pages, sources or campaigns bring visitors, signups or money.
4
+ ---
5
+
6
+ # Tracwell
7
+
8
+ Tracwell is product analytics for websites and apps. The `tracwell` CLI signs
9
+ in, adds a website, prints the install snippet, checks that events arrive, and
10
+ reads reports. Every command prints JSON.
11
+
12
+ ## Quick start
13
+
14
+ ```sh
15
+ npx tracwell init --url https://acme.com
16
+ ```
17
+
18
+ `init` signs in if needed, finds or adds the website, saves it as the default,
19
+ prints the install snippet, and installs this skill plus a short block in
20
+ `AGENTS.md`. Then do step 3 and step 4 below. The steps below are the same
21
+ thing one command at a time.
22
+
23
+ ## 1. Sign in
24
+
25
+ ```sh
26
+ npx tracwell login
27
+ ```
28
+
29
+ This opens the browser. The user signs in, picks websites and approves what
30
+ the CLI may do. Wait for them; do not try to complete the page yourself.
31
+ Ticking "Add websites" is needed for step 2.
32
+
33
+ Without a browser (CI, remote shells), ask the user for an API key from
34
+ Tracwell Settings → API & MCP and set `TRACWELL_API_KEY`. Keys can read reports
35
+ but cannot add websites.
36
+
37
+ ## 2. Add the website
38
+
39
+ Skip this if `npx tracwell sites` already lists it.
40
+
41
+ ```sh
42
+ npx tracwell sites add "Acme" https://acme.com
43
+ npx tracwell use <website-id>
44
+ ```
45
+
46
+ `use` saves the website so later commands do not need `--site`.
47
+
48
+ ## 3. Install the snippet
49
+
50
+ ```sh
51
+ npx tracwell snippet
52
+ ```
53
+
54
+ Put the returned script tag in the site's `<head>` on every page, or use the
55
+ npm setup it returns for apps built with a bundler. Do not invent a project
56
+ key; always use the one from `snippet`.
57
+
58
+ ## 4. Verify
59
+
60
+ Deploy or run the site, open a page, then:
61
+
62
+ ```sh
63
+ npx tracwell verify
64
+ ```
65
+
66
+ `waiting` means no event has arrived yet. `receiving` means it works.
67
+
68
+ ## 5. Read reports
69
+
70
+ Reports default to the last 7 days (UTC). Use `--days 30`, or `--from` and
71
+ `--to` as `YYYY-MM-DD`.
72
+
73
+ ```sh
74
+ npx tracwell overview
75
+ npx tracwell acquisition --days 30
76
+ npx tracwell revenue
77
+ npx tracwell events
78
+ npx tracwell funnels
79
+ ```
80
+
81
+ ## 6. Track what matters
82
+
83
+ Needs "Manage goals and funnels" approved at sign-in. Check with `--dry-run` first.
84
+
85
+ ```sh
86
+ npx tracwell goals add "Signed up" --event signup_completed --category signup
87
+ npx tracwell goals add "Viewed pricing" --page /pricing
88
+ npx tracwell funnels add "Checkout" --steps /pricing,event:checkout_started,event:purchase_completed
89
+ ```
90
+
91
+ Steps starting with `/` are pages; `event:name` (or a bare name) is an event.
92
+ Only use event names that `npx tracwell events` shows, or that you just added
93
+ to the code. Then read them with `npx tracwell goal --goal-id <id>` or
94
+ `npx tracwell funnel --funnel-id <id>`.
95
+
96
+ ## More
97
+
98
+ - `npx tracwell help <command>` lists a command's arguments, read live from Tracwell.
99
+ - `npx tracwell search <words>` finds a tool; `npx tracwell tools` lists them all.
100
+ Run any of them with `npx tracwell call <tool> --arg value`.
101
+ - Add `--dry-run` to see the tool and arguments without sending, before any write.
102
+ - Track custom events (signups, purchases) with `window.tracwell?.track("signup_completed")`.
103
+ Docs: https://tracwell.app/docs
104
+ - Exit codes: 0 ok, 1 request failed, 2 bad input, 3 sign-in needed (run `login`).