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 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
 
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.8.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": { "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",