sella-cli 0.6.5 → 0.6.6

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
@@ -177,6 +177,14 @@ SELLA_SETUP_CODE=SELLA-XXXX-XXXX-XXXX-XXXX npx sella-cli init --yes --json
177
177
  - You set the budget. You can revoke a key from the dashboard at any time.
178
178
  - Never paste API keys into URLs or shell history.
179
179
 
180
+ ## Telemetry (opt-in, off by default)
181
+
182
+ The CLI can count which commands run and whether they exit cleanly, so we know what to fix first. Nothing is sent unless you say yes to the one-time question at the end of `sella init`, or you set `SELLA_TELEMETRY=1`.
183
+
184
+ When enabled, each command sends one event: the command name (like `doctor`), the exit code, the CLI version, the OS name (like `darwin`), and a random install id. Never sent: search queries, emails, API keys, wallet addresses, file paths, or anything else you typed. Telemetry is always skipped in `--json`, `--yes`, and CI runs, and the send happens in a detached process, so it never delays a command.
185
+
186
+ Opt out anytime with `SELLA_TELEMETRY=0` or by deleting `~/.sella/telemetry.json`.
187
+
180
188
  ## FAQ
181
189
 
182
190
  **Is this crypto hype?**
@@ -204,7 +212,7 @@ Sella has two other ways in: your agent can onboard itself over MCP with your em
204
212
 
205
213
  ## Found a bug?
206
214
 
207
- Found a bug? Have an idea? [Open an issue](https://github.com/010100100100011101010100/ogsella/issues). We read every one.
215
+ Found a bug? Have an idea? [Mail us](rasesh@buildifi.ai). We read every one.
208
216
 
209
217
  ## License
210
218
 
package/dist/index.js CHANGED
@@ -11,8 +11,9 @@ import { getFundingInfo, annotateFunding } from './fund.js';
11
11
  import { capabilityLabel } from './chains.js';
12
12
  import { defaultIo, Printer } from './output.js';
13
13
  import { Ui } from './ui.js';
14
+ import { recordCliEvent, saveTelemetryDecision, shouldPromptTelemetry } from './telemetry.js';
14
15
  const DEFAULT_MCP_URL = 'https://sellag.vercel.app/api/mcp';
15
- const VERSION = '0.6.5';
16
+ const VERSION = '0.6.6';
16
17
  function parseFlags(argv) {
17
18
  const flags = {
18
19
  json: false, yes: false, noColor: false, dryRun: false, noKeychain: false,
@@ -310,6 +311,16 @@ async function cmdInit(ctx, flags, printer) {
310
311
  { tag: 'free', text: `Test Sella without spending — ${rfi}?uc=zero-dollar-demo`, tone: 'dim' },
311
312
  ]);
312
313
  ui.detail(`Browse all ideas: ${rfi}`);
314
+ // One-time telemetry choice: only a human at a TTY is ever asked, and only once.
315
+ if (!flags.dryRun && shouldPromptTelemetry({ env: ctx.env, ioEnv: ctx.io.env, interactive: ui.interactive })) {
316
+ ui.bar();
317
+ const share = await ui.select('Help improve this CLI with anonymous usage stats?', [
318
+ { value: 'share', label: 'Share stats', hint: 'command name and pass or fail only, never your data or keys' },
319
+ { value: 'off', label: 'Keep it off', hint: 'nothing is sent' },
320
+ ]);
321
+ saveTelemetryDecision(ctx.env, share === 'share');
322
+ ui.detail(share === 'share' ? 'Thanks. Opt out anytime: SELLA_TELEMETRY=0' : 'Nothing will be sent. Opt in later: SELLA_TELEMETRY=1');
323
+ }
313
324
  const took = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
314
325
  ui.outro(flags.dryRun ? 'Dry run complete — nothing was written.' : `Done in ${took}s — your agents can now buy on Sella.`);
315
326
  }
@@ -556,8 +567,17 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
556
567
  // Bin entry (skipped when imported by tests).
557
568
  const isMain = process.argv[1]?.endsWith('index.js') || process.argv[1]?.endsWith('sella');
558
569
  if (isMain) {
559
- runCli(process.argv.slice(2))
560
- .then((code) => process.exit(code))
570
+ const argv = process.argv.slice(2);
571
+ runCli(argv)
572
+ .then((code) => {
573
+ try {
574
+ recordCliEvent({ argv, exitCode: code, version: VERSION, env: defaultEnv(), ioEnv: process.env });
575
+ }
576
+ catch {
577
+ // Telemetry must never affect the exit path.
578
+ }
579
+ process.exit(code);
580
+ })
561
581
  .catch((err) => {
562
582
  console.error(err instanceof Error ? err.message : String(err));
563
583
  process.exit(1);
@@ -0,0 +1,18 @@
1
+ // Detached one-shot telemetry sender: the CLI spawns this and exits immediately, so a slow or
2
+ // dead network can never delay a command. argv carries no secrets (see telemetry.ts payload).
3
+ const [, , url, body] = process.argv;
4
+ if (url && body) {
5
+ try {
6
+ await fetch(url, {
7
+ method: 'POST',
8
+ headers: { 'content-type': 'application/json' },
9
+ body,
10
+ signal: AbortSignal.timeout(4000),
11
+ });
12
+ }
13
+ catch {
14
+ // Best-effort by contract: there is nobody to report to.
15
+ }
16
+ }
17
+ process.exit(0);
18
+ export {};
@@ -0,0 +1,153 @@
1
+ import { spawn } from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { randomUUID } from 'node:crypto';
6
+ /** Mirrored by CLI_TELEMETRY_COMMANDS in lib/telemetry/cli-event.ts (server side). */
7
+ export const TELEMETRY_COMMANDS = [
8
+ 'none',
9
+ 'init',
10
+ 'pair',
11
+ 'sandbox',
12
+ 'clients',
13
+ 'doctor',
14
+ 'status',
15
+ 'fund',
16
+ 'mcp',
17
+ 'publish',
18
+ 'publish:init',
19
+ 'publish:push',
20
+ 'version',
21
+ 'help',
22
+ 'unknown',
23
+ ];
24
+ const DEFAULT_ORIGIN = 'https://sellag.vercel.app';
25
+ /** Flags whose next token is a value, not a command (mirrors parseFlags in index.ts). */
26
+ const VALUE_FLAGS = new Set(['--client', '--clients', '--setup-code', '--email', '--card']);
27
+ export function telemetryPath(env) {
28
+ return path.join(env.home, '.sella', 'telemetry.json');
29
+ }
30
+ export function loadTelemetryDecision(env) {
31
+ try {
32
+ const raw = JSON.parse(fs.readFileSync(telemetryPath(env), 'utf8'));
33
+ if (typeof raw.enabled !== 'boolean')
34
+ return null;
35
+ return {
36
+ enabled: raw.enabled,
37
+ installId: typeof raw.installId === 'string' && raw.installId ? raw.installId : 'unset',
38
+ decidedAt: typeof raw.decidedAt === 'string' ? raw.decidedAt : '',
39
+ };
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ export function saveTelemetryDecision(env, enabled) {
46
+ const file = telemetryPath(env);
47
+ const existing = loadTelemetryDecision(env);
48
+ const decision = {
49
+ enabled,
50
+ installId: existing?.installId && existing.installId !== 'unset' ? existing.installId : randomUUID(),
51
+ decidedAt: new Date().toISOString(),
52
+ };
53
+ fs.mkdirSync(path.dirname(file), { recursive: true });
54
+ fs.writeFileSync(file, `${JSON.stringify(decision, null, 2)}\n`);
55
+ return { ...decision, path: file };
56
+ }
57
+ /** SELLA_TELEMETRY=1 forces on, =0 forces off, anything else defers to the stored decision. */
58
+ export function telemetryEnvOverride(ioEnv) {
59
+ const v = (ioEnv.SELLA_TELEMETRY || '').trim();
60
+ if (v === '1')
61
+ return true;
62
+ if (v === '0')
63
+ return false;
64
+ return null;
65
+ }
66
+ export function isCiRun(ioEnv) {
67
+ const v = (ioEnv.CI || '').trim().toLowerCase();
68
+ return v !== '' && v !== '0' && v !== 'false';
69
+ }
70
+ /** The single yes/no the sender consults. Defaults to off in every ambiguous case. */
71
+ export function telemetryEnabled(env, ioEnv) {
72
+ if (isCiRun(ioEnv))
73
+ return false;
74
+ const override = telemetryEnvOverride(ioEnv);
75
+ if (override !== null)
76
+ return override;
77
+ return loadTelemetryDecision(env)?.enabled === true;
78
+ }
79
+ /** Ask only when a human is present, nothing decided yet, and no env var already speaks. */
80
+ export function shouldPromptTelemetry(opts) {
81
+ if (!opts.interactive)
82
+ return false;
83
+ if (isCiRun(opts.ioEnv))
84
+ return false;
85
+ if (telemetryEnvOverride(opts.ioEnv) !== null)
86
+ return false;
87
+ return loadTelemetryDecision(opts.env) === null;
88
+ }
89
+ /** Collapse argv onto the fixed command vocabulary; free text can never pass through. */
90
+ export function commandFromArgv(argv) {
91
+ if (argv.includes('--version') || argv.includes('-v'))
92
+ return 'version';
93
+ if (argv.includes('--help') || argv.includes('-h'))
94
+ return 'help';
95
+ const positional = [];
96
+ for (let i = 0; i < argv.length; i += 1) {
97
+ const arg = argv[i];
98
+ if (VALUE_FLAGS.has(arg)) {
99
+ i += 1;
100
+ continue;
101
+ }
102
+ if (arg.startsWith('-'))
103
+ continue;
104
+ positional.push(arg);
105
+ }
106
+ const command = positional[0];
107
+ if (!command)
108
+ return 'none';
109
+ if (command === 'publish') {
110
+ const sub = positional[1];
111
+ if (sub === 'init')
112
+ return 'publish:init';
113
+ if (sub === 'push')
114
+ return 'publish:push';
115
+ return 'publish';
116
+ }
117
+ return TELEMETRY_COMMANDS.includes(command) && command !== 'unknown' && command !== 'none'
118
+ ? command
119
+ : 'unknown';
120
+ }
121
+ /**
122
+ * Fire one event for a finished command, if and only if the user opted in. Returns whether a
123
+ * send was attempted (for tests). Never throws; never delays the caller: the POST happens in a
124
+ * detached child that outlives this process.
125
+ */
126
+ export function recordCliEvent(opts) {
127
+ try {
128
+ // Agent/automation modes carry no telemetry at all, even for opted-in installs.
129
+ if (opts.argv.includes('--json') || opts.argv.includes('--yes') || opts.argv.includes('-y'))
130
+ return false;
131
+ if (!telemetryEnabled(opts.env, opts.ioEnv))
132
+ return false;
133
+ const payload = {
134
+ installId: loadTelemetryDecision(opts.env)?.installId || 'unset',
135
+ command: commandFromArgv(opts.argv),
136
+ exitCode: Number.isInteger(opts.exitCode) ? opts.exitCode : 1,
137
+ version: opts.version,
138
+ platform: process.platform,
139
+ };
140
+ const origin = (opts.ioEnv.SELLA_MCP_URL || `${DEFAULT_ORIGIN}/api/mcp`).replace(/\/api\/mcp\/?$/, '') || DEFAULT_ORIGIN;
141
+ const sender = opts.senderPath || fileURLToPath(new URL('./telemetry-send.js', import.meta.url));
142
+ const spawnFn = opts.spawnFn || spawn;
143
+ const child = spawnFn(process.execPath, [sender, `${origin}/api/telemetry/cli`, JSON.stringify(payload)], {
144
+ detached: true,
145
+ stdio: 'ignore',
146
+ });
147
+ child.unref();
148
+ return true;
149
+ }
150
+ catch {
151
+ return false;
152
+ }
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sella-cli",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "description": "Connect your AI agent to Sella, the marketplace where agents buy data and APIs, in one command: npx sella-cli. Installs the Sella MCP server into Claude Code, Cursor and more, then pairs, verifies, funds, and publishes.",
5
5
  "keywords": [
6
6
  "cli",