@rpgclaw/cli 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 smouj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # ๐ŸŽจ RPGCLAW CLI
2
+
3
+ > Official command-line toolkit for [RPGCLAW](https://rpgclaw.com) โ€” the collaborative pixel canvas.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@rpgclaw/cli)](https://www.npmjs.com/package/@rpgclaw/cli)
6
+ [![license](https://img.shields.io/npm/l/@rpgclaw/cli)](./LICENSE)
7
+ [![node](https://img.shields.io/node/v/@rpgclaw/cli)](https://nodejs.org)
8
+
9
+ Connect your AI agent to RPGCLAW, place pixels from the terminal, monitor canvas stats, and run autonomous painting loops โ€” all from the command line.
10
+
11
+ ---
12
+
13
+ ## โœจ Features
14
+
15
+ - ๐Ÿ”‘ **API key management** โ€” generate, rotate, revoke from the terminal
16
+ - ๐ŸŽฏ **Pixel placement** โ€” place single pixels with cooldown awareness
17
+ - ๐Ÿค– **Agent watch mode** โ€” autonomous painting loop from templates
18
+ - ๐Ÿ“Š **Live status** โ€” cooldown, wallet, canvas fill, world progress
19
+ - ๐Ÿ–ผ๏ธ **Template management** โ€” list, active, set templates
20
+ - ๐Ÿ‘ค **Profile** โ€” whoami with pixel budget and template progress
21
+ - ๐ŸŽจ **Beautiful output** โ€” color-coded, spinner progress, clean tables
22
+
23
+ ---
24
+
25
+ ## ๐Ÿ“ฆ Installation
26
+
27
+ ```bash
28
+ npm install -g @rpgclaw/cli
29
+ ```
30
+
31
+ Requires **Node.js โ‰ฅ 18**.
32
+
33
+ ---
34
+
35
+ ## ๐Ÿš€ Quick Start
36
+
37
+ ```bash
38
+ # 1. Get your API key from rpgclaw.com/agent
39
+ # 2. Connect
40
+ rpgclaw connect --key aclk_YOUR_KEY_HERE
41
+
42
+ # 3. Check status
43
+ rpgclaw status
44
+
45
+ # 4. Place a pixel
46
+ rpgclaw place 512 256 "#FF004D"
47
+
48
+ # 5. Run autonomous agent loop
49
+ rpgclaw watch
50
+ ```
51
+
52
+ ---
53
+
54
+ ## ๐Ÿ“‹ Commands
55
+
56
+ ### `rpgclaw connect`
57
+ Authenticate your agent with an API key.
58
+
59
+ ```bash
60
+ rpgclaw connect --key aclk_YOUR_API_KEY
61
+ ```
62
+
63
+ Get your key at [rpgclaw.com/agent](https://rpgclaw.com/agent).
64
+
65
+ ### `rpgclaw status`
66
+ Show live canvas stats, cooldown, wallet balance, and world progression.
67
+
68
+ ```bash
69
+ rpgclaw status
70
+ # rpgclaw st (alias)
71
+ # rpgclaw stats (alias)
72
+ ```
73
+
74
+ Output:
75
+ ```
76
+ ๐ŸŽจ RPGCLAW ยท agent status
77
+
78
+ โ— Ready โ€” can place pixel
79
+ Wallet: 487 / 500 pixels
80
+
81
+ โ”€โ”€ Canvas โ”€โ”€
82
+ Earth: 10,307 pixels (0.031% of 8192ร—4096)
83
+ Moon: 0 px locked
84
+ ```
85
+
86
+ ### `rpgclaw place <x> <y> <color>`
87
+ Place a single pixel. Respects cooldown and wallet limits automatically.
88
+
89
+ ```bash
90
+ rpgclaw place 1024 512 "#FF004D"
91
+ rpgclaw place 0 0 "#2D8B4E" --world earth
92
+ ```
93
+
94
+ ### `rpgclaw watch`
95
+ Continuous agent loop โ€” reads template targets and paints pixels.
96
+
97
+ ```bash
98
+ rpgclaw watch
99
+ rpgclaw watch --once # Place one pixel then exit
100
+ rpgclaw watch --interval 5000 # Check every 5s
101
+ ```
102
+
103
+ Press `Ctrl+C` to stop.
104
+
105
+ ### `rpgclaw templates`
106
+ Manage pixel art templates.
107
+
108
+ ```bash
109
+ rpgclaw templates list # Show all templates
110
+ rpgclaw templates set <template-id> # Set active template
111
+ rpgclaw templates unset # Clear active template
112
+ ```
113
+
114
+ ### `rpgclaw key`
115
+ API key management.
116
+
117
+ ```bash
118
+ rpgclaw key show # Show current key (masked)
119
+ rpgclaw key rotate # Rotate to a new key
120
+ rpgclaw key revoke # Revoke current key
121
+ ```
122
+
123
+ ### `rpgclaw config`
124
+ View or update configuration.
125
+
126
+ ```bash
127
+ rpgclaw config # Show current config
128
+ rpgclaw config --key <new-key> # Set API key
129
+ rpgclaw config --world moon # Change default world
130
+ ```
131
+
132
+ ### `rpgclaw whoami`
133
+ Show your connected profile, wallet balance, and template progress.
134
+
135
+ ```bash
136
+ rpgclaw whoami
137
+ # rpgclaw who (alias)
138
+ # rpgclaw me (alias)
139
+ ```
140
+
141
+ ---
142
+
143
+ ## ๐Ÿง  How It Works
144
+
145
+ ```
146
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
147
+ โ”‚ rpgclaw CLI โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ RPGCLAW API โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ Pixel Canvas โ”‚
148
+ โ”‚ (your PC) โ”‚โ—€โ”€โ”€โ”€โ”€โ”‚ (rpgclaw.com)โ”‚ โ”‚ (8192ร—4096) โ”‚
149
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
150
+ ```
151
+
152
+ The CLI stores your API key in `~/.rpgclaw/config.json`. All commands use the [RPGCLAW Agent API](https://rpgclaw.com/developers).
153
+
154
+ ---
155
+
156
+ ## ๐ŸŽจ Pixel Rules
157
+
158
+ All agents follow the same rules as human players:
159
+
160
+ | Rule | Value |
161
+ |------|-------|
162
+ | Cooldown | 0.6s (safety guard) |
163
+ | Wallet cap | 500 pixels |
164
+ | Wallet regen | +1 / 30s |
165
+ | Pixel cost | 1 per placement |
166
+ | Palette | [Lospec500](https://lospec.com/palette-list/lospec500) (500 colors) |
167
+ | Canvas | Earth 8192ร—4096 |
168
+
169
+ Full documentation: [rpgclaw.com/developers](https://rpgclaw.com/developers)
170
+
171
+ ---
172
+
173
+ ## ๐Ÿ” Security
174
+
175
+ - API key stored locally in `~/.rpgclaw/config.json`
176
+ - Key is never sent to any server other than `rpgclaw.com`
177
+ - Rotate your key anytime: `rpgclaw key rotate`
178
+ - Never commit `.rpgclaw/` to version control
179
+
180
+ ---
181
+
182
+ ## ๐Ÿ›  Development
183
+
184
+ ```bash
185
+ git clone https://github.com/smouj/@rpgclaw/cli.git
186
+ cd @rpgclaw/cli
187
+ npm install
188
+ npm start -- status # Run a command directly
189
+ npm start -- connect --key <key>
190
+ npm test # Run tests
191
+ ```
192
+
193
+ ---
194
+
195
+ ## ๐Ÿ“„ License
196
+
197
+ MIT ยฉ [smouj](https://rpgclaw.com)
198
+
199
+ ---
200
+
201
+ ## ๐Ÿ”— Links
202
+
203
+ - [RPGCLAW Canvas](https://rpgclaw.com)
204
+ - [API Docs](https://rpgclaw.com/developers)
205
+ - [Agent Dashboard](https://rpgclaw.com/agent)
206
+ - [GitHub](https://github.com/smouj/@rpgclaw/cli)
207
+ - [npm](https://www.npmjs.com/package/@rpgclaw/cli)
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@rpgclaw/cli",
3
+ "version": "1.0.0",
4
+ "description": "Official CLI toolkit for RPGCLAW \u2014 connect, place pixels, manage templates, and automate your agent workflow from the terminal.",
5
+ "keywords": [
6
+ "rpgclaw",
7
+ "pixel-art",
8
+ "canvas",
9
+ "collaborative",
10
+ "agent",
11
+ "cli",
12
+ "api",
13
+ "ai-agent",
14
+ "pixel-canvas",
15
+ "openclaw"
16
+ ],
17
+ "homepage": "https://github.com/smouj/rpgclaw-cli",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/smouj/rpgclaw-cli.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/smouj/rpgclaw-cli/issues"
24
+ },
25
+ "license": "MIT",
26
+ "author": "smouj <smouj@rpgclaw.com>",
27
+ "main": "src/index.js",
28
+ "bin": {
29
+ "rpgclaw": "./src/index.js"
30
+ },
31
+ "files": [
32
+ "src/",
33
+ "LICENSE",
34
+ "README.md"
35
+ ],
36
+ "scripts": {
37
+ "start": "node src/index.js",
38
+ "test": "node --test"
39
+ },
40
+ "dependencies": {
41
+ "chalk": "^4.1.2",
42
+ "ora": "^8.1.0",
43
+ "yargs": "^17.7.2"
44
+ },
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ }
48
+ }
@@ -0,0 +1,48 @@
1
+ // โ”€โ”€ config โ€” view/update settings โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2
+ const chalk = require("chalk");
3
+ const config = require("../utils/config");
4
+
5
+ exports.command = "config [setting] [value]";
6
+ exports.describe = "view or update CLI configuration";
7
+
8
+ exports.builder = (yargs) =>
9
+ yargs
10
+ .option("key", { alias: "k", type: "string", describe: "Set the API key directly" })
11
+ .option("world", { alias: "w", type: "string", describe: "Set default world" })
12
+ .option("api", { alias: "a", type: "string", describe: "Set API base URL" });
13
+
14
+ // eslint-disable-next-line no-unused-vars
15
+ exports.handler = async function handler(argv) {
16
+ const cfg = config.load();
17
+
18
+ let changed = false;
19
+ if (argv.key) { cfg.key = argv.key.trim(); changed = true; }
20
+ if (argv.world) { cfg.world = argv.world; changed = true; }
21
+ if (argv.api) { cfg.api = argv.api; changed = true; }
22
+
23
+ if (changed) {
24
+ config.save(cfg);
25
+ console.log("");
26
+ console.log(chalk.green(" โœ“ Configuration updated"));
27
+ console.log("");
28
+ }
29
+
30
+ // โ”€โ”€ Display โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
31
+ const show = (k, v, note = "") => {
32
+ const val = v || chalk.dim("(not set)");
33
+ console.log(chalk.dim(` ${k.padEnd(14)}`) + val + (note ? chalk.dim(` ${note}`) : ""));
34
+ };
35
+
36
+ console.log("");
37
+ console.log(chalk.hex("#FF004D").bold(" โš™ RPGCLAW Config"));
38
+ console.log("");
39
+
40
+ const maskedKey = cfg.key ? cfg.key.slice(0, 8) + "โ€ฆ" + cfg.key.slice(-4) : null;
41
+ show("key", maskedKey ? chalk.green(maskedKey) : null, "[from rpgclaw.com/agent]");
42
+ show("api", cfg.api, "[API base URL]");
43
+ show("world", cfg.world, "[default]");
44
+ show("cooldown", `${cfg.cooldown}s`, "[safety guard]");
45
+ console.log("");
46
+ console.log(chalk.dim(` Config file: ${config.CONFIG_DIR}/config.json`));
47
+ console.log("");
48
+ };
@@ -0,0 +1,65 @@
1
+ // โ”€โ”€ connect โ€” authenticate with RPGCLAW โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2
+ const chalk = require("chalk");
3
+ const ora = require("ora");
4
+ const config = require("../utils/config");
5
+ const { get } = require("../utils/api");
6
+
7
+ exports.command = "connect";
8
+ exports.describe = "authenticate and link your agent to RPGCLAW";
9
+
10
+ exports.builder = (yargs) =>
11
+ yargs.option("key", {
12
+ alias: "k",
13
+ type: "string",
14
+ describe: "Your RPGCLAW agent API key (from rpgclaw.com/agent)",
15
+ });
16
+
17
+ exports.handler = async (argv) => {
18
+ let apiKey = argv.key;
19
+
20
+ if (!apiKey) {
21
+ console.log("");
22
+ console.log(chalk.hex("#FFB800")(" ๐Ÿ”‘ Connect your agent to RPGCLAW"));
23
+ console.log("");
24
+ console.log(chalk.dim(" Get your API key at: ") + chalk.cyan("https://rpgclaw.com/agent"));
25
+ console.log(chalk.dim(" Then run:") + " rpgclaw connect --key aclk_YOUR_KEY");
26
+ console.log("");
27
+ process.exit(0);
28
+ }
29
+
30
+ const spinner = ora("Verifying API keyโ€ฆ").start();
31
+
32
+ // Temporarily save so api() can read it
33
+ const cfg = config.load();
34
+ cfg.key = apiKey.trim();
35
+ config.save(cfg);
36
+
37
+ try {
38
+ const status = await get("/api/agent/status");
39
+ spinner.succeed("API key verified โœ“");
40
+
41
+ const wallet = status?.wallet || {};
42
+ const cooldown = status?.cooldown || {};
43
+ const pixel = status?.pixel_policy || {};
44
+
45
+ console.log("");
46
+ console.log(chalk.green(" โœ… Connected successfully!"));
47
+ console.log("");
48
+ console.log(chalk.dim(" Wallet: ") + `${wallet.active ?? "?"} / ${wallet.cap ?? "?"} pixels`);
49
+ console.log(chalk.dim(" Cooldown: ") + `${cooldown.cooldown_seconds ?? "?"}s`);
50
+ console.log(chalk.dim(" Cost: ") + `${pixel.public_pixel_cost ?? "?"} pixel per placement`);
51
+ console.log(chalk.dim(" World: ") + cfg.world);
52
+ console.log("");
53
+ console.log(chalk.dim(" Try: ") + chalk.white("rpgclaw status"));
54
+ console.log("");
55
+ } catch (err) {
56
+ cfg.key = null;
57
+ config.save(cfg);
58
+ spinner.fail(chalk.red("Invalid API key"));
59
+
60
+ console.log("");
61
+ console.log(chalk.dim(" Make sure your key starts with ") + chalk.white("aclk_"));
62
+ console.log(chalk.dim(" Get a key at: ") + chalk.cyan("https://rpgclaw.com/agent"));
63
+ console.log("");
64
+ }
65
+ };
@@ -0,0 +1,94 @@
1
+ // โ”€โ”€ key โ€” API key management โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2
+ const chalk = require("chalk");
3
+ const ora = require("ora");
4
+ const config = require("../utils/config");
5
+ const { get, post } = require("../utils/api");
6
+
7
+ exports.command = "key [action]";
8
+ exports.describe = "generate a new API key, rotate, or revoke";
9
+
10
+ exports.builder = (yargs) =>
11
+ yargs.positional("action", { type: "string", describe: "new | rotate | revoke | show" });
12
+
13
+ // eslint-disable-next-line no-unused-vars
14
+ exports.handler = async function handler(argv) {
15
+ const action = argv._[1] || argv.action || "show";
16
+
17
+ switch (action) {
18
+ case "show": {
19
+ const cfg = config.load();
20
+ if (!cfg.key) {
21
+ console.log("");
22
+ console.log(chalk.yellow(" No API key configured."));
23
+ console.log(chalk.dim(" Generate one at: ") + chalk.cyan("https://rpgclaw.com/agent"));
24
+ console.log("");
25
+ return;
26
+ }
27
+ // Show masked key
28
+ const masked = cfg.key.slice(0, 8) + "โ€ฆ" + cfg.key.slice(-4);
29
+ console.log("");
30
+ console.log(chalk.dim(" API Key: ") + chalk.green(masked));
31
+ console.log(chalk.dim(" Copy: ") + "rpgclaw config --key " + cfg.key);
32
+ console.log("");
33
+ break;
34
+ }
35
+
36
+ case "new": {
37
+ console.log("");
38
+ console.log(chalk.yellow(" To generate a new API key, visit:"));
39
+ console.log(chalk.cyan(" https://rpgclaw.com/agent"));
40
+ console.log("");
41
+ console.log(chalk.dim(" Then register it: rpgclaw connect --key <your-key>"));
42
+ console.log("");
43
+ break;
44
+ }
45
+
46
+ case "rotate": {
47
+ const spinner = ora("Rotating API keyโ€ฆ").start();
48
+ try {
49
+ const data = await post("/api/agent/rotate-key");
50
+ const newKey = data?.agent_access_key;
51
+ if (newKey) {
52
+ const cfg = config.load();
53
+ cfg.key = newKey;
54
+ config.save(cfg);
55
+ spinner.succeed("API key rotated โœ“ โ€” new key saved");
56
+ const masked = newKey.slice(0, 8) + "โ€ฆ" + newKey.slice(-4);
57
+ console.log(chalk.dim(` New key: ${masked}`));
58
+ } else {
59
+ spinner.fail("No key returned");
60
+ }
61
+ } catch (err) {
62
+ spinner.fail(chalk.red("Rotation failed"));
63
+ console.log(chalk.dim(` ${err.message}`));
64
+ }
65
+ break;
66
+ }
67
+
68
+ case "revoke": {
69
+ const spinner = ora("Revoking API keyโ€ฆ").start();
70
+ try {
71
+ await post("/api/agent/revoke-key");
72
+ const cfg = config.load();
73
+ cfg.key = null;
74
+ config.save(cfg);
75
+ spinner.succeed("API key revoked โ€” local config cleared");
76
+ } catch (err) {
77
+ spinner.fail(chalk.red("Revocation failed"));
78
+ console.log(chalk.dim(` ${err.message}`));
79
+ }
80
+ break;
81
+ }
82
+
83
+ default:
84
+ console.log("");
85
+ console.log(chalk.hex("#FF004D")(" rpgclaw key") + chalk.dim(" โ€” manage your API key"));
86
+ console.log("");
87
+ console.log(chalk.dim(" Commands:"));
88
+ console.log(" show " + chalk.dim("Show current key (masked)"));
89
+ console.log(" new " + chalk.dim("Guide to generate a key"));
90
+ console.log(" rotate " + chalk.dim("Rotate to a new key"));
91
+ console.log(" revoke " + chalk.dim("Revoke current key"));
92
+ console.log("");
93
+ }
94
+ };
@@ -0,0 +1,80 @@
1
+ // โ”€โ”€ place โ€” place a pixel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2
+ const chalk = require("chalk");
3
+ const ora = require("ora");
4
+ const { get, post } = require("../utils/api");
5
+ const config = require("../utils/config");
6
+
7
+ exports.command = "place [x] [y] [color]";
8
+ exports.describe = "place a single pixel at (x,y) with a hex color";
9
+
10
+ exports.builder = (yargs) =>
11
+ yargs
12
+ .positional("x", { type: "number", describe: "X coordinate" })
13
+ .positional("y", { type: "number", describe: "Y coordinate" })
14
+ .positional("color", { type: "string", describe: "Hex color (e.g. #FF004D)" })
15
+ .option("world", { alias: "w", type: "string", describe: "World id (default: earth)" });
16
+
17
+ // eslint-disable-next-line no-unused-vars
18
+ exports.handler = async function handler(argv) {
19
+ const cfg = config.load();
20
+ const x = argv.x;
21
+ const y = argv.y;
22
+ const color = argv.color;
23
+ const world = argv.world || cfg.world || "earth";
24
+
25
+ if (x == null || y == null || !color) {
26
+ console.log("");
27
+ console.log(chalk.red(" โœ— Missing arguments"));
28
+ console.log(chalk.dim(" Usage:") + " rpgclaw place <x> <y> <#HEX> [--world earth]");
29
+ console.log(chalk.dim(" Example:") + " rpgclaw place 512 256 '#FF004D'");
30
+ console.log("");
31
+ process.exit(1);
32
+ }
33
+
34
+ // โ”€โ”€ Check cooldown first โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
35
+ const spinner = ora("Checking cooldownโ€ฆ").start();
36
+
37
+ try {
38
+ const status = await get("/api/agent/status");
39
+ const cd = status?.cooldown || {};
40
+
41
+ if (!cd.can_place && cd.seconds_remaining > 0) {
42
+ spinner.warn(chalk.yellow(`Cooldown active โ€” ${cd.seconds_remaining.toFixed(1)}s remaining`));
43
+ console.log("");
44
+ process.exit(0);
45
+ }
46
+
47
+ // Check wallet
48
+ const wallet = status?.wallet || {};
49
+ if ((wallet.active ?? 0) <= 0) {
50
+ spinner.warn(chalk.yellow("Wallet empty โ€” no active pixels left"));
51
+ console.log(chalk.dim(` Regen: +1 every ${status?.pixel_policy?.public_regen_interval_sec ?? 30}s`));
52
+ console.log("");
53
+ process.exit(0);
54
+ }
55
+
56
+ spinner.text = `Placing pixel at (${x}, ${y}) ${color}โ€ฆ`;
57
+
58
+ const result = await post("/api/agent/place", { x, y, color, world });
59
+
60
+ spinner.succeed(chalk.green(`Pixel placed! (${x}, ${y}) โ†’ ${color}`));
61
+
62
+ if (result?.cooldown) {
63
+ console.log(
64
+ chalk.dim(" Next: ") +
65
+ `${result.cooldown.cooldown_seconds ?? 0.6}s cooldown`,
66
+ );
67
+ }
68
+ if (result?.wallet) {
69
+ console.log(
70
+ chalk.dim(" Wallet: ") +
71
+ `${result.wallet.active ?? "?"}/${result.wallet.cap ?? "?"}`,
72
+ );
73
+ }
74
+ console.log("");
75
+ } catch (err) {
76
+ spinner.fail(chalk.red("Placement failed"));
77
+ console.log(chalk.dim(` ${err.message}`));
78
+ console.log("");
79
+ }
80
+ };
@@ -0,0 +1,85 @@
1
+ // โ”€โ”€ status โ€” canvas stats, cooldown, wallet โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2
+ const chalk = require("chalk");
3
+ const ora = require("ora");
4
+ const { get } = require("../utils/api");
5
+
6
+ exports.command = "status";
7
+ exports.describe = "check cooldown, wallet, canvas stats, and agent health";
8
+ exports.aliases = ["st", "stats"];
9
+
10
+ // eslint-disable-next-line no-unused-vars
11
+ exports.handler = async function handler() {
12
+ const spinner = ora("Fetching statusโ€ฆ").start();
13
+
14
+ try {
15
+ const [agentStatus, canvasStats] = await Promise.all([
16
+ get("/api/agent/status"),
17
+ get("/api/canvas/stats"),
18
+ ]);
19
+
20
+ spinner.stop();
21
+
22
+ const cooldown = agentStatus?.cooldown || {};
23
+ const wallet = agentStatus?.wallet || {};
24
+ const worldStats = canvasStats?.worlds || {};
25
+
26
+ // โ”€โ”€ Cooldown & Wallet โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
27
+ console.log("");
28
+ console.log(chalk.hex("#FF004D").bold(" ๐ŸŽจ RPGCLAW") + chalk.dim(" ยท agent status"));
29
+ console.log("");
30
+
31
+ const canPlace = cooldown.can_place ?? false;
32
+ const secs = cooldown.seconds_remaining ?? 0;
33
+ console.log(
34
+ " " +
35
+ (canPlace
36
+ ? chalk.green("โ— Ready") + chalk.dim(" โ€” can place pixel")
37
+ : chalk.yellow("โ—Œ Cooldown") +
38
+ chalk.dim(` โ€” ${secs.toFixed(1)}s remaining`)),
39
+ );
40
+
41
+ console.log(
42
+ chalk.dim(" Wallet: ") +
43
+ `${wallet.active ?? "?"} / ${wallet.cap ?? "?"} pixels`,
44
+ );
45
+
46
+ // โ”€โ”€ Canvas Stats โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
47
+ console.log("");
48
+ console.log(chalk.dim(" โ”€โ”€ Canvas โ”€โ”€"));
49
+
50
+ const earth = worldStats.earth || {};
51
+ const total = canvasStats?.total_pixels ?? 0;
52
+ const fillPct =
53
+ earth.total_cells > 0
54
+ ? ((total / earth.total_cells) * 100).toFixed(3)
55
+ : "?";
56
+
57
+ console.log(chalk.dim(" Earth: ") + `${total} pixels (${fillPct}% of 8192ร—4096)`);
58
+
59
+ for (const [id, w] of Object.entries(worldStats).slice(0, 5)) {
60
+ if (id === "earth") continue;
61
+ const pixels = w?.placed_pixels ?? w?.pixel_count ?? 0;
62
+ const unlocked = w?.unlocked ? chalk.green("unlocked") : chalk.dim("locked");
63
+ console.log(
64
+ chalk.dim(` ${w.name || id}:`.padEnd(12)) +
65
+ `${pixels} px ${unlocked}`,
66
+ );
67
+ }
68
+
69
+ // โ”€โ”€ Links โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
70
+ console.log("");
71
+ console.log(
72
+ chalk.dim(" Canvas: ") +
73
+ chalk.cyan("https://rpgclaw.com"),
74
+ );
75
+ console.log(
76
+ chalk.dim(" Docs: ") +
77
+ chalk.cyan("https://rpgclaw.com/developers"),
78
+ );
79
+ console.log("");
80
+ } catch (err) {
81
+ spinner.fail(chalk.red("Failed to fetch status"));
82
+ console.log(chalk.dim(` ${err.message}`));
83
+ console.log("");
84
+ }
85
+ };
@@ -0,0 +1,95 @@
1
+ // โ”€โ”€ templates โ€” list, create, set, remove โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2
+ const chalk = require("chalk");
3
+ const ora = require("ora");
4
+ const { get, post, del } = require("../utils/api");
5
+
6
+ exports.command = "templates [action]";
7
+ exports.describe = "list, create, set, or remove pixel art templates";
8
+ exports.aliases = ["tpl"];
9
+
10
+ exports.builder = (yargs) =>
11
+ yargs
12
+ .positional("action", {
13
+ type: "string",
14
+ describe: "list | set <id> | unset | create <name> <width> <height>",
15
+ })
16
+ .option("name", { type: "string", describe: "Template name (for create)" })
17
+ .option("width", { type: "number", describe: "Width in pixels" })
18
+ .option("height", { type: "number", describe: "Height in pixels" })
19
+ .option("id", { type: "string", describe: "Template ID to set as active" });
20
+
21
+ // eslint-disable-next-line no-unused-vars
22
+ exports.handler = async function handler(argv) {
23
+ const action = argv._[1] || argv.action || "list";
24
+
25
+ try {
26
+ switch (action) {
27
+ case "list": {
28
+ const spinner = ora("Fetching templatesโ€ฆ").start();
29
+ const data = await get("/api/agent/templates");
30
+ spinner.stop();
31
+
32
+ const templates = data?.templates || [];
33
+ const activeId = data?.active_template_id;
34
+
35
+ if (!templates.length) {
36
+ console.log("");
37
+ console.log(chalk.dim(" No templates. Create one: rpgclaw templates create <name> <w> <h>"));
38
+ console.log("");
39
+ return;
40
+ }
41
+
42
+ console.log("");
43
+ console.log(chalk.hex("#FF004D").bold(" ๐ŸŽจ Templates"));
44
+ console.log("");
45
+
46
+ for (const tpl of templates) {
47
+ const isActive = tpl._id === activeId || tpl.id === activeId;
48
+ const prefix = isActive ? chalk.green(" โ–ถ") : " ";
49
+ const statusStr = isActive ? chalk.green("ACTIVE") : chalk.dim("inactive");
50
+ console.log(
51
+ `${prefix} ${chalk.white(tpl.name || "Untitled")} ` +
52
+ chalk.dim(`${tpl.width}ร—${tpl.height}`) +
53
+ ` ${statusStr}`,
54
+ );
55
+ }
56
+ console.log("");
57
+ break;
58
+ }
59
+
60
+ case "set": {
61
+ const id = argv.id || argv._[2];
62
+ if (!id) {
63
+ console.log(chalk.red(" Usage: rpgclaw templates set <template-id>"));
64
+ return;
65
+ }
66
+ const spinner = ora("Setting active templateโ€ฆ").start();
67
+ await post("/api/agent/template/set", { template_id: id });
68
+ spinner.succeed("Active template set โœ“");
69
+ break;
70
+ }
71
+
72
+ case "unset": {
73
+ const spinner = ora("Clearing active templateโ€ฆ").start();
74
+ await del("/api/agent/template/unset");
75
+ spinner.succeed("Active template cleared โœ“");
76
+ break;
77
+ }
78
+
79
+ default:
80
+ console.log("");
81
+ console.log(chalk.hex("#FF004D")(" rpgclaw templates") + chalk.dim(" โ€” manage pixel art templates"));
82
+ console.log("");
83
+ console.log(chalk.dim(" Commands:"));
84
+ console.log(" list " + chalk.dim("Show your templates"));
85
+ console.log(" set <id> " + chalk.dim("Set a template as active"));
86
+ console.log(" unset " + chalk.dim("Clear the active template"));
87
+ console.log("");
88
+ console.log(chalk.dim(" To create templates, use the web:"));
89
+ console.log(chalk.cyan(" https://rpgclaw.com/templates"));
90
+ console.log("");
91
+ }
92
+ } catch (err) {
93
+ console.log(chalk.red(` Error: ${err.message}`));
94
+ }
95
+ };
@@ -0,0 +1,128 @@
1
+ // โ”€โ”€ watch โ€” continuous agent loop โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2
+ const chalk = require("chalk");
3
+ const { get, post } = require("../utils/api");
4
+ const config = require("../utils/config");
5
+
6
+ exports.command = "watch";
7
+ exports.describe = "run the agent loop continuously โ€” paint pixels from template";
8
+ exports.aliases = ["run"];
9
+
10
+ exports.builder = (yargs) =>
11
+ yargs
12
+ .option("once", { type: "boolean", describe: "Place one pixel and exit" })
13
+ .option("interval", { alias: "i", type: "number", describe: "Check interval in ms (default 2000)" });
14
+
15
+ // eslint-disable-next-line no-unused-vars
16
+ exports.handler = async function handler(argv) {
17
+ const cfg = config.load();
18
+ const checkInterval = argv.interval || 2000;
19
+ const once = Boolean(argv.once);
20
+
21
+ console.log("");
22
+ console.log(chalk.hex("#FF004D").bold(" โ–ถ RPGCLAW Agent") + chalk.dim(" ยท watchingโ€ฆ"));
23
+ console.log(chalk.dim(` Press Ctrl+C to stop`));
24
+ console.log("");
25
+
26
+ let running = true;
27
+ let placed = 0;
28
+ let skipped = 0;
29
+ let startTime = Date.now();
30
+
31
+ const statusLine = () => {
32
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
33
+ const min = Math.floor(elapsed / 60);
34
+ const sec = elapsed % 60;
35
+ process.stdout.write(
36
+ ` ${chalk.dim(`[${min}m ${sec}s]`)} ` +
37
+ `${chalk.green(`${placed} placed`)} ` +
38
+ `${chalk.yellow(`${skipped} skipped`)} ` +
39
+ "\r",
40
+ );
41
+ };
42
+
43
+ process.on("SIGINT", () => {
44
+ running = false;
45
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
46
+ console.log("");
47
+ console.log("");
48
+ console.log(chalk.dim(` Stopped after ${elapsed}s โ€” ${placed} placed, ${skipped} skipped`));
49
+ console.log("");
50
+ process.exit(0);
51
+ });
52
+
53
+ // eslint-disable-next-line no-unused-vars
54
+ async function tick() {
55
+ if (!running) return;
56
+
57
+ try {
58
+ const status = await get("/api/agent/status");
59
+ const cd = status?.cooldown || {};
60
+ const wallet = status?.wallet || {};
61
+
62
+ if (!cd.can_place || (wallet.active ?? 0) <= 0) {
63
+ skipped++;
64
+ statusLine();
65
+ if (once) {
66
+ console.log("");
67
+ console.log(chalk.yellow(" Cannot place right now โ€” cooldown or empty wallet"));
68
+ console.log("");
69
+ process.exit(0);
70
+ }
71
+ setTimeout(tick, Math.max(checkInterval, (cd.seconds_remaining ?? 1) * 1000 + 100));
72
+ return;
73
+ }
74
+
75
+ // Get next pixel from template
76
+ let target;
77
+ try {
78
+ target = await get("/api/agent/template/next");
79
+ } catch {
80
+ skipped++;
81
+ statusLine();
82
+ setTimeout(tick, checkInterval);
83
+ return;
84
+ }
85
+
86
+ if (!target?.has_target) {
87
+ if (once) {
88
+ console.log("");
89
+ console.log(chalk.yellow(" No template target set. Set up a template on the Agent page."));
90
+ console.log(chalk.dim(" https://rpgclaw.com/agent"));
91
+ console.log("");
92
+ process.exit(0);
93
+ }
94
+ skipped++;
95
+ statusLine();
96
+ setTimeout(tick, checkInterval);
97
+ return;
98
+ }
99
+
100
+ // Place the pixel
101
+ await post("/api/agent/place", {
102
+ x: target.x,
103
+ y: target.y,
104
+ color: target.color,
105
+ world: cfg.world || "earth",
106
+ });
107
+
108
+ placed++;
109
+ statusLine();
110
+
111
+ if (once) {
112
+ console.log("");
113
+ console.log(chalk.green(` โœ“ Placed ${target.color} at (${target.x}, ${target.y})`));
114
+ console.log("");
115
+ process.exit(0);
116
+ }
117
+
118
+ const wait = Math.max(checkInterval, (cd.cooldown_seconds ?? 0.6) * 1000 + 50);
119
+ setTimeout(tick, wait);
120
+ } catch (err) {
121
+ skipped++;
122
+ statusLine();
123
+ setTimeout(tick, checkInterval);
124
+ }
125
+ }
126
+
127
+ tick();
128
+ };
@@ -0,0 +1,58 @@
1
+ // โ”€โ”€ whoami โ€” user profile โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2
+ const chalk = require("chalk");
3
+ const ora = require("ora");
4
+ const { get } = require("../utils/api");
5
+
6
+ exports.command = "whoami";
7
+ exports.describe = "show your connected RPGCLAW user profile";
8
+ exports.aliases = ["who", "me"];
9
+
10
+ // eslint-disable-next-line no-unused-vars
11
+ exports.handler = async function handler() {
12
+ const spinner = ora("Fetching profileโ€ฆ").start();
13
+
14
+ try {
15
+ const [agentStatus] = await Promise.all([get("/api/agent/status")]);
16
+
17
+ spinner.stop();
18
+
19
+ console.log("");
20
+ console.log(chalk.hex("#FF004D").bold(" ๐Ÿ‘ค RPGCLAW Profile"));
21
+ console.log("");
22
+
23
+ console.log(chalk.dim(" User: ") + (agentStatus?.user_name || "?"));
24
+ console.log(chalk.dim(" Agent: ") + (agentStatus?.agent_name || "(not linked)"));
25
+ console.log(chalk.dim(" Template: ") + (agentStatus?.active_template || "(none)"));
26
+ console.log(chalk.dim(" World: ") + (agentStatus?.world_id || "earth"));
27
+
28
+ const wallet = agentStatus?.wallet || {};
29
+ console.log(
30
+ chalk.dim(" Wallet: ") +
31
+ `${wallet.active ?? "?"} / ${wallet.cap ?? "?"} pixels` +
32
+ chalk.dim(` (+1 / ${agentStatus?.pixel_policy?.public_regen_interval_sec ?? 30}s)`),
33
+ );
34
+
35
+ const progress = agentStatus?.template_progress;
36
+ if (progress) {
37
+ const pct = progress.total_pixels > 0
38
+ ? ((progress.placed_pixels / progress.total_pixels) * 100).toFixed(1)
39
+ : 0;
40
+ console.log(
41
+ chalk.dim(" Progress: ") +
42
+ `${progress.placed_pixels}/${progress.total_pixels} (${pct}%)`,
43
+ );
44
+ }
45
+
46
+ console.log("");
47
+ console.log(
48
+ chalk.dim(" Dashboard:") +
49
+ chalk.cyan(" https://rpgclaw.com/agent"),
50
+ );
51
+ console.log("");
52
+ } catch (err) {
53
+ spinner.fail(chalk.red("Failed to fetch profile"));
54
+ console.log(chalk.dim(` ${err.message}`));
55
+ console.log(chalk.dim(" Are you connected? Run: rpgclaw connect --key <your-key>"));
56
+ console.log("");
57
+ }
58
+ };
package/src/index.js ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+
3
+ // โ”€โ”€ RPGCLAW CLI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
4
+ // Official command-line toolkit for the RPGCLAW pixel canvas.
5
+ // https://rpgclaw.com/developers
6
+ //
7
+ // Usage:
8
+ // npm install -g @smouj013/rpgclaw-cli
9
+ // rpgclaw connect --key <your-api-key>
10
+ // rpgclaw status
11
+ // rpgclaw place 512 256 "#FF004D"
12
+ // rpgclaw watch
13
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
14
+
15
+ const yargs = require("yargs");
16
+ const { hideBin } = require("yargs/helpers");
17
+ const chalk = require("chalk");
18
+
19
+ // eslint-disable-next-line no-unused-expressions
20
+ yargs(hideBin(process.argv))
21
+ .scriptName("rpgclaw")
22
+ .usage(chalk.hex("#FF004D")("๐ŸŽจ rpgclaw <command>") + " [options]")
23
+ .command(require("./commands/connect"))
24
+ .command(require("./commands/status"))
25
+ .command(require("./commands/place"))
26
+ .command(require("./commands/watch"))
27
+ .command(require("./commands/templates"))
28
+ .command(require("./commands/key"))
29
+ .command(require("./commands/config"))
30
+ .command(require("./commands/whoami"))
31
+ .demandCommand(1, chalk.yellow("Use a command: rpgclaw --help"))
32
+ .strict()
33
+ .help("help", chalk.dim("Show this help"))
34
+ .alias("h", "help")
35
+ .alias("v", "version")
36
+ .wrap(Math.min(100, process.stdout.columns))
37
+ .epilogue(
38
+ chalk.dim("Docs: ") +
39
+ chalk.cyan("https://rpgclaw.com/developers") +
40
+ chalk.dim(" | GitHub: ") +
41
+ chalk.cyan("https://github.com/smouj/rpgclaw-cli"),
42
+ ).argv;
@@ -0,0 +1,36 @@
1
+ // โ”€โ”€ RPGCLAW API Client โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2
+ const config = require("./config");
3
+
4
+ async function api(method, path, body = null) {
5
+ const cfg = config.load();
6
+ const k = config.key();
7
+ const url = `${cfg.api}${path}`;
8
+
9
+ const opts = {
10
+ method,
11
+ headers: {
12
+ "X-Agent-Key": k,
13
+ "Content-Type": "application/json",
14
+ Accept: "application/json",
15
+ },
16
+ };
17
+ if (body) opts.body = JSON.stringify(body);
18
+
19
+ // Using global fetch (Node 18+) or node-fetch
20
+ const f = globalThis.fetch || require("node-fetch");
21
+ const res = await f(url, opts);
22
+ const data = await res.json().catch(() => ({ raw: true }));
23
+
24
+ if (!res.ok) {
25
+ const detail = data?.detail || res.statusText;
26
+ throw new Error(`${method} ${path} โ†’ ${res.status} ${detail}`);
27
+ }
28
+
29
+ return data;
30
+ }
31
+
32
+ exports.get = (path) => api("GET", path);
33
+ exports.post = (path, body) => api("POST", path, body);
34
+ exports.put = (path, body) => api("PUT", path, body);
35
+ exports.del = (path) => api("DELETE", path);
36
+ exports.api = api;
@@ -0,0 +1,39 @@
1
+ // โ”€โ”€ Config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2
+ const os = require("os");
3
+ const path = require("path");
4
+ const fs = require("fs");
5
+
6
+ const CONFIG_DIR = path.join(os.homedir(), ".rpgclaw");
7
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
8
+
9
+ const DEFAULTS = {
10
+ api: "https://www.rpgclaw.com",
11
+ world: "earth",
12
+ cooldown: 0.6,
13
+ color: "#FF004D",
14
+ };
15
+
16
+ function load() {
17
+ try {
18
+ if (!fs.existsSync(CONFIG_FILE)) return { ...DEFAULTS };
19
+ return { ...DEFAULTS, ...JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8")) };
20
+ } catch {
21
+ return { ...DEFAULTS };
22
+ }
23
+ }
24
+
25
+ function save(cfg) {
26
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
27
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf8");
28
+ }
29
+
30
+ function key() {
31
+ const cfg = load();
32
+ if (!cfg.key) {
33
+ console.error("No API key configured. Run: rpgclaw connect");
34
+ process.exit(1);
35
+ }
36
+ return cfg.key;
37
+ }
38
+
39
+ module.exports = { load, save, key, CONFIG_DIR, DEFAULTS };