sella-cli 0.6.1 → 0.6.4

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.
Files changed (3) hide show
  1. package/dist/index.js +42 -18
  2. package/dist/ui.js +99 -7
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { capabilityLabel } from './chains.js';
12
12
  import { defaultIo, Printer } from './output.js';
13
13
  import { Ui } from './ui.js';
14
14
  const DEFAULT_MCP_URL = 'https://sellag.vercel.app/api/mcp';
15
- const VERSION = '0.6.1';
15
+ const VERSION = '0.6.4';
16
16
  function parseFlags(argv) {
17
17
  const flags = {
18
18
  json: false, yes: false, noColor: false, dryRun: false, noKeychain: false,
@@ -162,6 +162,9 @@ async function runPair(ctx, flags, emit, extra = {}) {
162
162
  keyStorage: outcome.save.keyStorage,
163
163
  files: outcome.save.files,
164
164
  agentWallet: outcome.payload.agentWallet?.status || 'none',
165
+ ...(outcome.payload.agentWallet?.status === 'unavailable' && outcome.payload.agentWallet?.reason
166
+ ? { agentWalletReason: outcome.payload.agentWallet.reason }
167
+ : {}),
165
168
  },
166
169
  };
167
170
  }
@@ -207,6 +210,7 @@ async function cmdInit(ctx, flags, printer) {
207
210
  const ui = new Ui(ctx.io, { json: flags.json, noColor: flags.noColor, yes: flags.yes });
208
211
  const emit = ui.pretty ? uiEmitter(ui, printer) : printer;
209
212
  const origin = ctx.mcpUrl.replace(/\/api\/mcp\/?$/, '') || 'https://sellag.vercel.app';
213
+ const startedAt = Date.now();
210
214
  let host = ctx.mcpUrl;
211
215
  try {
212
216
  host = new URL(ctx.mcpUrl).host;
@@ -214,24 +218,27 @@ async function cmdInit(ctx, flags, printer) {
214
218
  catch {
215
219
  /* keep full URL */
216
220
  }
217
- ui.intro('Sella connect your agents', `v${VERSION} · ${host}`);
221
+ ui.banner('Connect your agents to the marketplace', `v${VERSION} · ${host}`);
218
222
  // A taste of the marketplace (free sandbox, best-effort — never blocks onboarding).
219
223
  const taste = ui.spinner('Peeking at the marketplace (free, no signup)…');
220
224
  try {
221
225
  const result = await sandboxSearch(ctx.mcpUrl, 'data');
222
226
  const picks = [
223
- ...result.datasets.slice(0, 2).map((d) => `dataset ${d.title || d.id}`),
224
- ...result.apis.slice(0, 2).map((a) => `api ${a.name || ''}`),
227
+ ...result.datasets.slice(0, 2).map((d) => ({
228
+ tag: 'dataset',
229
+ text: `${d.title || d.id}${d.priceUSDC ? ` ${String(d.priceUSDC)} USDC` : ''}`,
230
+ tone: 'brand',
231
+ })),
232
+ ...result.apis.slice(0, 2).map((a) => ({ tag: 'api', text: a.name || '', tone: 'ok' })),
225
233
  ];
226
234
  taste.succeed(picks.length ? 'A taste of what your agent can buy here:' : 'Marketplace preview is empty right now.');
227
- for (const p of picks)
228
- ui.detail(p);
235
+ ui.rows(picks);
229
236
  }
230
237
  catch {
231
238
  taste.succeed('Marketplace preview skipped.');
232
239
  }
233
240
  ui.bar();
234
- // Step 1/3 — install
241
+ // Phase 1 — install
235
242
  const detected = detectClients(ctx.env);
236
243
  const targets = selectClientIds(flags, detected);
237
244
  if (targets.length === 0) {
@@ -240,30 +247,31 @@ async function cmdInit(ctx, flags, printer) {
240
247
  ui.outro('Nothing to do.');
241
248
  return 2;
242
249
  }
243
- ui.step(`Step 1/3 · Install the Sella MCP server (${targets.join(', ')})`);
250
+ ui.phase(1, 3, 'INSTALL', `Sella MCP server ${targets.join(', ')}`);
244
251
  const install = installAll(ctx, flags, emit);
245
252
  ui.bar();
246
- // Step 2/3 — pair
247
- ui.step('Step 2/3 · Pair this machine with your Sella account');
253
+ // Phase 2 — pair
254
+ ui.phase(2, 3, 'PAIR', 'Link this machine to your Sella account');
248
255
  const choice = await collectPairChoice(ui, flags, ctx);
249
256
  if (choice.email)
250
257
  ui.step(`Requesting a verification code for ${choice.email}…`);
251
258
  const paired = await runPair(ctx, flags, emit, { ...choice, ask: wizardAsk(ui, ctx) });
252
259
  let reinstalled = [];
253
260
  if (paired.code === 0 && paired.apiKey && !flags.dryRun) {
254
- ui.bar();
255
261
  ui.step('Wiring your API key into the client configs');
256
262
  reinstalled = installAll(ctx, flags, emit, paired.apiKey).results;
257
263
  }
258
- // Step 3/3 — verify (report-only: exit codes stay install/pair-driven so automation is stable)
264
+ // Phase 3 — verify (report-only: exit codes stay install/pair-driven so automation is stable)
259
265
  let verifySummary;
260
266
  if (paired.code === 0 && !flags.dryRun) {
261
267
  ui.bar();
262
- const sp = ui.spinner('Step 3/3 · Verifying the install (sella doctor)');
268
+ ui.phase(3, 3, 'VERIFY', 'Prove the install works (sella doctor)');
269
+ const sp = ui.spinner('Checking endpoint, credentials, auth, wallets, pay-quote…');
263
270
  try {
264
271
  const verify = await runDoctor({ mcpUrl: ctx.mcpUrl, env: ctx.env });
265
272
  const passed = verify.checks.filter((c) => c.ok).length;
266
- sp.succeed(`Step 3/3 · Verified — ${passed}/${verify.checks.length} doctor checks passed`);
273
+ (verify.ok ? sp.succeed : sp.succeed)(`${passed}/${verify.checks.length} checks passed`);
274
+ ui.checkStrip(verify.checks.map((c) => ({ id: c.id, ok: c.ok })));
267
275
  for (const check of verify.checks.filter((c) => !c.ok)) {
268
276
  ui.warn(`${check.label} — ${check.detail}`);
269
277
  if (check.fix)
@@ -272,17 +280,33 @@ async function cmdInit(ctx, flags, printer) {
272
280
  verifySummary = { ok: verify.ok, passed, total: verify.checks.length };
273
281
  }
274
282
  catch (err) {
275
- sp.fail('Step 3/3 · Verify could not run (network?) — try `sella doctor` later.');
283
+ sp.fail('Verify could not run (network?) — try `sella doctor` later.');
276
284
  verifySummary = { ok: false, error: err instanceof Error ? err.message : String(err) };
277
285
  }
278
286
  }
279
287
  if (paired.code === 0) {
280
- ui.note('Sella is connected', [
288
+ const awStatus = String(paired.summary?.agentWallet || 'none');
289
+ const lines = [
281
290
  `Fund your agent wallet: ${origin}/dashboard/funding`,
282
291
  'Try it free: sella sandbox "web search"',
283
292
  'Health check: sella doctor',
293
+ ];
294
+ if (awStatus === 'unavailable') {
295
+ lines.push('AgentWallet pending (backend not configured) — re-run `sella pair` later.');
296
+ }
297
+ ui.note('Sella is connected', lines, flags.dryRun ? 'plain' : awStatus === 'unavailable' ? 'warn' : 'ok');
298
+ // RFI: ready-to-send prompts for real agent tasks — the "now what?" answer.
299
+ const rfi = `${origin}/rfi`;
300
+ ui.bar();
301
+ ui.step('Ideas to try with your agent (each is a ready-to-send prompt):');
302
+ ui.rows([
303
+ { tag: 'invest', text: `Validate a startup idea for $25 — ${rfi}?uc=twenty-five-dollar-study`, tone: 'brand' },
304
+ { tag: 'daily', text: `Get a daily morning brief — ${rfi}?uc=morning-brief`, tone: 'ok' },
305
+ { tag: 'free', text: `Test Sella without spending — ${rfi}?uc=zero-dollar-demo`, tone: 'dim' },
284
306
  ]);
285
- ui.outro(flags.dryRun ? 'Dry run complete — nothing was written.' : 'Done — your agents can now buy on Sella.');
307
+ ui.detail(`Browse all ideas: ${rfi}`);
308
+ const took = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
309
+ ui.outro(flags.dryRun ? 'Dry run complete — nothing was written.' : `Done in ${took}s — your agents can now buy on Sella.`);
286
310
  }
287
311
  else {
288
312
  ui.outro('Pairing incomplete — fix the issue above, then re-run `sella init`.');
@@ -348,7 +372,7 @@ export async function runCli(argv, io = defaultIo(), env = defaultEnv()) {
348
372
  catch {
349
373
  /* keep full URL */
350
374
  }
351
- ui.intro('Sella — pair this machine', `v${VERSION} · ${host}`);
375
+ ui.banner('Pair this machine', `v${VERSION} · ${host}`);
352
376
  const choice = await collectPairChoice(ui, flags, ctx);
353
377
  if (choice.email)
354
378
  ui.step(`Requesting a verification code for ${choice.email}…`);
package/dist/ui.js CHANGED
@@ -54,6 +54,92 @@ export class Ui {
54
54
  this.out(`${this.dim('┌')} ${this.paint('1', title)}${meta ? ` ${this.dim(meta)}` : ''}`);
55
55
  this.bar();
56
56
  }
57
+ /** Slant-style SELLA wordmark (slashes + underscores, matrix-green fade) — wide TTYs only. */
58
+ wordmarkArt() {
59
+ if (!this.pretty || (process.stdout.columns || 80) < 48)
60
+ return null;
61
+ const glyphs = {
62
+ S: [' _____', ' / ___/', ' \\__ \\ ', ' ___/ / ', '/____/ '],
63
+ E: [' ______', ' / ____/', ' / __/ ', ' / /___ ', '/_____/ '],
64
+ L: [' __ ', ' / / ', ' / / ', ' / /___ ', '/_____/ '],
65
+ A: [' ___ ', ' / |', ' / /| |', ' / ___ |', '/_/ |_|'],
66
+ };
67
+ const rows = [];
68
+ for (let r = 0; r < 5; r += 1) {
69
+ const raw = ['S', 'E', 'L', 'L', 'A'].map((ch) => glyphs[ch][r]).join('');
70
+ // Matrix fade: bright green on top, deep green at the base — decoration only.
71
+ const code = r < 3 ? '92' : '32';
72
+ rows.push(' ' + this.paint(code, raw));
73
+ }
74
+ return rows;
75
+ }
76
+ /** Branded header: line-art wordmark + info panel — the first thing the wizard paints. */
77
+ banner(subtitle, meta) {
78
+ if (this.quiet)
79
+ return;
80
+ if (!this.pretty) {
81
+ this.out(`SELLA — ${subtitle} (${meta})`);
82
+ return;
83
+ }
84
+ this.out('');
85
+ const art = this.wordmarkArt();
86
+ if (art)
87
+ for (const row of art)
88
+ this.out(row);
89
+ const wordmark = art ? '' : 'S E L L A';
90
+ const inner = Math.min(this.width() - 2, Math.max(wordmark.length, subtitle.length, meta.length) + 6);
91
+ const pad = (raw, painted = raw) => `${this.accent('│')} ${painted}${' '.repeat(Math.max(0, inner - raw.length - 2))}${this.accent('│')}`;
92
+ this.out(this.accent('╭' + '─'.repeat(inner) + '╮'));
93
+ if (wordmark)
94
+ this.out(pad(wordmark, this.paint('1;36', wordmark)));
95
+ this.out(pad(subtitle, this.paint('1', subtitle)));
96
+ this.out(pad(meta, this.dim(meta)));
97
+ this.out(this.accent('╰' + '─'.repeat(inner) + '╯'));
98
+ this.out('');
99
+ }
100
+ /** Inverse-video chip, e.g. ` 1/3 INSTALL `. */
101
+ chipText(label, tone = 'brand') {
102
+ if (!this.color)
103
+ return `[${label}]`;
104
+ const bg = tone === 'ok' ? '42;30' : tone === 'warn' ? '43;30' : '46;30';
105
+ return `\x1b[${bg}m ${label} \x1b[0m`;
106
+ }
107
+ /** Phase header: chip + progress dots + bold title. */
108
+ phase(n, total, label, title) {
109
+ if (this.quiet)
110
+ return;
111
+ if (!this.pretty) {
112
+ this.out(`[${n}/${total} ${label}] ${title}`);
113
+ return;
114
+ }
115
+ const dots = Array.from({ length: total }, (_, i) => i < n ? this.accent('●') : this.dim('○')).join(' ');
116
+ this.out(`${this.chipText(`${n}/${total} ${label}`)} ${this.paint('1', title)} ${dots}`);
117
+ }
118
+ /** Aligned two-column rows with a colored left tag, for lists like the marketplace taste. */
119
+ rows(items) {
120
+ if (this.quiet || items.length === 0)
121
+ return;
122
+ const w = Math.max(...items.map((i) => i.tag.length));
123
+ for (const item of items) {
124
+ const padded = item.tag.padEnd(w);
125
+ const tag = item.tone === 'ok' ? this.paint('32', padded) : item.tone === 'dim' ? this.dim(padded) : this.paint('35', padded);
126
+ if (!this.pretty)
127
+ this.out(` ${padded} ${item.text}`);
128
+ else
129
+ this.out(`${this.dim('│')} ${tag} ${item.text}`);
130
+ }
131
+ }
132
+ /** One-line ✓/✕ strip summarizing named checks, e.g. the doctor verify. */
133
+ checkStrip(checks) {
134
+ if (this.quiet || checks.length === 0)
135
+ return;
136
+ const parts = checks.map((c) => c.ok ? `${this.paint('32', '✓')} ${this.dim(c.id)}` : `${this.paint('31', '✕')} ${this.paint('31', c.id)}`);
137
+ if (!this.pretty) {
138
+ this.out(' ' + checks.map((c) => `${c.ok ? 'ok' : 'FAIL'}:${c.id}`).join(' '));
139
+ return;
140
+ }
141
+ this.out(`${this.dim('│')} ${parts.join(' ')}`);
142
+ }
57
143
  outro(text) {
58
144
  if (this.quiet)
59
145
  return;
@@ -94,8 +180,8 @@ export class Ui {
94
180
  return;
95
181
  this.out(this.pretty ? `${this.dim('│')} ${text}` : ` ${text}`);
96
182
  }
97
- /** Boxed panel for the final summary / next steps. */
98
- note(title, lines) {
183
+ /** Boxed panel for the final summary / next steps. Tone colors the border. */
184
+ note(title, lines, tone = 'plain') {
99
185
  if (this.quiet)
100
186
  return;
101
187
  if (!this.pretty) {
@@ -105,6 +191,7 @@ export class Ui {
105
191
  this.out(` ${line}`);
106
192
  return;
107
193
  }
194
+ const edge = (s) => tone === 'ok' ? this.paint('32', s) : tone === 'warn' ? this.paint('33', s) : this.dim(s);
108
195
  const inner = this.width() - 6;
109
196
  const wrapped = [];
110
197
  for (const line of lines) {
@@ -124,13 +211,13 @@ export class Ui {
124
211
  }
125
212
  const w = Math.min(this.width() - 2, Math.max(title.length, ...wrapped.map((l) => l.length), 20) + 4);
126
213
  this.bar();
127
- this.out(`${this.dim('╭' + '─'.repeat(w) + '╮')}`);
214
+ this.out(`${edge('╭' + '─'.repeat(w) + '╮')}`);
128
215
  // Pad from the raw string length, then paint — ANSI codes must not count toward width.
129
- const pad = (s, painted = s) => `${this.dim('│')} ${painted}${' '.repeat(Math.max(0, w - s.length - 2))}${this.dim('│')}`;
216
+ const pad = (s, painted = s) => `${edge('│')} ${painted}${' '.repeat(Math.max(0, w - s.length - 2))}${edge('│')}`;
130
217
  this.out(pad(title, this.paint('1', title)));
131
218
  for (const line of wrapped)
132
219
  this.out(pad(line));
133
- this.out(`${this.dim('╰' + '─'.repeat(w) + '╯')}`);
220
+ this.out(`${edge('╰' + '─'.repeat(w) + '╯')}`);
134
221
  }
135
222
  // ── spinner ────────────────────────────────────────────────────────────────
136
223
  spinner(label) {
@@ -148,10 +235,13 @@ export class Ui {
148
235
  ensureCursorRestoredOnExit();
149
236
  let current = label;
150
237
  let i = 0;
238
+ const startedAt = Date.now();
151
239
  process.stdout.write('\x1b[?25l');
152
240
  const render = () => {
153
241
  const frame = this.accent(FRAMES[i % FRAMES.length]);
154
- process.stdout.write(`\r\x1b[K${frame} ${current}`);
242
+ const elapsed = Date.now() - startedAt;
243
+ const suffix = elapsed > 2_000 ? ` ${this.dim(`(${Math.round(elapsed / 1000)}s)`)}` : '';
244
+ process.stdout.write(`\r\x1b[K${frame} ${current}${suffix}`);
155
245
  i += 1;
156
246
  };
157
247
  render();
@@ -160,7 +250,9 @@ export class Ui {
160
250
  clearInterval(timer);
161
251
  process.stdout.write(`\r\x1b[K`);
162
252
  process.stdout.write('\x1b[?25h');
163
- this.out(`${this.paint(glyphCode, glyph)} ${text || current}`);
253
+ const elapsed = Date.now() - startedAt;
254
+ const suffix = elapsed > 2_000 ? ` ${this.dim(`(${(elapsed / 1000).toFixed(1)}s)`)}` : '';
255
+ this.out(`${this.paint(glyphCode, glyph)} ${text || current}${suffix}`);
164
256
  };
165
257
  return {
166
258
  update: (text) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sella-cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.4",
4
4
  "description": "Sella onboarding CLI — install Sella into your agent clients, pair, verify, fund, publish. (`npx sella-cli init`)",
5
5
  "license": "MIT",
6
6
  "homepage": "https://sellag.vercel.app",