sella-cli 0.8.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 +1 -1
- package/dist/index.js +54 -0
- package/dist/skill.js +169 -0
- 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
|
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ 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
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';
|
|
@@ -63,6 +64,7 @@ Commands:
|
|
|
63
64
|
doctor Verify the install: endpoint, credentials, auth, wallets, pay-quote
|
|
64
65
|
status Show your key + AgentWallet balances
|
|
65
66
|
businesses Show your agent's named runs: spend, tool calls, outcome
|
|
67
|
+
skill Show the cached agent instruction bundle; 'skill update' refreshes it
|
|
66
68
|
fund Show deposit addresses + funding links to add USDC to your agent wallet
|
|
67
69
|
mcp Run as a local stdio MCP server that proxies Sella with your stored key
|
|
68
70
|
publish Publish a dataset from a CSV: 'publish init <file.csv>' then 'publish push'
|
|
@@ -290,6 +292,17 @@ async function cmdInit(ctx, flags, printer) {
|
|
|
290
292
|
sp.fail('Verify could not run (network?) — try `sella doctor` later.');
|
|
291
293
|
verifySummary = { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
292
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
|
+
}
|
|
293
306
|
}
|
|
294
307
|
if (paired.code === 0) {
|
|
295
308
|
const awStatus = String(paired.summary?.agentWallet || 'none');
|
|
@@ -484,6 +497,47 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
|
|
|
484
497
|
}
|
|
485
498
|
return 0;
|
|
486
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
|
+
}
|
|
487
541
|
case 'businesses': {
|
|
488
542
|
const stored = loadStoredKey(ctx.env);
|
|
489
543
|
if (!stored) {
|
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",
|