sella-cli 0.6.5 → 0.7.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 +14 -4
- package/dist/clients.js +79 -0
- package/dist/index.js +24 -4
- package/dist/telemetry-send.js +18 -0
- package/dist/telemetry.js +153 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ npx sella-cli
|
|
|
33
33
|
|
|
34
34
|
That one command is the whole setup. The wizard:
|
|
35
35
|
|
|
36
|
-
1. **Detects your agent clients**: Claude Code, Claude Desktop, Cursor, Windsurf, VS Code, Cline.
|
|
36
|
+
1. **Detects your agent clients**: Claude Code, Claude Desktop, Cursor, Windsurf, VS Code, Cline, Codex, Antigravity, OpenClaw.
|
|
37
37
|
2. **Installs the Sella MCP server** into each one. Writes are idempotent and your other MCP servers are preserved.
|
|
38
38
|
3. **Pairs the machine**: paste a setup code from the dashboard, or use your email and a 6-digit code.
|
|
39
39
|
4. **Proves it works** with a live `sella doctor` run at the end.
|
|
@@ -77,7 +77,8 @@ What's in there: a catalogue of 1,100+ machine-payable API providers, plus first
|
|
|
77
77
|
|
|
78
78
|
- **One-command onboarding**, so you never hand-edit an MCP config again.
|
|
79
79
|
- **Pay-per-call in USDC**: fractions of a cent for most calls, multi-chain, with budgets you set and keys you can revoke.
|
|
80
|
-
- **
|
|
80
|
+
- **One search across everything**: datasets, APIs, workflows, and Sella Native products are all discoverable in a single MCP call, ranked by computed quality, so your agent buys mid-task instead of stalling.
|
|
81
|
+
- **Policy-aware buying**: your agent can preview a quote and check it against your budget before paying, and every purchase leaves a decision receipt it can explain.
|
|
81
82
|
- **Publishing from a CSV**, so you can sell your own data without opening a browser.
|
|
82
83
|
- **Agent-grade output**: every command supports `--json`, `--yes`, and env vars, with meaningful exit codes. Your CI can run it. Your agent can run it.
|
|
83
84
|
|
|
@@ -118,7 +119,7 @@ Then ask your agent:
|
|
|
118
119
|
|
|
119
120
|
> Search Sella for a web search API and tell me what it costs per call.
|
|
120
121
|
|
|
121
|
-
The agent answers using
|
|
122
|
+
The agent answers using Sella's MCP tools, starting with `search_catalog` (one search across datasets, APIs, workflows, and Sella Native products) and `get_listing` for the details, with the key this CLI stored for it. When you fund the agent wallet, it can preview the price with `purchase_preview` and buy what it found.
|
|
122
123
|
|
|
123
124
|
### Real-world: publish a CSV and get paid in USDC
|
|
124
125
|
|
|
@@ -177,6 +178,14 @@ SELLA_SETUP_CODE=SELLA-XXXX-XXXX-XXXX-XXXX npx sella-cli init --yes --json
|
|
|
177
178
|
- You set the budget. You can revoke a key from the dashboard at any time.
|
|
178
179
|
- Never paste API keys into URLs or shell history.
|
|
179
180
|
|
|
181
|
+
## Telemetry (opt-in, off by default)
|
|
182
|
+
|
|
183
|
+
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`.
|
|
184
|
+
|
|
185
|
+
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.
|
|
186
|
+
|
|
187
|
+
Opt out anytime with `SELLA_TELEMETRY=0` or by deleting `~/.sella/telemetry.json`.
|
|
188
|
+
|
|
180
189
|
## FAQ
|
|
181
190
|
|
|
182
191
|
**Is this crypto hype?**
|
|
@@ -201,10 +210,11 @@ Sella has two other ways in: your agent can onboard itself over MCP with your em
|
|
|
201
210
|
| Agent task ideas | https://sellag.vercel.app/rfi |
|
|
202
211
|
| npm | https://www.npmjs.com/package/sella-cli |
|
|
203
212
|
| Issues | https://github.com/010100100100011101010100/ogsella/issues |
|
|
213
|
+
| Email | rasesh@buildifi.ai |
|
|
204
214
|
|
|
205
215
|
## Found a bug?
|
|
206
216
|
|
|
207
|
-
Found a bug? Have an idea? [Open an issue](https://github.com/010100100100011101010100/ogsella/issues). We read every one.
|
|
217
|
+
Found a bug? Have an idea? [Open an issue](https://github.com/010100100100011101010100/ogsella/issues) or [email us](mailto:rasesh@buildifi.ai). We read every one.
|
|
208
218
|
|
|
209
219
|
## License
|
|
210
220
|
|
package/dist/clients.js
CHANGED
|
@@ -27,6 +27,27 @@ function stdioBridgeEntry(mcpUrl, apiKey) {
|
|
|
27
27
|
args: ['-y', 'mcp-remote', mcpUrl, ...(apiKey ? ['--header', `Authorization: Bearer ${apiKey}`] : [])],
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Codex keeps MCP servers in ~/.codex/config.toml. We do a targeted section upsert instead of a
|
|
32
|
+
* TOML parse/serialize round-trip so user comments and formatting survive. The `url` key selects
|
|
33
|
+
* streamable HTTP; `http_headers` carries static auth headers (both per the Codex config docs).
|
|
34
|
+
* Only the standard `[mcp_servers.sella]` table form is recognized; exotic spellings (dotted keys,
|
|
35
|
+
* inline tables) are left alone and would shadow ours — the backup covers that case.
|
|
36
|
+
*/
|
|
37
|
+
export function upsertCodexToml(raw, mcpUrl, apiKey) {
|
|
38
|
+
const lines = [`[mcp_servers.sella]`, `url = "${mcpUrl}"`];
|
|
39
|
+
if (apiKey)
|
|
40
|
+
lines.push(`http_headers = { "Authorization" = "Bearer ${apiKey}" }`);
|
|
41
|
+
const section = lines.join('\n');
|
|
42
|
+
// Our table (plus any of its sub-tables) up to the next unrelated header or end of file.
|
|
43
|
+
const existing = /(^|\n)\[mcp_servers\.sella\][\s\S]*?(?=\n\[(?!mcp_servers\.sella[.\]])|$)/;
|
|
44
|
+
if (existing.test(raw)) {
|
|
45
|
+
const next = raw.replace(existing, (_match, lead) => `${lead}${section}`);
|
|
46
|
+
return next.endsWith('\n') ? next : `${next}\n`;
|
|
47
|
+
}
|
|
48
|
+
const head = raw.trimEnd();
|
|
49
|
+
return `${head ? `${head}\n\n` : ''}${section}\n`;
|
|
50
|
+
}
|
|
30
51
|
const CLIENTS = [
|
|
31
52
|
{
|
|
32
53
|
id: 'claude-code',
|
|
@@ -91,6 +112,43 @@ const CLIENTS = [
|
|
|
91
112
|
doc.mcpServers.sella = stdioBridgeEntry(mcpUrl, apiKey);
|
|
92
113
|
},
|
|
93
114
|
},
|
|
115
|
+
{
|
|
116
|
+
id: 'codex',
|
|
117
|
+
name: 'Codex',
|
|
118
|
+
marker: (env) => path.join(env.home, '.codex'),
|
|
119
|
+
configPath: (env) => path.join(env.home, '.codex', 'config.toml'),
|
|
120
|
+
applyRaw: upsertCodexToml,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
// Antigravity IDE + CLI share one MCP config at ~/.gemini/config/mcp_config.json.
|
|
124
|
+
// Remote servers use `serverUrl` (`httpUrl` is deprecated).
|
|
125
|
+
id: 'antigravity',
|
|
126
|
+
name: 'Antigravity',
|
|
127
|
+
marker: (env) => appSupport(env, 'Antigravity'),
|
|
128
|
+
configPath: (env) => path.join(env.home, '.gemini', 'config', 'mcp_config.json'),
|
|
129
|
+
apply: (doc, mcpUrl, apiKey) => {
|
|
130
|
+
doc.mcpServers = doc.mcpServers || {};
|
|
131
|
+
const entry = { serverUrl: mcpUrl };
|
|
132
|
+
if (apiKey)
|
|
133
|
+
entry.headers = { Authorization: `Bearer ${apiKey}` };
|
|
134
|
+
doc.mcpServers.sella = entry;
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
// OpenClaw stores MCP servers under mcp.servers in ~/.openclaw/openclaw.json.
|
|
139
|
+
id: 'openclaw',
|
|
140
|
+
name: 'OpenClaw',
|
|
141
|
+
marker: (env) => path.join(env.home, '.openclaw'),
|
|
142
|
+
configPath: (env) => path.join(env.home, '.openclaw', 'openclaw.json'),
|
|
143
|
+
apply: (doc, mcpUrl, apiKey) => {
|
|
144
|
+
doc.mcp = doc.mcp || {};
|
|
145
|
+
doc.mcp.servers = doc.mcp.servers || {};
|
|
146
|
+
const entry = { url: mcpUrl, transport: 'streamable-http' };
|
|
147
|
+
if (apiKey)
|
|
148
|
+
entry.headers = { Authorization: `Bearer ${apiKey}` };
|
|
149
|
+
doc.mcp.servers.sella = entry;
|
|
150
|
+
},
|
|
151
|
+
},
|
|
94
152
|
];
|
|
95
153
|
export function detectClients(env = defaultEnv()) {
|
|
96
154
|
return CLIENTS.map((spec) => ({
|
|
@@ -108,6 +166,25 @@ export function installSellaIntoClient(id, opts) {
|
|
|
108
166
|
throw new Error(`Unknown client: ${id}`);
|
|
109
167
|
const configPath = spec.configPath(env);
|
|
110
168
|
const exists = fs.existsSync(configPath);
|
|
169
|
+
// Raw-text configs (Codex's TOML): targeted rewrite, byte-compare for idempotency.
|
|
170
|
+
if (spec.applyRaw) {
|
|
171
|
+
const raw = exists ? fs.readFileSync(configPath, 'utf8') : '';
|
|
172
|
+
const next = spec.applyRaw(raw, opts.mcpUrl, opts.apiKey);
|
|
173
|
+
if (exists && raw === next) {
|
|
174
|
+
return { id, configPath, action: 'unchanged' };
|
|
175
|
+
}
|
|
176
|
+
let backupPath;
|
|
177
|
+
if (!opts.dryRun) {
|
|
178
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
179
|
+
if (exists && raw.trim()) {
|
|
180
|
+
backupPath = `${configPath}.sella-backup`;
|
|
181
|
+
if (!fs.existsSync(backupPath))
|
|
182
|
+
fs.writeFileSync(backupPath, raw);
|
|
183
|
+
}
|
|
184
|
+
fs.writeFileSync(configPath, next);
|
|
185
|
+
}
|
|
186
|
+
return { id, configPath, action: exists ? 'updated' : 'created', backupPath };
|
|
187
|
+
}
|
|
111
188
|
let doc = {};
|
|
112
189
|
if (exists) {
|
|
113
190
|
const raw = fs.readFileSync(configPath, 'utf8').trim();
|
|
@@ -121,6 +198,8 @@ export function installSellaIntoClient(id, opts) {
|
|
|
121
198
|
}
|
|
122
199
|
}
|
|
123
200
|
const before = JSON.stringify(doc);
|
|
201
|
+
if (!spec.apply)
|
|
202
|
+
throw new Error(`Client ${id} has no config writer`);
|
|
124
203
|
spec.apply(doc, opts.mcpUrl, opts.apiKey);
|
|
125
204
|
const after = JSON.stringify(doc, null, 2);
|
|
126
205
|
if (exists && JSON.stringify(JSON.parse(before)) === JSON.stringify(JSON.parse(after))) {
|
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.
|
|
16
|
+
const VERSION = '0.7.0';
|
|
16
17
|
function parseFlags(argv) {
|
|
17
18
|
const flags = {
|
|
18
19
|
json: false, yes: false, noColor: false, dryRun: false, noKeychain: false,
|
|
@@ -69,7 +70,7 @@ Options:
|
|
|
69
70
|
--setup-code <code> Pair with a dashboard setup code (also: SELLA_SETUP_CODE env)
|
|
70
71
|
--email <address> Pair with email + OTP (interactive)
|
|
71
72
|
--card <path> Dataset card for 'publish push' (default ./sella-dataset.json)
|
|
72
|
-
--client <ids> Comma-separated subset (claude-code,claude-desktop,cursor,windsurf,vscode,cline)
|
|
73
|
+
--client <ids> Comma-separated subset (claude-code,claude-desktop,cursor,windsurf,vscode,cline,codex,antigravity,openclaw)
|
|
73
74
|
--no-keychain Skip the OS keychain; store the key in a 0600 file (also: SELLA_NO_KEYCHAIN)
|
|
74
75
|
--dry-run Show what would change without writing
|
|
75
76
|
--json Machine-readable output (one JSON document on stdout)
|
|
@@ -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
|
-
|
|
560
|
-
|
|
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.
|
|
3
|
+
"version": "0.7.0",
|
|
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",
|