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 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,100+ machine-payable API providers, plus first-party datasets, workflows, and Sella Native products such as Cradle. Free previews let your agent check quality before it pays.
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, e.g. Stellar.
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 fix or remove it, then re-run.`);
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
  }
@@ -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 that 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 that is how MCP clients read headers today. Never commit or share these files.`;
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} ${toolNames.length} tools advertised`,
59
- ...(toolNames.length === 0 ? { fix: 'The server answered but advertised no tools check SELLA_MCP_URL points at /api/mcp.' } : {}),
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 fix SELLA_MCP_URL.' }),
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 re-run `sella pair` later)'}`;
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` wallet keys are delivered once at pairing.' }),
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 paid calls may still work; check the server logs.' }),
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 check the marketplace is up.',
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
- const VERSION = '0.8.0';
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 the Sella onboarding CLI
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 search the live marketplace (rate-limited)
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} — status: ${outcome.payload.status || 'ok'}`);
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'}) MCP access still works; re-run \`sella pair\` later.`);
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 mint one on the dashboard (Connect your agent).'),
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} ${check.detail}`);
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?) try `sella doctor` later.');
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) — re-run `sella pair` later.');
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 ${rfi}?uc=twenty-five-dollar-study`, tone: 'brand' },
311
- { tag: 'daily', text: `Get a daily morning brief ${rfi}?uc=morning-brief`, tone: 'ok' },
312
- { tag: 'free', text: `Test Sella without spending ${rfi}?uc=zero-dollar-demo`, tone: 'dim' },
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 nothing was written.' : `Done in ${took}s your agents can now buy on Sella.`);
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 fix the issue above, then re-run `sella init`.');
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 starting onboarding (Ctrl+C to abort, `sella --help` for commands).');
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 try `sella doctor` next.' : 'Pairing failed see above.');
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 ? ` $${d.priceUSDC}/call` : ' free'}`);
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 try a broader query.');
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 run `sella pair` first, then `sella fund`.');
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} ${check.detail}`);
496
+ printer.ok(`${check.label}: ${check.detail}`);
456
497
  else {
457
- printer.error(`${check.label} ${check.detail}`);
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 see the fixes above.');
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 run `sella pair`');
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 run `sella pair` first, then `sella publish push`.');
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 — status: ${result.status}`);
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 — re-check later with `sella status` or the dashboard.');
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 bridging free/sandbox tools only; run `sella pair` to enable paid tools.');
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
  }
@@ -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 ? ' run `sella pair` to (re)authenticate this machine' : '';
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 only a header was found.');
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 run `sella pair` to (re)authenticate this machine.' };
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 ${subtitle} (${meta})`);
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 ? ` ${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.8.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": { "url": "https://github.com/010100100100011101010100/ogsella/issues" },
26
- "repository": { "type": "git", "url": "https://github.com/010100100100011101010100/ogsella", "directory": "cli" },
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": { "sella": "./dist/index.js" },
34
+ "bin": {
35
+ "sella": "./dist/index.js"
36
+ },
29
37
  "main": "./dist/index.js",
30
- "files": ["dist", "README.md"],
31
- "engines": { "node": ">=18" },
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",