lobster-farmer-cli 0.1.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 murong
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,64 @@
1
+ # Lobster Farmer
2
+
3
+ ## Use in OpenClaw
4
+
5
+ 1. Install and start the game service in this project:
6
+
7
+ ```bash
8
+ npm i -g lobster-farmer-cli
9
+ lobster-farmer start
10
+ ```
11
+
12
+ 2. Install the auto-feeding skill with ClawHub (one-time):
13
+
14
+ ```bash
15
+ npx clawhub@latest install lobster-farmer-feeder
16
+ ```
17
+
18
+ 3. Startup prompt
19
+ ```text
20
+ From now on, enable "Auto Lobster Farming Mode" and keep it enabled until I explicitly say "Disable auto lobster farming mode".
21
+
22
+ Rules:
23
+
24
+ 1. After every formal reply you send, automatically run one feeding action.
25
+ 2. Before feeding, check service status with: lobster-farmer status; if not running, start with lobster-farmer start.
26
+ 3. Feeding command: lobster-farmer feed --model "<actual model name used in this turn>" --input-tokens <current input> --output-tokens <current output>.
27
+ 4. Use real token stats when available; if exact per-turn values are unavailable, use an approximate delta and mark it as "approx".
28
+ 5. If feeding fails, retry once automatically (including port correction if needed). If it still fails, report a short error.
29
+ 6. After each feed, append one line: 🦞 Fed | model=... | in=... | out=... | total=... | size=....
30
+ 7. If I specify a port, always use that --port for later feed actions.
31
+ ```
32
+
33
+ 4. Chat with AI as usual. One feeding action will run automatically per turn.
34
+
35
+ 5. You can also provide explicit feed parameters in your message:
36
+
37
+ ```text
38
+ Please feed the lobster: model=gpt-4.1, input_tokens=1200, output_tokens=300
39
+ ```
40
+
41
+ ## Notes
42
+
43
+ - Auto-feed behavior is controlled by `AGENTS.md` in the workspace root (already included in this project).
44
+ - If you use `OPENCLAW_STATE_DIR`, replace `~/.openclaw` with `$OPENCLAW_STATE_DIR`.
45
+ - Default auto-feed values: `model=auto-agent`, `input_tokens=1`, `output_tokens=1`.
46
+ - Default service port: `18990`.
47
+
48
+ ## Game Asset Credits
49
+
50
+ - Lobster sprites
51
+ - `public/game-assets/lobster/spr_lobster_walk_strip6.png`
52
+ - `public/game-assets/lobster/spr_lobster_searching_bubble_strip10.png`
53
+ - Source: OpenGameArt - A Lobster Sprite
54
+ - Link: https://opengameart.org/content/a-lobster-sprite
55
+ - License: CC-BY 3.0 or CC-BY-SA 3.0
56
+
57
+ - Underwater background
58
+ - `public/game-assets/background/underwater-tileable.png`
59
+ - Source: OpenGameArt - Underwater Scene (loopable)
60
+ - Link: https://opengameart.org/content/underwater-scene-loopable
61
+ - License: CC0
62
+
63
+ - Repository license record
64
+ - `assets/packs/lobster-swim/docs/LICENSES.md`
@@ -0,0 +1,54 @@
1
+ # Lobster Farmer
2
+
3
+ ## 在 OpenClaw 中使用
4
+
5
+ 1. 在项目目录启动游戏服务:
6
+
7
+ ```bash
8
+ npm i -g lobster-farmer-cli
9
+ lobster-farmer start --port 18990
10
+ ```
11
+
12
+ 2. 使用 ClawHub 安装自动喂养 skill(只需一次):
13
+
14
+ ```bash
15
+ npx clawhub@latest install lobster-farmer-feeder
16
+ ```
17
+
18
+ 3. 启用提示词
19
+ ```text
20
+ 从现在开始启用「自动养龙虾模式」并保持开启,直到我明确说“关闭自动养龙虾模式”。
21
+
22
+ 规则:
23
+
24
+ 1. 你每次给我发送任何正式回复后,都自动执行一次喂龙虾。
25
+ 2. 喂龙虾前先检查服务:lobster-farmer status;若未运行则先 lobster-farmer start。
26
+ 3. 喂食命令:lobster-farmer feed --model "<本次实际模型名>" --input-tokens <本次input> --output-tokens <本次output>。
27
+ 4. token 使用真实统计值;若拿不到单次精确值,则用“本次累计差值”作为近似,并标注“approx”。
28
+ 5. 失败时自动重试一次(含端口修正);仍失败再简短报错。
29
+ 6. 每次喂完都附一行:🦞 已喂养 | model=... | in=... | out=... | total=... | size=...。
30
+ 7. 若我指定端口,后续固定使用该 --port。
31
+ ```
32
+
33
+ 4. 正常和 AI 对话即可,每回合会自动执行一次喂养。
34
+
35
+ 5. 你也可以在提问里指定喂养参数:
36
+
37
+ ```text
38
+ 帮我喂养龙虾:model=gpt-4.1, input_tokens=1200, output_tokens=300
39
+ ```
40
+
41
+ ## 游戏静态资源来源
42
+
43
+ - 龙虾精灵
44
+ - `public/game-assets/lobster/spr_lobster_walk_strip6.png`
45
+ - `public/game-assets/lobster/spr_lobster_searching_bubble_strip10.png`
46
+ - 来源:OpenGameArt - A Lobster Sprite
47
+ - 链接:https://opengameart.org/content/a-lobster-sprite
48
+ - 许可:CC-BY 3.0 或 CC-BY-SA 3.0
49
+
50
+ - 海底背景
51
+ - `public/game-assets/background/underwater-tileable.png`
52
+ - 来源:OpenGameArt - Underwater Scene (loopable)
53
+ - 链接:https://opengameart.org/content/underwater-scene-loopable
54
+ - 许可:CC0
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn, spawnSync } = require("node:child_process");
4
+ const { existsSync, rmSync, mkdirSync, writeFileSync, readFileSync, openSync } = require("node:fs");
5
+ const { homedir } = require("node:os");
6
+ const { resolve } = require("node:path");
7
+
8
+ const entry = resolve(__dirname, "../dist/index.js");
9
+ const tscBin = resolve(__dirname, "../node_modules/typescript/bin/tsc");
10
+ const tsConfig = resolve(__dirname, "../tsconfig.json");
11
+ const dataDir = resolve(homedir(), ".lobster-farmer");
12
+ const dbBase = resolve(dataDir, "data.sqlite");
13
+ const pidFile = resolve(dataDir, "lobster-farmer.pid");
14
+ const logFile = resolve(dataDir, "lobster-farmer.log");
15
+ const defaultPort = "18990";
16
+
17
+ function printHelp() {
18
+ console.log(`Lobster Farmer - Node single-service lobster game
19
+
20
+ Usage:
21
+ lobster-farmer start [--port ${defaultPort}] [--foreground]
22
+ Start game server (default: daemon)
23
+ lobster-farmer feed --model <name> [--input-tokens <n>] [--output-tokens <n>] [--port ${defaultPort}]
24
+ Feed a model lobster through API
25
+ lobster-farmer stop Stop daemon server
26
+ lobster-farmer status Show daemon status
27
+ lobster-farmer reset Reset sqlite data files
28
+ lobster-farmer --help Show help
29
+
30
+ Environment:
31
+ PORT Override server port (default: ${defaultPort})
32
+ `);
33
+ }
34
+
35
+ function parsePort(value) {
36
+ const numericPort = Number(value);
37
+ if (!Number.isInteger(numericPort) || numericPort <= 0 || numericPort > 65535) {
38
+ throw new Error("Invalid --port value");
39
+ }
40
+ return String(numericPort);
41
+ }
42
+
43
+ function parseNonNegativeInt(value, optionName) {
44
+ const numberValue = Number(value);
45
+ if (!Number.isFinite(numberValue)) {
46
+ throw new Error(`Invalid ${optionName} value`);
47
+ }
48
+ const intValue = Math.floor(numberValue);
49
+ if (intValue < 0) {
50
+ throw new Error(`${optionName} must be >= 0`);
51
+ }
52
+ return intValue;
53
+ }
54
+
55
+ function parseArgs(argv) {
56
+ const args = [...argv];
57
+ let command = "start";
58
+ let port;
59
+ let foreground = false;
60
+ let model;
61
+ let inputTokens;
62
+ let outputTokens;
63
+
64
+ if (args.length > 0 && !args[0].startsWith("-")) {
65
+ command = args.shift();
66
+ }
67
+
68
+ for (let index = 0; index < args.length; index += 1) {
69
+ const arg = args[index];
70
+
71
+ if (arg === "--help" || arg === "-h") {
72
+ command = "help";
73
+ continue;
74
+ }
75
+
76
+ if (arg === "--foreground" || arg === "-f") {
77
+ foreground = true;
78
+ continue;
79
+ }
80
+
81
+ if (arg === "--port" || arg === "-p") {
82
+ const value = args[index + 1];
83
+ index += 1;
84
+ if (!value) {
85
+ throw new Error("Missing value for --port");
86
+ }
87
+ port = parsePort(value);
88
+ continue;
89
+ }
90
+
91
+ if (arg.startsWith("--port=")) {
92
+ const value = arg.split("=")[1];
93
+ port = parsePort(value);
94
+ continue;
95
+ }
96
+
97
+ if (arg === "--model" || arg === "-m") {
98
+ const value = args[index + 1];
99
+ index += 1;
100
+ if (!value) {
101
+ throw new Error("Missing value for --model");
102
+ }
103
+ model = String(value);
104
+ continue;
105
+ }
106
+
107
+ if (arg.startsWith("--model=")) {
108
+ model = String(arg.split("=")[1] ?? "");
109
+ continue;
110
+ }
111
+
112
+ if (arg === "--input-tokens" || arg === "-i") {
113
+ const value = args[index + 1];
114
+ index += 1;
115
+ if (value === undefined) {
116
+ throw new Error("Missing value for --input-tokens");
117
+ }
118
+ inputTokens = parseNonNegativeInt(value, "--input-tokens");
119
+ continue;
120
+ }
121
+
122
+ if (arg.startsWith("--input-tokens=")) {
123
+ inputTokens = parseNonNegativeInt(arg.split("=")[1], "--input-tokens");
124
+ continue;
125
+ }
126
+
127
+ if (arg === "--output-tokens" || arg === "-o") {
128
+ const value = args[index + 1];
129
+ index += 1;
130
+ if (value === undefined) {
131
+ throw new Error("Missing value for --output-tokens");
132
+ }
133
+ outputTokens = parseNonNegativeInt(value, "--output-tokens");
134
+ continue;
135
+ }
136
+
137
+ if (arg.startsWith("--output-tokens=")) {
138
+ outputTokens = parseNonNegativeInt(arg.split("=")[1], "--output-tokens");
139
+ continue;
140
+ }
141
+
142
+ throw new Error(`Unknown option: ${arg}`);
143
+ }
144
+
145
+ return { command, port, foreground, model, inputTokens, outputTokens };
146
+ }
147
+
148
+ function removeIfExists(path) {
149
+ if (existsSync(path)) {
150
+ rmSync(path, { force: true });
151
+ }
152
+ }
153
+
154
+ function resetData() {
155
+ removeIfExists(dbBase);
156
+ removeIfExists(`${dbBase}-shm`);
157
+ removeIfExists(`${dbBase}-wal`);
158
+ removeIfExists(pidFile);
159
+ removeIfExists(logFile);
160
+ console.log("SQLite data reset complete.");
161
+ }
162
+
163
+ function ensureBuild() {
164
+ if (!existsSync(entry)) {
165
+ const build = spawnSync(process.execPath, [tscBin, "-p", tsConfig], { stdio: "inherit" });
166
+ if (build.status !== 0) {
167
+ process.exit(build.status || 1);
168
+ }
169
+ }
170
+ }
171
+
172
+ function readPid() {
173
+ if (!existsSync(pidFile)) {
174
+ return null;
175
+ }
176
+
177
+ const content = readFileSync(pidFile, "utf8").trim();
178
+ const pid = Number(content);
179
+ if (!Number.isInteger(pid) || pid <= 0) {
180
+ return null;
181
+ }
182
+
183
+ return pid;
184
+ }
185
+
186
+ function isProcessRunning(pid) {
187
+ try {
188
+ process.kill(pid, 0);
189
+ return true;
190
+ } catch (error) {
191
+ if (error && error.code === "EPERM") {
192
+ return true;
193
+ }
194
+ return false;
195
+ }
196
+ }
197
+
198
+ function getRunningPid() {
199
+ const pid = readPid();
200
+ if (!pid) {
201
+ return null;
202
+ }
203
+
204
+ if (isProcessRunning(pid)) {
205
+ return pid;
206
+ }
207
+
208
+ removeIfExists(pidFile);
209
+ return null;
210
+ }
211
+
212
+ function startServer(port, foreground) {
213
+ ensureBuild();
214
+
215
+ mkdirSync(dataDir, { recursive: true });
216
+
217
+ const env = { ...process.env };
218
+ if (port) {
219
+ env.PORT = port;
220
+ }
221
+
222
+ if (foreground) {
223
+ const child = spawn(process.execPath, [entry], {
224
+ stdio: "inherit",
225
+ env
226
+ });
227
+
228
+ child.on("error", (error) => {
229
+ console.error(error.message || String(error));
230
+ process.exit(1);
231
+ });
232
+
233
+ child.on("exit", (code, signal) => {
234
+ if (signal) {
235
+ process.kill(process.pid, signal);
236
+ return;
237
+ }
238
+ process.exit(code || 0);
239
+ });
240
+
241
+ return;
242
+ }
243
+
244
+ const existingPid = getRunningPid();
245
+ if (existingPid) {
246
+ console.log(`Lobster Farmer already running (pid: ${existingPid})`);
247
+ return;
248
+ }
249
+
250
+ const out = openSync(logFile, "a");
251
+ const err = openSync(logFile, "a");
252
+ const child = spawn(process.execPath, [entry], {
253
+ detached: true,
254
+ stdio: ["ignore", out, err],
255
+ env
256
+ });
257
+ child.unref();
258
+
259
+ writeFileSync(pidFile, String(child.pid));
260
+ console.log(`Lobster Farmer started in background.`);
261
+ console.log(`pid: ${child.pid}`);
262
+ console.log(`log: ${logFile}`);
263
+ }
264
+
265
+ function stopServer() {
266
+ const pid = getRunningPid();
267
+ if (!pid) {
268
+ console.log("Lobster Farmer is not running.");
269
+ return;
270
+ }
271
+
272
+ try {
273
+ process.kill(pid, "SIGTERM");
274
+ } catch (error) {
275
+ console.error(error.message || String(error));
276
+ process.exit(1);
277
+ }
278
+
279
+ removeIfExists(pidFile);
280
+ console.log(`Lobster Farmer stopped (pid: ${pid}).`);
281
+ }
282
+
283
+ function showStatus() {
284
+ const pid = getRunningPid();
285
+ if (!pid) {
286
+ console.log("Lobster Farmer status: stopped");
287
+ return;
288
+ }
289
+
290
+ console.log(`Lobster Farmer status: running (pid: ${pid})`);
291
+ console.log(`log: ${logFile}`);
292
+ }
293
+
294
+ function resolvePort(portArg) {
295
+ return parsePort(portArg || process.env.PORT || defaultPort);
296
+ }
297
+
298
+ async function feedThroughApi(parsed) {
299
+ const model = typeof parsed.model === "string" ? parsed.model.trim() : "";
300
+ if (!model) {
301
+ throw new Error("feed command requires --model");
302
+ }
303
+
304
+ const inputTokens = parsed.inputTokens ?? 0;
305
+ const outputTokens = parsed.outputTokens ?? 0;
306
+ if (inputTokens + outputTokens <= 0) {
307
+ throw new Error("feed command requires input/output tokens > 0");
308
+ }
309
+
310
+ const port = resolvePort(parsed.port);
311
+ const url = `http://127.0.0.1:${port}/api/feed`;
312
+
313
+ let response;
314
+ try {
315
+ response = await fetch(url, {
316
+ method: "POST",
317
+ headers: {
318
+ "Content-Type": "application/json"
319
+ },
320
+ body: JSON.stringify({
321
+ model,
322
+ input_tokens: inputTokens,
323
+ output_tokens: outputTokens
324
+ })
325
+ });
326
+ } catch (error) {
327
+ throw new Error(`feed request failed: ${error.message || String(error)}`);
328
+ }
329
+
330
+ let payload = {};
331
+ try {
332
+ payload = await response.json();
333
+ } catch (_error) {
334
+ payload = {};
335
+ }
336
+
337
+ if (!response.ok) {
338
+ const apiError = payload && typeof payload.error === "string" ? payload.error : `HTTP ${response.status}`;
339
+ throw new Error(`feed request failed: ${apiError}`);
340
+ }
341
+
342
+ const lobster = payload && typeof payload === "object" ? payload.lobster : null;
343
+ const size = lobster && typeof lobster.size === "number" ? lobster.size : "n/a";
344
+ const feeds = lobster && typeof lobster.feeds === "number" ? lobster.feeds : "n/a";
345
+ const totalTokens = lobster && typeof lobster.tokens === "number" ? lobster.tokens : "n/a";
346
+
347
+ console.log(`Fed lobster: ${model}`);
348
+ console.log(`input_tokens: ${inputTokens}, output_tokens: ${outputTokens}`);
349
+ console.log(`lobster tokens: ${totalTokens}, feeds: ${feeds}, size: ${size}`);
350
+ }
351
+
352
+ async function main() {
353
+ let parsed;
354
+ try {
355
+ parsed = parseArgs(process.argv.slice(2));
356
+ } catch (error) {
357
+ console.error(error.message || String(error));
358
+ printHelp();
359
+ process.exit(1);
360
+ }
361
+
362
+ if (parsed.command === "help") {
363
+ printHelp();
364
+ return;
365
+ }
366
+
367
+ if (parsed.command === "reset") {
368
+ resetData();
369
+ return;
370
+ }
371
+
372
+ if (parsed.command === "stop") {
373
+ stopServer();
374
+ return;
375
+ }
376
+
377
+ if (parsed.command === "status") {
378
+ showStatus();
379
+ return;
380
+ }
381
+
382
+ if (parsed.command === "start") {
383
+ startServer(parsed.port, parsed.foreground);
384
+ return;
385
+ }
386
+
387
+ if (parsed.command === "feed") {
388
+ await feedThroughApi(parsed);
389
+ return;
390
+ }
391
+
392
+ console.error(`Unknown command: ${parsed.command}`);
393
+ printHelp();
394
+ process.exit(1);
395
+ }
396
+
397
+ main().catch((error) => {
398
+ console.error(error.message || String(error));
399
+ process.exit(1);
400
+ });