sella-cli 0.7.0 → 0.9.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 +19 -3
- package/dist/api.js +60 -19
- package/dist/doctor.js +4 -1
- package/dist/index.js +96 -4
- package/dist/skill.js +169 -0
- package/package.json +19 -6
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
20
|
<a href="https://sellag.vercel.app">Website</a> ·
|
|
21
|
-
<a href="https://
|
|
21
|
+
<a href="https://docs.selltoagent.dev">Docs</a> ·
|
|
22
22
|
<a href="https://sellag.vercel.app/marketplace">Marketplace</a> ·
|
|
23
23
|
<a href="https://github.com/010100100100011101010100/ogsella/issues">Report a bug</a>
|
|
24
24
|
</p>
|
|
@@ -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
|
|
|
@@ -79,6 +79,7 @@ What's in there: a catalogue of 1,100+ machine-payable API providers, plus first
|
|
|
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
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.
|
|
82
|
+
- **Businesses**: your agent can open a named run before it starts work, and everything it buys, calls, and decides gets attached to that run. You see what a piece of work actually cost instead of one undifferentiated spend total. Run `sella businesses` to read them from the terminal.
|
|
82
83
|
- **Publishing from a CSV**, so you can sell your own data without opening a browser.
|
|
83
84
|
- **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.
|
|
84
85
|
|
|
@@ -92,6 +93,7 @@ What's in there: a catalogue of 1,100+ machine-payable API providers, plus first
|
|
|
92
93
|
| `sella clients` | List detected agent clients. `--install` writes configs, `--client a,b` filters, `--dry-run` previews. |
|
|
93
94
|
| `sella doctor` | Six live checks, from endpoint reachability to a real x402 pay quote. Every failure names its fix. |
|
|
94
95
|
| `sella status` | Your API key and agent wallet balances, each chain labelled live or deposit-only. |
|
|
96
|
+
| `sella businesses` | Your agent's named runs, newest first: status, spend, tool calls, and the outcome it wrote. `--active` or `--closed` filters. |
|
|
95
97
|
| `sella fund` | Per-chain USDC deposit addresses, plus the funding page with QR codes and a fiat on-ramp. |
|
|
96
98
|
| `sella mcp` | Run a local stdio MCP server that proxies Sella with your stored key, for clients that cannot use a remote endpoint. |
|
|
97
99
|
| `sella publish init <file.csv>` | Scaffold `sella-dataset.json` and pre-check the CSV structure. |
|
|
@@ -121,6 +123,20 @@ Then ask your agent:
|
|
|
121
123
|
|
|
122
124
|
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.
|
|
123
125
|
|
|
126
|
+
### Keep track of what a piece of work cost
|
|
127
|
+
|
|
128
|
+
Ask your agent to open a business before it starts:
|
|
129
|
+
|
|
130
|
+
> Start a Sella business called "Q3 pricing research", then find me comparable pricing datasets under $20.
|
|
131
|
+
|
|
132
|
+
It calls `business_start`, and from that point every Sella call it makes is attached to that run. When it finishes it closes the run with a summary. Then:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
sella businesses
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
You get each run with its spend, tool-call count, and the outcome the agent wrote, instead of a single spend figure you cannot attribute to anything. A business is a label over the agent wallet you already funded, so there are no extra wallets, keys, or balances to manage.
|
|
139
|
+
|
|
124
140
|
### Real-world: publish a CSV and get paid in USDC
|
|
125
141
|
|
|
126
142
|
```bash
|
|
@@ -205,7 +221,7 @@ Sella has two other ways in: your agent can onboard itself over MCP with your em
|
|
|
205
221
|
| | |
|
|
206
222
|
|---|---|
|
|
207
223
|
| Website | https://sellag.vercel.app |
|
|
208
|
-
| Docs | https://
|
|
224
|
+
| Docs | https://docs.selltoagent.dev |
|
|
209
225
|
| Marketplace | https://sellag.vercel.app/marketplace |
|
|
210
226
|
| Agent task ideas | https://sellag.vercel.app/rfi |
|
|
211
227
|
| npm | https://www.npmjs.com/package/sella-cli |
|
package/dist/api.js
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
* the CLI speaks the exact same tools/call surface agents use (sella_setup_claim,
|
|
4
4
|
* sella_auth_start/complete), so CLI, web, and agent onboarding cannot drift.
|
|
5
5
|
*/
|
|
6
|
-
export async function callToolRaw(mcpUrl, name, args, fetchImpl = fetch) {
|
|
6
|
+
export async function callToolRaw(mcpUrl, name, args, fetchImpl = fetch, apiKey) {
|
|
7
7
|
let res;
|
|
8
8
|
try {
|
|
9
9
|
res = await fetchImpl(mcpUrl, {
|
|
10
10
|
method: 'POST',
|
|
11
|
-
headers: {
|
|
11
|
+
headers: {
|
|
12
|
+
'content-type': 'application/json',
|
|
13
|
+
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
|
|
14
|
+
},
|
|
12
15
|
body: JSON.stringify({ jsonrpc: '2.0', id: name, method: 'tools/call', params: { name, arguments: args } }),
|
|
13
16
|
});
|
|
14
17
|
}
|
|
@@ -17,7 +20,9 @@ export async function callToolRaw(mcpUrl, name, args, fetchImpl = fetch) {
|
|
|
17
20
|
'Check your connection, proxy settings, or SELLA_MCP_URL.');
|
|
18
21
|
}
|
|
19
22
|
if (res.status === 429) {
|
|
20
|
-
throw new Error(
|
|
23
|
+
throw new Error(apiKey
|
|
24
|
+
? 'Sella rate limit reached. Wait a moment and try again.'
|
|
25
|
+
: 'Sandbox rate limit reached. Run `sella pair` for unmetered access.');
|
|
21
26
|
}
|
|
22
27
|
if (!res.ok) {
|
|
23
28
|
throw new Error(`Sella endpoint at ${mcpUrl} responded ${res.status}. Check SELLA_MCP_URL.`);
|
|
@@ -38,24 +43,60 @@ export async function callToolRaw(mcpUrl, name, args, fetchImpl = fetch) {
|
|
|
38
43
|
throw new Error('Unexpected response shape from the Sella MCP endpoint.');
|
|
39
44
|
}
|
|
40
45
|
export const callAuthTool = (mcpUrl, name, args, fetchImpl = fetch) => callToolRaw(mcpUrl, name, args, fetchImpl);
|
|
41
|
-
/**
|
|
46
|
+
/** Price on a search_catalog result is `{ amountUSDC, minUSDC, ... }`; older shapes were a number. */
|
|
47
|
+
function priceUSDCOf(entry) {
|
|
48
|
+
const raw = entry?.price ?? entry?.priceUSDC;
|
|
49
|
+
const n = typeof raw === 'number' ? raw : Number(raw?.amountUSDC ?? raw?.minUSDC);
|
|
50
|
+
return Number.isFinite(n) ? n : undefined;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Zero-identity demo: ONE free `search_catalog` call across every listing kind.
|
|
54
|
+
*
|
|
55
|
+
* Replaces the previous pair of calls to `search_datasets` + `list_market_apis` (both deprecated
|
|
56
|
+
* aliases scheduled for removal, and the latter reads a backend that can be empty or asleep).
|
|
57
|
+
* One call now covers datasets, APIs, workflows and Sella Native products, so the sandbox shows
|
|
58
|
+
* the same ranked surface an agent sees.
|
|
59
|
+
*/
|
|
42
60
|
export async function sandboxSearch(mcpUrl, query, fetchImpl = fetch) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
const payload = await callToolRaw(mcpUrl, 'search_catalog', { query, limit: 10 }, fetchImpl);
|
|
62
|
+
const results = Array.isArray(payload?.results)
|
|
63
|
+
? payload.results
|
|
64
|
+
: Array.isArray(payload)
|
|
65
|
+
? payload
|
|
66
|
+
: [];
|
|
67
|
+
const datasets = results
|
|
68
|
+
.filter((r) => r?.kind === 'dataset')
|
|
69
|
+
.slice(0, 5)
|
|
70
|
+
.map((r) => ({
|
|
71
|
+
id: String(r?.id ?? ''),
|
|
72
|
+
title: r?.title,
|
|
73
|
+
tier: r?.attributes?.tier,
|
|
74
|
+
priceUSDC: priceUSDCOf(r),
|
|
75
|
+
}));
|
|
76
|
+
// Everything that is not a dataset (api, workflow, native) prints in the "api" column.
|
|
77
|
+
const apis = results
|
|
78
|
+
.filter((r) => r?.kind && r.kind !== 'dataset')
|
|
79
|
+
.slice(0, 5)
|
|
80
|
+
.map((r) => ({
|
|
81
|
+
name: r?.title,
|
|
82
|
+
description: r?.description,
|
|
83
|
+
priceUSDC: priceUSDCOf(r),
|
|
84
|
+
chains: Array.isArray(r?.chains) ? r.chains : undefined,
|
|
85
|
+
}));
|
|
86
|
+
return { datasets, apis };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* `business_list`: the caller's named agent runs, newest first. Businesses label the existing
|
|
90
|
+
* agent wallet rather than creating new ones, so this is a read over the same key the CLI stored.
|
|
91
|
+
*/
|
|
92
|
+
export async function listBusinesses(mcpUrl, apiKey, args = {}, fetchImpl = fetch) {
|
|
93
|
+
const payload = await callToolRaw(mcpUrl, 'business_list', { ...args }, fetchImpl, apiKey);
|
|
94
|
+
if (payload?.error)
|
|
95
|
+
throw new Error(String(payload.error));
|
|
56
96
|
return {
|
|
57
|
-
|
|
58
|
-
|
|
97
|
+
businesses: Array.isArray(payload?.businesses) ? payload.businesses : [],
|
|
98
|
+
active: payload?.active ?? null,
|
|
99
|
+
count: Number(payload?.count ?? 0),
|
|
59
100
|
};
|
|
60
101
|
}
|
|
61
102
|
export const claimSetupCode = (mcpUrl, code, fetchImpl) => callAuthTool(mcpUrl, 'sella_setup_claim', { code, client: 'sella-cli' }, fetchImpl);
|
package/dist/doctor.js
CHANGED
|
@@ -91,7 +91,10 @@ export async function runDoctor(opts) {
|
|
|
91
91
|
// 4. auth — the stored key actually authenticates a protected call
|
|
92
92
|
if (stored) {
|
|
93
93
|
try {
|
|
94
|
-
const called = await rpc(opts.mcpUrl,
|
|
94
|
+
const called = await rpc(opts.mcpUrl,
|
|
95
|
+
// search_catalog, not the deprecated list_datasets alias: this check must outlive the
|
|
96
|
+
// one-release deprecation window for the legacy discovery names.
|
|
97
|
+
{ method: 'tools/call', params: { name: 'search_catalog', arguments: { limit: 1 } } }, fetchImpl, stored.apiKey);
|
|
95
98
|
const isError = Boolean(called?.result?.isError) || Boolean(called?.error);
|
|
96
99
|
checks.push({
|
|
97
100
|
id: 'auth',
|
package/dist/index.js
CHANGED
|
@@ -6,14 +6,15 @@ import { pair } from './pairing.js';
|
|
|
6
6
|
import { runDoctor, runStatus, loadStoredKey } from './doctor.js';
|
|
7
7
|
import { runMcpBridge } from './mcp-bridge.js';
|
|
8
8
|
import { scaffoldCard, pushDataset, CARD_FILENAME } from './publish.js';
|
|
9
|
-
import { sandboxSearch } from './api.js';
|
|
9
|
+
import { sandboxSearch, listBusinesses } from './api.js';
|
|
10
|
+
import { skillStatus, syncSkillBundle } from './skill.js';
|
|
10
11
|
import { getFundingInfo, annotateFunding } from './fund.js';
|
|
11
12
|
import { capabilityLabel } from './chains.js';
|
|
12
13
|
import { defaultIo, Printer } from './output.js';
|
|
13
14
|
import { Ui } from './ui.js';
|
|
14
15
|
import { recordCliEvent, saveTelemetryDecision, shouldPromptTelemetry } from './telemetry.js';
|
|
15
16
|
const DEFAULT_MCP_URL = 'https://sellag.vercel.app/api/mcp';
|
|
16
|
-
const VERSION = '0.
|
|
17
|
+
const VERSION = '0.8.0';
|
|
17
18
|
function parseFlags(argv) {
|
|
18
19
|
const flags = {
|
|
19
20
|
json: false, yes: false, noColor: false, dryRun: false, noKeychain: false,
|
|
@@ -62,6 +63,8 @@ Commands:
|
|
|
62
63
|
clients List detected agent clients (--install to write configs)
|
|
63
64
|
doctor Verify the install: endpoint, credentials, auth, wallets, pay-quote
|
|
64
65
|
status Show your key + AgentWallet balances
|
|
66
|
+
businesses Show your agent's named runs: spend, tool calls, outcome
|
|
67
|
+
skill Show the cached agent instruction bundle; 'skill update' refreshes it
|
|
65
68
|
fund Show deposit addresses + funding links to add USDC to your agent wallet
|
|
66
69
|
mcp Run as a local stdio MCP server that proxies Sella with your stored key
|
|
67
70
|
publish Publish a dataset from a CSV: 'publish init <file.csv>' then 'publish push'
|
|
@@ -86,7 +89,7 @@ Environment:
|
|
|
86
89
|
|
|
87
90
|
Links:
|
|
88
91
|
Website https://sellag.vercel.app
|
|
89
|
-
Docs https://
|
|
92
|
+
Docs https://docs.selltoagent.dev
|
|
90
93
|
Issues https://github.com/010100100100011101010100/ogsella/issues
|
|
91
94
|
`;
|
|
92
95
|
function selectClientIds(flags, detected) {
|
|
@@ -289,6 +292,17 @@ async function cmdInit(ctx, flags, printer) {
|
|
|
289
292
|
sp.fail('Verify could not run (network?) — try `sella doctor` later.');
|
|
290
293
|
verifySummary = { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
291
294
|
}
|
|
295
|
+
// Cache the agent instruction bundle alongside the credentials, so the agent reads its
|
|
296
|
+
// instructions from disk instead of re-fetching them every session. Best effort on purpose:
|
|
297
|
+
// onboarding must not fail because a documentation download did, and `sella skill update`
|
|
298
|
+
// fixes it later.
|
|
299
|
+
try {
|
|
300
|
+
const bundle = await syncSkillBundle({ env: ctx.env, origin });
|
|
301
|
+
ui.detail(`Skill bundle ${bundle.version} cached at ${bundle.dir}`);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
ui.detail('Skill bundle not cached (network?). Run `sella skill update` later.');
|
|
305
|
+
}
|
|
292
306
|
}
|
|
293
307
|
if (paired.code === 0) {
|
|
294
308
|
const awStatus = String(paired.summary?.agentWallet || 'none');
|
|
@@ -339,7 +353,7 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
|
|
|
339
353
|
io.stdout(VERSION);
|
|
340
354
|
// TTY-only: scripts capturing `sella --version` must still get a bare semver.
|
|
341
355
|
if (io.isTTY)
|
|
342
|
-
io.stdout('docs: https://
|
|
356
|
+
io.stdout('docs: https://docs.selltoagent.dev · issues: https://github.com/010100100100011101010100/ogsella/issues');
|
|
343
357
|
return 0;
|
|
344
358
|
}
|
|
345
359
|
if (flags.help) {
|
|
@@ -483,6 +497,84 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
|
|
|
483
497
|
}
|
|
484
498
|
return 0;
|
|
485
499
|
}
|
|
500
|
+
case 'skill': {
|
|
501
|
+
const origin = ctx.mcpUrl.replace(/\/api\/mcp\/?$/, '');
|
|
502
|
+
const sub = flags.positional[1];
|
|
503
|
+
if (sub === 'update') {
|
|
504
|
+
const result = await syncSkillBundle({ env: ctx.env, origin, force: flags.positional.includes('--force') });
|
|
505
|
+
printer.jsonOut(result);
|
|
506
|
+
if (!flags.json) {
|
|
507
|
+
if (result.upToDate) {
|
|
508
|
+
printer.info(`Already on ${result.version}. Nothing to download.`);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
const moved = result.previousVersion ? `${result.previousVersion} -> ${result.version}` : result.version;
|
|
512
|
+
printer.info(`Skill bundle ${moved}`);
|
|
513
|
+
printer.info(` ${result.written.length} downloaded, ${result.reused.length} unchanged`);
|
|
514
|
+
printer.info(` ${result.dir}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return 0;
|
|
518
|
+
}
|
|
519
|
+
const status = await skillStatus(ctx.env, origin);
|
|
520
|
+
printer.jsonOut(status);
|
|
521
|
+
if (!flags.json) {
|
|
522
|
+
if (!status.localVersion) {
|
|
523
|
+
printer.info(`No local bundle. Live version is ${status.remoteVersion}.`);
|
|
524
|
+
printer.info('Run `sella skill update` to cache it.');
|
|
525
|
+
}
|
|
526
|
+
else if (status.upToDate) {
|
|
527
|
+
printer.info(`Skill bundle ${status.localVersion} (current).`);
|
|
528
|
+
if (status.dir)
|
|
529
|
+
printer.info(` ${status.dir}`);
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
printer.info(`Skill bundle ${status.localVersion} is out of date. Live version is ${status.remoteVersion}.`);
|
|
533
|
+
if (status.breaking) {
|
|
534
|
+
printer.info(' This is a breaking change: a tool may have been removed or changed.');
|
|
535
|
+
}
|
|
536
|
+
printer.info(' Run `sella skill update`.');
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return status.localVersion && !status.upToDate ? 1 : 0;
|
|
540
|
+
}
|
|
541
|
+
case 'businesses': {
|
|
542
|
+
const stored = loadStoredKey(ctx.env);
|
|
543
|
+
if (!stored) {
|
|
544
|
+
printer.error('Not paired yet. Run `sella pair` (or `sella init`) first.');
|
|
545
|
+
return 2;
|
|
546
|
+
}
|
|
547
|
+
const statusFlag = flags.positional.includes('--active')
|
|
548
|
+
? 'active'
|
|
549
|
+
: flags.positional.includes('--closed')
|
|
550
|
+
? 'closed'
|
|
551
|
+
: undefined;
|
|
552
|
+
const result = await listBusinesses(ctx.mcpUrl, stored.apiKey, {
|
|
553
|
+
limit: 20,
|
|
554
|
+
...(statusFlag ? { status: statusFlag } : {}),
|
|
555
|
+
});
|
|
556
|
+
printer.jsonOut(result);
|
|
557
|
+
if (!flags.json) {
|
|
558
|
+
if (!result.businesses.length) {
|
|
559
|
+
printer.info('No businesses yet. Ask your agent to start one before it works:\n' +
|
|
560
|
+
' "Start a Sella business called <name>, then research X."');
|
|
561
|
+
return 0;
|
|
562
|
+
}
|
|
563
|
+
printer.info(`Your agent's runs (${result.count}):`);
|
|
564
|
+
for (const b of result.businesses) {
|
|
565
|
+
const t = b.totals || {};
|
|
566
|
+
const spend = `$${Number(t.spendUSDC || 0).toFixed(2)}`;
|
|
567
|
+
const mark = b.status === 'active' ? '●' : '·';
|
|
568
|
+
printer.info(` ${mark} ${b.name}`);
|
|
569
|
+
printer.info(` ${String(b.status).padEnd(7)} ${spend.padStart(8)} ${String(t.toolCalls || 0).padStart(4)} calls ${b.businessId}`);
|
|
570
|
+
const summary = b.outcome || b.goal;
|
|
571
|
+
if (summary)
|
|
572
|
+
printer.info(` ${summary.length > 88 ? `${summary.slice(0, 88)}…` : summary}`);
|
|
573
|
+
}
|
|
574
|
+
printer.info('\nFull detail: https://sellag.vercel.app/dashboard/businesses');
|
|
575
|
+
}
|
|
576
|
+
return 0;
|
|
577
|
+
}
|
|
486
578
|
case 'publish': {
|
|
487
579
|
const sub = flags.positional[1];
|
|
488
580
|
const origin = ctx.mcpUrl.replace(/\/api\/mcp\/?$/, '') || 'https://sellag.vercel.app';
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sella-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -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",
|