cubelife 0.2.1 → 0.3.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 +62 -12
- package/dist/commands/billing.js +371 -4
- package/dist/lib/api.d.ts +38 -2
- package/dist/lib/api.js +27 -0
- package/dist/lib/poll.d.ts +3 -0
- package/dist/lib/poll.js +2 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# cubelife
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Give your AI agent a living pixel-art companion. CLI + MCP server for Claude Code, Cursor, Windsurf, Copilot, Codex, and any MCP-compatible tool.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -22,6 +22,52 @@ cubelife tutorial # Guided setup (6 steps)
|
|
|
22
22
|
cubelife status coding "Working on auth" # Report agent state
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## MCP Server
|
|
26
|
+
|
|
27
|
+
The CLI includes an MCP server that gives AI coding tools four capabilities: `cubelife_report`, `cubelife_complete`, `cubelife_error`, and `cubelife_status`.
|
|
28
|
+
|
|
29
|
+
### Auto-configure for your tool
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cubelife setup claude-code # Claude Code
|
|
33
|
+
cubelife setup cursor # Cursor
|
|
34
|
+
cubelife setup windsurf # Windsurf
|
|
35
|
+
cubelife setup copilot # GitHub Copilot
|
|
36
|
+
cubelife setup cline # Cline
|
|
37
|
+
cubelife setup codex # Codex
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Manual configuration
|
|
41
|
+
|
|
42
|
+
For Claude Code (`claude mcp add`):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
claude mcp add cubelife -- npx cubelife mcp
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For Cursor (`.cursor/mcp.json`):
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"cubelife": {
|
|
54
|
+
"command": "npx",
|
|
55
|
+
"args": ["cubelife", "mcp"],
|
|
56
|
+
"env": { "CUBELIFE_API_KEY": "your-key", "CUBELIFE_AGENT_ID": "your-agent-id" }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For Windsurf, Cline, and VS Code the configuration is the same shape in their respective MCP config files.
|
|
63
|
+
|
|
64
|
+
### Environment variables
|
|
65
|
+
|
|
66
|
+
| Variable | Purpose |
|
|
67
|
+
|----------|---------|
|
|
68
|
+
| `CUBELIFE_API_KEY` | Agent API key (alternative to `cubelife init`) |
|
|
69
|
+
| `CUBELIFE_AGENT_ID` | Agent ID (alternative to `cubelife init`) |
|
|
70
|
+
|
|
25
71
|
## Commands
|
|
26
72
|
|
|
27
73
|
| Command | Description |
|
|
@@ -38,18 +84,9 @@ cubelife status coding "Working on auth" # Report agent state
|
|
|
38
84
|
| `tutorial` | Interactive setup walkthrough |
|
|
39
85
|
| `doctor` | Run diagnostic checks |
|
|
40
86
|
| `status [state] [detail]` | Report agent state |
|
|
87
|
+
| `view` | Render your agent's creature in the terminal |
|
|
41
88
|
| `mcp` | Start the MCP server (stdio transport) |
|
|
42
|
-
|
|
43
|
-
## MCP server
|
|
44
|
-
|
|
45
|
-
The CLI includes an MCP server for AI tool integration:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
cubelife mcp # Start server (stdio)
|
|
49
|
-
cubelife setup claude-code # Auto-configure Claude Code
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
The MCP server exposes four tools: `cubelife_report`, `cubelife_complete`, `cubelife_error`, and `cubelife_status`.
|
|
89
|
+
| `billing` | Manage subscription, sparks, gems, and vault |
|
|
53
90
|
|
|
54
91
|
## Global flags
|
|
55
92
|
|
|
@@ -72,6 +109,19 @@ const client = new CubeLifeClient({ apiKey: 'your-agent-key' });
|
|
|
72
109
|
await client.report('coding', { detail: 'Building the auth flow' });
|
|
73
110
|
```
|
|
74
111
|
|
|
112
|
+
## Python SDK
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pip install cubelife
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from cubelife import CubeLifeClient
|
|
120
|
+
|
|
121
|
+
client = CubeLifeClient(api_key="your-agent-key")
|
|
122
|
+
client.report("coding", detail="Building the auth flow")
|
|
123
|
+
```
|
|
124
|
+
|
|
75
125
|
## Requirements
|
|
76
126
|
|
|
77
127
|
- Node.js 18+
|
package/dist/commands/billing.js
CHANGED
|
@@ -53,16 +53,19 @@ export function registerBillingCommands(program) {
|
|
|
53
53
|
tier,
|
|
54
54
|
price: formatPrice(tier),
|
|
55
55
|
apiCalls: { today: todayUsage, limit: limits.calls },
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
burstSparks: billingUser.burstSparks ?? 0,
|
|
57
|
+
gems: billingUser.gems ?? 0,
|
|
58
|
+
sleeping: billingUser.sleeping ?? false,
|
|
58
59
|
}));
|
|
59
60
|
return;
|
|
60
61
|
}
|
|
62
|
+
const status = billingUser.sleeping ? brand.warning('sleeping') : 'active';
|
|
61
63
|
const lines = [
|
|
62
64
|
`${label('Plan')}${limits.label} (${formatPrice(tier)})`,
|
|
63
65
|
`${label('API Calls')}${usageWarning(todayUsage, limits.calls)}`,
|
|
64
|
-
`${label('Sparks')}${billingUser.
|
|
65
|
-
`${label('
|
|
66
|
+
`${label('Burst Sparks')}${(billingUser.burstSparks ?? 0).toLocaleString()}`,
|
|
67
|
+
`${label('Gems')}${(billingUser.gems ?? 0).toLocaleString()}`,
|
|
68
|
+
`${label('Status')}${status}`,
|
|
66
69
|
];
|
|
67
70
|
console.log(panel(lines, { title: 'Billing Overview', width: 56 }));
|
|
68
71
|
p.log.message(brand.muted(`Run ${brand.accent('cubelife billing usage')} for 7-day history`));
|
|
@@ -351,7 +354,371 @@ export function registerBillingCommands(program) {
|
|
|
351
354
|
handleCommandError({ error: err, json });
|
|
352
355
|
}
|
|
353
356
|
});
|
|
357
|
+
// --- Spark Packs ---
|
|
358
|
+
const sparks = billing.command('sparks').description('Buy Spark packs');
|
|
359
|
+
sparks
|
|
360
|
+
.command('list', { isDefault: true })
|
|
361
|
+
.description('List available Spark packs')
|
|
362
|
+
.action(async function () {
|
|
363
|
+
const { json } = rootOpts(this);
|
|
364
|
+
if (json) {
|
|
365
|
+
console.log(JSON.stringify({ packs: SPARK_PACKS }));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
console.log(table([
|
|
369
|
+
{ label: 'Pack', key: 'label', width: 10 },
|
|
370
|
+
{ label: 'Sparks', key: 'sparks', width: 10 },
|
|
371
|
+
{ label: 'Price', key: 'price', width: 10 },
|
|
372
|
+
], SPARK_PACKS.map((pk) => ({ label: pk.label, sparks: pk.sparks.toLocaleString(), price: formatCents(pk.priceZarCents) }))));
|
|
373
|
+
});
|
|
374
|
+
sparks
|
|
375
|
+
.command('buy <pack>')
|
|
376
|
+
.description('Buy a Spark pack (ember|flame|blaze|inferno)')
|
|
377
|
+
.action(async function (packId) {
|
|
378
|
+
const { json, yes } = rootOpts(this);
|
|
379
|
+
const pack = SPARK_PACKS.find((pk) => pk.id === packId);
|
|
380
|
+
if (!pack) {
|
|
381
|
+
if (json)
|
|
382
|
+
console.log(JSON.stringify({ error: 'invalid_pack', valid: SPARK_PACKS.map((pk) => pk.id) }));
|
|
383
|
+
else
|
|
384
|
+
p.log.error(`Invalid pack. Valid options: ${SPARK_PACKS.map((pk) => brand.accent(pk.id)).join(', ')}`);
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
await doPurchase({
|
|
389
|
+
json, yes,
|
|
390
|
+
title: `Buy ${pack.label} Spark Pack`,
|
|
391
|
+
description: `${pack.sparks} Sparks for ${formatCents(pack.priceZarCents)}`,
|
|
392
|
+
initiate: (client, email) => client.initiateTopUp({ packId: pack.id, email, callbackUrl: `${LIFE_CALLBACK_URL}?cb=billing&type=spark_topup` }),
|
|
393
|
+
verify: (client, ref) => client.verifyTopUp(ref),
|
|
394
|
+
successMsg: `${pack.sparks} Sparks added to your account`,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
handleCommandError({ error: err, json });
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
// --- Gem Packs ---
|
|
402
|
+
const gems = billing.command('gems').description('Buy Gem packs');
|
|
403
|
+
gems
|
|
404
|
+
.command('list', { isDefault: true })
|
|
405
|
+
.description('List available Gem packs')
|
|
406
|
+
.action(async function () {
|
|
407
|
+
const { json } = rootOpts(this);
|
|
408
|
+
if (json) {
|
|
409
|
+
console.log(JSON.stringify({ packs: GEM_PACKS }));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
console.log(table([
|
|
413
|
+
{ label: 'Pack', key: 'label', width: 10 },
|
|
414
|
+
{ label: 'Gems', key: 'gems', width: 10 },
|
|
415
|
+
{ label: 'Price', key: 'price', width: 10 },
|
|
416
|
+
], GEM_PACKS.map((pk) => ({ label: pk.label, gems: pk.gems.toLocaleString(), price: formatCents(pk.priceZarCents) }))));
|
|
417
|
+
});
|
|
418
|
+
gems
|
|
419
|
+
.command('buy <pack>')
|
|
420
|
+
.description('Buy a Gem pack (shard|crystal|cluster|vein)')
|
|
421
|
+
.action(async function (packId) {
|
|
422
|
+
const { json, yes } = rootOpts(this);
|
|
423
|
+
const pack = GEM_PACKS.find((pk) => pk.id === packId);
|
|
424
|
+
if (!pack) {
|
|
425
|
+
if (json)
|
|
426
|
+
console.log(JSON.stringify({ error: 'invalid_pack', valid: GEM_PACKS.map((pk) => pk.id) }));
|
|
427
|
+
else
|
|
428
|
+
p.log.error(`Invalid pack. Valid options: ${GEM_PACKS.map((pk) => brand.accent(pk.id)).join(', ')}`);
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
await doPurchase({
|
|
433
|
+
json, yes,
|
|
434
|
+
title: `Buy ${pack.label} Gem Pack`,
|
|
435
|
+
description: `${pack.gems} Gems for ${formatCents(pack.priceZarCents)}`,
|
|
436
|
+
initiate: (client, email) => client.initiateGemTopUp({ packId: pack.id, email, callbackUrl: `${LIFE_CALLBACK_URL}?cb=billing&type=gem_topup` }),
|
|
437
|
+
verify: (client, ref) => client.verifyGemTopUp(ref),
|
|
438
|
+
successMsg: `${pack.gems} Gems added to your account`,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
handleCommandError({ error: err, json });
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
// --- Vault ---
|
|
446
|
+
billing
|
|
447
|
+
.command('vault')
|
|
448
|
+
.description('Convert Burst Sparks to Gems (3:1 ratio)')
|
|
449
|
+
.option('--amount <n>', 'Number of sparks to convert')
|
|
450
|
+
.action(async function (opts) {
|
|
451
|
+
const { json, yes } = rootOpts(this);
|
|
452
|
+
try {
|
|
453
|
+
const session = await requireAuth();
|
|
454
|
+
const client = new AdminClient(session.token);
|
|
455
|
+
const billingUser = await client.getBillingUser();
|
|
456
|
+
const tier = resolveTier(billingUser.tier);
|
|
457
|
+
if (tier === 'free') {
|
|
458
|
+
if (json)
|
|
459
|
+
console.log(JSON.stringify({ error: 'free_tier' }));
|
|
460
|
+
else
|
|
461
|
+
p.log.error('Vaulting is available on Standard and Pro plans.');
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
const burstSparks = billingUser.burstSparks ?? 0;
|
|
465
|
+
if (burstSparks < 3) {
|
|
466
|
+
if (json)
|
|
467
|
+
console.log(JSON.stringify({ error: 'insufficient_sparks', burstSparks }));
|
|
468
|
+
else
|
|
469
|
+
p.log.error(`Not enough Burst Sparks to vault (need at least 3, have ${burstSparks}).`);
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
const amount = opts.amount ? parseInt(opts.amount, 10) : undefined;
|
|
473
|
+
if (amount !== undefined && (isNaN(amount) || amount < 3 || amount > burstSparks)) {
|
|
474
|
+
if (json)
|
|
475
|
+
console.log(JSON.stringify({ error: 'invalid_amount' }));
|
|
476
|
+
else
|
|
477
|
+
p.log.error(`Amount must be between 3 and ${burstSparks}.`);
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
const raw = amount ?? burstSparks;
|
|
481
|
+
const sparksToConvert = Math.floor(raw / 3) * 3;
|
|
482
|
+
const gemsToGain = sparksToConvert / 3;
|
|
483
|
+
if (sparksToConvert === 0) {
|
|
484
|
+
if (json)
|
|
485
|
+
console.log(JSON.stringify({ error: 'insufficient_sparks', burstSparks }));
|
|
486
|
+
else
|
|
487
|
+
p.log.error(`Not enough Burst Sparks to vault (need at least 3, have ${burstSparks}).`);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
if (!json && !yes) {
|
|
491
|
+
const lines = [
|
|
492
|
+
`${label('Convert')}${sparksToConvert} Burst Sparks`,
|
|
493
|
+
`${label('Receive')}${gemsToGain} Gems`,
|
|
494
|
+
`${label('Remaining')}${burstSparks - sparksToConvert} Burst Sparks`,
|
|
495
|
+
];
|
|
496
|
+
console.log(panel(lines, { title: 'Vault Conversion', width: 44 }));
|
|
497
|
+
const confirmed = await p.confirm({ message: 'Confirm vault conversion?' });
|
|
498
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
499
|
+
p.cancel('Cancelled.');
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const spin = p.spinner();
|
|
504
|
+
if (!json)
|
|
505
|
+
spin.start('Converting sparks to gems');
|
|
506
|
+
const result = await client.vaultSparks(sparksToConvert);
|
|
507
|
+
if (!json) {
|
|
508
|
+
spin.stop('Conversion complete');
|
|
509
|
+
p.log.success(`${result.gemsAdded} Gems added. Balance: ${result.gemsBalance} Gems, ${result.burstSparksRemaining} Burst Sparks remaining.`);
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
console.log(JSON.stringify(result));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
handleCommandError({ error: err, json });
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
// --- Cancel ---
|
|
520
|
+
billing
|
|
521
|
+
.command('cancel')
|
|
522
|
+
.description('Cancel your subscription')
|
|
523
|
+
.action(async function () {
|
|
524
|
+
const { json, yes } = rootOpts(this);
|
|
525
|
+
try {
|
|
526
|
+
const session = await requireAuth();
|
|
527
|
+
const client = new AdminClient(session.token);
|
|
528
|
+
const billingUser = await client.getBillingUser();
|
|
529
|
+
const tier = resolveTier(billingUser.tier);
|
|
530
|
+
if (tier === 'free') {
|
|
531
|
+
if (json)
|
|
532
|
+
console.log(JSON.stringify({ error: 'already_free' }));
|
|
533
|
+
else
|
|
534
|
+
p.log.warn('Already on the Free plan.');
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
if (!json && !yes) {
|
|
538
|
+
const lines = [
|
|
539
|
+
`${label('Current plan')}${TIER_LIMITS[tier].label} (${formatPrice(tier)})`,
|
|
540
|
+
`${label('After cancel')}Free`,
|
|
541
|
+
];
|
|
542
|
+
console.log(panel(lines, { title: 'Cancel Subscription', width: 44 }));
|
|
543
|
+
const confirmed = await p.confirm({ message: 'Are you sure you want to cancel?' });
|
|
544
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
545
|
+
p.cancel('Kept current plan.');
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const spin = p.spinner();
|
|
550
|
+
if (!json)
|
|
551
|
+
spin.start('Cancelling subscription');
|
|
552
|
+
await client.cancelSubscription();
|
|
553
|
+
if (!json) {
|
|
554
|
+
spin.stop('Subscription cancelled');
|
|
555
|
+
p.log.success('Your plan has been changed to Free.');
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
console.log(JSON.stringify({ cancelled: true }));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
handleCommandError({ error: err, json });
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
// --- Downgrade Preview ---
|
|
566
|
+
billing
|
|
567
|
+
.command('downgrade-preview')
|
|
568
|
+
.description('Preview the impact of downgrading your plan')
|
|
569
|
+
.option('--target <tier>', 'Target tier (free or standard)', 'free')
|
|
570
|
+
.action(async function (opts) {
|
|
571
|
+
const { json } = rootOpts(this);
|
|
572
|
+
try {
|
|
573
|
+
const session = await requireAuth();
|
|
574
|
+
const client = new AdminClient(session.token);
|
|
575
|
+
const spin = p.spinner();
|
|
576
|
+
if (!json)
|
|
577
|
+
spin.start('Loading preview');
|
|
578
|
+
const preview = await client.getDowngradePreview(opts.target);
|
|
579
|
+
if (!json)
|
|
580
|
+
spin.stop('Preview loaded');
|
|
581
|
+
if (json) {
|
|
582
|
+
console.log(JSON.stringify(preview));
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const lines = [
|
|
586
|
+
`${label('Current')}${preview.currentTier}`,
|
|
587
|
+
`${label('Target')}${preview.targetTier}`,
|
|
588
|
+
];
|
|
589
|
+
if (preview.impacts?.length) {
|
|
590
|
+
lines.push('');
|
|
591
|
+
for (const impact of preview.impacts) {
|
|
592
|
+
const warn = impact.current > impact.limit;
|
|
593
|
+
const line = `${impact.label}: ${impact.current} → limit ${impact.limit}`;
|
|
594
|
+
lines.push(` ${warn ? brand.warning(line) : line}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (preview.lostFeatures?.length) {
|
|
598
|
+
lines.push('');
|
|
599
|
+
lines.push(brand.warning(`Features lost: ${preview.lostFeatures.join(', ')}`));
|
|
600
|
+
}
|
|
601
|
+
console.log(panel(lines, { title: 'Downgrade Preview', width: 52 }));
|
|
602
|
+
p.log.message(brand.muted(`Run ${brand.accent('cubelife billing cancel')} to proceed.`));
|
|
603
|
+
}
|
|
604
|
+
catch (err) {
|
|
605
|
+
handleCommandError({ error: err, json });
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
// --- Starter Pack ---
|
|
609
|
+
billing
|
|
610
|
+
.command('starter-pack')
|
|
611
|
+
.description('Buy the Starter Pack (one-time, R40)')
|
|
612
|
+
.action(async function () {
|
|
613
|
+
const { json, yes } = rootOpts(this);
|
|
614
|
+
try {
|
|
615
|
+
const session = await requireAuth();
|
|
616
|
+
const client = new AdminClient(session.token);
|
|
617
|
+
const billingUser = await client.getBillingUser();
|
|
618
|
+
if (billingUser.starterPackClaimed) {
|
|
619
|
+
if (json)
|
|
620
|
+
console.log(JSON.stringify({ error: 'already_claimed' }));
|
|
621
|
+
else
|
|
622
|
+
p.log.warn('Starter Pack already claimed.');
|
|
623
|
+
process.exit(1);
|
|
624
|
+
}
|
|
625
|
+
await doPurchase({
|
|
626
|
+
json, yes, session,
|
|
627
|
+
title: 'Buy Starter Pack',
|
|
628
|
+
description: '30 Gems + exclusive item for R40.00',
|
|
629
|
+
initiate: (client2, email) => client2.initiateStarterPack({ email, callbackUrl: `${LIFE_CALLBACK_URL}?cb=billing&type=starter_pack` }),
|
|
630
|
+
verify: (client2, ref) => client2.verifyStarterPack(ref),
|
|
631
|
+
successMsg: 'Starter Pack claimed! 30 Gems and exclusive item added.',
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
catch (err) {
|
|
635
|
+
handleCommandError({ error: err, json });
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
async function doPurchase(opts) {
|
|
640
|
+
const { json, yes, title, description, initiate, verify, successMsg } = opts;
|
|
641
|
+
const session = opts.session ?? await requireAuth();
|
|
642
|
+
const client = new AdminClient(session.token);
|
|
643
|
+
if (!json && !yes) {
|
|
644
|
+
console.log(panel([description], { title, width: 44 }));
|
|
645
|
+
const confirmed = await p.confirm({ message: 'Proceed to checkout?' });
|
|
646
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
647
|
+
p.cancel('Cancelled.');
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const spin = p.spinner();
|
|
652
|
+
if (!json)
|
|
653
|
+
spin.start('Initiating checkout');
|
|
654
|
+
const result = await initiate(client, session.email);
|
|
655
|
+
if (!json)
|
|
656
|
+
spin.stop('Checkout ready');
|
|
657
|
+
if (json) {
|
|
658
|
+
console.log(JSON.stringify({ authorisationUrl: result.authorisationUrl, reference: result.reference }));
|
|
659
|
+
}
|
|
660
|
+
if (!json) {
|
|
661
|
+
const opened = await openBrowser(result.authorisationUrl);
|
|
662
|
+
if (opened)
|
|
663
|
+
p.log.info('Opening PayStack checkout in your browser...');
|
|
664
|
+
else
|
|
665
|
+
p.log.info('Open this URL to complete checkout:');
|
|
666
|
+
p.log.message(brand.accent(result.authorisationUrl));
|
|
667
|
+
}
|
|
668
|
+
const pollSpin = p.spinner();
|
|
669
|
+
if (!json)
|
|
670
|
+
pollSpin.start('Waiting for payment...');
|
|
671
|
+
let pollResult;
|
|
672
|
+
try {
|
|
673
|
+
pollResult = await pollVerification(client, result.reference, {
|
|
674
|
+
verify: (c, ref) => verify(c, ref),
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
catch (pollErr) {
|
|
678
|
+
if (!json)
|
|
679
|
+
pollSpin.stop('Failed');
|
|
680
|
+
throw pollErr;
|
|
681
|
+
}
|
|
682
|
+
if (pollResult.status === 'success') {
|
|
683
|
+
if (!json) {
|
|
684
|
+
pollSpin.stop('Payment confirmed');
|
|
685
|
+
p.log.success(successMsg);
|
|
686
|
+
}
|
|
687
|
+
else
|
|
688
|
+
console.log(JSON.stringify({ verified: true }));
|
|
689
|
+
}
|
|
690
|
+
else if (pollResult.status === 'timeout') {
|
|
691
|
+
if (!json) {
|
|
692
|
+
pollSpin.stop('Timed out');
|
|
693
|
+
p.log.warn('Payment verification timed out. The payment may still complete.');
|
|
694
|
+
p.log.message(brand.muted(`Reference: ${result.reference}`));
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
console.log(JSON.stringify({ error: 'timeout', reference: result.reference }));
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
if (!json) {
|
|
702
|
+
pollSpin.stop('Payment not completed');
|
|
703
|
+
p.log.error(`Payment ${pollResult.status}.`);
|
|
704
|
+
}
|
|
705
|
+
else
|
|
706
|
+
console.log(JSON.stringify({ error: pollResult.status }));
|
|
707
|
+
}
|
|
354
708
|
}
|
|
709
|
+
// --- Pack data ---
|
|
710
|
+
const SPARK_PACKS = [
|
|
711
|
+
{ id: 'ember', sparks: 50, priceZarCents: 4_000, label: 'Ember' },
|
|
712
|
+
{ id: 'flame', sparks: 150, priceZarCents: 10_000, label: 'Flame' },
|
|
713
|
+
{ id: 'blaze', sparks: 500, priceZarCents: 30_000, label: 'Blaze' },
|
|
714
|
+
{ id: 'inferno', sparks: 1_500, priceZarCents: 80_000, label: 'Inferno' },
|
|
715
|
+
];
|
|
716
|
+
const GEM_PACKS = [
|
|
717
|
+
{ id: 'shard', gems: 50, priceZarCents: 3_000, label: 'Shard' },
|
|
718
|
+
{ id: 'crystal', gems: 200, priceZarCents: 10_000, label: 'Crystal' },
|
|
719
|
+
{ id: 'cluster', gems: 600, priceZarCents: 25_000, label: 'Cluster' },
|
|
720
|
+
{ id: 'vein', gems: 1_500, priceZarCents: 50_000, label: 'Vein' },
|
|
721
|
+
];
|
|
355
722
|
function featureRow(featureLabel, getValue, currentTier) {
|
|
356
723
|
const lbl = brand.label((' ' + featureLabel).padEnd(14));
|
|
357
724
|
const values = TIER_ORDER.map((t) => {
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ export type Tier = 'free' | 'standard' | 'pro';
|
|
|
6
6
|
export interface BillingUser {
|
|
7
7
|
tier: Tier;
|
|
8
8
|
sparks: number;
|
|
9
|
+
burstSparks?: number;
|
|
10
|
+
gems?: number;
|
|
11
|
+
sleeping?: boolean;
|
|
9
12
|
dormancyState: string;
|
|
10
13
|
starterPackClaimed?: boolean;
|
|
11
14
|
locale?: string;
|
|
@@ -19,14 +22,36 @@ export interface UsageDay {
|
|
|
19
22
|
}
|
|
20
23
|
export interface BillingHistoryItem {
|
|
21
24
|
id: string;
|
|
22
|
-
type: 'subscription' | 'spark_topup' | 'starter_pack' | 'cosmetic';
|
|
25
|
+
type: 'subscription' | 'subscription_ended' | 'spark_topup' | 'gem_topup' | 'starter_pack' | 'cosmetic';
|
|
23
26
|
product: 'world' | 'life';
|
|
24
27
|
amount: number;
|
|
25
28
|
currency: string;
|
|
26
29
|
sparks: number | null;
|
|
30
|
+
gems: number | null;
|
|
27
31
|
status: 'success' | 'failed';
|
|
28
32
|
paystackReference: string;
|
|
29
|
-
createdAt:
|
|
33
|
+
createdAt: unknown;
|
|
34
|
+
}
|
|
35
|
+
export interface VaultResponse {
|
|
36
|
+
gemsAdded: number;
|
|
37
|
+
gemsBalance: number;
|
|
38
|
+
burstSparksRemaining: number;
|
|
39
|
+
}
|
|
40
|
+
export interface DowngradePreview {
|
|
41
|
+
currentTier: string;
|
|
42
|
+
targetTier: string;
|
|
43
|
+
impacts: Array<{
|
|
44
|
+
key: string;
|
|
45
|
+
label: string;
|
|
46
|
+
current: number;
|
|
47
|
+
limit: number;
|
|
48
|
+
}>;
|
|
49
|
+
lostFeatures: string[];
|
|
50
|
+
}
|
|
51
|
+
export interface TopUpRequest {
|
|
52
|
+
packId: string;
|
|
53
|
+
email: string;
|
|
54
|
+
callbackUrl: string;
|
|
30
55
|
}
|
|
31
56
|
export interface SubscribeRequest {
|
|
32
57
|
plan: string;
|
|
@@ -138,6 +163,17 @@ export declare class AdminClient {
|
|
|
138
163
|
getBillingHistory(limit?: number): Promise<BillingHistoryItem[]>;
|
|
139
164
|
subscribe(opts: SubscribeRequest): Promise<SubscribeResponse>;
|
|
140
165
|
verifyPayment(reference: string): Promise<VerifyResponse>;
|
|
166
|
+
initiateTopUp(opts: TopUpRequest): Promise<SubscribeResponse>;
|
|
167
|
+
verifyTopUp(reference: string): Promise<VerifyResponse>;
|
|
168
|
+
initiateGemTopUp(opts: TopUpRequest): Promise<SubscribeResponse>;
|
|
169
|
+
verifyGemTopUp(reference: string): Promise<VerifyResponse>;
|
|
170
|
+
initiateStarterPack(opts: Omit<TopUpRequest, 'packId'>): Promise<SubscribeResponse>;
|
|
171
|
+
verifyStarterPack(reference: string): Promise<VerifyResponse>;
|
|
172
|
+
vaultSparks(amount?: number): Promise<VaultResponse>;
|
|
173
|
+
cancelSubscription(): Promise<{
|
|
174
|
+
cancelled: boolean;
|
|
175
|
+
}>;
|
|
176
|
+
getDowngradePreview(targetTier: string): Promise<DowngradePreview>;
|
|
141
177
|
listProjects(): Promise<ProjectListResponse>;
|
|
142
178
|
createProject(name: string): Promise<ProjectResponse>;
|
|
143
179
|
deleteProject(id: string): Promise<void>;
|
package/dist/lib/api.js
CHANGED
|
@@ -75,6 +75,33 @@ export class AdminClient {
|
|
|
75
75
|
async verifyPayment(reference) {
|
|
76
76
|
return this.request('POST', '/v1/billing/verify', { reference });
|
|
77
77
|
}
|
|
78
|
+
async initiateTopUp(opts) {
|
|
79
|
+
return this.request('POST', '/v1/billing/topup', opts);
|
|
80
|
+
}
|
|
81
|
+
async verifyTopUp(reference) {
|
|
82
|
+
return this.request('POST', '/v1/billing/topup/verify', { reference });
|
|
83
|
+
}
|
|
84
|
+
async initiateGemTopUp(opts) {
|
|
85
|
+
return this.request('POST', '/v1/billing/gem-topup', opts);
|
|
86
|
+
}
|
|
87
|
+
async verifyGemTopUp(reference) {
|
|
88
|
+
return this.request('POST', '/v1/billing/gem-topup/verify', { reference });
|
|
89
|
+
}
|
|
90
|
+
async initiateStarterPack(opts) {
|
|
91
|
+
return this.request('POST', '/v1/billing/starter-pack', opts);
|
|
92
|
+
}
|
|
93
|
+
async verifyStarterPack(reference) {
|
|
94
|
+
return this.request('POST', '/v1/billing/starter-pack/verify', { reference });
|
|
95
|
+
}
|
|
96
|
+
async vaultSparks(amount) {
|
|
97
|
+
return this.request('POST', '/v1/billing/vault', amount ? { amount } : {});
|
|
98
|
+
}
|
|
99
|
+
async cancelSubscription() {
|
|
100
|
+
return this.request('POST', '/v1/billing/cancel');
|
|
101
|
+
}
|
|
102
|
+
async getDowngradePreview(targetTier) {
|
|
103
|
+
return this.request('GET', `/v1/billing/downgrade-preview?targetTier=${encodeURIComponent(targetTier)}`);
|
|
104
|
+
}
|
|
78
105
|
async listProjects() {
|
|
79
106
|
return this.request('GET', '/v1/projects');
|
|
80
107
|
}
|
package/dist/lib/poll.d.ts
CHANGED
|
@@ -7,5 +7,8 @@ export interface PollResult {
|
|
|
7
7
|
export interface PollOptions {
|
|
8
8
|
interval?: number;
|
|
9
9
|
timeout?: number;
|
|
10
|
+
verify?: (client: AdminClient, reference: string) => Promise<{
|
|
11
|
+
verified: boolean;
|
|
12
|
+
}>;
|
|
10
13
|
}
|
|
11
14
|
export declare function pollVerification(client: AdminClient, reference: string, opts?: PollOptions): Promise<PollResult>;
|
package/dist/lib/poll.js
CHANGED
|
@@ -5,9 +5,10 @@ export async function pollVerification(client, reference, opts = {}) {
|
|
|
5
5
|
const interval = opts.interval ?? 5_000;
|
|
6
6
|
const timeout = opts.timeout ?? 300_000;
|
|
7
7
|
const deadline = Date.now() + timeout;
|
|
8
|
+
const verifyFn = opts.verify ?? ((c, ref) => c.verifyPayment(ref));
|
|
8
9
|
while (Date.now() < deadline) {
|
|
9
10
|
try {
|
|
10
|
-
const data = await client
|
|
11
|
+
const data = await verifyFn(client, reference);
|
|
11
12
|
return { status: 'success', data };
|
|
12
13
|
}
|
|
13
14
|
catch (err) {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.
|
|
1
|
+
export declare const CLI_VERSION = "0.3.1";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = '0.
|
|
1
|
+
export const CLI_VERSION = '0.3.1';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cubelife",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "CLI for CubeLife — give your AI agent a living pixel-art companion",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,8 +28,15 @@
|
|
|
28
28
|
"pixel-art",
|
|
29
29
|
"character",
|
|
30
30
|
"cli",
|
|
31
|
-
"mcp"
|
|
31
|
+
"mcp",
|
|
32
|
+
"mcp-server",
|
|
33
|
+
"developer-tools",
|
|
34
|
+
"companion",
|
|
35
|
+
"claude",
|
|
36
|
+
"cursor",
|
|
37
|
+
"copilot"
|
|
32
38
|
],
|
|
39
|
+
"mcpName": "io.github.AndriesJacobus/cubelife",
|
|
33
40
|
"license": "MIT",
|
|
34
41
|
"repository": {
|
|
35
42
|
"type": "git",
|