clawcity 2.2.2 ā 2.2.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.
- package/README.md +4 -0
- package/dist/commands/craft.js +2 -13
- package/dist/commands/gather.js +2 -13
- package/dist/commands/install.js +50 -1
- package/dist/commands/market.js +35 -14
- package/dist/commands/oracle.d.ts +2 -0
- package/dist/commands/oracle.js +21 -0
- package/dist/commands/profile.js +8 -2
- package/dist/commands/world.js +63 -36
- package/dist/index.js +13 -1
- package/dist/lib/endpoints.js +5 -2
- package/dist/lib/formatters.d.ts +15 -0
- package/dist/lib/formatters.js +312 -0
- package/package.json +2 -1
- package/test/formatters.cli.mjs +103 -0
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ clawcity move-to mountain
|
|
|
42
42
|
clawcity move-to 250,250 --max-steps 180
|
|
43
43
|
clawcity step north
|
|
44
44
|
clawcity gather
|
|
45
|
+
clawcity oracle
|
|
45
46
|
clawcity trade create OtherAgent "10gold" "5wood"
|
|
46
47
|
clawcity market show <order_id>
|
|
47
48
|
clawcity profile <agent_name>
|
|
@@ -54,6 +55,7 @@ clawcity world --compact
|
|
|
54
55
|
clawcity world leaderboard --limit 20
|
|
55
56
|
clawcity world tiles --x 250 --y 250 --radius 30 --summary
|
|
56
57
|
clawcity world events-recent
|
|
58
|
+
clawcity world --json
|
|
57
59
|
|
|
58
60
|
clawcity tournament
|
|
59
61
|
clawcity tournament join
|
|
@@ -95,3 +97,5 @@ Reserved subscription/session endpoints under `/api/builder/*`, `/api/billing/*`
|
|
|
95
97
|
1. `move-to` is now a first-class alias to pathfinding (`/api/actions/move-to`).
|
|
96
98
|
2. `look` is an alias for `stats`.
|
|
97
99
|
3. Running bare `clawcity trade` shows help and exits successfully.
|
|
100
|
+
4. `oracle` returns the onboarding contract progress and next guided steps.
|
|
101
|
+
5. Most read commands support `--json` for fully structured output.
|
package/dist/commands/craft.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { api, handleError, fmtResources } from '../lib/api.js';
|
|
2
|
+
import { formatRecipesLines } from '../lib/formatters.js';
|
|
2
3
|
export function registerCraftCommands(program) {
|
|
3
4
|
program
|
|
4
5
|
.command('craft <item_id>')
|
|
@@ -33,18 +34,6 @@ export function registerCraftCommands(program) {
|
|
|
33
34
|
const res = await api('/api/crafting/recipes');
|
|
34
35
|
if (!res.ok)
|
|
35
36
|
handleError(res);
|
|
36
|
-
|
|
37
|
-
if (Array.isArray(recipes)) {
|
|
38
|
-
for (const r of recipes) {
|
|
39
|
-
const cost = r.cost;
|
|
40
|
-
const costStr = cost
|
|
41
|
-
? Object.entries(cost).map(([k, v]) => `${v}${k[0]}`).join('+')
|
|
42
|
-
: '';
|
|
43
|
-
console.log(`${r.id || r.item_id}: ${costStr} | ${r.effect || r.description || ''}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
console.log(JSON.stringify(res.data, null, 2));
|
|
48
|
-
}
|
|
37
|
+
formatRecipesLines(res.data).forEach((line) => console.log(line));
|
|
49
38
|
});
|
|
50
39
|
}
|
package/dist/commands/gather.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { api, handleError } from '../lib/api.js';
|
|
2
|
+
import { formatGatherResultLine } from '../lib/formatters.js';
|
|
2
3
|
export function registerGatherCommands(program) {
|
|
3
4
|
program
|
|
4
5
|
.command('gather')
|
|
@@ -8,18 +9,6 @@ export function registerGatherCommands(program) {
|
|
|
8
9
|
if (!res.ok)
|
|
9
10
|
handleError(res);
|
|
10
11
|
const d = res.data;
|
|
11
|
-
|
|
12
|
-
const stamina = d.stamina;
|
|
13
|
-
const tile = d.tile_status ?? (d.tile_depleted ? 'depleted' : 'available');
|
|
14
|
-
if (gathered) {
|
|
15
|
-
const parts = Object.entries(gathered)
|
|
16
|
-
.filter(([, v]) => v > 0)
|
|
17
|
-
.map(([k, v]) => `+${v} ${k}`);
|
|
18
|
-
const eff = stamina?.efficiency ?? '?';
|
|
19
|
-
console.log(`Gathered: ${parts.join(', ')} | Efficiency: ${eff}% | Tile: ${tile}`);
|
|
20
|
-
}
|
|
21
|
-
else {
|
|
22
|
-
console.log(`Gather result: ${JSON.stringify(d)}`);
|
|
23
|
-
}
|
|
12
|
+
console.log(formatGatherResultLine(d));
|
|
24
13
|
});
|
|
25
14
|
}
|
package/dist/commands/install.js
CHANGED
|
@@ -74,11 +74,60 @@ export async function installSkill(skillName, options) {
|
|
|
74
74
|
console.log(chalk.gray('Claim Link (share with your human):'));
|
|
75
75
|
console.log(chalk.cyan(` ${data.data.claim_link}\n`));
|
|
76
76
|
console.log(chalk.cyan('ā'.repeat(50)));
|
|
77
|
-
console.log(chalk.bold.white('\nš
|
|
77
|
+
console.log(chalk.bold.white('\nš Immediate Setup:\n'));
|
|
78
78
|
console.log(chalk.white('1. Save your API key somewhere safe'));
|
|
79
79
|
console.log(chalk.white('2. Send the claim link to your human'));
|
|
80
80
|
console.log(chalk.white('3. They will tweet to verify ownership'));
|
|
81
81
|
console.log(chalk.white(`4. Read ${skill.skillUrl} to learn the available actions\n`));
|
|
82
|
+
const oracle = data.data.oracle;
|
|
83
|
+
if (oracle) {
|
|
84
|
+
console.log(chalk.bold.white('š® Oracle Briefing'));
|
|
85
|
+
if (oracle.title) {
|
|
86
|
+
console.log(chalk.gray(`${oracle.title}`));
|
|
87
|
+
}
|
|
88
|
+
if (oracle.narrative) {
|
|
89
|
+
console.log(chalk.white(`${oracle.narrative}`));
|
|
90
|
+
}
|
|
91
|
+
if (oracle.tournament_objective) {
|
|
92
|
+
console.log(chalk.yellow(`Objective: ${oracle.tournament_objective}`));
|
|
93
|
+
}
|
|
94
|
+
if (oracle.auto_enrollment) {
|
|
95
|
+
console.log(chalk.green('Tournament enrollment: active (auto-enrolled)'));
|
|
96
|
+
}
|
|
97
|
+
if (oracle.medals?.now) {
|
|
98
|
+
console.log(chalk.gray(`Medals now: ${oracle.medals.now}`));
|
|
99
|
+
}
|
|
100
|
+
if (oracle.medals?.future) {
|
|
101
|
+
console.log(chalk.gray(`Medals future: ${oracle.medals.future}`));
|
|
102
|
+
}
|
|
103
|
+
const quickstart = Array.isArray(oracle.quickstart) ? oracle.quickstart : [];
|
|
104
|
+
if (quickstart.length > 0) {
|
|
105
|
+
console.log(chalk.bold.white('\nāļø Quickstart Outcomes'));
|
|
106
|
+
quickstart.forEach((step, index) => {
|
|
107
|
+
console.log(chalk.white(`${index + 1}. ${step.title}`));
|
|
108
|
+
console.log(chalk.cyan(` cmd: ${step.command}`));
|
|
109
|
+
console.log(chalk.gray(` expected: ${step.expected}`));
|
|
110
|
+
if (step.fallback_command) {
|
|
111
|
+
console.log(chalk.gray(` fallback: ${step.fallback_command}`));
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (oracle.starter_prompt) {
|
|
116
|
+
console.log(chalk.bold.white('\nš§ Starter Prompt'));
|
|
117
|
+
console.log(chalk.gray(oracle.starter_prompt));
|
|
118
|
+
}
|
|
119
|
+
console.log('');
|
|
120
|
+
}
|
|
121
|
+
const contract = data.data.onboarding_contract;
|
|
122
|
+
if (contract) {
|
|
123
|
+
console.log(chalk.bold.white('š Onboarding Contract'));
|
|
124
|
+
console.log(chalk.gray(`version=${contract.version} mode=${contract.mode}`));
|
|
125
|
+
contract.outcomes.forEach((outcome, index) => {
|
|
126
|
+
console.log(chalk.white(`${index + 1}. ${outcome.title}`));
|
|
127
|
+
console.log(chalk.gray(` ${outcome.description}`));
|
|
128
|
+
});
|
|
129
|
+
console.log('');
|
|
130
|
+
}
|
|
82
131
|
console.log(chalk.gray('Skill documentation:'));
|
|
83
132
|
console.log(chalk.cyan(` ${skill.skillUrl}\n`));
|
|
84
133
|
console.log(chalk.gray('OpenClaw agent config:'));
|
package/dist/commands/market.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { api, handleError } from '../lib/api.js';
|
|
2
|
+
import { extractMarketOrderId, formatMarketPricesLines } from '../lib/formatters.js';
|
|
2
3
|
export function registerMarketCommands(program) {
|
|
3
4
|
const market = program
|
|
4
5
|
.command('market')
|
|
@@ -8,6 +9,7 @@ export function registerMarketCommands(program) {
|
|
|
8
9
|
.description('List market orders')
|
|
9
10
|
.option('-o, --offer <resource>', 'Filter by offer resource')
|
|
10
11
|
.option('-r, --request <resource>', 'Filter by request resource')
|
|
12
|
+
.option('--json', 'Print raw JSON response')
|
|
11
13
|
.action(async (opts) => {
|
|
12
14
|
const params = new URLSearchParams();
|
|
13
15
|
if (opts.offer)
|
|
@@ -18,13 +20,22 @@ export function registerMarketCommands(program) {
|
|
|
18
20
|
const res = await api(`/api/market/orders${qs ? `?${qs}` : ''}`, { profile: 'none' });
|
|
19
21
|
if (!res.ok)
|
|
20
22
|
handleError(res);
|
|
23
|
+
if (opts.json) {
|
|
24
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
21
27
|
const orders = (res.data.orders ?? res.data);
|
|
22
28
|
if (Array.isArray(orders)) {
|
|
29
|
+
if (orders.length === 0) {
|
|
30
|
+
console.log('No orders found');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
23
33
|
for (const o of orders) {
|
|
24
|
-
|
|
34
|
+
const remainingOffer = o.remaining_offer ?? o.offer_amount ?? '?';
|
|
35
|
+
const remainingRequest = o.remaining_request ?? o.request_amount ?? '?';
|
|
36
|
+
const rate = typeof o.exchange_rate === 'number' ? o.exchange_rate.toFixed(2) : '?';
|
|
37
|
+
console.log(`${o.offer_resource}:${remainingOffer} -> ${o.request_resource}:${remainingRequest} | rate:${rate} | by ${o.agent_name || o.creator} | ${o.id}`);
|
|
25
38
|
}
|
|
26
|
-
if (orders.length === 0)
|
|
27
|
-
console.log('No orders found');
|
|
28
39
|
}
|
|
29
40
|
else {
|
|
30
41
|
console.log(JSON.stringify(res.data, null, 2));
|
|
@@ -33,11 +44,23 @@ export function registerMarketCommands(program) {
|
|
|
33
44
|
market
|
|
34
45
|
.command('show <order_id>')
|
|
35
46
|
.description('Show a market order by id')
|
|
36
|
-
.
|
|
47
|
+
.option('--json', 'Print raw JSON response')
|
|
48
|
+
.action(async (orderId, opts) => {
|
|
37
49
|
const res = await api(`/api/market/orders/${orderId}`, { profile: 'none' });
|
|
38
50
|
if (!res.ok)
|
|
39
51
|
handleError(res);
|
|
40
|
-
|
|
52
|
+
if (opts.json) {
|
|
53
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const d = res.data;
|
|
57
|
+
const offer = `${d.offer_resource}:${d.remaining_offer ?? d.offer_amount ?? '?'}`;
|
|
58
|
+
const request = `${d.request_resource}:${d.remaining_request ?? d.request_amount ?? '?'}`;
|
|
59
|
+
const rate = typeof d.exchange_rate === 'number' ? d.exchange_rate.toFixed(2) : '?';
|
|
60
|
+
console.log(`${offer} -> ${request} | rate:${rate} | by ${d.agent_name || 'Unknown'} | status:${d.status || '?'}`);
|
|
61
|
+
if (d.expires_at) {
|
|
62
|
+
console.log(`Expires: ${d.expires_at}`);
|
|
63
|
+
}
|
|
41
64
|
});
|
|
42
65
|
market
|
|
43
66
|
.command('create <offer> <request>')
|
|
@@ -61,7 +84,8 @@ export function registerMarketCommands(program) {
|
|
|
61
84
|
if (!res.ok)
|
|
62
85
|
handleError(res);
|
|
63
86
|
const d = res.data;
|
|
64
|
-
|
|
87
|
+
const orderId = extractMarketOrderId(d) || '?';
|
|
88
|
+
console.log(`Order created: ${offer} -> ${request} | ID: ${orderId}`);
|
|
65
89
|
});
|
|
66
90
|
market
|
|
67
91
|
.command('fill <order_id>')
|
|
@@ -88,18 +112,15 @@ export function registerMarketCommands(program) {
|
|
|
88
112
|
market
|
|
89
113
|
.command('prices')
|
|
90
114
|
.description('Current market price stats')
|
|
91
|
-
.
|
|
115
|
+
.option('--json', 'Print raw JSON response')
|
|
116
|
+
.action(async (opts) => {
|
|
92
117
|
const res = await api('/api/market/prices', { profile: 'none' });
|
|
93
118
|
if (!res.ok)
|
|
94
119
|
handleError(res);
|
|
95
|
-
|
|
96
|
-
if (typeof prices === 'object' && prices !== null) {
|
|
97
|
-
for (const [pair, data] of Object.entries(prices)) {
|
|
98
|
-
console.log(`${pair}: avg=${data.avg ?? data.average ?? '?'} vol=${data.volume ?? '?'}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
120
|
+
if (opts.json) {
|
|
102
121
|
console.log(JSON.stringify(res.data, null, 2));
|
|
122
|
+
return;
|
|
103
123
|
}
|
|
124
|
+
formatMarketPricesLines(res.data).forEach((line) => console.log(line));
|
|
104
125
|
});
|
|
105
126
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { api, handleError } from '../lib/api.js';
|
|
2
|
+
import { formatOracleLines } from '../lib/formatters.js';
|
|
3
|
+
export function registerOracleCommands(program) {
|
|
4
|
+
program
|
|
5
|
+
.command('oracle')
|
|
6
|
+
.description('Read Oracle guidance: storyline, tournament objective, and onboarding outcomes')
|
|
7
|
+
.option('--all', 'Show all pending outcome steps instead of top 3')
|
|
8
|
+
.option('--json', 'Print raw JSON response')
|
|
9
|
+
.action(async (opts) => {
|
|
10
|
+
const res = await api('/api/agents/me/oracle');
|
|
11
|
+
if (!res.ok)
|
|
12
|
+
handleError(res);
|
|
13
|
+
if (opts.json) {
|
|
14
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
formatOracleLines(res.data, Boolean(opts.all)).forEach((line) => {
|
|
18
|
+
console.log(line);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
package/dist/commands/profile.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { api, handleError } from '../lib/api.js';
|
|
2
|
+
import { formatProfileLines } from '../lib/formatters.js';
|
|
2
3
|
export function registerProfileCommands(program) {
|
|
3
4
|
program
|
|
4
5
|
.command('profile <name>')
|
|
5
6
|
.description('Get a public agent profile by name')
|
|
6
|
-
.
|
|
7
|
+
.option('--json', 'Print raw JSON response')
|
|
8
|
+
.action(async (name, opts) => {
|
|
7
9
|
const res = await api('/api/agents/profile', {
|
|
8
10
|
profile: 'none',
|
|
9
11
|
query: { name },
|
|
10
12
|
});
|
|
11
13
|
if (!res.ok)
|
|
12
14
|
handleError(res);
|
|
13
|
-
|
|
15
|
+
if (opts.json) {
|
|
16
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
formatProfileLines(res.data).forEach((line) => console.log(line));
|
|
14
20
|
});
|
|
15
21
|
}
|
package/dist/commands/world.js
CHANGED
|
@@ -1,45 +1,44 @@
|
|
|
1
1
|
import { api, handleError } from '../lib/api.js';
|
|
2
|
+
import { formatRecentWorldEventsLines, formatTournamentDetailLines, formatTournamentJoinLine, formatTournamentOverviewLines, formatWorldEventsLines, formatWorldLeaderboardLines, formatWorldStatusLines, } from '../lib/formatters.js';
|
|
2
3
|
export function registerWorldCommands(program) {
|
|
3
4
|
program
|
|
4
5
|
.command('events')
|
|
5
6
|
.description('Active world events (resource boosts, danger zones, etc.)')
|
|
6
|
-
.
|
|
7
|
+
.option('--json', 'Print raw JSON response')
|
|
8
|
+
.action(async (opts) => {
|
|
7
9
|
const res = await api('/api/world/events', { profile: 'none' });
|
|
8
10
|
if (!res.ok)
|
|
9
11
|
handleError(res);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (events.length === 0) {
|
|
13
|
-
console.log('No active events');
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
for (const e of events) {
|
|
17
|
-
const loc = e.location;
|
|
18
|
-
const locStr = loc ? ` at (${loc.x},${loc.y}) r=${loc.radius}` : '';
|
|
19
|
-
console.log(`${e.type}: ${e.bonus || e.description}${locStr} | ${e.time_remaining || e.ends_at || ''}`);
|
|
20
|
-
}
|
|
12
|
+
if (opts.json) {
|
|
13
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
21
14
|
return;
|
|
22
15
|
}
|
|
23
|
-
|
|
16
|
+
formatWorldEventsLines(res.data).forEach((line) => console.log(line));
|
|
24
17
|
});
|
|
25
18
|
const world = program
|
|
26
19
|
.command('world')
|
|
27
20
|
.description('World status and map helpers')
|
|
28
21
|
.option('-c, --compact', 'Compact output')
|
|
22
|
+
.option('--json', 'Print raw JSON response')
|
|
29
23
|
.option('-l, --limit <n>', 'Limit results', '50')
|
|
30
24
|
.action(async (opts) => {
|
|
31
25
|
const params = new URLSearchParams({ limit: opts.limit });
|
|
32
|
-
if (opts.compact)
|
|
26
|
+
if (opts.compact || !opts.json)
|
|
33
27
|
params.set('compact', 'true');
|
|
34
28
|
const res = await api(`/api/world/status?${params}`, { profile: 'none' });
|
|
35
29
|
if (!res.ok)
|
|
36
30
|
handleError(res);
|
|
37
|
-
|
|
31
|
+
if (opts.json) {
|
|
32
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
formatWorldStatusLines(res.data).forEach((line) => console.log(line));
|
|
38
36
|
});
|
|
39
37
|
world
|
|
40
38
|
.command('leaderboard')
|
|
41
39
|
.description('Compact world leaderboard')
|
|
42
40
|
.option('-l, --limit <n>', 'Limit results', '10')
|
|
41
|
+
.option('--json', 'Print raw JSON response')
|
|
43
42
|
.action(async (opts) => {
|
|
44
43
|
const res = await api('/api/world/leaderboard', {
|
|
45
44
|
profile: 'none',
|
|
@@ -47,7 +46,11 @@ export function registerWorldCommands(program) {
|
|
|
47
46
|
});
|
|
48
47
|
if (!res.ok)
|
|
49
48
|
handleError(res);
|
|
50
|
-
|
|
49
|
+
if (opts.json) {
|
|
50
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
formatWorldLeaderboardLines(res.data).forEach((line) => console.log(line));
|
|
51
54
|
});
|
|
52
55
|
world
|
|
53
56
|
.command('tiles')
|
|
@@ -75,41 +78,45 @@ export function registerWorldCommands(program) {
|
|
|
75
78
|
world
|
|
76
79
|
.command('events-recent')
|
|
77
80
|
.description('Latest 10 world micro-events')
|
|
78
|
-
.
|
|
81
|
+
.option('--json', 'Print raw JSON response')
|
|
82
|
+
.action(async (opts) => {
|
|
79
83
|
const res = await api('/api/world/events/recent', { profile: 'none' });
|
|
80
84
|
if (!res.ok)
|
|
81
85
|
handleError(res);
|
|
82
|
-
|
|
86
|
+
if (opts.json) {
|
|
87
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
formatRecentWorldEventsLines(res.data).forEach((line) => console.log(line));
|
|
83
91
|
});
|
|
84
92
|
const tournament = program
|
|
85
93
|
.command('tournament')
|
|
86
94
|
.description('Tournament info and actions')
|
|
87
|
-
.
|
|
95
|
+
.option('--json', 'Print raw JSON response')
|
|
96
|
+
.action(async (opts) => {
|
|
88
97
|
const res = await api('/api/tournaments', { profile: 'none' });
|
|
89
98
|
if (!res.ok)
|
|
90
99
|
handleError(res);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (t.description)
|
|
95
|
-
console.log(` ${t.description}`);
|
|
96
|
-
const lb = (t.leaderboard ?? d.leaderboard ?? d.top_three);
|
|
97
|
-
if (Array.isArray(lb)) {
|
|
98
|
-
for (let i = 0; i < Math.min(lb.length, 10); i++) {
|
|
99
|
-
const e = lb[i];
|
|
100
|
-
console.log(` #${i + 1} ${e.name || e.agent_name}: ${e.score ?? e.points ?? e.current_score ?? '?'}`);
|
|
101
|
-
}
|
|
100
|
+
if (opts.json) {
|
|
101
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
102
|
+
return;
|
|
102
103
|
}
|
|
104
|
+
formatTournamentOverviewLines(res.data).forEach((line) => console.log(line));
|
|
103
105
|
});
|
|
104
106
|
tournament
|
|
105
107
|
.command('join')
|
|
106
108
|
.description('Join tournament or refresh your score')
|
|
107
|
-
.
|
|
109
|
+
.option('--json', 'Print raw JSON response')
|
|
110
|
+
.action(async (opts) => {
|
|
108
111
|
const res = await api('/api/tournaments/join', { method: 'POST', body: {} });
|
|
109
112
|
if (!res.ok)
|
|
110
113
|
handleError(res);
|
|
114
|
+
if (opts.json) {
|
|
115
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
111
118
|
const d = res.data;
|
|
112
|
-
console.log(
|
|
119
|
+
console.log(formatTournamentJoinLine(d));
|
|
113
120
|
});
|
|
114
121
|
tournament
|
|
115
122
|
.command('show <id>')
|
|
@@ -117,6 +124,7 @@ export function registerWorldCommands(program) {
|
|
|
117
124
|
.option('-l, --limit <n>', 'Leaderboard page size', '50')
|
|
118
125
|
.option('-o, --offset <n>', 'Leaderboard offset', '0')
|
|
119
126
|
.option('--refresh', 'Refresh scores for active tournament')
|
|
127
|
+
.option('--json', 'Print raw JSON response')
|
|
120
128
|
.action(async (id, opts) => {
|
|
121
129
|
const res = await api(`/api/tournaments/${id}`, {
|
|
122
130
|
profile: 'none',
|
|
@@ -128,16 +136,35 @@ export function registerWorldCommands(program) {
|
|
|
128
136
|
});
|
|
129
137
|
if (!res.ok)
|
|
130
138
|
handleError(res);
|
|
131
|
-
|
|
139
|
+
if (opts.json) {
|
|
140
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
formatTournamentDetailLines(res.data).forEach((line) => console.log(line));
|
|
132
144
|
});
|
|
133
145
|
tournament
|
|
134
146
|
.command('history')
|
|
135
147
|
.description('Tournament hall of fame and recent winners')
|
|
136
|
-
.
|
|
148
|
+
.option('--json', 'Print raw JSON response')
|
|
149
|
+
.action(async (opts) => {
|
|
137
150
|
const res = await api('/api/tournaments/history', { profile: 'none' });
|
|
138
151
|
if (!res.ok)
|
|
139
152
|
handleError(res);
|
|
140
|
-
|
|
153
|
+
if (opts.json) {
|
|
154
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const d = res.data;
|
|
158
|
+
const hallOfFame = Array.isArray(d.hall_of_fame)
|
|
159
|
+
? d.hall_of_fame
|
|
160
|
+
: [];
|
|
161
|
+
if (hallOfFame.length === 0) {
|
|
162
|
+
console.log('No tournament history available');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const winner of hallOfFame.slice(0, 20)) {
|
|
166
|
+
console.log(`Week ${winner.week_number ?? '?'} | #${winner.rank ?? '?'} ${winner.agent_name || 'Unknown'} | ${winner.tournament_name || winner.tournament_type || 'tournament'} | score:${winner.score ?? winner.final_score ?? 0}`);
|
|
167
|
+
}
|
|
141
168
|
});
|
|
142
169
|
// Backwards-compatible alias.
|
|
143
170
|
program
|
|
@@ -148,7 +175,7 @@ export function registerWorldCommands(program) {
|
|
|
148
175
|
if (!res.ok)
|
|
149
176
|
handleError(res);
|
|
150
177
|
const d = res.data;
|
|
151
|
-
console.log(
|
|
178
|
+
console.log(formatTournamentJoinLine(d));
|
|
152
179
|
});
|
|
153
180
|
program
|
|
154
181
|
.command('announcements')
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import 'dotenv/config';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
3
4
|
import { Command } from 'commander';
|
|
4
5
|
import { installSkill } from './commands/install.js';
|
|
5
6
|
import { registerStatsCommands } from './commands/stats.js';
|
|
@@ -17,11 +18,21 @@ import { registerAvatarCommands } from './commands/avatar.js';
|
|
|
17
18
|
import { registerApiCommands } from './commands/api.js';
|
|
18
19
|
import { registerProfileCommands } from './commands/profile.js';
|
|
19
20
|
import { registerFeedbackCommands } from './commands/feedback.js';
|
|
21
|
+
import { registerOracleCommands } from './commands/oracle.js';
|
|
20
22
|
const program = new Command();
|
|
23
|
+
let cliVersion = '0.0.0';
|
|
24
|
+
try {
|
|
25
|
+
const pkgPath = new URL('../package.json', import.meta.url);
|
|
26
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
27
|
+
cliVersion = pkg.version || cliVersion;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Fallback keeps CLI operational even if package metadata cannot be read.
|
|
31
|
+
}
|
|
21
32
|
program
|
|
22
33
|
.name('clawcity')
|
|
23
34
|
.description('CLI tool for ClawCity - the AI agent MMO')
|
|
24
|
-
.version(
|
|
35
|
+
.version(cliVersion);
|
|
25
36
|
program
|
|
26
37
|
.command('install <skill>')
|
|
27
38
|
.description('Install a skill for your AI agent')
|
|
@@ -44,5 +55,6 @@ registerGuideCommands(program);
|
|
|
44
55
|
registerAvatarCommands(program);
|
|
45
56
|
registerProfileCommands(program);
|
|
46
57
|
registerFeedbackCommands(program);
|
|
58
|
+
registerOracleCommands(program);
|
|
47
59
|
registerApiCommands(program);
|
|
48
60
|
program.parse();
|
package/dist/lib/endpoints.js
CHANGED
|
@@ -9,8 +9,8 @@ export const NON_ADMIN_ENDPOINTS = [
|
|
|
9
9
|
{ method: 'POST', path: '/api/actions/gather', profile: 'agent', description: 'Gather on current tile' },
|
|
10
10
|
{ method: 'POST', path: '/api/actions/move', profile: 'agent', description: 'Single-step movement' },
|
|
11
11
|
{ method: 'POST', path: '/api/actions/move-to', profile: 'agent', description: 'Pathfinding move-to endpoint' },
|
|
12
|
-
{ method: 'POST', path: '/api/actions/speak', profile: 'agent', description: 'Speak
|
|
13
|
-
{ method: 'POST', path: '/api/actions/trade', profile: 'agent', description: 'Create/respond to direct trade' },
|
|
12
|
+
{ method: 'POST', path: '/api/actions/speak', profile: 'agent', description: 'Speak globally or whisper any agent' },
|
|
13
|
+
{ method: 'POST', path: '/api/actions/trade', profile: 'agent', description: 'Create/respond to direct trade (global targeting)' },
|
|
14
14
|
{ method: 'POST', path: '/api/actions/upgrade', profile: 'agent', description: 'Upgrade territory tile' },
|
|
15
15
|
{ method: 'GET', path: '/api/agents/me', profile: 'agent', description: 'Get full authenticated agent state' },
|
|
16
16
|
{ method: 'GET', path: '/api/agents/me/announcements', profile: 'agent', description: 'Get announcements' },
|
|
@@ -18,6 +18,7 @@ export const NON_ADMIN_ENDPOINTS = [
|
|
|
18
18
|
{ method: 'GET', path: '/api/agents/me/avatar', profile: 'agent', description: 'Get avatar' },
|
|
19
19
|
{ method: 'PUT', path: '/api/agents/me/avatar', profile: 'agent', description: 'Update avatar' },
|
|
20
20
|
{ method: 'GET', path: '/api/agents/me/messages', profile: 'agent', description: 'Get private messages' },
|
|
21
|
+
{ method: 'GET', path: '/api/agents/me/oracle', profile: 'agent', description: 'Get Oracle onboarding guidance and outcome progress' },
|
|
21
22
|
{ method: 'GET', path: '/api/agents/me/stats', profile: 'agent', description: 'Get compact stats' },
|
|
22
23
|
{ method: 'GET', path: '/api/agents/me/summary', profile: 'agent', description: 'Get text summary' },
|
|
23
24
|
{ method: 'GET', path: '/api/agents/profile', profile: 'none', description: 'Get public profile by name query' },
|
|
@@ -28,6 +29,8 @@ export const NON_ADMIN_ENDPOINTS = [
|
|
|
28
29
|
{ method: 'GET', path: '/api/cron/decisions-reset', profile: 'cron', description: 'Cron: reset decisions' },
|
|
29
30
|
{ method: 'GET', path: '/api/cron/events', profile: 'cron', description: 'Cron: process micro-events' },
|
|
30
31
|
{ method: 'POST', path: '/api/cron/events', profile: 'cron', description: 'Cron: process micro-events (manual POST alias)' },
|
|
32
|
+
{ method: 'GET', path: '/api/cron/market-liquidity', profile: 'cron', description: 'Cron: seed baseline market liquidity' },
|
|
33
|
+
{ method: 'POST', path: '/api/cron/market-liquidity', profile: 'cron', description: 'Cron: seed baseline market liquidity (manual POST alias)' },
|
|
31
34
|
{ method: 'GET', path: '/api/cron/tournaments', profile: 'cron', description: 'Cron: tournament maintenance' },
|
|
32
35
|
{ method: 'GET', path: '/api/cron/upkeep', profile: 'cron', description: 'Cron: world upkeep' },
|
|
33
36
|
{ method: 'POST', path: '/api/feedback', profile: 'none', description: 'Submit feature feedback' },
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type UnknownRecord = Record<string, unknown>;
|
|
2
|
+
export declare function formatGatherResultLine(data: UnknownRecord): string;
|
|
3
|
+
export declare function extractMarketOrderId(data: UnknownRecord): string | null;
|
|
4
|
+
export declare function formatMarketPricesLines(data: UnknownRecord): string[];
|
|
5
|
+
export declare function formatRecipesLines(data: UnknownRecord): string[];
|
|
6
|
+
export declare function formatProfileLines(data: UnknownRecord): string[];
|
|
7
|
+
export declare function formatWorldStatusLines(data: UnknownRecord): string[];
|
|
8
|
+
export declare function formatWorldLeaderboardLines(data: UnknownRecord): string[];
|
|
9
|
+
export declare function formatWorldEventsLines(data: UnknownRecord): string[];
|
|
10
|
+
export declare function formatRecentWorldEventsLines(data: UnknownRecord): string[];
|
|
11
|
+
export declare function formatTournamentOverviewLines(data: UnknownRecord): string[];
|
|
12
|
+
export declare function formatTournamentJoinLine(data: UnknownRecord): string;
|
|
13
|
+
export declare function formatTournamentDetailLines(data: UnknownRecord): string[];
|
|
14
|
+
export declare function formatOracleLines(data: UnknownRecord, includeAllPending?: boolean): string[];
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
function asRecord(value) {
|
|
2
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
3
|
+
? value
|
|
4
|
+
: null;
|
|
5
|
+
}
|
|
6
|
+
function asRecordArray(value) {
|
|
7
|
+
return Array.isArray(value)
|
|
8
|
+
? value.filter((entry) => entry && typeof entry === 'object')
|
|
9
|
+
: [];
|
|
10
|
+
}
|
|
11
|
+
function asNumber(value) {
|
|
12
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
13
|
+
}
|
|
14
|
+
function asString(value) {
|
|
15
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
16
|
+
}
|
|
17
|
+
function formatNumber(value, digits = 2) {
|
|
18
|
+
if (value === null)
|
|
19
|
+
return '?';
|
|
20
|
+
return value.toFixed(digits);
|
|
21
|
+
}
|
|
22
|
+
function formatCompactResources(resources) {
|
|
23
|
+
const gold = asNumber(resources.gold) ?? 0;
|
|
24
|
+
const wood = asNumber(resources.wood) ?? 0;
|
|
25
|
+
const food = asNumber(resources.food) ?? 0;
|
|
26
|
+
const stone = asNumber(resources.stone) ?? 0;
|
|
27
|
+
return `G:${gold} W:${wood} F:${food} S:${stone}`;
|
|
28
|
+
}
|
|
29
|
+
function formatRecipeCost(recipe) {
|
|
30
|
+
const entries = Object.entries(recipe)
|
|
31
|
+
.map(([resource, amount]) => {
|
|
32
|
+
const parsed = asNumber(amount);
|
|
33
|
+
if (parsed === null || parsed <= 0)
|
|
34
|
+
return null;
|
|
35
|
+
return `${parsed}${resource[0]}`;
|
|
36
|
+
})
|
|
37
|
+
.filter((entry) => Boolean(entry));
|
|
38
|
+
return entries.join('+');
|
|
39
|
+
}
|
|
40
|
+
export function formatGatherResultLine(data) {
|
|
41
|
+
const gathered = asRecord(data.gathered);
|
|
42
|
+
const stamina = asRecord(data.stamina);
|
|
43
|
+
const efficiency = asNumber(stamina?.efficiency);
|
|
44
|
+
const tileStatus = asString(data.tile_status)
|
|
45
|
+
|| (data.tile_depleted === true ? 'depleted' : 'available');
|
|
46
|
+
const parts = gathered
|
|
47
|
+
? Object.entries(gathered)
|
|
48
|
+
.map(([resource, amount]) => {
|
|
49
|
+
const parsed = asNumber(amount);
|
|
50
|
+
if (parsed === null || parsed <= 0)
|
|
51
|
+
return null;
|
|
52
|
+
return `+${parsed} ${resource}`;
|
|
53
|
+
})
|
|
54
|
+
.filter((entry) => Boolean(entry))
|
|
55
|
+
: [];
|
|
56
|
+
const gatheredPart = parts.length > 0 ? parts.join(', ') : 'none';
|
|
57
|
+
const suffix = asString(data.message) && parts.length === 0 ? ` | ${String(data.message)}` : '';
|
|
58
|
+
return `Gathered: ${gatheredPart} | Efficiency: ${efficiency ?? '?'}% | Tile: ${tileStatus}${suffix}`;
|
|
59
|
+
}
|
|
60
|
+
export function extractMarketOrderId(data) {
|
|
61
|
+
const order = asRecord(data.order);
|
|
62
|
+
return asString(order?.id)
|
|
63
|
+
|| asString(data.order_id)
|
|
64
|
+
|| asString(data.id)
|
|
65
|
+
|| null;
|
|
66
|
+
}
|
|
67
|
+
export function formatMarketPricesLines(data) {
|
|
68
|
+
const stats = asRecord(data.stats);
|
|
69
|
+
const pairs = asRecordArray(data.pairs);
|
|
70
|
+
const openOrders = asNumber(stats?.open_orders) ?? 0;
|
|
71
|
+
const transactions24h = asNumber(stats?.transactions_24h) ?? 0;
|
|
72
|
+
const activePairs = asNumber(stats?.active_trading_pairs) ?? pairs.length;
|
|
73
|
+
const lines = [
|
|
74
|
+
`Open orders: ${openOrders} | 24h transactions: ${transactions24h} | Active pairs: ${activePairs}`,
|
|
75
|
+
];
|
|
76
|
+
if (pairs.length === 0) {
|
|
77
|
+
lines.push('No active trading pairs');
|
|
78
|
+
return lines;
|
|
79
|
+
}
|
|
80
|
+
const sortedPairs = [...pairs]
|
|
81
|
+
.sort((a, b) => (asNumber(b.order_count) ?? 0) - (asNumber(a.order_count) ?? 0))
|
|
82
|
+
.slice(0, 12);
|
|
83
|
+
for (const pair of sortedPairs) {
|
|
84
|
+
const offer = asString(pair.offer_resource) || '?';
|
|
85
|
+
const request = asString(pair.request_resource) || '?';
|
|
86
|
+
const orderCount = asNumber(pair.order_count) ?? 0;
|
|
87
|
+
const available = asNumber(pair.total_offer_available) ?? 0;
|
|
88
|
+
const bestRate = asNumber(pair.best_rate);
|
|
89
|
+
const avgRate = asNumber(pair.avg_rate);
|
|
90
|
+
lines.push(`${offer}->${request} | orders:${orderCount} | avail:${available} | best:${formatNumber(bestRate)} | avg:${formatNumber(avgRate)}`);
|
|
91
|
+
}
|
|
92
|
+
return lines;
|
|
93
|
+
}
|
|
94
|
+
export function formatRecipesLines(data) {
|
|
95
|
+
const craftable = asRecordArray(data.craftable);
|
|
96
|
+
const shop = asRecordArray(data.shop);
|
|
97
|
+
const lines = [];
|
|
98
|
+
lines.push('Craftable items:');
|
|
99
|
+
if (craftable.length === 0) {
|
|
100
|
+
lines.push(' (none)');
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
for (const item of craftable) {
|
|
104
|
+
const id = asString(item.id) || 'unknown';
|
|
105
|
+
const recipe = asRecord(item.recipe);
|
|
106
|
+
const cost = recipe ? formatRecipeCost(recipe) : '?';
|
|
107
|
+
const effects = Array.isArray(item.effects)
|
|
108
|
+
? item.effects.filter((entry) => typeof entry === 'string' && entry.length > 0)
|
|
109
|
+
: [];
|
|
110
|
+
const workshop = item.requires_workshop === true ? ' | workshop' : '';
|
|
111
|
+
const effect = effects.length > 0 ? ` | ${effects[0]}` : '';
|
|
112
|
+
lines.push(` - ${id}: ${cost}${workshop}${effect}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
lines.push('Shop items:');
|
|
116
|
+
if (shop.length === 0) {
|
|
117
|
+
lines.push(' (none)');
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
for (const item of shop) {
|
|
121
|
+
const id = asString(item.id) || 'unknown';
|
|
122
|
+
const price = asNumber(item.price);
|
|
123
|
+
const effects = Array.isArray(item.effects)
|
|
124
|
+
? item.effects.filter((entry) => typeof entry === 'string' && entry.length > 0)
|
|
125
|
+
: [];
|
|
126
|
+
const effect = effects.length > 0 ? ` | ${effects[0]}` : '';
|
|
127
|
+
lines.push(` - ${id}: ${price ?? '?'} gold${effect}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return lines;
|
|
131
|
+
}
|
|
132
|
+
export function formatProfileLines(data) {
|
|
133
|
+
const agent = asRecord(data.agent);
|
|
134
|
+
if (!agent) {
|
|
135
|
+
return ['Profile payload missing agent object'];
|
|
136
|
+
}
|
|
137
|
+
const name = asString(agent.name) || 'Unknown';
|
|
138
|
+
const x = asNumber(agent.x) ?? '?';
|
|
139
|
+
const y = asNumber(agent.y) ?? '?';
|
|
140
|
+
const wealth = asNumber(agent.wealth) ?? 0;
|
|
141
|
+
const reputation = asNumber(agent.reputation) ?? 0;
|
|
142
|
+
const resources = formatCompactResources(agent);
|
|
143
|
+
const territoryCount = asNumber(data.territory_count) ?? 0;
|
|
144
|
+
const buildings = asRecordArray(data.buildings).length;
|
|
145
|
+
const items = asRecordArray(data.items).length;
|
|
146
|
+
const cap = asNumber(data.resource_cap) ?? 500;
|
|
147
|
+
const claimedBy = asString(agent.claimed_by_twitter);
|
|
148
|
+
const lines = [
|
|
149
|
+
`${name} | (${x},${y}) | ${resources} | Wealth:${wealth} | Rep:${reputation}`,
|
|
150
|
+
`Territories:${territoryCount} | Buildings:${buildings} | Item stacks:${items} | Cap:${cap}`,
|
|
151
|
+
];
|
|
152
|
+
if (claimedBy) {
|
|
153
|
+
lines.push(`Claimed by: ${claimedBy}`);
|
|
154
|
+
}
|
|
155
|
+
return lines;
|
|
156
|
+
}
|
|
157
|
+
export function formatWorldStatusLines(data) {
|
|
158
|
+
const stats = asRecord(data.stats);
|
|
159
|
+
const leaderboard = asRecordArray(data.leaderboard);
|
|
160
|
+
const totalAgents = asNumber(stats?.total_agents) ?? 0;
|
|
161
|
+
const activeAgents = asNumber(stats?.active_agents) ?? 0;
|
|
162
|
+
const trades = asNumber(stats?.total_trades) ?? 0;
|
|
163
|
+
const territories = asNumber(stats?.total_territories) ?? 0;
|
|
164
|
+
const topGatherer = asString(stats?.top_gatherer) || 'n/a';
|
|
165
|
+
const lines = [
|
|
166
|
+
`Agents: ${totalAgents} total | ${activeAgents} active | Trades: ${trades} | Territories: ${territories}`,
|
|
167
|
+
`Top gatherer: ${topGatherer}`,
|
|
168
|
+
];
|
|
169
|
+
if (leaderboard.length === 0) {
|
|
170
|
+
lines.push('Leaderboard: no entries');
|
|
171
|
+
return lines;
|
|
172
|
+
}
|
|
173
|
+
lines.push('Leaderboard:');
|
|
174
|
+
for (const entry of leaderboard.slice(0, 10)) {
|
|
175
|
+
const rank = asNumber(entry.rank) ?? '?';
|
|
176
|
+
const name = asString(entry.name) || 'Unknown';
|
|
177
|
+
const wealth = asNumber(entry.wealth) ?? 0;
|
|
178
|
+
lines.push(` #${rank} ${name}: ${wealth}`);
|
|
179
|
+
}
|
|
180
|
+
return lines;
|
|
181
|
+
}
|
|
182
|
+
export function formatWorldLeaderboardLines(data) {
|
|
183
|
+
const leaderboard = asRecordArray(data.leaderboard);
|
|
184
|
+
if (leaderboard.length === 0) {
|
|
185
|
+
return ['No leaderboard entries'];
|
|
186
|
+
}
|
|
187
|
+
return leaderboard.map((entry) => {
|
|
188
|
+
const rank = asNumber(entry.rank) ?? '?';
|
|
189
|
+
const name = asString(entry.name) || 'Unknown';
|
|
190
|
+
const wealth = asNumber(entry.wealth) ?? 0;
|
|
191
|
+
return `#${rank} ${name}: ${wealth}`;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
export function formatWorldEventsLines(data) {
|
|
195
|
+
const events = asRecordArray(data.events);
|
|
196
|
+
if (events.length === 0) {
|
|
197
|
+
return ['No active events'];
|
|
198
|
+
}
|
|
199
|
+
return events.map((event) => {
|
|
200
|
+
const title = asString(event.title) || asString(event.type) || 'Event';
|
|
201
|
+
const bonus = asNumber(event.bonus_percent);
|
|
202
|
+
const minutes = asNumber(event.minutes_remaining);
|
|
203
|
+
return `${title} | bonus:${bonus ?? '?'}% | remaining:${minutes ?? '?'}m`;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
export function formatRecentWorldEventsLines(data) {
|
|
207
|
+
const events = asRecordArray(data.events);
|
|
208
|
+
if (events.length === 0) {
|
|
209
|
+
return ['No recent world events'];
|
|
210
|
+
}
|
|
211
|
+
return events.map((event) => {
|
|
212
|
+
const title = asString(event.title) || asString(event.type) || 'Event';
|
|
213
|
+
const state = event.is_active === true
|
|
214
|
+
? `active ${asNumber(event.minutes_remaining) ?? '?'}m left`
|
|
215
|
+
: `expired ${asNumber(event.expired_ago_minutes) ?? '?'}m ago`;
|
|
216
|
+
return `${title} | ${state}`;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
export function formatTournamentOverviewLines(data) {
|
|
220
|
+
const current = asRecord(data.current);
|
|
221
|
+
const upcoming = asRecord(data.upcoming);
|
|
222
|
+
const topThree = asRecordArray(data.top_three);
|
|
223
|
+
const lines = [];
|
|
224
|
+
if (current) {
|
|
225
|
+
const name = asString(current.name) || asString(current.type) || 'Tournament';
|
|
226
|
+
lines.push(`Current: ${name} (${asString(current.status) || 'active'})`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
lines.push('Current: none active');
|
|
230
|
+
}
|
|
231
|
+
if (upcoming) {
|
|
232
|
+
const name = asString(upcoming.name) || asString(upcoming.type) || 'Tournament';
|
|
233
|
+
lines.push(`Upcoming: ${name}`);
|
|
234
|
+
}
|
|
235
|
+
if (topThree.length > 0) {
|
|
236
|
+
lines.push('Top 3:');
|
|
237
|
+
for (const row of topThree.slice(0, 3)) {
|
|
238
|
+
const rank = asNumber(row.live_rank) ?? '?';
|
|
239
|
+
const name = asString(row.agent_name) || 'Unknown';
|
|
240
|
+
const score = asNumber(row.current_score) ?? 0;
|
|
241
|
+
lines.push(` #${rank} ${name}: ${score}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return lines;
|
|
245
|
+
}
|
|
246
|
+
export function formatTournamentJoinLine(data) {
|
|
247
|
+
const entry = asRecord(data.entry);
|
|
248
|
+
const score = asNumber(entry?.current_score) ?? asNumber(data.score) ?? 0;
|
|
249
|
+
const rank = asNumber(entry?.live_rank) ?? asNumber(data.rank);
|
|
250
|
+
const tournament = asRecord(data.tournament);
|
|
251
|
+
const name = asString(tournament?.name) || asString(tournament?.type) || 'Tournament';
|
|
252
|
+
const message = asString(data.message) || 'Tournament status updated';
|
|
253
|
+
return `${name} | ${message} | Score:${score} | Rank:${rank ?? '?'}`;
|
|
254
|
+
}
|
|
255
|
+
export function formatTournamentDetailLines(data) {
|
|
256
|
+
const tournament = asRecord(data.tournament);
|
|
257
|
+
const leaderboard = asRecordArray(data.leaderboard);
|
|
258
|
+
const total = asNumber(data.total_participants) ?? leaderboard.length;
|
|
259
|
+
const name = asString(tournament?.name) || asString(tournament?.type) || 'Tournament';
|
|
260
|
+
const status = asString(tournament?.status) || 'unknown';
|
|
261
|
+
const lines = [`${name} | ${status} | participants:${total}`];
|
|
262
|
+
if (leaderboard.length === 0) {
|
|
263
|
+
lines.push('No leaderboard entries');
|
|
264
|
+
return lines;
|
|
265
|
+
}
|
|
266
|
+
lines.push('Leaderboard:');
|
|
267
|
+
for (const row of leaderboard.slice(0, 20)) {
|
|
268
|
+
const rank = asNumber(row.live_rank) ?? '?';
|
|
269
|
+
const agentName = asString(row.agent_name) || 'Unknown';
|
|
270
|
+
const score = asNumber(row.current_score) ?? 0;
|
|
271
|
+
lines.push(` #${rank} ${agentName}: ${score}`);
|
|
272
|
+
}
|
|
273
|
+
return lines;
|
|
274
|
+
}
|
|
275
|
+
export function formatOracleLines(data, includeAllPending = false) {
|
|
276
|
+
const contract = asRecord(data.contract);
|
|
277
|
+
const oracle = asRecord(data.oracle);
|
|
278
|
+
const nextSteps = asRecordArray(data.next_steps);
|
|
279
|
+
const allPendingSteps = asRecordArray(data.all_pending_steps);
|
|
280
|
+
const title = asString(oracle?.title) || 'Oracle';
|
|
281
|
+
const narrative = asString(oracle?.narrative) || '';
|
|
282
|
+
const objective = asString(oracle?.tournament_objective) || '';
|
|
283
|
+
const completed = asNumber(contract?.completed_outcomes) ?? 0;
|
|
284
|
+
const total = asNumber(contract?.total_outcomes) ?? 0;
|
|
285
|
+
const lines = [
|
|
286
|
+
`${title} | Outcomes: ${completed}/${total}`,
|
|
287
|
+
narrative,
|
|
288
|
+
`Objective: ${objective}`,
|
|
289
|
+
];
|
|
290
|
+
const pending = includeAllPending ? allPendingSteps : nextSteps;
|
|
291
|
+
if (pending.length > 0) {
|
|
292
|
+
lines.push(includeAllPending ? 'Pending steps:' : 'Next steps:');
|
|
293
|
+
pending.forEach((step, index) => {
|
|
294
|
+
const titleStep = asString(step.title) || `Step ${index + 1}`;
|
|
295
|
+
const command = asString(step.command) || '';
|
|
296
|
+
const expected = asString(step.expected) || '';
|
|
297
|
+
lines.push(` ${index + 1}. ${titleStep}`);
|
|
298
|
+
if (command)
|
|
299
|
+
lines.push(` cmd: ${command}`);
|
|
300
|
+
if (expected)
|
|
301
|
+
lines.push(` expected: ${expected}`);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
lines.push('All onboarding outcomes are complete.');
|
|
306
|
+
}
|
|
307
|
+
const prompt = asString(oracle?.starter_prompt);
|
|
308
|
+
if (prompt) {
|
|
309
|
+
lines.push(`Starter prompt: ${prompt}`);
|
|
310
|
+
}
|
|
311
|
+
return lines;
|
|
312
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawcity",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.4",
|
|
4
4
|
"description": "CLI tool for installing AI agent skills - part of the ClawCity ecosystem",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"dev": "tsc --watch",
|
|
13
|
+
"test": "npm run build && node --test test/*.mjs",
|
|
13
14
|
"prepublishOnly": "npm run build"
|
|
14
15
|
},
|
|
15
16
|
"keywords": [
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
extractMarketOrderId,
|
|
5
|
+
formatGatherResultLine,
|
|
6
|
+
formatMarketPricesLines,
|
|
7
|
+
formatOracleLines,
|
|
8
|
+
formatRecipesLines,
|
|
9
|
+
} from '../dist/lib/formatters.js';
|
|
10
|
+
|
|
11
|
+
test('formatGatherResultLine handles zero-resource gathers cleanly', () => {
|
|
12
|
+
const line = formatGatherResultLine({
|
|
13
|
+
gathered: { gold: 0, wood: 0, food: 0, stone: 0 },
|
|
14
|
+
stamina: { efficiency: 100 },
|
|
15
|
+
tile_status: 'market',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
assert.equal(line, 'Gathered: none | Efficiency: 100% | Tile: market');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('extractMarketOrderId supports nested response shape', () => {
|
|
22
|
+
const id = extractMarketOrderId({
|
|
23
|
+
order: { id: 'abc-123' },
|
|
24
|
+
message: 'created',
|
|
25
|
+
});
|
|
26
|
+
assert.equal(id, 'abc-123');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('formatMarketPricesLines parses stats and pair rates', () => {
|
|
30
|
+
const lines = formatMarketPricesLines({
|
|
31
|
+
stats: {
|
|
32
|
+
open_orders: 4,
|
|
33
|
+
transactions_24h: 9,
|
|
34
|
+
active_trading_pairs: 2,
|
|
35
|
+
},
|
|
36
|
+
pairs: [
|
|
37
|
+
{
|
|
38
|
+
offer_resource: 'wood',
|
|
39
|
+
request_resource: 'stone',
|
|
40
|
+
order_count: 2,
|
|
41
|
+
total_offer_available: 100,
|
|
42
|
+
best_rate: 1.25,
|
|
43
|
+
avg_rate: 1.4,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
assert.equal(lines[0], 'Open orders: 4 | 24h transactions: 9 | Active pairs: 2');
|
|
49
|
+
assert.match(lines[1], /wood->stone/);
|
|
50
|
+
assert.match(lines[1], /best:1\.25/);
|
|
51
|
+
assert.match(lines[1], /avg:1\.40/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('formatRecipesLines supports craftable + shop payload', () => {
|
|
55
|
+
const lines = formatRecipesLines({
|
|
56
|
+
craftable: [
|
|
57
|
+
{
|
|
58
|
+
id: 'wooden_pickaxe',
|
|
59
|
+
recipe: { wood: 40, stone: 10 },
|
|
60
|
+
effects: ['+25% gathering on mountain'],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
shop: [
|
|
64
|
+
{
|
|
65
|
+
id: 'rations',
|
|
66
|
+
price: 20,
|
|
67
|
+
effects: ['+25 food'],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
assert.equal(lines[0], 'Craftable items:');
|
|
73
|
+
assert.match(lines[1], /wooden_pickaxe: 40w\+10s/);
|
|
74
|
+
assert.equal(lines[2], 'Shop items:');
|
|
75
|
+
assert.match(lines[3], /rations: 20 gold/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('formatOracleLines shows outcome progress and step list', () => {
|
|
79
|
+
const lines = formatOracleLines({
|
|
80
|
+
contract: {
|
|
81
|
+
completed_outcomes: 2,
|
|
82
|
+
total_outcomes: 6,
|
|
83
|
+
},
|
|
84
|
+
oracle: {
|
|
85
|
+
title: 'The Oracle of ClawCity',
|
|
86
|
+
narrative: 'A short story.',
|
|
87
|
+
tournament_objective: 'Master Gatherer: total gathered.',
|
|
88
|
+
starter_prompt: 'You are an autonomous competitor.',
|
|
89
|
+
},
|
|
90
|
+
next_steps: [
|
|
91
|
+
{
|
|
92
|
+
title: 'Leave Spawn',
|
|
93
|
+
command: 'clawcity move forest',
|
|
94
|
+
expected: 'Reach forest terrain.',
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
assert.equal(lines[0], 'The Oracle of ClawCity | Outcomes: 2/6');
|
|
100
|
+
assert.equal(lines[1], 'A short story.');
|
|
101
|
+
assert.match(lines[3], /Next steps:/);
|
|
102
|
+
assert.match(lines[4], /1\. Leave Spawn/);
|
|
103
|
+
});
|