sella-cli 0.8.0 → 0.9.1
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 +2 -1
- package/dist/chains.js +10 -1
- package/dist/clients.js +1 -1
- package/dist/credentials.js +2 -2
- package/dist/doctor.js +7 -7
- package/dist/index.js +109 -27
- package/dist/mcp-bridge.js +1 -1
- package/dist/publish.js +2 -2
- package/dist/skill.js +169 -0
- package/dist/ui.js +2 -2
- package/package.json +19 -6
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ npx sella-cli sandbox "web search"
|
|
|
71
71
|
<img src="https://sellag.vercel.app/readme/cli-sandbox.svg" alt="sella sandbox returns live marketplace results: Exa, Tavily and more, with no account" width="740" />
|
|
72
72
|
</p>
|
|
73
73
|
|
|
74
|
-
What's in there: a catalogue of 1,
|
|
74
|
+
What's in there: a catalogue of 1,500+ machine-payable API providers with 8,000+ verified callable endpoints, plus first-party datasets, workflows, and Sella Native products such as Cradle. Free previews let your agent check quality before it pays.
|
|
75
75
|
|
|
76
76
|
## What you get
|
|
77
77
|
|
|
@@ -96,6 +96,7 @@ What's in there: a catalogue of 1,100+ machine-payable API providers, plus first
|
|
|
96
96
|
| `sella businesses` | Your agent's named runs, newest first: status, spend, tool calls, and the outcome it wrote. `--active` or `--closed` filters. |
|
|
97
97
|
| `sella fund` | Per-chain USDC deposit addresses, plus the funding page with QR codes and a fiat on-ramp. |
|
|
98
98
|
| `sella mcp` | Run a local stdio MCP server that proxies Sella with your stored key, for clients that cannot use a remote endpoint. |
|
|
99
|
+
| `sella skill` | Show the cached agent instruction bundle and its version. `sella skill update` refreshes it. A pinned local copy means two runs of the same prompt read the same instructions, instead of re-fetching 25 KB that can change mid-task. |
|
|
99
100
|
| `sella publish init <file.csv>` | Scaffold `sella-dataset.json` and pre-check the CSV structure. |
|
|
100
101
|
| `sella publish push` | Publish the dataset through the same route the web dashboard uses. |
|
|
101
102
|
|
package/dist/chains.js
CHANGED
|
@@ -47,8 +47,17 @@ export async function fetchChainRegistry(apiBase, fetchImpl = fetch) {
|
|
|
47
47
|
* What can the agent actually do with funds on this chain *today*?
|
|
48
48
|
* - `live` — Sella can settle an outbound provider payment from here → spendable now.
|
|
49
49
|
* - `deposit-only` — accepts deposits, but outbound settle isn't proven yet (fund it, can't spend).
|
|
50
|
-
* - `inbound-only` — settles inbound (agents paying Sella) with no agent-wallet spend
|
|
50
|
+
* - `inbound-only` — settles inbound (agents paying Sella) with no agent-wallet spend.
|
|
51
51
|
* - `unknown` — chain not in the registry (or registry unreachable).
|
|
52
|
+
*
|
|
53
|
+
* `inbound-only` used to name Stellar as its example. Stellar was removed from the registry on
|
|
54
|
+
* 2026-08-03 and there is no replacement to name: as of 2026-08-31 NO chain carries
|
|
55
|
+
* `settle.inbound: true`, so this branch is unreachable through the registry alone.
|
|
56
|
+
*
|
|
57
|
+
* That is not a bug in either place. Inbound is switched on per deployment by
|
|
58
|
+
* `X402_INBOUND_NETWORKS`, while the registry flag records only what has been PROVEN on-chain,
|
|
59
|
+
* which is deliberately slower-moving. The branch stays because a row can set the flag at any time,
|
|
60
|
+
* and reporting `unknown` for such a chain would be worse than reporting `inbound-only`.
|
|
52
61
|
*/
|
|
53
62
|
export function chainCapability(info) {
|
|
54
63
|
if (!info)
|
package/dist/clients.js
CHANGED
|
@@ -193,7 +193,7 @@ export function installSellaIntoClient(id, opts) {
|
|
|
193
193
|
doc = JSON.parse(raw);
|
|
194
194
|
}
|
|
195
195
|
catch {
|
|
196
|
-
throw new Error(`${spec.name} config at ${configPath} is not valid JSON
|
|
196
|
+
throw new Error(`${spec.name} config at ${configPath} is not valid JSON. Fix or remove it, then re-run.`);
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
}
|
package/dist/credentials.js
CHANGED
|
@@ -45,7 +45,7 @@ export function saveCredentials(payload, opts) {
|
|
|
45
45
|
files.push(p);
|
|
46
46
|
}
|
|
47
47
|
const custodyNote = keychainOk
|
|
48
|
-
? `API key stored in the macOS keychain (service "${KEYCHAIN_SERVICE}"). Wallet files are owner-read-only; client configs carry the key in plaintext
|
|
49
|
-
: `Secrets stored as owner-read-only files (0600)${env.platform === 'win32' ? '
|
|
48
|
+
? `API key stored in the macOS keychain (service "${KEYCHAIN_SERVICE}"). Wallet files are owner-read-only; client configs carry the key in plaintext, which is how MCP clients read headers today.`
|
|
49
|
+
: `Secrets stored as owner-read-only files (0600)${env.platform === 'win32' ? '. On Windows, protect your user profile' : ''}. Client configs carry the key in plaintext, which is how MCP clients read headers today. Never commit or share these files.`;
|
|
50
50
|
return { files, keyStorage: keychainOk ? 'keychain' : 'file', custodyNote };
|
|
51
51
|
}
|
package/dist/doctor.js
CHANGED
|
@@ -55,8 +55,8 @@ export async function runDoctor(opts) {
|
|
|
55
55
|
id: 'endpoint',
|
|
56
56
|
label: 'Sella MCP endpoint reachable',
|
|
57
57
|
ok: toolNames.length > 0,
|
|
58
|
-
detail: `${opts.mcpUrl}
|
|
59
|
-
...(toolNames.length === 0 ? { fix: 'The server answered but advertised no tools
|
|
58
|
+
detail: `${opts.mcpUrl}: ${toolNames.length} tools advertised`,
|
|
59
|
+
...(toolNames.length === 0 ? { fix: 'The server answered but advertised no tools. Check SELLA_MCP_URL points at /api/mcp.' } : {}),
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
catch (err) {
|
|
@@ -76,7 +76,7 @@ export async function runDoctor(opts) {
|
|
|
76
76
|
label: 'Onboarding tools advertised',
|
|
77
77
|
ok: hasBootstrap,
|
|
78
78
|
detail: hasBootstrap ? 'sella_setup_claim + sella_auth_* available' : `advertised: ${toolNames.slice(0, 6).join(', ')}…`,
|
|
79
|
-
...(hasBootstrap ? {} : { fix: 'The endpoint is not a Sella MCP server
|
|
79
|
+
...(hasBootstrap ? {} : { fix: 'The endpoint is not a Sella MCP server. Fix SELLA_MCP_URL.' }),
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
// 3. credentials — stored key present
|
|
@@ -122,7 +122,7 @@ export async function runDoctor(opts) {
|
|
|
122
122
|
try {
|
|
123
123
|
JSON.parse(fs.readFileSync(walletPath, 'utf8'));
|
|
124
124
|
walletOk = true;
|
|
125
|
-
walletDetail = `${walletPath} ok${fs.existsSync(agentWalletPath) ? ' · AgentWallet config ok' : ' · AgentWallet config absent (proxy payments off
|
|
125
|
+
walletDetail = `${walletPath} ok${fs.existsSync(agentWalletPath) ? ' · AgentWallet config ok' : ' · AgentWallet config absent (proxy payments off, re-run `sella pair` later)'}`;
|
|
126
126
|
}
|
|
127
127
|
catch {
|
|
128
128
|
/* stays missing/corrupt */
|
|
@@ -132,7 +132,7 @@ export async function runDoctor(opts) {
|
|
|
132
132
|
label: 'Wallet config files',
|
|
133
133
|
ok: walletOk,
|
|
134
134
|
detail: walletDetail,
|
|
135
|
-
...(walletOk ? {} : { fix: 'Run `sella pair
|
|
135
|
+
...(walletOk ? {} : { fix: 'Run `sella pair`. Wallet keys are delivered once at pairing.' }),
|
|
136
136
|
});
|
|
137
137
|
// 6. quote — paid-call economics computable (price + platform fee + settleable chains)
|
|
138
138
|
try {
|
|
@@ -147,7 +147,7 @@ export async function runDoctor(opts) {
|
|
|
147
147
|
detail: quote
|
|
148
148
|
? `${slug}: $${quote.totalUSD} total (${quote.feeBps} bps fee) on ${quote.payChains?.join('/') || 'no chains'}`
|
|
149
149
|
: 'catalog reachable but no quote returned',
|
|
150
|
-
...(quote ? {} : { fix: 'Catalog/quote API unavailable
|
|
150
|
+
...(quote ? {} : { fix: 'Catalog/quote API unavailable. Paid calls may still work; check the server logs.' }),
|
|
151
151
|
});
|
|
152
152
|
}
|
|
153
153
|
catch (err) {
|
|
@@ -156,7 +156,7 @@ export async function runDoctor(opts) {
|
|
|
156
156
|
label: 'x402 pay quote',
|
|
157
157
|
ok: false,
|
|
158
158
|
detail: err instanceof Error ? err.message : 'quote fetch failed',
|
|
159
|
-
fix: 'Could not fetch a pay quote from the catalog API
|
|
159
|
+
fix: 'Could not fetch a pay quote from the catalog API. Check the marketplace is up.',
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
162
|
return { ok: checks.every((c) => c.ok), checks };
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as readline from 'node:readline/promises';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
4
6
|
import { detectClients, installSellaIntoClient, defaultEnv } from './clients.js';
|
|
5
7
|
import { pair } from './pairing.js';
|
|
6
8
|
import { runDoctor, runStatus, loadStoredKey } from './doctor.js';
|
|
7
9
|
import { runMcpBridge } from './mcp-bridge.js';
|
|
8
10
|
import { scaffoldCard, pushDataset, CARD_FILENAME } from './publish.js';
|
|
9
11
|
import { sandboxSearch, listBusinesses } from './api.js';
|
|
12
|
+
import { skillStatus, syncSkillBundle } from './skill.js';
|
|
10
13
|
import { getFundingInfo, annotateFunding } from './fund.js';
|
|
11
14
|
import { capabilityLabel } from './chains.js';
|
|
12
15
|
import { defaultIo, Printer } from './output.js';
|
|
13
16
|
import { Ui } from './ui.js';
|
|
14
17
|
import { recordCliEvent, saveTelemetryDecision, shouldPromptTelemetry } from './telemetry.js';
|
|
15
18
|
const DEFAULT_MCP_URL = 'https://sellag.vercel.app/api/mcp';
|
|
16
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Read from package.json rather than hardcoded.
|
|
21
|
+
*
|
|
22
|
+
* It was hardcoded, and it drifted: this constant still said 0.8.0 while package.json and npm both
|
|
23
|
+
* served 0.9.0. `sella --version` therefore reported the wrong version on every install for weeks,
|
|
24
|
+
* the banner printed it, and `recordCliEvent` shipped it, so telemetry attributed 0.9.0 runs to
|
|
25
|
+
* 0.8.0. A version string that has to be bumped in two places is a version string that will be
|
|
26
|
+
* bumped in one.
|
|
27
|
+
*
|
|
28
|
+
* `../package.json` resolves correctly from both dist/index.js and src/index.ts, and npm always
|
|
29
|
+
* includes package.json in the tarball regardless of the `files` list, so it is there at runtime.
|
|
30
|
+
* Read with fs rather than a JSON import: import attributes are not available across the whole
|
|
31
|
+
* `engines: >=18` range this package claims to support.
|
|
32
|
+
*/
|
|
33
|
+
const VERSION = (() => {
|
|
34
|
+
try {
|
|
35
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
const raw = readFileSync(path.join(here, '..', 'package.json'), 'utf8');
|
|
37
|
+
const v = JSON.parse(raw)?.version;
|
|
38
|
+
return typeof v === 'string' && v ? v : 'unknown';
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Never let a missing or unreadable manifest take down the CLI: the version is informational
|
|
42
|
+
// everywhere it is used.
|
|
43
|
+
return 'unknown';
|
|
44
|
+
}
|
|
45
|
+
})();
|
|
17
46
|
function parseFlags(argv) {
|
|
18
47
|
const flags = {
|
|
19
48
|
json: false, yes: false, noColor: false, dryRun: false, noKeychain: false,
|
|
@@ -50,7 +79,7 @@ function parseFlags(argv) {
|
|
|
50
79
|
}
|
|
51
80
|
return flags;
|
|
52
81
|
}
|
|
53
|
-
const HELP = `sella
|
|
82
|
+
const HELP = `sella: the Sella onboarding CLI
|
|
54
83
|
|
|
55
84
|
Usage: sella <command> [options]
|
|
56
85
|
|
|
@@ -58,11 +87,12 @@ Commands:
|
|
|
58
87
|
init Guided onboarding: install the Sella MCP server, pair, verify
|
|
59
88
|
(also runs when you invoke \`sella\` with no command and no credentials yet)
|
|
60
89
|
pair Pair this machine only (setup code or email + OTP)
|
|
61
|
-
sandbox Try Sella with no signup
|
|
90
|
+
sandbox Try Sella with no signup. Search the live marketplace (rate-limited)
|
|
62
91
|
clients List detected agent clients (--install to write configs)
|
|
63
92
|
doctor Verify the install: endpoint, credentials, auth, wallets, pay-quote
|
|
64
93
|
status Show your key + AgentWallet balances
|
|
65
94
|
businesses Show your agent's named runs: spend, tool calls, outcome
|
|
95
|
+
skill Show the cached agent instruction bundle; 'skill update' refreshes it
|
|
66
96
|
fund Show deposit addresses + funding links to add USDC to your agent wallet
|
|
67
97
|
mcp Run as a local stdio MCP server that proxies Sella with your stored key
|
|
68
98
|
publish Publish a dataset from a CSV: 'publish init <file.csv>' then 'publish push'
|
|
@@ -152,12 +182,12 @@ async function runPair(ctx, flags, emit, extra = {}) {
|
|
|
152
182
|
emit.error(outcome.error);
|
|
153
183
|
return { code: outcome.exitCode, summary: { paired: false, error: outcome.error } };
|
|
154
184
|
}
|
|
155
|
-
emit.ok(`Paired via ${outcome.method}
|
|
185
|
+
emit.ok(`Paired via ${outcome.method}. Status: ${outcome.payload.status || 'ok'}`);
|
|
156
186
|
for (const file of outcome.save.files)
|
|
157
187
|
emit.ok(`wrote ${file}`);
|
|
158
188
|
emit.info(outcome.save.custodyNote);
|
|
159
189
|
if (outcome.payload.agentWallet?.status === 'unavailable') {
|
|
160
|
-
emit.warn(`AgentWallet not provisioned (${outcome.payload.agentWallet.reason || 'backend unavailable'})
|
|
190
|
+
emit.warn(`AgentWallet not provisioned (${outcome.payload.agentWallet.reason || 'backend unavailable'}). MCP access still works; re-run \`sella pair\` later.`);
|
|
161
191
|
}
|
|
162
192
|
return {
|
|
163
193
|
code: 0,
|
|
@@ -191,7 +221,7 @@ async function collectPairChoice(ui, flags, ctx) {
|
|
|
191
221
|
if (method === 'setup-code') {
|
|
192
222
|
const code = await ui.text('Paste your setup code', {
|
|
193
223
|
placeholder: 'SELLA-XXXX-XXXX-XXXX-XXXX',
|
|
194
|
-
validate: (v) => (v ? undefined : 'A setup code is required
|
|
224
|
+
validate: (v) => (v ? undefined : 'A setup code is required. Mint one on the dashboard (Connect your agent).'),
|
|
195
225
|
});
|
|
196
226
|
return { setupCode: code };
|
|
197
227
|
}
|
|
@@ -280,16 +310,27 @@ async function cmdInit(ctx, flags, printer) {
|
|
|
280
310
|
(verify.ok ? sp.succeed : sp.succeed)(`${passed}/${verify.checks.length} checks passed`);
|
|
281
311
|
ui.checkStrip(verify.checks.map((c) => ({ id: c.id, ok: c.ok })));
|
|
282
312
|
for (const check of verify.checks.filter((c) => !c.ok)) {
|
|
283
|
-
ui.warn(`${check.label}
|
|
313
|
+
ui.warn(`${check.label}: ${check.detail}`);
|
|
284
314
|
if (check.fix)
|
|
285
315
|
ui.detail(`fix: ${check.fix}`);
|
|
286
316
|
}
|
|
287
317
|
verifySummary = { ok: verify.ok, passed, total: verify.checks.length };
|
|
288
318
|
}
|
|
289
319
|
catch (err) {
|
|
290
|
-
sp.fail('Verify could not run (network?)
|
|
320
|
+
sp.fail('Verify could not run (network?). Try `sella doctor` later.');
|
|
291
321
|
verifySummary = { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
292
322
|
}
|
|
323
|
+
// Cache the agent instruction bundle alongside the credentials, so the agent reads its
|
|
324
|
+
// instructions from disk instead of re-fetching them every session. Best effort on purpose:
|
|
325
|
+
// onboarding must not fail because a documentation download did, and `sella skill update`
|
|
326
|
+
// fixes it later.
|
|
327
|
+
try {
|
|
328
|
+
const bundle = await syncSkillBundle({ env: ctx.env, origin });
|
|
329
|
+
ui.detail(`Skill bundle ${bundle.version} cached at ${bundle.dir}`);
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
ui.detail('Skill bundle not cached (network?). Run `sella skill update` later.');
|
|
333
|
+
}
|
|
293
334
|
}
|
|
294
335
|
if (paired.code === 0) {
|
|
295
336
|
const awStatus = String(paired.summary?.agentWallet || 'none');
|
|
@@ -299,7 +340,7 @@ async function cmdInit(ctx, flags, printer) {
|
|
|
299
340
|
'Health check: sella doctor',
|
|
300
341
|
];
|
|
301
342
|
if (awStatus === 'unavailable') {
|
|
302
|
-
lines.push('AgentWallet pending (backend not configured)
|
|
343
|
+
lines.push('AgentWallet pending (backend not configured). Re-run `sella pair` later.');
|
|
303
344
|
}
|
|
304
345
|
ui.note('Sella is connected', lines, flags.dryRun ? 'plain' : awStatus === 'unavailable' ? 'warn' : 'ok');
|
|
305
346
|
// RFI: ready-to-send prompts for real agent tasks — the "now what?" answer.
|
|
@@ -307,9 +348,9 @@ async function cmdInit(ctx, flags, printer) {
|
|
|
307
348
|
ui.bar();
|
|
308
349
|
ui.step('Ideas to try with your agent (each is a ready-to-send prompt):');
|
|
309
350
|
ui.rows([
|
|
310
|
-
{ tag: 'invest', text: `Validate a startup idea for $25
|
|
311
|
-
{ tag: 'daily', text: `Get a daily morning brief
|
|
312
|
-
{ tag: 'free', text: `Test Sella without spending
|
|
351
|
+
{ tag: 'invest', text: `Validate a startup idea for $25: ${rfi}?uc=twenty-five-dollar-study`, tone: 'brand' },
|
|
352
|
+
{ tag: 'daily', text: `Get a daily morning brief: ${rfi}?uc=morning-brief`, tone: 'ok' },
|
|
353
|
+
{ tag: 'free', text: `Test Sella without spending: ${rfi}?uc=zero-dollar-demo`, tone: 'dim' },
|
|
313
354
|
]);
|
|
314
355
|
ui.detail(`Browse all ideas: ${rfi}`);
|
|
315
356
|
// One-time telemetry choice: only a human at a TTY is ever asked, and only once.
|
|
@@ -323,10 +364,10 @@ async function cmdInit(ctx, flags, printer) {
|
|
|
323
364
|
ui.detail(share === 'share' ? 'Thanks. Opt out anytime: SELLA_TELEMETRY=0' : 'Nothing will be sent. Opt in later: SELLA_TELEMETRY=1');
|
|
324
365
|
}
|
|
325
366
|
const took = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
|
|
326
|
-
ui.outro(flags.dryRun ? 'Dry run complete
|
|
367
|
+
ui.outro(flags.dryRun ? 'Dry run complete. Nothing was written.' : `Done in ${took}s. Your agents can now buy on Sella.`);
|
|
327
368
|
}
|
|
328
369
|
else {
|
|
329
|
-
ui.outro('Pairing incomplete
|
|
370
|
+
ui.outro('Pairing incomplete. Fix the issue above, then re-run `sella init`.');
|
|
330
371
|
}
|
|
331
372
|
printer.jsonOut({ installed: install.results, pairing: paired.summary, reinstalled, verify: verifySummary });
|
|
332
373
|
return install.failed ? 1 : paired.code;
|
|
@@ -353,7 +394,7 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
|
|
|
353
394
|
// --json/--yes runs — automation must not fall into an interactive flow.
|
|
354
395
|
const interactive = io.isTTY && Boolean(process.stdin.isTTY) && !flags.json && !flags.yes;
|
|
355
396
|
if (interactive && !loadStoredKey(ctx.env)) {
|
|
356
|
-
printer.info('No Sella credentials on this machine yet
|
|
397
|
+
printer.info('No Sella credentials on this machine yet. Starting onboarding (Ctrl+C to abort, `sella --help` for commands).');
|
|
357
398
|
return await cmdInit(ctx, flags, printer);
|
|
358
399
|
}
|
|
359
400
|
io.stdout(HELP);
|
|
@@ -397,7 +438,7 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
|
|
|
397
438
|
if (choice.email)
|
|
398
439
|
ui.step(`Requesting a verification code for ${choice.email}…`);
|
|
399
440
|
const paired = await runPair(ctx, flags, emit, { ...choice, ask: wizardAsk(ui, ctx) });
|
|
400
|
-
ui.outro(paired.code === 0 ? 'Paired
|
|
441
|
+
ui.outro(paired.code === 0 ? 'Paired. Try `sella doctor` next.' : 'Pairing failed. See above.');
|
|
401
442
|
printer.jsonOut(paired.summary || {});
|
|
402
443
|
return paired.code;
|
|
403
444
|
}
|
|
@@ -415,12 +456,12 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
|
|
|
415
456
|
const total = result.datasets.length + result.apis.length;
|
|
416
457
|
printer.info(`Sandbox results for "${query}" (no account, rate-limited):`);
|
|
417
458
|
for (const d of result.datasets) {
|
|
418
|
-
printer.info(` dataset ${d.title || d.id}${d.priceUSDC ?
|
|
459
|
+
printer.info(` dataset ${d.title || d.id}${d.priceUSDC ? `, $${d.priceUSDC}/call` : ', free'}`);
|
|
419
460
|
}
|
|
420
461
|
for (const a of result.apis) {
|
|
421
462
|
printer.info(` api ${a.name || ''}${a.chains?.length ? ` [${a.chains.join('/')}]` : ''}`);
|
|
422
463
|
}
|
|
423
|
-
printer.info(total ? '\nPair to buy any of these: `sella init`.' : 'No matches
|
|
464
|
+
printer.info(total ? '\nPair to buy any of these: `sella init`.' : 'No matches. Try a broader query.');
|
|
424
465
|
}
|
|
425
466
|
return 0;
|
|
426
467
|
}
|
|
@@ -429,7 +470,7 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
|
|
|
429
470
|
printer.jsonOut(info);
|
|
430
471
|
if (!flags.json) {
|
|
431
472
|
if (!info.paired) {
|
|
432
|
-
printer.error('Not paired yet
|
|
473
|
+
printer.error('Not paired yet. Run `sella pair` first, then `sella fund`.');
|
|
433
474
|
return 2;
|
|
434
475
|
}
|
|
435
476
|
printer.info('Fund your Sella agent wallet with USDC:\n');
|
|
@@ -452,14 +493,14 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
|
|
|
452
493
|
printer.info('Sella doctor:');
|
|
453
494
|
for (const check of result.checks) {
|
|
454
495
|
if (check.ok)
|
|
455
|
-
printer.ok(`${check.label}
|
|
496
|
+
printer.ok(`${check.label}: ${check.detail}`);
|
|
456
497
|
else {
|
|
457
|
-
printer.error(`${check.label}
|
|
498
|
+
printer.error(`${check.label}: ${check.detail}`);
|
|
458
499
|
if (check.fix)
|
|
459
500
|
printer.info(` ↳ ${check.fix}`);
|
|
460
501
|
}
|
|
461
502
|
}
|
|
462
|
-
printer.info(result.ok ? '\nAll checks passed. Your agent is ready to buy.' : '\nSome checks failed
|
|
503
|
+
printer.info(result.ok ? '\nAll checks passed. Your agent is ready to buy.' : '\nSome checks failed. See the fixes above.');
|
|
463
504
|
}
|
|
464
505
|
return result.ok ? 0 : 1;
|
|
465
506
|
}
|
|
@@ -468,7 +509,7 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
|
|
|
468
509
|
printer.jsonOut(status);
|
|
469
510
|
if (!flags.json) {
|
|
470
511
|
const key = status.key;
|
|
471
|
-
printer.info(key.present ? `API key: ${key.prefix}… (${key.source})` : 'API key: not paired
|
|
512
|
+
printer.info(key.present ? `API key: ${key.prefix}… (${key.source})` : 'API key: not paired. Run `sella pair`');
|
|
472
513
|
const aw = status.agentWallet;
|
|
473
514
|
if (aw?.username) {
|
|
474
515
|
printer.info(`AgentWallet: ${aw.username}${aw.fundingStatus ? ` · ${aw.fundingStatus}` : ''}`);
|
|
@@ -484,6 +525,47 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
|
|
|
484
525
|
}
|
|
485
526
|
return 0;
|
|
486
527
|
}
|
|
528
|
+
case 'skill': {
|
|
529
|
+
const origin = ctx.mcpUrl.replace(/\/api\/mcp\/?$/, '');
|
|
530
|
+
const sub = flags.positional[1];
|
|
531
|
+
if (sub === 'update') {
|
|
532
|
+
const result = await syncSkillBundle({ env: ctx.env, origin, force: flags.positional.includes('--force') });
|
|
533
|
+
printer.jsonOut(result);
|
|
534
|
+
if (!flags.json) {
|
|
535
|
+
if (result.upToDate) {
|
|
536
|
+
printer.info(`Already on ${result.version}. Nothing to download.`);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
const moved = result.previousVersion ? `${result.previousVersion} -> ${result.version}` : result.version;
|
|
540
|
+
printer.info(`Skill bundle ${moved}`);
|
|
541
|
+
printer.info(` ${result.written.length} downloaded, ${result.reused.length} unchanged`);
|
|
542
|
+
printer.info(` ${result.dir}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return 0;
|
|
546
|
+
}
|
|
547
|
+
const status = await skillStatus(ctx.env, origin);
|
|
548
|
+
printer.jsonOut(status);
|
|
549
|
+
if (!flags.json) {
|
|
550
|
+
if (!status.localVersion) {
|
|
551
|
+
printer.info(`No local bundle. Live version is ${status.remoteVersion}.`);
|
|
552
|
+
printer.info('Run `sella skill update` to cache it.');
|
|
553
|
+
}
|
|
554
|
+
else if (status.upToDate) {
|
|
555
|
+
printer.info(`Skill bundle ${status.localVersion} (current).`);
|
|
556
|
+
if (status.dir)
|
|
557
|
+
printer.info(` ${status.dir}`);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
printer.info(`Skill bundle ${status.localVersion} is out of date. Live version is ${status.remoteVersion}.`);
|
|
561
|
+
if (status.breaking) {
|
|
562
|
+
printer.info(' This is a breaking change: a tool may have been removed or changed.');
|
|
563
|
+
}
|
|
564
|
+
printer.info(' Run `sella skill update`.');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return status.localVersion && !status.upToDate ? 1 : 0;
|
|
568
|
+
}
|
|
487
569
|
case 'businesses': {
|
|
488
570
|
const stored = loadStoredKey(ctx.env);
|
|
489
571
|
if (!stored) {
|
|
@@ -551,7 +633,7 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
|
|
|
551
633
|
if (sub === 'push') {
|
|
552
634
|
const stored = loadStoredKey(ctx.env);
|
|
553
635
|
if (!stored) {
|
|
554
|
-
printer.error('Not paired
|
|
636
|
+
printer.error('Not paired. Run `sella pair` first, then `sella publish push`.');
|
|
555
637
|
printer.jsonOut({ ok: false, error: 'not_paired' });
|
|
556
638
|
return 2;
|
|
557
639
|
}
|
|
@@ -566,12 +648,12 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
|
|
|
566
648
|
printer.error(` ${k}: ${v}`);
|
|
567
649
|
}
|
|
568
650
|
else {
|
|
569
|
-
printer.ok(`Published
|
|
651
|
+
printer.ok(`Published. Status: ${result.status}`);
|
|
570
652
|
printer.info(` listing: ${result.listingUrl}`);
|
|
571
653
|
if (result.status === 'manual_review')
|
|
572
654
|
printer.info(' Sella is reviewing it; it goes live once approved.');
|
|
573
655
|
else if (result.status === 'processing')
|
|
574
|
-
printer.info(' Still scoring
|
|
656
|
+
printer.info(' Still scoring. Re-check later with `sella status` or the dashboard.');
|
|
575
657
|
}
|
|
576
658
|
}
|
|
577
659
|
return result.ok ? 0 : 1;
|
|
@@ -587,7 +669,7 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
|
|
|
587
669
|
ctx.io.stderr(`ok Sella MCP bridge → ${ctx.mcpUrl} (key from ${stored.source})`);
|
|
588
670
|
}
|
|
589
671
|
else {
|
|
590
|
-
ctx.io.stderr('warn No stored key
|
|
672
|
+
ctx.io.stderr('warn No stored key. Bridging free/sandbox tools only; run `sella pair` to enable paid tools.');
|
|
591
673
|
}
|
|
592
674
|
return await runMcpBridge({ mcpUrl: ctx.mcpUrl, apiKey: stored?.apiKey });
|
|
593
675
|
}
|
package/dist/mcp-bridge.js
CHANGED
|
@@ -40,7 +40,7 @@ export async function forwardMessage(msg, opts) {
|
|
|
40
40
|
});
|
|
41
41
|
const text = await res.text();
|
|
42
42
|
if (!res.ok) {
|
|
43
|
-
const hint = res.status === 401 ? '
|
|
43
|
+
const hint = res.status === 401 ? '. Run `sella pair` to (re)authenticate this machine' : '';
|
|
44
44
|
return { jsonrpc: '2.0', id, error: { code: -32000, message: `Sella endpoint responded ${res.status}${hint}` } };
|
|
45
45
|
}
|
|
46
46
|
if (!text)
|
package/dist/publish.js
CHANGED
|
@@ -33,7 +33,7 @@ export function precheckCsv(text) {
|
|
|
33
33
|
warnings.push(`Duplicate header(s): ${[...new Set(dupes)].join(', ')}.`);
|
|
34
34
|
const dataLines = lines.slice(1);
|
|
35
35
|
if (dataLines.length === 0)
|
|
36
|
-
warnings.push('No data rows
|
|
36
|
+
warnings.push('No data rows. Only a header was found.');
|
|
37
37
|
let emptyCells = 0;
|
|
38
38
|
let ragged = 0;
|
|
39
39
|
const nonEmptyPerColumn = new Array(columns.length).fill(0);
|
|
@@ -138,7 +138,7 @@ export async function pushDataset(opts) {
|
|
|
138
138
|
});
|
|
139
139
|
const json = await res.json().catch(() => ({}));
|
|
140
140
|
if (res.status === 401)
|
|
141
|
-
return { ok: false, error: 'Not authenticated
|
|
141
|
+
return { ok: false, error: 'Not authenticated. Run `sella pair` to (re)authenticate this machine.' };
|
|
142
142
|
if (res.status === 422)
|
|
143
143
|
return { ok: false, error: 'The listing was rejected by validation.', fieldErrors: json.fieldErrors };
|
|
144
144
|
if (!res.ok)
|
package/dist/skill.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Local skill-bundle cache.
|
|
6
|
+
*
|
|
7
|
+
* An agent that re-fetches 25 KB of instructions every session burns context and gets whatever is
|
|
8
|
+
* live, which can change mid-task. A pinned local copy is cheaper and reproducible: two runs of the
|
|
9
|
+
* same prompt read the same instructions, so when something goes wrong you know which version
|
|
10
|
+
* produced it.
|
|
11
|
+
*
|
|
12
|
+
* The danger is the obvious one. A cache that never expires is a confidently wrong copy of a world
|
|
13
|
+
* that moved, which is exactly how the old hand-written skill.json ended up advertising four
|
|
14
|
+
* deprecated tools. So the bundle is stored under its version, the server stamps stale callers on
|
|
15
|
+
* calls they were already making, and `sella skill update` is one command.
|
|
16
|
+
*/
|
|
17
|
+
/** Placeholder the server substitutes per request host. See lib/agent-docs.ts. */
|
|
18
|
+
const ORIGIN_PLACEHOLDER = '{{SELLA_ORIGIN}}';
|
|
19
|
+
export class SkillSyncError extends Error {
|
|
20
|
+
}
|
|
21
|
+
function skillRoot(env) {
|
|
22
|
+
return path.join(env.home, '.sella', 'skill');
|
|
23
|
+
}
|
|
24
|
+
function pointerPath(env) {
|
|
25
|
+
return path.join(skillRoot(env), 'current');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A plain text pointer rather than a symlink. Symlink creation needs elevation or developer mode on
|
|
29
|
+
* Windows, and this has to work for every operator, not just the ones on a unix box.
|
|
30
|
+
*/
|
|
31
|
+
export function readCurrentVersion(env) {
|
|
32
|
+
try {
|
|
33
|
+
const v = fs.readFileSync(pointerPath(env), 'utf8').trim();
|
|
34
|
+
return v || null;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function readLocalBundle(env) {
|
|
41
|
+
const version = readCurrentVersion(env);
|
|
42
|
+
if (!version)
|
|
43
|
+
return null;
|
|
44
|
+
const dir = path.join(skillRoot(env), version);
|
|
45
|
+
let meta = {};
|
|
46
|
+
try {
|
|
47
|
+
meta = JSON.parse(fs.readFileSync(path.join(dir, 'bundle.json'), 'utf8'));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
let files = [];
|
|
53
|
+
try {
|
|
54
|
+
files = fs.readdirSync(dir).filter((f) => f !== 'bundle.json');
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return { version, dir, files, fetchedAt: meta.fetchedAt || '' };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Reverses the server's origin substitution so a downloaded document hashes to the value the
|
|
63
|
+
* manifest published.
|
|
64
|
+
*
|
|
65
|
+
* The manifest hashes the RAW template, before substitution, so that a bundle cached on one Sella
|
|
66
|
+
* domain is not invalidated by re-checking from another. What arrives over HTTP is the rendered
|
|
67
|
+
* form. Putting the placeholder back is exact rather than approximate because the server guarantees
|
|
68
|
+
* no template contains a literal absolute origin (asserted by findLiteralOrigins in the app), so
|
|
69
|
+
* every occurrence of the origin in a served document came from the placeholder.
|
|
70
|
+
*/
|
|
71
|
+
export function canonicalize(body, origin) {
|
|
72
|
+
const trimmed = origin.replace(/\/+$/, '');
|
|
73
|
+
return trimmed ? body.split(trimmed).join(ORIGIN_PLACEHOLDER) : body;
|
|
74
|
+
}
|
|
75
|
+
export function hashCanonical(body, origin) {
|
|
76
|
+
return `sha256:${crypto.createHash('sha256').update(canonicalize(body, origin), 'utf8').digest('hex')}`;
|
|
77
|
+
}
|
|
78
|
+
export async function fetchManifest(origin, fetchImpl = fetch) {
|
|
79
|
+
const base = origin.replace(/\/+$/, '');
|
|
80
|
+
const res = await fetchImpl(`${base}/skill.json`);
|
|
81
|
+
if (!res.ok)
|
|
82
|
+
throw new SkillSyncError(`Could not read ${base}/skill.json (HTTP ${res.status}).`);
|
|
83
|
+
const manifest = (await res.json());
|
|
84
|
+
if (!manifest?.version || !manifest?.files) {
|
|
85
|
+
throw new SkillSyncError(`${base}/skill.json is not a skill manifest.`);
|
|
86
|
+
}
|
|
87
|
+
return manifest;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Brings the local bundle to the version the server is serving.
|
|
91
|
+
*
|
|
92
|
+
* Unchanged files are copied forward from the previous bundle rather than re-downloaded, which is
|
|
93
|
+
* why the manifest carries a per-file hash at all. A version bump for a one-line prose fix then
|
|
94
|
+
* costs one small request instead of the whole bundle.
|
|
95
|
+
*/
|
|
96
|
+
export async function syncSkillBundle(opts) {
|
|
97
|
+
const { env, origin } = opts;
|
|
98
|
+
const fetchImpl = opts.fetchImpl || fetch;
|
|
99
|
+
const manifest = await fetchManifest(origin, fetchImpl);
|
|
100
|
+
const previousVersion = readCurrentVersion(env);
|
|
101
|
+
const dir = path.join(skillRoot(env), manifest.version);
|
|
102
|
+
const alreadyCurrent = previousVersion === manifest.version && fs.existsSync(dir);
|
|
103
|
+
if (alreadyCurrent && !opts.force) {
|
|
104
|
+
return {
|
|
105
|
+
version: manifest.version,
|
|
106
|
+
previousVersion,
|
|
107
|
+
dir,
|
|
108
|
+
written: [],
|
|
109
|
+
reused: Object.keys(manifest.files),
|
|
110
|
+
upToDate: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
114
|
+
const written = [];
|
|
115
|
+
const reused = [];
|
|
116
|
+
for (const [name, entry] of Object.entries(manifest.files)) {
|
|
117
|
+
const target = path.join(dir, name);
|
|
118
|
+
// Reuse an identical file from the previous bundle when the hash says it did not change.
|
|
119
|
+
if (!opts.force && previousVersion) {
|
|
120
|
+
const old = path.join(skillRoot(env), previousVersion, name);
|
|
121
|
+
try {
|
|
122
|
+
const body = fs.readFileSync(old, 'utf8');
|
|
123
|
+
if (hashCanonical(body, origin) === entry.sha256) {
|
|
124
|
+
fs.writeFileSync(target, body);
|
|
125
|
+
reused.push(name);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Not present or unreadable in the old bundle; fall through and download it.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const res = await fetchImpl(entry.url);
|
|
134
|
+
if (!res.ok)
|
|
135
|
+
throw new SkillSyncError(`Could not download ${name} (HTTP ${res.status}).`);
|
|
136
|
+
const body = await res.text();
|
|
137
|
+
// Integrity, not just freshness. A truncated or tampered document is worse than a stale one
|
|
138
|
+
// because it looks authoritative, so a mismatch fails the sync rather than being written.
|
|
139
|
+
const actual = hashCanonical(body, origin);
|
|
140
|
+
if (actual !== entry.sha256) {
|
|
141
|
+
throw new SkillSyncError(`${name} does not match the hash in the manifest. Expected ${entry.sha256}, got ${actual}. ` +
|
|
142
|
+
`Nothing was written. Retry, and if it persists report it rather than using the file.`);
|
|
143
|
+
}
|
|
144
|
+
fs.writeFileSync(target, body);
|
|
145
|
+
written.push(name);
|
|
146
|
+
}
|
|
147
|
+
fs.writeFileSync(path.join(dir, 'bundle.json'), JSON.stringify({ version: manifest.version, origin: origin.replace(/\/+$/, ''), fetchedAt: new Date().toISOString() }, null, 2) + '\n');
|
|
148
|
+
fs.mkdirSync(skillRoot(env), { recursive: true });
|
|
149
|
+
fs.writeFileSync(pointerPath(env), `${manifest.version}\n`);
|
|
150
|
+
return { version: manifest.version, previousVersion, dir, written, reused, upToDate: false };
|
|
151
|
+
}
|
|
152
|
+
function major(version) {
|
|
153
|
+
const m = /^(\d+)\.\d+\.\d+$/.exec(String(version || '').trim());
|
|
154
|
+
return m ? Number(m[1]) : null;
|
|
155
|
+
}
|
|
156
|
+
export async function skillStatus(env, origin, fetchImpl = fetch) {
|
|
157
|
+
const manifest = await fetchManifest(origin, fetchImpl);
|
|
158
|
+
const local = readLocalBundle(env);
|
|
159
|
+
const localMajor = major(local?.version || null);
|
|
160
|
+
const remoteMajor = major(manifest.version);
|
|
161
|
+
return {
|
|
162
|
+
localVersion: local?.version || null,
|
|
163
|
+
remoteVersion: manifest.version,
|
|
164
|
+
upToDate: Boolean(local && local.version === manifest.version),
|
|
165
|
+
breaking: localMajor !== null && remoteMajor !== null && localMajor < remoteMajor,
|
|
166
|
+
dir: local?.dir || null,
|
|
167
|
+
readFirst: manifest.readFirst || null,
|
|
168
|
+
};
|
|
169
|
+
}
|
package/dist/ui.js
CHANGED
|
@@ -78,7 +78,7 @@ export class Ui {
|
|
|
78
78
|
if (this.quiet)
|
|
79
79
|
return;
|
|
80
80
|
if (!this.pretty) {
|
|
81
|
-
this.out(`SELLA
|
|
81
|
+
this.out(`SELLA · ${subtitle} (${meta})`);
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
this.out('');
|
|
@@ -277,7 +277,7 @@ export class Ui {
|
|
|
277
277
|
throw new Error(`Cannot prompt for "${label}" in a non-interactive run.`);
|
|
278
278
|
if (!this.pretty) {
|
|
279
279
|
this.out(label);
|
|
280
|
-
options.forEach((o, idx) => this.out(` ${idx + 1}) ${o.label}${o.hint ? `
|
|
280
|
+
options.forEach((o, idx) => this.out(` ${idx + 1}) ${o.label}${o.hint ? ` (${o.hint})` : ''}`));
|
|
281
281
|
for (;;) {
|
|
282
282
|
const answer = (await this.plainQuestion(`Choose 1-${options.length} (${initial + 1}): `)).trim();
|
|
283
283
|
if (!answer)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sella-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
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",
|
|
@@ -22,13 +22,26 @@
|
|
|
22
22
|
],
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"homepage": "https://sellag.vercel.app",
|
|
25
|
-
"bugs": {
|
|
26
|
-
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/010100100100011101010100/ogsella/issues"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/010100100100011101010100/ogsella",
|
|
31
|
+
"directory": "cli"
|
|
32
|
+
},
|
|
27
33
|
"type": "module",
|
|
28
|
-
"bin": {
|
|
34
|
+
"bin": {
|
|
35
|
+
"sella": "./dist/index.js"
|
|
36
|
+
},
|
|
29
37
|
"main": "./dist/index.js",
|
|
30
|
-
"files": [
|
|
31
|
-
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
32
45
|
"scripts": {
|
|
33
46
|
"build": "tsc -p tsconfig.json",
|
|
34
47
|
"prepublishOnly": "npm run build",
|