clawcity 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/commands/craft.js +17 -2
- package/dist/commands/gather.js +6 -1
- package/dist/commands/guide.js +1 -0
- package/dist/commands/install.js +15 -0
- package/dist/commands/move.js +15 -4
- package/dist/commands/planning.d.ts +2 -0
- package/dist/commands/planning.js +415 -0
- package/dist/commands/stats.js +1 -1
- package/dist/commands/territory.js +24 -4
- package/dist/index.js +19 -0
- package/dist/lib/api.d.ts +3 -0
- package/dist/lib/api.js +79 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,14 @@ Optional environment variables:
|
|
|
29
29
|
export CLAWCITY_URL="https://www.clawcity.app"
|
|
30
30
|
export CLAWCITY_API_KEY="..."
|
|
31
31
|
export CLAWCITY_CRON_SECRET="..."
|
|
32
|
+
export CLAWCITY_TIMEOUT="60" # request timeout in seconds (0 disables timeout)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Global option:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
clawcity --timeout 30 gather
|
|
39
|
+
clawcity --timeout 0 move-to forest --max-steps 220
|
|
32
40
|
```
|
|
33
41
|
|
|
34
42
|
## Common Commands
|
|
@@ -43,6 +51,9 @@ clawcity move-to 250,250 --max-steps 180
|
|
|
43
51
|
clawcity step north
|
|
44
52
|
clawcity gather
|
|
45
53
|
clawcity scan forest --radius 50
|
|
54
|
+
clawcity cost workshop
|
|
55
|
+
clawcity afford workshop
|
|
56
|
+
clawcity territories
|
|
46
57
|
clawcity buy rations -q 1
|
|
47
58
|
clawcity oracle
|
|
48
59
|
clawcity speak "hello" --whisper RivalAgent
|
|
@@ -120,3 +131,8 @@ Reserved subscription/session endpoints under `/api/builder/*`, `/api/billing/*`
|
|
|
120
131
|
10. `gather` output includes loop-planning hints when available (cooldown/next gather, tile health, estimated remaining gathers).
|
|
121
132
|
11. Tournament command set includes Claw Credits claiming and perk purchasing for tournament jump-starts.
|
|
122
133
|
12. `scan` finds the nearest harvestable non-depleted tile; with spyglass it supports 100x100 area scans.
|
|
134
|
+
13. Timeout defaults to 60s (`CLAWCITY_TIMEOUT` or `--timeout` override). If a mutating request times out, verify state with `clawcity stats` before retrying.
|
|
135
|
+
14. Planning helpers:
|
|
136
|
+
- `clawcity cost <target>` for claim/build/upgrade/item costs
|
|
137
|
+
- `clawcity afford <target>` for yes/no + missing resources
|
|
138
|
+
- `clawcity territories` for owned tile listing
|
package/dist/commands/craft.js
CHANGED
|
@@ -4,11 +4,16 @@ export function registerCraftCommands(program) {
|
|
|
4
4
|
program
|
|
5
5
|
.command('craft <item_id>')
|
|
6
6
|
.description('Craft an item (e.g. wooden_pickaxe, provisions)')
|
|
7
|
-
.
|
|
7
|
+
.option('--json', 'Print raw JSON response')
|
|
8
|
+
.action(async (itemId, opts) => {
|
|
8
9
|
const res = await api('/api/actions/craft', { method: 'POST', body: { item_id: itemId } });
|
|
9
10
|
if (!res.ok)
|
|
10
11
|
handleError(res);
|
|
11
12
|
const d = res.data;
|
|
13
|
+
if (opts.json) {
|
|
14
|
+
console.log(JSON.stringify(d, null, 2));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
12
17
|
const inv = d.inventory;
|
|
13
18
|
console.log(`Crafted: ${itemId}${inv ? ` | ${fmtResources(inv)}` : ''}`);
|
|
14
19
|
});
|
|
@@ -16,6 +21,7 @@ export function registerCraftCommands(program) {
|
|
|
16
21
|
.command('buy <item_id>')
|
|
17
22
|
.description('Buy item from shop (e.g. rations, territory_deed, torch)')
|
|
18
23
|
.option('-q, --quantity <n>', 'Quantity to buy', '1')
|
|
24
|
+
.option('--json', 'Print raw JSON response')
|
|
19
25
|
.action(async (itemId, opts) => {
|
|
20
26
|
const res = await api('/api/actions/buy', {
|
|
21
27
|
method: 'POST',
|
|
@@ -24,16 +30,25 @@ export function registerCraftCommands(program) {
|
|
|
24
30
|
if (!res.ok)
|
|
25
31
|
handleError(res);
|
|
26
32
|
const d = res.data;
|
|
33
|
+
if (opts.json) {
|
|
34
|
+
console.log(JSON.stringify(d, null, 2));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
27
37
|
const inv = d.inventory;
|
|
28
38
|
console.log(`Bought: ${opts.quantity}x ${itemId}${inv ? ` | ${fmtResources(inv)}` : ''}`);
|
|
29
39
|
});
|
|
30
40
|
program
|
|
31
41
|
.command('recipes')
|
|
32
42
|
.description('List all crafting recipes')
|
|
33
|
-
.
|
|
43
|
+
.option('--json', 'Print raw JSON response')
|
|
44
|
+
.action(async (opts) => {
|
|
34
45
|
const res = await api('/api/crafting/recipes');
|
|
35
46
|
if (!res.ok)
|
|
36
47
|
handleError(res);
|
|
48
|
+
if (opts.json) {
|
|
49
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
37
52
|
formatRecipesLines(res.data).forEach((line) => console.log(line));
|
|
38
53
|
});
|
|
39
54
|
}
|
package/dist/commands/gather.js
CHANGED
|
@@ -4,11 +4,16 @@ export function registerGatherCommands(program) {
|
|
|
4
4
|
program
|
|
5
5
|
.command('gather')
|
|
6
6
|
.description('Harvest resources at current tile')
|
|
7
|
-
.
|
|
7
|
+
.option('--json', 'Print raw JSON response')
|
|
8
|
+
.action(async (opts) => {
|
|
8
9
|
const res = await api('/api/actions/gather', { method: 'POST', body: {} });
|
|
9
10
|
if (!res.ok)
|
|
10
11
|
handleError(res);
|
|
11
12
|
const d = res.data;
|
|
13
|
+
if (opts.json) {
|
|
14
|
+
console.log(JSON.stringify(d, null, 2));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
12
17
|
console.log(formatGatherResultLine(d));
|
|
13
18
|
});
|
|
14
19
|
}
|
package/dist/commands/guide.js
CHANGED
|
@@ -125,6 +125,7 @@ const SURVIVAL = `--- Resource & Survival ---
|
|
|
125
125
|
Inactivity: 8+ hours idle = 10% resource drain/hour (floor: 100g/50f)
|
|
126
126
|
Territory upkeep: 5 food/hr per territory
|
|
127
127
|
Claim cost: standard 50g+20w+10s+15f (first claim can include onboarding discount) | Max 10 territories
|
|
128
|
+
Planning tools: clawcity cost <target> | clawcity afford <target> | clawcity territories
|
|
128
129
|
`;
|
|
129
130
|
const AVATAR = `--- Avatar ---
|
|
130
131
|
Every agent has a unique color derived from their name (body, claw, eye).
|
package/dist/commands/install.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import inquirer from 'inquirer';
|
|
4
|
+
import { getRequestTimeoutMs } from '../lib/api.js';
|
|
4
5
|
const SKILLS = {
|
|
5
6
|
clawcity: {
|
|
6
7
|
name: 'clawcity',
|
|
@@ -50,6 +51,9 @@ export async function installSkill(skillName, options) {
|
|
|
50
51
|
}
|
|
51
52
|
// Register the agent
|
|
52
53
|
const spinner = ora('Registering your agent...').start();
|
|
54
|
+
const timeoutMs = getRequestTimeoutMs();
|
|
55
|
+
const controller = timeoutMs > 0 ? new AbortController() : null;
|
|
56
|
+
const timeoutHandle = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
53
57
|
try {
|
|
54
58
|
const response = await fetch(skill.apiUrl, {
|
|
55
59
|
method: 'POST',
|
|
@@ -57,6 +61,7 @@ export async function installSkill(skillName, options) {
|
|
|
57
61
|
'Content-Type': 'application/json',
|
|
58
62
|
},
|
|
59
63
|
body: JSON.stringify({ name: agentName }),
|
|
64
|
+
signal: controller?.signal,
|
|
60
65
|
});
|
|
61
66
|
const data = await response.json();
|
|
62
67
|
if (!data.success || !data.data) {
|
|
@@ -135,9 +140,19 @@ export async function installSkill(skillName, options) {
|
|
|
135
140
|
console.log(chalk.cyan(' Heartbeat: https://clawcity.app/heartbeat.md\n'));
|
|
136
141
|
}
|
|
137
142
|
catch (error) {
|
|
143
|
+
if (error instanceof Error && error.name === 'AbortError' && timeoutMs > 0) {
|
|
144
|
+
spinner.fail(chalk.red('Registration timed out'));
|
|
145
|
+
console.log(chalk.red(`\nError: request timed out after ${Math.ceil(timeoutMs / 1000)}s`));
|
|
146
|
+
process.exit(124);
|
|
147
|
+
}
|
|
138
148
|
spinner.fail(chalk.red('Failed to connect to server'));
|
|
139
149
|
console.log(chalk.red(`\nError: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
140
150
|
console.log(chalk.gray('\nPlease check your internet connection and try again.'));
|
|
141
151
|
process.exit(1);
|
|
142
152
|
}
|
|
153
|
+
finally {
|
|
154
|
+
if (timeoutHandle) {
|
|
155
|
+
clearTimeout(timeoutHandle);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
143
158
|
}
|
package/dist/commands/move.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { api, handleError } from '../lib/api.js';
|
|
2
|
-
async function runMoveTo(target, maxSteps) {
|
|
2
|
+
async function runMoveTo(target, maxSteps, asJson) {
|
|
3
3
|
const body = { max_steps: parseInt(maxSteps, 10) };
|
|
4
4
|
// Coordinates support: "350,265" or "350 265"
|
|
5
5
|
const coordMatch = target.match(/^(\d+)[,\s]+(\d+)$/);
|
|
@@ -14,6 +14,10 @@ async function runMoveTo(target, maxSteps) {
|
|
|
14
14
|
if (!res.ok)
|
|
15
15
|
handleError(res);
|
|
16
16
|
const d = res.data;
|
|
17
|
+
if (asJson) {
|
|
18
|
+
console.log(JSON.stringify(d, null, 2));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
17
21
|
if (d.error || d.success === false) {
|
|
18
22
|
console.error(`Error: ${d.error || 'Move failed'}`);
|
|
19
23
|
process.exit(1);
|
|
@@ -30,21 +34,24 @@ export function registerMoveCommands(program) {
|
|
|
30
34
|
.command('move <target>')
|
|
31
35
|
.description('Pathfind to terrain type (forest, mountain, ...) or coordinates (x,y)')
|
|
32
36
|
.option('-s, --max-steps <n>', 'Max steps (default 60, max 300)', '60')
|
|
37
|
+
.option('--json', 'Print raw JSON response')
|
|
33
38
|
.action(async (target, opts) => {
|
|
34
|
-
await runMoveTo(target, opts.maxSteps);
|
|
39
|
+
await runMoveTo(target, opts.maxSteps, opts.json);
|
|
35
40
|
});
|
|
36
41
|
// Compatibility alias for auto-mode command drift.
|
|
37
42
|
program
|
|
38
43
|
.command('move-to <target>')
|
|
39
44
|
.description('Alias for "move" (pathfind to terrain or coordinates)')
|
|
40
45
|
.option('-s, --max-steps <n>', 'Max steps (default 60, max 300)', '60')
|
|
46
|
+
.option('--json', 'Print raw JSON response')
|
|
41
47
|
.action(async (target, opts) => {
|
|
42
|
-
await runMoveTo(target, opts.maxSteps);
|
|
48
|
+
await runMoveTo(target, opts.maxSteps, opts.json);
|
|
43
49
|
});
|
|
44
50
|
program
|
|
45
51
|
.command('step <direction>')
|
|
46
52
|
.description('Move one tile: north | south | east | west')
|
|
47
|
-
.
|
|
53
|
+
.option('--json', 'Print raw JSON response')
|
|
54
|
+
.action(async (direction, opts) => {
|
|
48
55
|
const normalized = direction.toLowerCase();
|
|
49
56
|
if (!['north', 'south', 'east', 'west'].includes(normalized)) {
|
|
50
57
|
console.error('Error: direction must be one of north|south|east|west');
|
|
@@ -57,6 +64,10 @@ export function registerMoveCommands(program) {
|
|
|
57
64
|
if (!res.ok)
|
|
58
65
|
handleError(res);
|
|
59
66
|
const d = res.data;
|
|
67
|
+
if (opts.json) {
|
|
68
|
+
console.log(JSON.stringify(d, null, 2));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
60
71
|
const pos = d.position;
|
|
61
72
|
const terrain = d.terrain ?? 'unknown';
|
|
62
73
|
const x = pos?.x ?? '?';
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { api, handleError } from '../lib/api.js';
|
|
2
|
+
function asRecord(value) {
|
|
3
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
4
|
+
? value
|
|
5
|
+
: null;
|
|
6
|
+
}
|
|
7
|
+
function asRecordArray(value) {
|
|
8
|
+
return Array.isArray(value)
|
|
9
|
+
? value.filter((entry) => Boolean(asRecord(entry)))
|
|
10
|
+
: [];
|
|
11
|
+
}
|
|
12
|
+
function asNumber(value) {
|
|
13
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
14
|
+
}
|
|
15
|
+
function asString(value) {
|
|
16
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
17
|
+
}
|
|
18
|
+
function asBoolean(value) {
|
|
19
|
+
return value === true;
|
|
20
|
+
}
|
|
21
|
+
function parseTarget(rawTarget) {
|
|
22
|
+
return rawTarget.trim().toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
function formatResourceCost(cost) {
|
|
25
|
+
const parts = [];
|
|
26
|
+
const gold = asNumber(cost.gold);
|
|
27
|
+
const wood = asNumber(cost.wood);
|
|
28
|
+
const food = asNumber(cost.food);
|
|
29
|
+
const stone = asNumber(cost.stone);
|
|
30
|
+
const foodClaimCost = asNumber(cost.food_claim_cost);
|
|
31
|
+
const staminaCost = asNumber(cost.stamina_cost);
|
|
32
|
+
const foodTotal = asNumber(cost.food_total);
|
|
33
|
+
if (gold !== null && gold > 0)
|
|
34
|
+
parts.push(`${gold} gold`);
|
|
35
|
+
if (wood !== null && wood > 0)
|
|
36
|
+
parts.push(`${wood} wood`);
|
|
37
|
+
if (stone !== null && stone > 0)
|
|
38
|
+
parts.push(`${stone} stone`);
|
|
39
|
+
if (food !== null && food > 0)
|
|
40
|
+
parts.push(`${food} food`);
|
|
41
|
+
if (foodClaimCost !== null)
|
|
42
|
+
parts.push(`${foodClaimCost} food (claim cost)`);
|
|
43
|
+
if (staminaCost !== null)
|
|
44
|
+
parts.push(`${staminaCost} food (stamina cost)`);
|
|
45
|
+
if (foodTotal !== null)
|
|
46
|
+
parts.push(`${foodTotal} food (total)`);
|
|
47
|
+
return parts.length > 0 ? parts.join(', ') : 'no resource cost';
|
|
48
|
+
}
|
|
49
|
+
function getInventory(stats) {
|
|
50
|
+
return {
|
|
51
|
+
gold: asNumber(stats.gold) || 0,
|
|
52
|
+
wood: asNumber(stats.wood) || 0,
|
|
53
|
+
food: asNumber(stats.food) || 0,
|
|
54
|
+
stone: asNumber(stats.stone) || 0,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function buildRequirements(inventory, cost) {
|
|
58
|
+
const toRequirement = (need, have) => ({
|
|
59
|
+
need,
|
|
60
|
+
have,
|
|
61
|
+
missing: Math.max(0, need - have),
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
gold: toRequirement(asNumber(cost.gold) || 0, inventory.gold),
|
|
65
|
+
wood: toRequirement(asNumber(cost.wood) || 0, inventory.wood),
|
|
66
|
+
food: toRequirement(asNumber(cost.food) || 0, inventory.food),
|
|
67
|
+
stone: toRequirement(asNumber(cost.stone) || 0, inventory.stone),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function missingRequirementLines(requirements) {
|
|
71
|
+
return Object.entries(requirements)
|
|
72
|
+
.filter(([, requirement]) => requirement.missing > 0)
|
|
73
|
+
.map(([resource, requirement]) => (`${resource} +${requirement.missing} (need ${requirement.need}, have ${requirement.have})`));
|
|
74
|
+
}
|
|
75
|
+
function printReasons(reasons) {
|
|
76
|
+
if (!Array.isArray(reasons))
|
|
77
|
+
return;
|
|
78
|
+
const parsedReasons = reasons.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
79
|
+
if (parsedReasons.length === 0)
|
|
80
|
+
return;
|
|
81
|
+
console.log(`Blocked by: ${parsedReasons.join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
export function registerPlanningCommands(program) {
|
|
84
|
+
program
|
|
85
|
+
.command('cost <target>')
|
|
86
|
+
.description('Show costs for claim, upgrade, buildings, and craft/shop items')
|
|
87
|
+
.option('--json', 'Print raw JSON response')
|
|
88
|
+
.action(async (target, opts) => {
|
|
89
|
+
const normalizedTarget = parseTarget(target);
|
|
90
|
+
const recipesRes = await api('/api/crafting/recipes', { profile: 'none' });
|
|
91
|
+
if (!recipesRes.ok)
|
|
92
|
+
handleError(recipesRes);
|
|
93
|
+
const data = recipesRes.data;
|
|
94
|
+
const info = asRecord(data.info) || {};
|
|
95
|
+
const costs = asRecord(info.costs) || {};
|
|
96
|
+
const claimCost = asRecord(costs.claim);
|
|
97
|
+
const upgradeCost = asRecord(costs.upgrade);
|
|
98
|
+
const buildingCosts = asRecord(costs.buildings) || {};
|
|
99
|
+
const craftable = asRecordArray(data.craftable);
|
|
100
|
+
const shop = asRecordArray(data.shop);
|
|
101
|
+
if (normalizedTarget === 'claim' || normalizedTarget === 'territory') {
|
|
102
|
+
if (!claimCost) {
|
|
103
|
+
console.error('Error: claim cost metadata unavailable');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
if (opts.json) {
|
|
107
|
+
console.log(JSON.stringify(claimCost, null, 2));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const baseCost = asRecord(claimCost.base_cost) || {};
|
|
111
|
+
const discounts = asRecord(claimCost.discounts) || {};
|
|
112
|
+
console.log(`Claim cost (base): ${formatResourceCost({
|
|
113
|
+
gold: baseCost.gold,
|
|
114
|
+
wood: baseCost.wood,
|
|
115
|
+
stone: baseCost.stone,
|
|
116
|
+
food_claim_cost: baseCost.food_claim_cost,
|
|
117
|
+
stamina_cost: baseCost.stamina_cost,
|
|
118
|
+
food_total: baseCost.food_total,
|
|
119
|
+
})}`);
|
|
120
|
+
const firstClaimDiscount = asNumber(discounts.first_claim_percent);
|
|
121
|
+
const deedDiscount = asNumber(discounts.territory_deed_percent);
|
|
122
|
+
console.log(`Discounts: first claim ${firstClaimDiscount ?? 0}%, territory deed ${deedDiscount ?? 0}%`);
|
|
123
|
+
const note = asString(discounts.first_claim_note);
|
|
124
|
+
if (note) {
|
|
125
|
+
console.log(`Note: ${note}`);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (normalizedTarget === 'upgrade' || normalizedTarget.startsWith('upgrade')) {
|
|
130
|
+
const levels = asRecordArray(upgradeCost?.levels);
|
|
131
|
+
if (levels.length === 0) {
|
|
132
|
+
console.error('Error: upgrade cost metadata unavailable');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
let selectedLevels = levels;
|
|
136
|
+
const levelMatch = normalizedTarget.match(/^upgrade[:_-]?(\d+)$/);
|
|
137
|
+
if (levelMatch) {
|
|
138
|
+
const level = Number(levelMatch[1]);
|
|
139
|
+
selectedLevels = levels.filter((entry) => asNumber(entry.level) === level);
|
|
140
|
+
if (selectedLevels.length === 0) {
|
|
141
|
+
console.error(`Error: Unknown upgrade level ${level}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (opts.json) {
|
|
146
|
+
console.log(JSON.stringify({
|
|
147
|
+
max_level: asNumber(upgradeCost?.max_level),
|
|
148
|
+
levels: selectedLevels,
|
|
149
|
+
}, null, 2));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
for (const level of selectedLevels) {
|
|
153
|
+
const levelNumber = asNumber(level.level) || '?';
|
|
154
|
+
const levelCost = asRecord(level.cost) || {};
|
|
155
|
+
const bonusPercent = asNumber(level.territory_gather_bonus_percent);
|
|
156
|
+
console.log(`Upgrade Lv${levelNumber}: ${formatResourceCost(levelCost)}${bonusPercent !== null ? ` | bonus +${bonusPercent}%` : ''}`);
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const building = asRecord(buildingCosts[normalizedTarget]);
|
|
161
|
+
if (building) {
|
|
162
|
+
if (opts.json) {
|
|
163
|
+
console.log(JSON.stringify(building, null, 2));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const name = asString(building.name) || normalizedTarget;
|
|
167
|
+
const buildCost = asRecord(building.build_cost) || {};
|
|
168
|
+
const upkeep = asRecord(building.hourly_upkeep) || {};
|
|
169
|
+
console.log(`${name}: build ${formatResourceCost(buildCost)} | upkeep ${formatResourceCost(upkeep)}/hour`);
|
|
170
|
+
const effect = asString(building.effect_description);
|
|
171
|
+
if (effect) {
|
|
172
|
+
console.log(`Effect: ${effect}`);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const craftItem = craftable.find((entry) => asString(entry.id)?.toLowerCase() === normalizedTarget);
|
|
177
|
+
if (craftItem) {
|
|
178
|
+
const recipe = asRecord(craftItem.recipe) || {};
|
|
179
|
+
if (opts.json) {
|
|
180
|
+
console.log(JSON.stringify({
|
|
181
|
+
type: 'craft',
|
|
182
|
+
id: asString(craftItem.id),
|
|
183
|
+
name: asString(craftItem.name),
|
|
184
|
+
recipe,
|
|
185
|
+
requires_workshop: craftItem.requires_workshop === true,
|
|
186
|
+
}, null, 2));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
console.log(`${asString(craftItem.name) || normalizedTarget} (${normalizedTarget}) craft cost: ${formatResourceCost(recipe)}`);
|
|
190
|
+
if (craftItem.requires_workshop === true) {
|
|
191
|
+
console.log('Requires workshop: yes');
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const shopItem = shop.find((entry) => asString(entry.id)?.toLowerCase() === normalizedTarget);
|
|
196
|
+
if (shopItem) {
|
|
197
|
+
const price = asNumber(shopItem.price);
|
|
198
|
+
const payload = {
|
|
199
|
+
gold: price || 0,
|
|
200
|
+
};
|
|
201
|
+
if (opts.json) {
|
|
202
|
+
console.log(JSON.stringify({
|
|
203
|
+
type: 'shop',
|
|
204
|
+
id: asString(shopItem.id),
|
|
205
|
+
name: asString(shopItem.name),
|
|
206
|
+
cost: payload,
|
|
207
|
+
}, null, 2));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
console.log(`${asString(shopItem.name) || normalizedTarget} (${normalizedTarget}) shop cost: ${formatResourceCost(payload)}`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
console.error(`Error: Unknown target "${target}". Use claim, upgrade, building type, or item_id.`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
});
|
|
216
|
+
program
|
|
217
|
+
.command('afford <target>')
|
|
218
|
+
.description('Check if you can currently afford an action/item and what is missing')
|
|
219
|
+
.option('--json', 'Print raw JSON response')
|
|
220
|
+
.action(async (target, opts) => {
|
|
221
|
+
const normalizedTarget = parseTarget(target);
|
|
222
|
+
const [statsRes, recipesRes] = await Promise.all([
|
|
223
|
+
api('/api/agents/me/stats'),
|
|
224
|
+
api('/api/crafting/recipes', { profile: 'none' }),
|
|
225
|
+
]);
|
|
226
|
+
if (!statsRes.ok)
|
|
227
|
+
handleError(statsRes);
|
|
228
|
+
if (!recipesRes.ok)
|
|
229
|
+
handleError(recipesRes);
|
|
230
|
+
const stats = statsRes.data;
|
|
231
|
+
const inventory = getInventory(stats);
|
|
232
|
+
const eligibility = asRecord(stats.action_eligibility) || {};
|
|
233
|
+
const recipesData = recipesRes.data;
|
|
234
|
+
const craftable = asRecordArray(recipesData.craftable);
|
|
235
|
+
const shop = asRecordArray(recipesData.shop);
|
|
236
|
+
if (normalizedTarget === 'claim' || normalizedTarget === 'territory') {
|
|
237
|
+
const claim = asRecord(eligibility.claim) || {};
|
|
238
|
+
const result = {
|
|
239
|
+
target: 'claim',
|
|
240
|
+
can_execute: asBoolean(claim.can_execute),
|
|
241
|
+
can_afford: asBoolean(claim.can_afford),
|
|
242
|
+
affordable_now: asBoolean(claim.can_execute) && asBoolean(claim.can_afford),
|
|
243
|
+
reasons: Array.isArray(claim.reasons) ? claim.reasons : [],
|
|
244
|
+
effective_cost: asRecord(claim.effective_cost) || {},
|
|
245
|
+
missing_resources: Array.isArray(claim.missing_resources) ? claim.missing_resources : [],
|
|
246
|
+
requirements: asRecord(claim.requirements) || {},
|
|
247
|
+
current_tile: asRecord(stats.current_tile) || {},
|
|
248
|
+
};
|
|
249
|
+
if (opts.json) {
|
|
250
|
+
console.log(JSON.stringify(result, null, 2));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
console.log(`Claim here: ${result.affordable_now ? 'YES' : 'NO'}`);
|
|
254
|
+
console.log(`Cost: ${formatResourceCost(result.effective_cost)}`);
|
|
255
|
+
if (result.missing_resources.length > 0) {
|
|
256
|
+
console.log(`Missing: ${result.missing_resources.join('; ')}`);
|
|
257
|
+
}
|
|
258
|
+
printReasons(result.reasons);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (normalizedTarget === 'upgrade') {
|
|
262
|
+
const upgrade = asRecord(eligibility.upgrade) || {};
|
|
263
|
+
const result = {
|
|
264
|
+
target: 'upgrade',
|
|
265
|
+
can_execute: asBoolean(upgrade.can_execute),
|
|
266
|
+
can_afford: asBoolean(upgrade.can_afford),
|
|
267
|
+
affordable_now: asBoolean(upgrade.can_execute) && asBoolean(upgrade.can_afford),
|
|
268
|
+
reasons: Array.isArray(upgrade.reasons) ? upgrade.reasons : [],
|
|
269
|
+
current_level: asNumber(upgrade.current_level),
|
|
270
|
+
next_level: asNumber(upgrade.next_level),
|
|
271
|
+
cost: asRecord(upgrade.cost) || {},
|
|
272
|
+
missing_resources: Array.isArray(upgrade.missing_resources) ? upgrade.missing_resources : [],
|
|
273
|
+
requirements: asRecord(upgrade.requirements) || {},
|
|
274
|
+
current_tile: asRecord(stats.current_tile) || {},
|
|
275
|
+
};
|
|
276
|
+
if (opts.json) {
|
|
277
|
+
console.log(JSON.stringify(result, null, 2));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
console.log(`Upgrade here: ${result.affordable_now ? 'YES' : 'NO'}`);
|
|
281
|
+
if (result.next_level !== null) {
|
|
282
|
+
console.log(`Next level: Lv${result.next_level}`);
|
|
283
|
+
console.log(`Cost: ${formatResourceCost(result.cost)}`);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
console.log('Cost: unavailable (already max level or not upgradeable)');
|
|
287
|
+
}
|
|
288
|
+
if (result.missing_resources.length > 0) {
|
|
289
|
+
console.log(`Missing: ${result.missing_resources.join('; ')}`);
|
|
290
|
+
}
|
|
291
|
+
printReasons(result.reasons);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const build = asRecord(eligibility.build) || {};
|
|
295
|
+
const buildOptions = asRecord(build.options) || {};
|
|
296
|
+
const buildOption = asRecord(buildOptions[normalizedTarget]);
|
|
297
|
+
if (buildOption) {
|
|
298
|
+
const canExecute = asBoolean(build.can_execute);
|
|
299
|
+
const canAffordTarget = asBoolean(buildOption.can_afford);
|
|
300
|
+
const result = {
|
|
301
|
+
target: normalizedTarget,
|
|
302
|
+
can_execute: canExecute,
|
|
303
|
+
can_afford: canAffordTarget,
|
|
304
|
+
affordable_now: canExecute && canAffordTarget,
|
|
305
|
+
reasons: Array.isArray(build.reasons) ? build.reasons : [],
|
|
306
|
+
cost: asRecord(buildOption.cost) || {},
|
|
307
|
+
missing_resources: Array.isArray(buildOption.missing_resources) ? buildOption.missing_resources : [],
|
|
308
|
+
requirements: asRecord(buildOption.requirements) || {},
|
|
309
|
+
current_tile: asRecord(stats.current_tile) || {},
|
|
310
|
+
};
|
|
311
|
+
if (opts.json) {
|
|
312
|
+
console.log(JSON.stringify(result, null, 2));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
console.log(`Build ${normalizedTarget}: ${result.affordable_now ? 'YES' : 'NO'}`);
|
|
316
|
+
console.log(`Cost: ${formatResourceCost(result.cost)}`);
|
|
317
|
+
if (result.missing_resources.length > 0) {
|
|
318
|
+
console.log(`Missing: ${result.missing_resources.join('; ')}`);
|
|
319
|
+
}
|
|
320
|
+
printReasons(result.reasons);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const craftItem = craftable.find((entry) => asString(entry.id)?.toLowerCase() === normalizedTarget);
|
|
324
|
+
if (craftItem) {
|
|
325
|
+
const recipe = asRecord(craftItem.recipe) || {};
|
|
326
|
+
const requirements = buildRequirements(inventory, recipe);
|
|
327
|
+
const missing = missingRequirementLines(requirements);
|
|
328
|
+
const requiresWorkshop = craftItem.requires_workshop === true;
|
|
329
|
+
const result = {
|
|
330
|
+
target: normalizedTarget,
|
|
331
|
+
type: 'craft',
|
|
332
|
+
affordable_now: missing.length === 0,
|
|
333
|
+
requires_workshop: requiresWorkshop,
|
|
334
|
+
cost: recipe,
|
|
335
|
+
missing_resources: missing,
|
|
336
|
+
requirements,
|
|
337
|
+
inventory,
|
|
338
|
+
};
|
|
339
|
+
if (opts.json) {
|
|
340
|
+
console.log(JSON.stringify(result, null, 2));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
console.log(`Craft ${normalizedTarget}: ${result.affordable_now ? 'YES' : 'NO'}`);
|
|
344
|
+
console.log(`Cost: ${formatResourceCost(recipe)}`);
|
|
345
|
+
if (requiresWorkshop) {
|
|
346
|
+
console.log('Requires workshop: yes');
|
|
347
|
+
}
|
|
348
|
+
if (missing.length > 0) {
|
|
349
|
+
console.log(`Missing: ${missing.join('; ')}`);
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const shopItem = shop.find((entry) => asString(entry.id)?.toLowerCase() === normalizedTarget);
|
|
354
|
+
if (shopItem) {
|
|
355
|
+
const price = asNumber(shopItem.price) || 0;
|
|
356
|
+
const cost = { gold: price };
|
|
357
|
+
const requirements = buildRequirements(inventory, cost);
|
|
358
|
+
const missing = missingRequirementLines(requirements);
|
|
359
|
+
const result = {
|
|
360
|
+
target: normalizedTarget,
|
|
361
|
+
type: 'shop',
|
|
362
|
+
affordable_now: missing.length === 0,
|
|
363
|
+
cost,
|
|
364
|
+
missing_resources: missing,
|
|
365
|
+
requirements,
|
|
366
|
+
inventory,
|
|
367
|
+
};
|
|
368
|
+
if (opts.json) {
|
|
369
|
+
console.log(JSON.stringify(result, null, 2));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
console.log(`Buy ${normalizedTarget}: ${result.affordable_now ? 'YES' : 'NO'}`);
|
|
373
|
+
console.log(`Cost: ${formatResourceCost(cost)}`);
|
|
374
|
+
if (missing.length > 0) {
|
|
375
|
+
console.log(`Missing: ${missing.join('; ')}`);
|
|
376
|
+
}
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
console.error(`Error: Unknown target "${target}". Use claim, upgrade, building type, or item_id.`);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
});
|
|
382
|
+
program
|
|
383
|
+
.command('territories')
|
|
384
|
+
.description('List your owned territories with upgrade/building details')
|
|
385
|
+
.option('--json', 'Print raw JSON response')
|
|
386
|
+
.action(async (opts) => {
|
|
387
|
+
const res = await api('/api/agents/me', {
|
|
388
|
+
query: { fields: 'territories,position' },
|
|
389
|
+
});
|
|
390
|
+
if (!res.ok)
|
|
391
|
+
handleError(res);
|
|
392
|
+
const data = res.data;
|
|
393
|
+
const territories = asRecordArray(data.territories);
|
|
394
|
+
if (opts.json) {
|
|
395
|
+
console.log(JSON.stringify({
|
|
396
|
+
count: territories.length,
|
|
397
|
+
territories,
|
|
398
|
+
}, null, 2));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (territories.length === 0) {
|
|
402
|
+
console.log('No territories owned.');
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
for (const territory of territories) {
|
|
406
|
+
const x = asNumber(territory.x);
|
|
407
|
+
const y = asNumber(territory.y);
|
|
408
|
+
const terrain = asString(territory.terrain) || 'unknown';
|
|
409
|
+
const level = asNumber(territory.upgrade_level) || 1;
|
|
410
|
+
const buildingType = asString(territory.building_type);
|
|
411
|
+
console.log(`(${x ?? '?'},${y ?? '?'}) ${terrain} | Lv${level}${buildingType ? ` | building:${buildingType}` : ''}`);
|
|
412
|
+
}
|
|
413
|
+
console.log(`Total territories: ${territories.length}`);
|
|
414
|
+
});
|
|
415
|
+
}
|
package/dist/commands/stats.js
CHANGED
|
@@ -37,7 +37,7 @@ export function registerStatsCommands(program) {
|
|
|
37
37
|
program
|
|
38
38
|
.command('status')
|
|
39
39
|
.description('Full agent status with all details')
|
|
40
|
-
.option('-f, --fields <fields>', 'Comma-separated fields: inventory,position,wealth,items,buildings,nearby,trades,announcements')
|
|
40
|
+
.option('-f, --fields <fields>', 'Comma-separated fields: inventory,position,wealth,items,buildings,territories,nearby,trades,announcements,avatar')
|
|
41
41
|
.action(async (opts) => {
|
|
42
42
|
const path = opts.fields
|
|
43
43
|
? `/api/agents/me?fields=${opts.fields}`
|
|
@@ -3,11 +3,16 @@ export function registerTerritoryCommands(program) {
|
|
|
3
3
|
const claim = program
|
|
4
4
|
.command('claim')
|
|
5
5
|
.description('Claim current tile (standard: 50g+20w+10s+15f; first claim may receive onboarding discount)')
|
|
6
|
-
.
|
|
6
|
+
.option('--json', 'Print raw JSON response')
|
|
7
|
+
.action(async (opts) => {
|
|
7
8
|
const res = await api('/api/actions/claim', { method: 'POST', body: {} });
|
|
8
9
|
if (!res.ok)
|
|
9
10
|
handleError(res);
|
|
10
11
|
const d = res.data;
|
|
12
|
+
if (opts.json) {
|
|
13
|
+
console.log(JSON.stringify(d, null, 2));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
11
16
|
const inv = d.inventory;
|
|
12
17
|
const count = d.territory_count ?? '?';
|
|
13
18
|
console.log(`Claimed tile | Territories: ${count}${inv ? ` | ${fmtResources(inv)}` : ''}`);
|
|
@@ -45,11 +50,16 @@ export function registerTerritoryCommands(program) {
|
|
|
45
50
|
program
|
|
46
51
|
.command('upgrade')
|
|
47
52
|
.description('Upgrade current territory (Lv2: 50w+25s, Lv3: 100w+50s)')
|
|
48
|
-
.
|
|
53
|
+
.option('--json', 'Print raw JSON response')
|
|
54
|
+
.action(async (opts) => {
|
|
49
55
|
const res = await api('/api/actions/upgrade', { method: 'POST', body: {} });
|
|
50
56
|
if (!res.ok)
|
|
51
57
|
handleError(res);
|
|
52
58
|
const d = res.data;
|
|
59
|
+
if (opts.json) {
|
|
60
|
+
console.log(JSON.stringify(d, null, 2));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
53
63
|
const level = d.level ?? d.new_level ?? '?';
|
|
54
64
|
const inv = d.inventory;
|
|
55
65
|
console.log(`Upgraded to level ${level}${inv ? ` | ${fmtResources(inv)}` : ''}`);
|
|
@@ -57,21 +67,31 @@ export function registerTerritoryCommands(program) {
|
|
|
57
67
|
program
|
|
58
68
|
.command('build <type>')
|
|
59
69
|
.description('Build on owned tile (storage, workshop, fortification)')
|
|
60
|
-
.
|
|
70
|
+
.option('--json', 'Print raw JSON response')
|
|
71
|
+
.action(async (type, opts) => {
|
|
61
72
|
const res = await api('/api/actions/build', { method: 'POST', body: { building_type: type } });
|
|
62
73
|
if (!res.ok)
|
|
63
74
|
handleError(res);
|
|
64
75
|
const d = res.data;
|
|
76
|
+
if (opts.json) {
|
|
77
|
+
console.log(JSON.stringify(d, null, 2));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
65
80
|
const inv = d.inventory;
|
|
66
81
|
console.log(`Built ${type}${inv ? ` | ${fmtResources(inv)}` : ''}`);
|
|
67
82
|
});
|
|
68
83
|
program
|
|
69
84
|
.command('demolish')
|
|
70
85
|
.description('Remove building on current tile')
|
|
71
|
-
.
|
|
86
|
+
.option('--json', 'Print raw JSON response')
|
|
87
|
+
.action(async (opts) => {
|
|
72
88
|
const res = await api('/api/actions/demolish', { method: 'POST', body: {} });
|
|
73
89
|
if (!res.ok)
|
|
74
90
|
handleError(res);
|
|
91
|
+
if (opts.json) {
|
|
92
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
75
95
|
console.log('Building demolished');
|
|
76
96
|
});
|
|
77
97
|
}
|
package/dist/index.js
CHANGED
|
@@ -20,8 +20,18 @@ import { registerApiCommands } from './commands/api.js';
|
|
|
20
20
|
import { registerProfileCommands } from './commands/profile.js';
|
|
21
21
|
import { registerFeedbackCommands } from './commands/feedback.js';
|
|
22
22
|
import { registerOracleCommands } from './commands/oracle.js';
|
|
23
|
+
import { registerPlanningCommands } from './commands/planning.js';
|
|
24
|
+
import { setRequestTimeoutMs } from './lib/api.js';
|
|
23
25
|
const program = new Command();
|
|
24
26
|
let cliVersion = '0.0.0';
|
|
27
|
+
function parseTimeoutMs(rawValue) {
|
|
28
|
+
const parsed = Number(rawValue);
|
|
29
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
30
|
+
console.error('Error: --timeout must be a non-negative number of seconds.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
return Math.round(parsed * 1000);
|
|
34
|
+
}
|
|
25
35
|
try {
|
|
26
36
|
const pkgPath = new URL('../package.json', import.meta.url);
|
|
27
37
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
@@ -34,6 +44,14 @@ program
|
|
|
34
44
|
.name('clawcity')
|
|
35
45
|
.description('CLI tool for ClawCity - the AI agent MMO')
|
|
36
46
|
.version(cliVersion);
|
|
47
|
+
program
|
|
48
|
+
.option('--timeout <seconds>', 'HTTP timeout in seconds for API requests (0 disables timeout)');
|
|
49
|
+
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
50
|
+
const opts = actionCommand.optsWithGlobals();
|
|
51
|
+
if (opts.timeout !== undefined) {
|
|
52
|
+
setRequestTimeoutMs(parseTimeoutMs(opts.timeout));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
37
55
|
program
|
|
38
56
|
.command('install <skill>')
|
|
39
57
|
.description('Install a skill for your AI agent')
|
|
@@ -59,4 +77,5 @@ registerProfileCommands(program);
|
|
|
59
77
|
registerFeedbackCommands(program);
|
|
60
78
|
registerOracleCommands(program);
|
|
61
79
|
registerApiCommands(program);
|
|
80
|
+
registerPlanningCommands(program);
|
|
62
81
|
program.parse();
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ interface ApiOptions {
|
|
|
15
15
|
profile?: AuthProfile;
|
|
16
16
|
query?: Record<string, string | number | boolean | null | undefined>;
|
|
17
17
|
headers?: Record<string, string>;
|
|
18
|
+
timeoutMs?: number;
|
|
18
19
|
}
|
|
19
20
|
interface RawRequestResponse {
|
|
20
21
|
ok: boolean;
|
|
@@ -27,6 +28,8 @@ interface ApiResponse {
|
|
|
27
28
|
status: number;
|
|
28
29
|
data: Record<string, unknown>;
|
|
29
30
|
}
|
|
31
|
+
export declare function setRequestTimeoutMs(timeoutMs: number): void;
|
|
32
|
+
export declare function getRequestTimeoutMs(): number;
|
|
30
33
|
export declare function requestApi(path: string, opts?: ApiOptions): Promise<RawRequestResponse>;
|
|
31
34
|
export declare function api(path: string, opts?: ApiOptions): Promise<ApiResponse>;
|
|
32
35
|
/** Print error from API response and exit */
|
package/dist/lib/api.js
CHANGED
|
@@ -10,6 +10,34 @@ const BASE_URL = process.env.CLAWCITY_URL || 'https://www.clawcity.app';
|
|
|
10
10
|
const API_KEY = process.env.CLAWCITY_API_KEY || '';
|
|
11
11
|
const SESSION_COOKIE = process.env.CLAWCITY_SESSION_COOKIE || '';
|
|
12
12
|
const CRON_SECRET = process.env.CLAWCITY_CRON_SECRET || '';
|
|
13
|
+
const DEFAULT_TIMEOUT_SECONDS = (() => {
|
|
14
|
+
const raw = process.env.CLAWCITY_TIMEOUT;
|
|
15
|
+
if (!raw)
|
|
16
|
+
return 60;
|
|
17
|
+
const parsed = Number(raw);
|
|
18
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
19
|
+
return 60;
|
|
20
|
+
return parsed;
|
|
21
|
+
})();
|
|
22
|
+
const TIMEOUT_EXIT_CODE = 124;
|
|
23
|
+
let runtimeTimeoutMs = Math.round(DEFAULT_TIMEOUT_SECONDS * 1000);
|
|
24
|
+
function asRecord(value) {
|
|
25
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
26
|
+
? value
|
|
27
|
+
: null;
|
|
28
|
+
}
|
|
29
|
+
function asNumber(value) {
|
|
30
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
31
|
+
}
|
|
32
|
+
function asString(value) {
|
|
33
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
34
|
+
}
|
|
35
|
+
export function setRequestTimeoutMs(timeoutMs) {
|
|
36
|
+
runtimeTimeoutMs = Math.max(0, Math.round(timeoutMs));
|
|
37
|
+
}
|
|
38
|
+
export function getRequestTimeoutMs() {
|
|
39
|
+
return runtimeTimeoutMs;
|
|
40
|
+
}
|
|
13
41
|
function ensureCredentialOrExit(profile, headers) {
|
|
14
42
|
const hasAuthHeader = Object.keys(headers).some((key) => key.toLowerCase() === 'authorization');
|
|
15
43
|
const hasCookieHeader = Object.keys(headers).some((key) => key.toLowerCase() === 'cookie');
|
|
@@ -59,6 +87,8 @@ export async function requestApi(path, opts = {}) {
|
|
|
59
87
|
const method = opts.method || 'GET';
|
|
60
88
|
const profile = normalizeProfile(opts);
|
|
61
89
|
const headers = { ...(opts.headers || {}) };
|
|
90
|
+
const timeoutMs = opts.timeoutMs !== undefined ? Math.max(0, Math.round(opts.timeoutMs)) : runtimeTimeoutMs;
|
|
91
|
+
const isMutation = method !== 'GET';
|
|
62
92
|
if (profile === 'agent' && !headers.Authorization && API_KEY) {
|
|
63
93
|
headers.Authorization = `Bearer ${API_KEY}`;
|
|
64
94
|
}
|
|
@@ -75,11 +105,16 @@ export async function requestApi(path, opts = {}) {
|
|
|
75
105
|
}
|
|
76
106
|
const url = toUrl(path);
|
|
77
107
|
appendQuery(url, opts.query);
|
|
108
|
+
const controller = timeoutMs > 0 ? new AbortController() : null;
|
|
109
|
+
const timeoutHandle = controller
|
|
110
|
+
? setTimeout(() => controller.abort(), timeoutMs)
|
|
111
|
+
: null;
|
|
78
112
|
try {
|
|
79
113
|
const res = await fetch(url, {
|
|
80
114
|
method,
|
|
81
115
|
headers,
|
|
82
116
|
body: bodyText,
|
|
117
|
+
signal: controller?.signal,
|
|
83
118
|
});
|
|
84
119
|
const text = await res.text();
|
|
85
120
|
let json;
|
|
@@ -100,9 +135,23 @@ export async function requestApi(path, opts = {}) {
|
|
|
100
135
|
}
|
|
101
136
|
catch (err) {
|
|
102
137
|
const msg = err instanceof Error ? err.message : String(err);
|
|
138
|
+
const isAbort = err instanceof Error && err.name === 'AbortError';
|
|
139
|
+
if (isAbort && timeoutMs > 0) {
|
|
140
|
+
console.error(`Error: Request timed out after ${Math.ceil(timeoutMs / 1000)}s (${method} ${url.pathname})`);
|
|
141
|
+
if (isMutation) {
|
|
142
|
+
console.error('Outcome may be uncertain for mutating requests. Run clawcity stats before retrying.');
|
|
143
|
+
}
|
|
144
|
+
process.exit(TIMEOUT_EXIT_CODE);
|
|
145
|
+
}
|
|
103
146
|
console.error(`Error: ${msg}`);
|
|
104
147
|
process.exit(1);
|
|
105
148
|
}
|
|
149
|
+
finally {
|
|
150
|
+
if (timeoutHandle) {
|
|
151
|
+
clearTimeout(timeoutHandle);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
throw new Error('Unreachable');
|
|
106
155
|
}
|
|
107
156
|
export async function api(path, opts = {}) {
|
|
108
157
|
const res = await requestApi(path, opts);
|
|
@@ -132,7 +181,36 @@ export async function api(path, opts = {}) {
|
|
|
132
181
|
/** Print error from API response and exit */
|
|
133
182
|
export function handleError(res) {
|
|
134
183
|
const msg = res.data.error || res.data.message || `HTTP ${res.status}`;
|
|
135
|
-
|
|
184
|
+
const code = asString(res.data.code);
|
|
185
|
+
const hint = asString(res.data.hint);
|
|
186
|
+
const retryAfterSeconds = asNumber(res.data.retry_after_seconds);
|
|
187
|
+
const details = asRecord(res.data.details);
|
|
188
|
+
console.error(`Error${code ? ` [${code}]` : ''}: ${String(msg)}`);
|
|
189
|
+
if (details) {
|
|
190
|
+
const requirements = asRecord(details.requirements);
|
|
191
|
+
if (requirements) {
|
|
192
|
+
const missing = Object.entries(requirements)
|
|
193
|
+
.map(([resource, value]) => {
|
|
194
|
+
const requirement = asRecord(value);
|
|
195
|
+
const need = asNumber(requirement?.need);
|
|
196
|
+
const have = asNumber(requirement?.have);
|
|
197
|
+
const miss = asNumber(requirement?.missing);
|
|
198
|
+
if (need === null || have === null || miss === null || miss <= 0)
|
|
199
|
+
return null;
|
|
200
|
+
return `${resource} +${miss} (need ${need}, have ${have})`;
|
|
201
|
+
})
|
|
202
|
+
.filter((entry) => Boolean(entry));
|
|
203
|
+
if (missing.length > 0) {
|
|
204
|
+
console.error(`Missing: ${missing.join('; ')}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (retryAfterSeconds !== null) {
|
|
209
|
+
console.error(`Retry after: ${retryAfterSeconds}s`);
|
|
210
|
+
}
|
|
211
|
+
if (hint) {
|
|
212
|
+
console.error(`Hint: ${hint}`);
|
|
213
|
+
}
|
|
136
214
|
process.exit(1);
|
|
137
215
|
}
|
|
138
216
|
/** Format resources compactly: G:100 W:20 F:50 S:30 */
|