Skip to content

Commit acb6092

Browse files
committed
feat: add privacy-first email lookup CLI
1 parent 2b19cc5 commit acb6092

6 files changed

Lines changed: 296 additions & 73 deletions

File tree

README.md

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ Analytics your AI agent can actually use — track, analyze, experiment, optimiz
99
Try the seeded public demo without signing in:
1010

1111
```bash
12-
npx --yes @agent-analytics/cli@0.5.20 demo
13-
npx --yes @agent-analytics/cli@0.5.20 --demo projects
14-
npx --yes @agent-analytics/cli@0.5.20 --demo funnel agentanalytics-demo --steps "page_view,signup_started,signup"
15-
npx --yes @agent-analytics/cli@0.5.20 --demo experiments list agentanalytics-demo
12+
npx --yes @agent-analytics/cli@0.5.24 demo
13+
npx --yes @agent-analytics/cli@0.5.24 --demo projects
14+
npx --yes @agent-analytics/cli@0.5.24 --demo funnel agentanalytics-demo --steps "page_view,signup_started,signup"
15+
npx --yes @agent-analytics/cli@0.5.24 --demo experiments list agentanalytics-demo
1616
```
1717

1818
Demo mode fetches a short-lived read-only `aas_*` session from the hosted API. It does not expose a raw `aak_*` API key, does not write local CLI config, and blocks mutating commands before making API requests.
@@ -21,19 +21,19 @@ Get the fastest path to useful analytics before installing events:
2121

2222
```bash
2323
# 1. Preview what your agent should track first
24-
npx --yes @agent-analytics/cli@0.5.20 scan https://mysite.com --json
24+
npx --yes @agent-analytics/cli@0.5.24 scan https://mysite.com --json
2525

2626
# 2. Sign in when you want the full instrumentation plan
27-
npx --yes @agent-analytics/cli@0.5.20 login
27+
npx --yes @agent-analytics/cli@0.5.24 login
2828

2929
# 3. Create or identify the project domain
30-
npx --yes @agent-analytics/cli@0.5.20 create my-site --domain https://mysite.com
30+
npx --yes @agent-analytics/cli@0.5.24 create my-site --domain https://mysite.com
3131

3232
# 4. Run the full signed-in analysis for that project domain
33-
npx --yes @agent-analytics/cli@0.5.20 scan https://mysite.com --full --project my-site --json
33+
npx --yes @agent-analytics/cli@0.5.24 scan https://mysite.com --full --project my-site --json
3434

3535
# Or resume and upgrade the anonymous analysis after login
36-
npx --yes @agent-analytics/cli@0.5.20 scan \
36+
npx --yes @agent-analytics/cli@0.5.24 scan \
3737
--resume <analysis_id> \
3838
--resume-token <resume_token> \
3939
--full \
@@ -45,19 +45,19 @@ Anonymous `scan` returns a one-analysis `rst_*` resume token, not an `aas_*` age
4545

4646
```bash
4747
# 1. Start agent login or signup in the browser
48-
npx --yes @agent-analytics/cli@0.5.20 login
48+
npx --yes @agent-analytics/cli@0.5.24 login
4949

5050
# 2. Create a project
51-
npx --yes @agent-analytics/cli@0.5.20 create my-site --domain https://mysite.com
51+
npx --yes @agent-analytics/cli@0.5.24 create my-site --domain https://mysite.com
5252

5353
# 3. Watch it live
54-
npx --yes @agent-analytics/cli@0.5.20 live
54+
npx --yes @agent-analytics/cli@0.5.24 live
5555

5656
# Optional detached login for remote or issue-based agent work
57-
npx --yes @agent-analytics/cli@0.5.20 login --detached
57+
npx --yes @agent-analytics/cli@0.5.24 login --detached
5858

5959
# Optional: clear your saved local auth later
60-
npx --yes @agent-analytics/cli@0.5.20 logout
60+
npx --yes @agent-analytics/cli@0.5.24 logout
6161
```
6262

6363
## Commands
@@ -110,6 +110,12 @@ context set <name> --json '{...}' Store compact goals, activation events, glossa
110110
portfolio-context get Read stored account portfolio context
111111
portfolio-context set --json '{...}'
112112
Store shared goals, surface roles, milestones, and glossary
113+
portfolios list List identity lookup portfolios
114+
portfolios create <slug> --name "Portfolio" --projects app,docs [--move]
115+
Create a project portfolio; --move allows reassigning projects
116+
portfolios get <slug-or-id> Show a portfolio and its member projects
117+
portfolios update <slug-or-id> Update name and/or projects with optional --move
118+
portfolios delete <slug-or-id> Delete a portfolio
113119

114120
# Experiments — A/B testing your agent can actually use
115121
experiments list <project> List experiments
@@ -128,17 +134,17 @@ The CLI is agent-session-first. It stores a renewable Agent Analytics session lo
128134
When a free account hits a Pro-only analytics task, run an explicit upgrade handoff:
129135

130136
```bash
131-
npx --yes @agent-analytics/cli@0.5.20 upgrade-link --detached \
137+
npx --yes @agent-analytics/cli@0.5.24 upgrade-link --detached \
132138
--reason "Need funnel and retention reads for this analysis" \
133-
--command "npx --yes @agent-analytics/cli@0.5.20 funnel my-site --steps page_view,signup,purchase"
139+
--command "npx --yes @agent-analytics/cli@0.5.24 funnel my-site --steps page_view,signup,purchase"
134140
```
135141

136142
The CLI prints an `app.agentanalytics.sh` link. The human confirms the logged-in dashboard account, pays in Lemon Squeezy, and returns to the agent after Pro activates. Use `upgrade-link --wait` when the local shell should keep polling for activation.
137143

138144
Project management commands accept exact project names or project IDs. For local browser QA, update origins through the CLI while keeping the production origin:
139145

140146
```bash
141-
npx --yes @agent-analytics/cli@0.5.20 update stylio --origins 'https://stylio.app,http://lvh.me:3101'
147+
npx --yes @agent-analytics/cli@0.5.24 update stylio --origins 'https://stylio.app,http://lvh.me:3101'
142148
```
143149

144150
Use `scan` before tracker installation when you want judgment instead of generic event lists. The preview is intentionally small: prioritized minimum viable instrumentation, what each event unlocks, current blind spots, and what not to track yet. The stable JSON is designed for agent skills to install only the high-priority events first and verify the first useful recommended event.
@@ -151,27 +157,29 @@ Bounce metrics (`insights`, `pages`, `sessions`) treat a session as a bounce whe
151157
`query` keeps `/events` raw and lossless, but `/query` uses activation-safe dedupe (`session_then_user`) as the default for `event_count`: session-backed rows count by session, no-session rows fall back to `user_id` only when that user has no session-backed row in the same filtered/grouped result set, and fully anonymous rows fall back to event `id`. For recent signup or ingestion debugging, check `events <project> --event <actual_event_name>` first, then use `query` after verifying the raw event names the project emits. `--count-mode` only affects `event_count`. Use `--count-mode raw` when you need the old ingested-row count for debugging or audit work:
152158

153159
```bash
154-
npx --yes @agent-analytics/cli@0.5.20 query my-site --metrics event_count --count-mode raw
160+
npx --yes @agent-analytics/cli@0.5.24 query my-site --metrics event_count --count-mode raw
155161
```
156162

157163
Property filters must use canonical `properties.*` fields. Built-in filter fields are only `event`, `user_id`, `date`, `country`, `session_id`, and `timestamp`. Example:
158164

159165
```bash
160-
npx --yes @agent-analytics/cli@0.5.20 query my-site --filter '[{"field":"properties.referrer","op":"contains","value":"clawflows.com"}]'
166+
npx --yes @agent-analytics/cli@0.5.24 query my-site --filter '[{"field":"properties.referrer","op":"contains","value":"clawflows.com"}]'
161167
```
162168

163169
Invalid filter fields now fail loudly and return property discovery guidance instead of being silently ignored.
164170

171+
Identity lookup with `--email` sends the normalized email to Agent Analytics over HTTPS for server-side project-scoped HMAC matching. The CLI no longer computes or sends a local `email_hash`; raw email is not stored in event rows or profile traits.
172+
165173
Store compact project context when the product has custom goals, activation events, event meanings, or date annotations that should travel with analytics results. Keep this short because project-scoped analytics endpoints include it as `project_context`. `context set` accepts an encoded JSON body up to 512KB.
166174

167175
Use annotations for major product changes that could explain later graph movement: landing page, pricing, onboarding, feature, release, or experiment changes. Do not store git commit logs, noisy edits, temporary metric notes, PII, secrets, or long release notes. Direct `context get` returns all annotations; project-scoped analytics responses include annotations only for the requested analytics date range plus one day before and after.
168176

169177
Before setting or refreshing the glossary, inspect the project's current event names:
170178

171179
```bash
172-
npx --yes @agent-analytics/cli@0.5.20 properties my-site
173-
npx --yes @agent-analytics/cli@0.5.20 properties-received my-site
174-
npx --yes @agent-analytics/cli@0.5.20 context set my-site --json '{
180+
npx --yes @agent-analytics/cli@0.5.24 properties my-site
181+
npx --yes @agent-analytics/cli@0.5.24 properties-received my-site
182+
npx --yes @agent-analytics/cli@0.5.24 context set my-site --json '{
175183
"goals": ["Increase activated Agent Analytics accounts"],
176184
"activation_events": ["signup_completed", "project_created", "first_event_received"],
177185
"glossary": [
@@ -196,7 +204,7 @@ npx --yes @agent-analytics/cli@0.5.20 context set my-site --json '{
196204
Use the CLI feedback command when Agent Analytics was confusing, a task took too long, or the agent had to do manual analysis that the product should have handled:
197205

198206
```bash
199-
npx --yes @agent-analytics/cli@0.5.20 feedback \
207+
npx --yes @agent-analytics/cli@0.5.24 feedback \
200208
--message "The agent had to calculate the funnel drop-off manually" \
201209
--project my-site \
202210
--command "agent-analytics funnel my-site --steps page_view,signup,purchase" \
@@ -213,24 +221,24 @@ Claude Code, OpenClaw, Cursor, Codex — any AI agent that can run `npx`. Or add
213221
claude mcp add agent-analytics --transport http https://mcp.agentanalytics.sh/mcp
214222
```
215223

216-
For managed, issue-based, or remote runtimes that cannot receive a localhost callback or keep a long-running process alive, use `npx --yes @agent-analytics/cli@0.5.20 login --detached`. It prints the approval URL and exits. After browser approval, resume with the printed `login --auth-request <id> --exchange-code <code>` command.
224+
For managed, issue-based, or remote runtimes that cannot receive a localhost callback or keep a long-running process alive, use `npx --yes @agent-analytics/cli@0.5.24 login --detached`. It prints the approval URL and exits. After browser approval, resume with the printed `login --auth-request <id> --exchange-code <code>` command.
217225

218226
For managed runtimes where the default home config path may not persist, point auth storage at a persistent runtime/workspace directory:
219227

220228
```bash
221229
export AGENT_ANALYTICS_CONFIG_DIR="$PWD/.openclaw/agent-analytics"
222-
npx --yes @agent-analytics/cli@0.5.20 login --detached
223-
npx --yes @agent-analytics/cli@0.5.20 auth status
230+
npx --yes @agent-analytics/cli@0.5.24 login --detached
231+
npx --yes @agent-analytics/cli@0.5.24 auth status
224232
```
225233

226234
For one-off commands, use `--config-dir "$PWD/.openclaw/agent-analytics"` before or after the command. The CLI stores the same `config.json` file in that directory and does not migrate credentials from the default path.
227235

228-
For a local shell where it is useful to keep waiting, use `npx --yes @agent-analytics/cli@0.5.20 login --detached --wait`.
236+
For a local shell where it is useful to keep waiting, use `npx --yes @agent-analytics/cli@0.5.24 login --detached --wait`.
229237

230238
If your saved session predates CLI `0.5.9`, run a fresh login before calling `projects`. Older saved agent-session tokens were minted without `projects:read`, so they will keep failing until you re-authenticate. Verify with:
231239

232240
```bash
233-
npx --yes @agent-analytics/cli@0.5.20 projects
241+
npx --yes @agent-analytics/cli@0.5.24 projects
234242
```
235243

236244
## Agent Skill

bin/cli.mjs

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
* npx @agent-analytics/cli context set <project> --json '{...}' — Set goals, activation events, glossary
3535
* npx @agent-analytics/cli portfolio-context get — Get stored account portfolio context
3636
* npx @agent-analytics/cli portfolio-context set --json '{...}' — Set goals, surface roles, milestones, glossary
37+
* npx @agent-analytics/cli portfolios list — List identity lookup portfolios
38+
* npx @agent-analytics/cli portfolios create <slug> --name "Portfolio" --projects app,docs [--move]
3739
* npx @agent-analytics/cli update <name-or-id> — Update a project
3840
* npx @agent-analytics/cli delete <name-or-id> — Delete a project
3941
* npx @agent-analytics/cli live [name] — Real-time live view
@@ -52,7 +54,6 @@
5254

5355
import { AgentAnalyticsAPI } from '../lib/api.mjs';
5456
import { finishManualExchange, loginDetached, loginInteractive, startDetachedLogin } from '../lib/auth-flow.mjs';
55-
import { createHash } from 'node:crypto';
5657
import { readFileSync } from 'node:fs';
5758
import {
5859
clearStoredAuth,
@@ -90,16 +91,10 @@ function normalizeEmail(email) {
9091
return String(email || '').trim().toLowerCase();
9192
}
9293

93-
function hashEmail(email) {
94-
const normalized = normalizeEmail(email);
95-
if (!normalized) return null;
96-
return createHash('sha256').update(normalized).digest('hex');
97-
}
98-
9994
function identityOptions(opts = {}) {
10095
const out = {};
10196
if (opts.user_id) out.user_id = opts.user_id;
102-
if (opts.email) out.email_hash = hashEmail(opts.email);
97+
if (opts.email) out.email = normalizeEmail(opts.email);
10398
return out;
10499
}
105100

@@ -1288,11 +1283,11 @@ const cmdQuery = withApi(async (api, project, opts = {}) => {
12881283
--order-by event_count, unique_users, session_count, date, event
12891284
--order asc or desc
12901285
--limit Max rows (default 100, max 1000)
1291-
--email Filter by local-only normalized SHA-256 email hash lookup
1286+
--email Filter by server-side scoped HMAC email lookup
12921287
12931288
Property filters must use the canonical properties.<key> form.
12941289
Example: properties.referrer, properties.utm_source, properties.first_utm_source
1295-
Email lookup uses normalized SHA-256, not a keyed HMAC. It keeps raw email out of API requests but hashes may be guessable for known emails.
1290+
Email lookup sends the email to Agent Analytics over HTTPS and matches with a project-scoped HMAC index. Raw email is not stored in event rows or profile traits.
12961291
Invalid filter fields now fail loudly instead of being ignored.
12971292
12981293
${BOLD}Examples:${RESET}
@@ -1515,6 +1510,75 @@ const cmdPortfolioContext = withApi(async (subcommandApi, subcommand, opts = {})
15151510
logPortfolioContext(data);
15161511
});
15171512

1513+
function parseProjectList(value) {
1514+
if (!value) return [];
1515+
return String(value).split(',').map((item) => item.trim()).filter(Boolean);
1516+
}
1517+
1518+
function logPortfolio(data) {
1519+
const portfolio = data?.portfolio || data;
1520+
if (!portfolio) {
1521+
log('No portfolio returned.');
1522+
return;
1523+
}
1524+
heading(`${portfolio.name || portfolio.slug} (${portfolio.slug || portfolio.id})`);
1525+
log(`${DIM}id:${RESET} ${portfolio.id}`);
1526+
const members = portfolio.members || [];
1527+
if (members.length === 0) {
1528+
log(` ${DIM}No member projects.${RESET}`);
1529+
return;
1530+
}
1531+
for (const member of members) {
1532+
log(` • ${member.project || member.project_id} ${DIM}${member.project_id}${RESET}`);
1533+
}
1534+
}
1535+
1536+
const cmdPortfolios = withApi(async (api, subcommand = 'list', target, opts = {}) => {
1537+
if (!['list', 'create', 'get', 'update', 'delete'].includes(subcommand)) {
1538+
error('Usage: npx @agent-analytics/cli portfolios <list|create|get|update|delete> [slug] [--name name] [--projects a,b] [--move]');
1539+
}
1540+
1541+
if (subcommand === 'list') {
1542+
const data = await api.listPortfolios();
1543+
const portfolios = data.portfolios || [];
1544+
if (portfolios.length === 0) {
1545+
log('No identity portfolios yet.');
1546+
return;
1547+
}
1548+
portfolios.forEach((portfolio) => log(`${portfolio.slug}\t${portfolio.name}\t${portfolio.id}`));
1549+
return;
1550+
}
1551+
1552+
if (subcommand === 'create') {
1553+
const slug = target || opts.slug;
1554+
if (!slug) error('Usage: npx @agent-analytics/cli portfolios create <slug> --name "Name" [--projects a,b]');
1555+
const data = await api.createPortfolio({ slug, name: opts.name || slug, projects: parseProjectList(opts.projects), allow_move: Boolean(opts.move) });
1556+
success(`Portfolio ${data.portfolio?.slug || slug} created`);
1557+
logPortfolio(data);
1558+
return;
1559+
}
1560+
1561+
if (!target) error(`Usage: npx @agent-analytics/cli portfolios ${subcommand} <slug-or-id>`);
1562+
1563+
if (subcommand === 'get') {
1564+
logPortfolio(await api.getPortfolio(target));
1565+
return;
1566+
}
1567+
1568+
if (subcommand === 'update') {
1569+
if (!opts.name && !opts.projects) error('Provide --name and/or --projects to update');
1570+
const data = await api.updatePortfolio(target, { name: opts.name, projects: opts.projects ? parseProjectList(opts.projects) : undefined, allow_move: Boolean(opts.move) });
1571+
success(`Portfolio ${data.portfolio?.slug || target} updated`);
1572+
logPortfolio(data);
1573+
return;
1574+
}
1575+
1576+
if (subcommand === 'delete') {
1577+
await api.deletePortfolio(target);
1578+
success(`Portfolio ${target} deleted`);
1579+
}
1580+
});
1581+
15181582
const cmdUpdate = withApi(async (api, target, opts = {}) => {
15191583
if (!target) error('Usage: npx @agent-analytics/cli update <project-name-or-id> [--name new-name] [--origins "https://example.com"]');
15201584
if (!opts.name && !opts.allowed_origins) error('Provide --name and/or --origins to update');
@@ -1938,6 +2002,11 @@ ${BOLD}ANALYTICS${RESET}
19382002
${CYAN}context set${RESET} <name> Set compact project context with --json
19392003
${CYAN}portfolio-context get${RESET} Read stored account portfolio context
19402004
${CYAN}portfolio-context set${RESET} Set compact portfolio context with --json
2005+
${CYAN}portfolios list${RESET} List identity lookup portfolios
2006+
${CYAN}portfolios create${RESET} <slug> Create a portfolio with --name and --projects
2007+
${CYAN}portfolios get${RESET} <slug> Show a portfolio and member projects
2008+
${CYAN}portfolios update${RESET} <slug> Update name/projects with optional --move
2009+
${CYAN}portfolios delete${RESET} <slug> Delete a portfolio
19412010
19422011
${BOLD}EXPERIMENTS${RESET} ${DIM}— A/B testing your agent can actually use${RESET}
19432012
${CYAN}experiments list${RESET} <project> List experiments
@@ -1969,8 +2038,8 @@ ${BOLD}KEY OPTIONS${RESET}
19692038
--property <key> Property to break down (path, referrer, utm_source, country)
19702039
--event <name> Filter by event name
19712040
--user-id <id> Filter events or journeys to one known user id
1972-
--email <email> Filter events, journeys, or query by local-only email hash lookup
1973-
Uses normalized SHA-256, not a keyed HMAC; hashes may be guessable for known emails
2041+
--email <email> Filter events, journeys, or query by server-side scoped HMAC email lookup
2042+
Raw email is sent over HTTPS for lookup and is not stored in event rows or profile traits
19742043
--message <text> Feedback message for the product team
19752044
--filter <json> Filters for query (e.g. '[{"field":"country","op":"eq","value":"US"}]')
19762045
--interval <N> Live view refresh in seconds (default: 5)
@@ -2056,6 +2125,9 @@ function isDemoMutation(commandName, commandArgs) {
20562125
if (commandName === 'context') {
20572126
return commandArgs[1] === 'set';
20582127
}
2128+
if (commandName === 'portfolios') {
2129+
return ['create', 'update', 'delete'].includes(commandArgs[1]);
2130+
}
20592131
if (commandName === 'experiments') {
20602132
return ['create', 'pause', 'resume', 'complete', 'delete'].includes(commandArgs[1]);
20612133
}
@@ -2195,6 +2267,14 @@ try {
21952267
json: getArg('--json'),
21962268
});
21972269
break;
2270+
case 'portfolios':
2271+
await cmdPortfolios(args[1] || 'list', args[2], {
2272+
slug: getArg('--slug'),
2273+
name: getArg('--name'),
2274+
projects: getArg('--projects'),
2275+
move: args.includes('--move'),
2276+
});
2277+
break;
21982278
case 'update':
21992279
await cmdUpdate(args[1], {
22002280
name: getArg('--name'),

0 commit comments

Comments
 (0)