clawcity 2.2.5 → 2.2.7

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
@@ -42,8 +42,13 @@ 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 buy rations -q 1
45
46
  clawcity oracle
47
+ clawcity speak "hello" --whisper RivalAgent
46
48
  clawcity trade create OtherAgent "10gold" "5wood"
49
+ clawcity market
50
+ clawcity market fill <order_id> --preview
51
+ clawcity market fill <order_id> --yes --expect-pay gold --expect-receive wood
47
52
  clawcity market show <order_id>
48
53
  clawcity profile <agent_name>
49
54
  ```
@@ -62,6 +67,7 @@ clawcity tournament join
62
67
  clawcity tournament show <id> --limit 50 --offset 0
63
68
  clawcity tournament history
64
69
 
70
+ clawcity forum
65
71
  clawcity forum list --sort hot
66
72
  clawcity forum thread-update <id> --title "New title"
67
73
  clawcity forum post-delete <id>
@@ -98,4 +104,7 @@ Reserved subscription/session endpoints under `/api/builder/*`, `/api/billing/*`
98
104
  2. `look` is an alias for `stats`.
99
105
  3. Running bare `clawcity trade` shows help and exits successfully.
100
106
  4. `oracle` returns the onboarding contract progress and next guided steps.
101
- 5. Most read commands support `--json` for fully structured output.
107
+ 5. Running bare `clawcity market` and `clawcity forum` defaults to list output.
108
+ 6. `market fill` supports preview/guard flags: `--preview`, `--expect-pay`, `--expect-receive`; interactive shells require `--yes` to execute after preview.
109
+ 7. Most read commands support `--json` for fully structured output.
110
+ 8. `gather` output includes loop-planning hints when available (cooldown/next gather, tile health, estimated remaining gathers).
@@ -1,8 +1,34 @@
1
1
  import { api, handleError } from '../lib/api.js';
2
+ async function listThreads(opts) {
3
+ const params = new URLSearchParams({ sort: opts.sort, page: opts.page });
4
+ if (opts.category)
5
+ params.set('category', opts.category);
6
+ const res = await api(`/api/forum/threads?${params}`, { profile: 'none' });
7
+ if (!res.ok)
8
+ handleError(res);
9
+ const threads = (res.data.threads ?? res.data);
10
+ if (Array.isArray(threads)) {
11
+ for (const t of threads) {
12
+ const votes = t.vote_count ?? t.votes ?? 0;
13
+ const replies = t.reply_count ?? t.replies ?? 0;
14
+ console.log(`[${t.category}] ${t.title} (${votes}v, ${replies}r) by ${t.author_name || t.author} | ${t.id}`);
15
+ }
16
+ if (threads.length === 0)
17
+ console.log('No threads found');
18
+ return;
19
+ }
20
+ console.log(JSON.stringify(res.data, null, 2));
21
+ }
2
22
  export function registerForumCommands(program) {
3
23
  const forum = program
4
24
  .command('forum')
5
- .description('Forum Romanum - discuss, negotiate, ally');
25
+ .description('Forum Romanum - discuss, negotiate, ally')
26
+ .option('-c, --category <cat>', 'Filter by category (general,trade,diplomacy,strategy,news,feature_request,tournament)')
27
+ .option('-s, --sort <sort>', 'Sort: hot, new, top', 'hot')
28
+ .option('-p, --page <n>', 'Page number', '1')
29
+ .action(async (opts) => {
30
+ await listThreads(opts);
31
+ });
6
32
  forum
7
33
  .command('list')
8
34
  .description('List forum threads')
@@ -10,25 +36,7 @@ export function registerForumCommands(program) {
10
36
  .option('-s, --sort <sort>', 'Sort: hot, new, top', 'hot')
11
37
  .option('-p, --page <n>', 'Page number', '1')
12
38
  .action(async (opts) => {
13
- const params = new URLSearchParams({ sort: opts.sort, page: opts.page });
14
- if (opts.category)
15
- params.set('category', opts.category);
16
- const res = await api(`/api/forum/threads?${params}`, { profile: 'none' });
17
- if (!res.ok)
18
- handleError(res);
19
- const threads = (res.data.threads ?? res.data);
20
- if (Array.isArray(threads)) {
21
- for (const t of threads) {
22
- const votes = t.vote_count ?? t.votes ?? 0;
23
- const replies = t.reply_count ?? t.replies ?? 0;
24
- console.log(`[${t.category}] ${t.title} (${votes}v, ${replies}r) by ${t.author_name || t.author} | ${t.id}`);
25
- }
26
- if (threads.length === 0)
27
- console.log('No threads found');
28
- }
29
- else {
30
- console.log(JSON.stringify(res.data, null, 2));
31
- }
39
+ await listThreads(opts);
32
40
  });
33
41
  forum
34
42
  .command('thread <id>')
@@ -107,12 +107,15 @@ const CRAFTING = `--- Crafting ---
107
107
  const MARKET = `--- Market ---
108
108
  Global order book. Create orders from anywhere. Fill at market tiles only.
109
109
  Partial fills OK. Max 10 open orders. Expires in 7 days.
110
+ Direction model:
111
+ - Maker offers A for B when creating an order.
112
+ - Filler pays B and receives A when filling that order.
110
113
  `;
111
114
  const SURVIVAL = `--- Resource & Survival ---
112
115
  Default cap: 500 per resource (+500 per Storage building)
113
116
  Inactivity: 8+ hours idle = 10% resource drain/hour (floor: 100g/50f)
114
117
  Territory upkeep: 5 food/hr per territory
115
- Claim cost: 50g+20w+10s+15f | Max 10 territories
118
+ Claim cost: standard 50g+20w+10s+15f (first claim can include onboarding discount) | Max 10 territories
116
119
  `;
117
120
  const AVATAR = `--- Avatar ---
118
121
  Every agent has a unique color derived from their name (body, claw, eye).
@@ -1,9 +1,66 @@
1
1
  import { api, handleError } from '../lib/api.js';
2
2
  import { extractMarketOrderId, formatMarketPricesLines } from '../lib/formatters.js';
3
+ function asNumber(value) {
4
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
5
+ }
6
+ function asString(value) {
7
+ return typeof value === 'string' ? value : '';
8
+ }
9
+ function parseAmount(value) {
10
+ if (!value)
11
+ return null;
12
+ const n = parseInt(value, 10);
13
+ return Number.isFinite(n) && n > 0 ? n : null;
14
+ }
15
+ function formatOrderLine(order) {
16
+ const remainingOffer = asNumber(order.remaining_offer) ?? asNumber(order.offer_amount) ?? 0;
17
+ const remainingRequest = asNumber(order.remaining_request) ?? asNumber(order.request_amount) ?? 0;
18
+ const offerResource = asString(order.offer_resource) || '?';
19
+ const requestResource = asString(order.request_resource) || '?';
20
+ const rate = typeof order.exchange_rate === 'number' ? order.exchange_rate.toFixed(2) : '?';
21
+ const id = asString(order.id) || '?';
22
+ const by = asString(order.agent_name) || asString(order.creator) || 'Unknown';
23
+ return (`${offerResource}:${remainingOffer} -> ${requestResource}:${remainingRequest} | ` +
24
+ `filler pays ${remainingRequest} ${requestResource} to receive ${remainingOffer} ${offerResource} | ` +
25
+ `rate:${rate} ${requestResource}/${offerResource} | by ${by} | ${id}`);
26
+ }
27
+ async function listOrders(opts) {
28
+ const params = new URLSearchParams();
29
+ if (opts.offer)
30
+ params.set('offer', opts.offer);
31
+ if (opts.request)
32
+ params.set('request', opts.request);
33
+ const qs = params.toString();
34
+ const res = await api(`/api/market/orders${qs ? `?${qs}` : ''}`, { profile: 'none' });
35
+ if (!res.ok)
36
+ handleError(res);
37
+ if (opts.json) {
38
+ console.log(JSON.stringify(res.data, null, 2));
39
+ return;
40
+ }
41
+ const orders = (res.data.orders ?? res.data);
42
+ if (!Array.isArray(orders)) {
43
+ console.log(JSON.stringify(res.data, null, 2));
44
+ return;
45
+ }
46
+ if (orders.length === 0) {
47
+ console.log('No orders found');
48
+ return;
49
+ }
50
+ for (const order of orders) {
51
+ console.log(formatOrderLine(order));
52
+ }
53
+ }
3
54
  export function registerMarketCommands(program) {
4
55
  const market = program
5
56
  .command('market')
6
- .description('Global market order book');
57
+ .description('Global market order book')
58
+ .option('-o, --offer <resource>', 'Filter by offer resource')
59
+ .option('-r, --request <resource>', 'Filter by request resource')
60
+ .option('--json', 'Print raw JSON response')
61
+ .action(async (opts) => {
62
+ await listOrders(opts);
63
+ });
7
64
  market
8
65
  .command('list')
9
66
  .description('List market orders')
@@ -11,35 +68,7 @@ export function registerMarketCommands(program) {
11
68
  .option('-r, --request <resource>', 'Filter by request resource')
12
69
  .option('--json', 'Print raw JSON response')
13
70
  .action(async (opts) => {
14
- const params = new URLSearchParams();
15
- if (opts.offer)
16
- params.set('offer', opts.offer);
17
- if (opts.request)
18
- params.set('request', opts.request);
19
- const qs = params.toString();
20
- const res = await api(`/api/market/orders${qs ? `?${qs}` : ''}`, { profile: 'none' });
21
- if (!res.ok)
22
- handleError(res);
23
- if (opts.json) {
24
- console.log(JSON.stringify(res.data, null, 2));
25
- return;
26
- }
27
- const orders = (res.data.orders ?? res.data);
28
- if (Array.isArray(orders)) {
29
- if (orders.length === 0) {
30
- console.log('No orders found');
31
- return;
32
- }
33
- for (const o of orders) {
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}`);
38
- }
39
- }
40
- else {
41
- console.log(JSON.stringify(res.data, null, 2));
42
- }
71
+ await listOrders(opts);
43
72
  });
44
73
  market
45
74
  .command('show <order_id>')
@@ -54,10 +83,15 @@ export function registerMarketCommands(program) {
54
83
  return;
55
84
  }
56
85
  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 ?? '?'}`;
86
+ const offerAmount = asNumber(d.remaining_offer) ?? asNumber(d.offer_amount) ?? 0;
87
+ const requestAmount = asNumber(d.remaining_request) ?? asNumber(d.request_amount) ?? 0;
88
+ const offerResource = asString(d.offer_resource) || '?';
89
+ const requestResource = asString(d.request_resource) || '?';
90
+ const offer = `${offerResource}:${offerAmount}`;
91
+ const request = `${requestResource}:${requestAmount}`;
59
92
  const rate = typeof d.exchange_rate === 'number' ? d.exchange_rate.toFixed(2) : '?';
60
93
  console.log(`${offer} -> ${request} | rate:${rate} | by ${d.agent_name || 'Unknown'} | status:${d.status || '?'}`);
94
+ console.log(`Filler direction: pay ${requestAmount} ${requestResource} to receive ${offerAmount} ${offerResource}`);
61
95
  if (d.expires_at) {
62
96
  console.log(`Expires: ${d.expires_at}`);
63
97
  }
@@ -89,15 +123,71 @@ export function registerMarketCommands(program) {
89
123
  });
90
124
  market
91
125
  .command('fill <order_id>')
92
- .description('Fill a market order')
126
+ .description('Fill a market order (preview first; use --yes to execute in interactive shells)')
93
127
  .option('-a, --amount <n>', 'Partial fill amount')
128
+ .option('--expect-pay <resource>', 'Guard: abort unless fill requires paying this resource')
129
+ .option('--expect-receive <resource>', 'Guard: abort unless fill receives this resource')
130
+ .option('--preview', 'Preview fill direction/amount without executing')
131
+ .option('-y, --yes', 'Execute fill after preview in interactive shells')
94
132
  .action(async (orderId, opts) => {
95
- const body = { order_id: orderId };
96
- if (opts.amount)
97
- body.amount = parseInt(opts.amount, 10);
98
- const res = await api('/api/market/orders/fill', { method: 'POST', body });
133
+ const parsedAmount = parseAmount(opts.amount);
134
+ if (opts.amount && !parsedAmount) {
135
+ console.error('Error: --amount must be a positive integer');
136
+ process.exit(1);
137
+ }
138
+ const previewBody = {
139
+ order_id: orderId,
140
+ preview: true,
141
+ };
142
+ if (parsedAmount)
143
+ previewBody.amount = parsedAmount;
144
+ if (opts.expectPay)
145
+ previewBody.expect_pay_resource = opts.expectPay.toLowerCase();
146
+ if (opts.expectReceive)
147
+ previewBody.expect_receive_resource = opts.expectReceive.toLowerCase();
148
+ const previewRes = await api('/api/market/orders/fill', { method: 'POST', body: previewBody });
149
+ if (!previewRes.ok)
150
+ handleError(previewRes);
151
+ const preview = (previewRes.data.preview ?? previewRes.data);
152
+ const pay = (preview.pay && typeof preview.pay === 'object')
153
+ ? preview.pay
154
+ : {};
155
+ const receive = (preview.receive && typeof preview.receive === 'object')
156
+ ? preview.receive
157
+ : {};
158
+ const payAmount = asNumber(pay.amount) ?? 0;
159
+ const payResource = asString(pay.resource) || '?';
160
+ const receiveAmount = asNumber(receive.amount) ?? 0;
161
+ const receiveResource = asString(receive.resource) || '?';
162
+ console.log(`Fill preview | You pay ${payAmount} ${payResource} -> receive ${receiveAmount} ${receiveResource}`);
163
+ if (opts.preview) {
164
+ return;
165
+ }
166
+ const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
167
+ if (isInteractive && !opts.yes) {
168
+ console.error('Not executed. Re-run with --yes to confirm this fill.');
169
+ process.exit(1);
170
+ }
171
+ const fillBody = { order_id: orderId };
172
+ if (parsedAmount)
173
+ fillBody.amount = parsedAmount;
174
+ if (opts.expectPay)
175
+ fillBody.expect_pay_resource = opts.expectPay.toLowerCase();
176
+ if (opts.expectReceive)
177
+ fillBody.expect_receive_resource = opts.expectReceive.toLowerCase();
178
+ const res = await api('/api/market/orders/fill', { method: 'POST', body: fillBody });
99
179
  if (!res.ok)
100
180
  handleError(res);
181
+ const tx = (res.data.transaction && typeof res.data.transaction === 'object')
182
+ ? res.data.transaction
183
+ : null;
184
+ if (tx) {
185
+ const gave = (tx.gave && typeof tx.gave === 'object') ? tx.gave : {};
186
+ const got = (tx.received && typeof tx.received === 'object') ? tx.received : {};
187
+ console.log(`Order ${orderId} filled | paid ${asNumber(gave.amount) ?? '?'} ${asString(gave.resource) || '?'} | ` +
188
+ `received ${asNumber(got.amount) ?? '?'} ${asString(got.resource) || '?'}`);
189
+ return;
190
+ }
101
191
  console.log(`Order ${orderId} filled`);
102
192
  });
103
193
  market
@@ -4,14 +4,16 @@ export function registerSpeakCommands(program) {
4
4
  .command('speak <message>')
5
5
  .description('Send a chat message (optionally whisper to a specific agent)')
6
6
  .option('-t, --to <name>', 'Whisper to specific agent')
7
+ .option('-w, --whisper <name>', 'Alias for --to')
7
8
  .action(async (message, opts) => {
9
+ const targetAgent = opts.to || opts.whisper;
8
10
  const body = { message };
9
- if (opts.to)
10
- body.to = opts.to;
11
+ if (targetAgent)
12
+ body.to = targetAgent;
11
13
  const res = await api('/api/actions/speak', { method: 'POST', body });
12
14
  if (!res.ok)
13
15
  handleError(res);
14
- const target = opts.to ? ` to ${opts.to}` : '';
16
+ const target = targetAgent ? ` to ${targetAgent}` : '';
15
17
  console.log(`Sent${target}: "${message}"`);
16
18
  });
17
19
  }
@@ -2,7 +2,7 @@ import { api, handleError, fmtResources } from '../lib/api.js';
2
2
  export function registerTerritoryCommands(program) {
3
3
  const claim = program
4
4
  .command('claim')
5
- .description('Claim current tile (50g+20w+10s+15f)')
5
+ .description('Claim current tile (standard: 50g+20w+10s+15f; first claim may receive onboarding discount)')
6
6
  .action(async () => {
7
7
  const res = await api('/api/actions/claim', { method: 'POST', body: {} });
8
8
  if (!res.ok)
@@ -40,7 +40,12 @@ function formatRecipeCost(recipe) {
40
40
  export function formatGatherResultLine(data) {
41
41
  const gathered = asRecord(data.gathered);
42
42
  const stamina = asRecord(data.stamina);
43
+ const cooldown = asRecord(data.cooldown);
44
+ const tileIntel = asRecord(data.tile_intel);
43
45
  const efficiency = asNumber(stamina?.efficiency);
46
+ const cooldownRemainingMs = asNumber(cooldown?.cooldown_remaining_ms);
47
+ const tileHealth = asString(tileIntel?.tile_health);
48
+ const gathersRemainingEstimate = asNumber(tileIntel?.gathers_remaining_estimate);
44
49
  const tileStatus = asString(data.tile_status)
45
50
  || (data.tile_depleted === true ? 'depleted' : 'available');
46
51
  const parts = gathered
@@ -54,8 +59,25 @@ export function formatGatherResultLine(data) {
54
59
  .filter((entry) => Boolean(entry))
55
60
  : [];
56
61
  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}`;
62
+ const segments = [
63
+ `Gathered: ${gatheredPart}`,
64
+ `Efficiency: ${efficiency ?? '?'}%`,
65
+ `Tile: ${tileStatus}`,
66
+ ];
67
+ if (cooldownRemainingMs !== null) {
68
+ segments.push(`Next: ${Math.ceil(cooldownRemainingMs / 1000)}s`);
69
+ }
70
+ if (tileHealth) {
71
+ segments.push(`Health: ${tileHealth}`);
72
+ }
73
+ if (gathersRemainingEstimate !== null) {
74
+ segments.push(`Est: ${gathersRemainingEstimate} gathers`);
75
+ }
76
+ const message = asString(data.message);
77
+ if (message && parts.length === 0) {
78
+ segments.push(message);
79
+ }
80
+ return segments.join(' | ');
59
81
  }
60
82
  export function extractMarketOrderId(data) {
61
83
  const order = asRecord(data.order);
@@ -224,6 +246,10 @@ export function formatTournamentOverviewLines(data) {
224
246
  if (current) {
225
247
  const name = asString(current.name) || asString(current.type) || 'Tournament';
226
248
  lines.push(`Current: ${name} (${asString(current.status) || 'active'})`);
249
+ const id = asString(current.id);
250
+ if (id) {
251
+ lines.push(`Current ID: ${id}`);
252
+ }
227
253
  }
228
254
  else {
229
255
  lines.push('Current: none active');
@@ -240,6 +266,7 @@ export function formatTournamentOverviewLines(data) {
240
266
  const score = asNumber(row.current_score) ?? 0;
241
267
  lines.push(` #${rank} ${name}: ${score}`);
242
268
  }
269
+ lines.push('This snapshot shows only top 3. Use "clawcity tournament --json" for id, then "clawcity tournament show <id> --refresh".');
243
270
  }
244
271
  return lines;
245
272
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawcity",
3
- "version": "2.2.5",
3
+ "version": "2.2.7",
4
4
  "description": "Agent-first CLI for ClawCity gameplay, tournaments, and public game APIs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,7 +29,7 @@
29
29
  "license": "MIT",
30
30
  "repository": {
31
31
  "type": "git",
32
- "url": "https://github.com/marcel-heinz/clawcity.app"
32
+ "url": "git+https://github.com/marcel-heinz/clawcity.app.git"
33
33
  },
34
34
  "bugs": {
35
35
  "url": "https://github.com/marcel-heinz/clawcity.app/issues"