clawtan 0.2.5 → 0.2.6
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/clawtan/cli.py +194 -13
- package/package.json +1 -1
package/clawtan/cli.py
CHANGED
|
@@ -7,6 +7,7 @@ by LLM agents. Session credentials are saved automatically on join.
|
|
|
7
7
|
|
|
8
8
|
Typical agent flow:
|
|
9
9
|
clawtan quick-join --name "LobsterBot" # session saved to ~/.clawtan_session
|
|
10
|
+
clawtan whoami # verify correct session
|
|
10
11
|
clawtan board # once, to learn the map
|
|
11
12
|
clawtan wait # blocks until your turn
|
|
12
13
|
clawtan act ROLL_THE_SHELLS
|
|
@@ -14,6 +15,10 @@ Typical agent flow:
|
|
|
14
15
|
clawtan act END_TIDE
|
|
15
16
|
clawtan wait # next turn...
|
|
16
17
|
|
|
18
|
+
Between games:
|
|
19
|
+
clawtan clear-session # remove default session
|
|
20
|
+
clawtan clear-session --all # remove ALL saved sessions
|
|
21
|
+
|
|
17
22
|
Multi-player on one machine (each terminal):
|
|
18
23
|
clawtan join <GAME_ID> # join, note your assigned color
|
|
19
24
|
export CLAWTAN_COLOR=<COLOR> # lock this terminal to your player
|
|
@@ -152,7 +157,22 @@ def _save_session(game_id: str, token: str, color: str):
|
|
|
152
157
|
json.dump(data, f)
|
|
153
158
|
except OSError as e:
|
|
154
159
|
print(f"Warning: could not save session to {path}: {e}", file=sys.stderr)
|
|
160
|
+
|
|
161
|
+
# Warn if the default session pointed at a different game (stale session)
|
|
155
162
|
default = os.environ.get("CLAWTAN_SESSION_FILE") or os.path.expanduser("~/.clawtan_session")
|
|
163
|
+
try:
|
|
164
|
+
with open(default) as f:
|
|
165
|
+
prev = json.load(f)
|
|
166
|
+
prev_game = prev.get("GAME")
|
|
167
|
+
if prev_game and prev_game != game_id:
|
|
168
|
+
print(
|
|
169
|
+
f" Note: overwriting previous session (game {prev_game})."
|
|
170
|
+
f" Run 'clawtan clear-session --game {prev_game}' to clean up old files.",
|
|
171
|
+
file=sys.stderr,
|
|
172
|
+
)
|
|
173
|
+
except (OSError, json.JSONDecodeError):
|
|
174
|
+
pass
|
|
175
|
+
|
|
156
176
|
try:
|
|
157
177
|
with open(default, "w") as f:
|
|
158
178
|
json.dump(data, f)
|
|
@@ -179,24 +199,35 @@ def _find_session(game_hint: str | None = None, color_hint: str | None = None) -
|
|
|
179
199
|
pass
|
|
180
200
|
|
|
181
201
|
if (game_hint or color_hint) and os.path.isdir(_SESSIONS_DIR):
|
|
182
|
-
matches = []
|
|
202
|
+
matches = [] # list of (mtime, data) tuples
|
|
183
203
|
for fname in os.listdir(_SESSIONS_DIR):
|
|
184
204
|
if not fname.endswith(".json"):
|
|
185
205
|
continue
|
|
206
|
+
fpath = os.path.join(_SESSIONS_DIR, fname)
|
|
186
207
|
try:
|
|
187
|
-
with open(
|
|
208
|
+
with open(fpath) as f:
|
|
188
209
|
data = json.load(f)
|
|
189
210
|
if game_hint and data.get("GAME") != game_hint:
|
|
190
211
|
continue
|
|
191
212
|
if color_hint and data.get("COLOR") != color_hint:
|
|
192
213
|
continue
|
|
193
|
-
|
|
214
|
+
mtime = os.path.getmtime(fpath)
|
|
215
|
+
matches.append((mtime, data))
|
|
194
216
|
except (OSError, json.JSONDecodeError):
|
|
195
217
|
continue
|
|
196
|
-
if len(matches) == 1:
|
|
197
|
-
return matches[0]
|
|
198
218
|
if matches:
|
|
199
|
-
|
|
219
|
+
# Sort by mtime descending so most recently written session wins
|
|
220
|
+
matches.sort(key=lambda m: m[0], reverse=True)
|
|
221
|
+
if len(matches) > 1:
|
|
222
|
+
chosen = matches[0][1]
|
|
223
|
+
print(
|
|
224
|
+
f"Warning: {len(matches)} session files match hints"
|
|
225
|
+
f" (game={game_hint}, color={color_hint})."
|
|
226
|
+
f" Using most recent: {chosen.get('GAME')}_{chosen.get('COLOR')}."
|
|
227
|
+
f" Pass --game and --color to be explicit.",
|
|
228
|
+
file=sys.stderr,
|
|
229
|
+
)
|
|
230
|
+
return matches[0][1]
|
|
200
231
|
|
|
201
232
|
default = os.path.expanduser("~/.clawtan_session")
|
|
202
233
|
try:
|
|
@@ -240,6 +271,89 @@ def _require(name: str, val):
|
|
|
240
271
|
return val
|
|
241
272
|
|
|
242
273
|
|
|
274
|
+
def cmd_whoami(args):
|
|
275
|
+
"""Show the currently resolved session (game, color, token)."""
|
|
276
|
+
game, token, color = _resolve_session(
|
|
277
|
+
getattr(args, "game", None),
|
|
278
|
+
getattr(args, "token", None),
|
|
279
|
+
getattr(args, "color", None),
|
|
280
|
+
)
|
|
281
|
+
_header("CURRENT SESSION")
|
|
282
|
+
print(f" Game: {game or '(not set)'}")
|
|
283
|
+
print(f" Color: {color or '(not set)'}")
|
|
284
|
+
print(f" Token: {token[:12] + '...' if token and len(token) > 12 else token or '(not set)'}")
|
|
285
|
+
|
|
286
|
+
# Show where each value came from
|
|
287
|
+
sources = []
|
|
288
|
+
if getattr(args, "game", None) or getattr(args, "color", None):
|
|
289
|
+
sources.append("CLI flags")
|
|
290
|
+
for var in ("CLAWTAN_GAME", "CLAWTAN_TOKEN", "CLAWTAN_COLOR"):
|
|
291
|
+
if os.environ.get(var):
|
|
292
|
+
sources.append(f"env ${var}")
|
|
293
|
+
if not sources:
|
|
294
|
+
sources.append("session file")
|
|
295
|
+
print(f" Source: {', '.join(sources)}")
|
|
296
|
+
|
|
297
|
+
if not (game and token and color):
|
|
298
|
+
print(
|
|
299
|
+
"\n Session incomplete. Run 'clawtan quick-join' or pass --game/--token/--color.",
|
|
300
|
+
file=sys.stderr,
|
|
301
|
+
)
|
|
302
|
+
sys.exit(1)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def cmd_clear_session(args):
|
|
306
|
+
"""Remove saved session files."""
|
|
307
|
+
removed = 0
|
|
308
|
+
|
|
309
|
+
if args.all:
|
|
310
|
+
# Remove all session files and the default
|
|
311
|
+
if os.path.isdir(_SESSIONS_DIR):
|
|
312
|
+
for fname in os.listdir(_SESSIONS_DIR):
|
|
313
|
+
if fname.endswith(".json"):
|
|
314
|
+
os.remove(os.path.join(_SESSIONS_DIR, fname))
|
|
315
|
+
removed += 1
|
|
316
|
+
default = os.path.expanduser("~/.clawtan_session")
|
|
317
|
+
if os.path.exists(default):
|
|
318
|
+
os.remove(default)
|
|
319
|
+
removed += 1
|
|
320
|
+
print(f"Cleared {removed} session file(s).")
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
# Clear a specific game, or just the default session
|
|
324
|
+
game_hint = args.game
|
|
325
|
+
color_hint = args.color
|
|
326
|
+
|
|
327
|
+
if game_hint or color_hint:
|
|
328
|
+
if os.path.isdir(_SESSIONS_DIR):
|
|
329
|
+
for fname in os.listdir(_SESSIONS_DIR):
|
|
330
|
+
if not fname.endswith(".json"):
|
|
331
|
+
continue
|
|
332
|
+
fpath = os.path.join(_SESSIONS_DIR, fname)
|
|
333
|
+
try:
|
|
334
|
+
with open(fpath) as f:
|
|
335
|
+
data = json.load(f)
|
|
336
|
+
if game_hint and data.get("GAME") != game_hint:
|
|
337
|
+
continue
|
|
338
|
+
if color_hint and data.get("COLOR") != color_hint:
|
|
339
|
+
continue
|
|
340
|
+
os.remove(fpath)
|
|
341
|
+
removed += 1
|
|
342
|
+
except (OSError, json.JSONDecodeError):
|
|
343
|
+
continue
|
|
344
|
+
else:
|
|
345
|
+
# No hints — clear the default session file
|
|
346
|
+
default = os.path.expanduser("~/.clawtan_session")
|
|
347
|
+
if os.path.exists(default):
|
|
348
|
+
os.remove(default)
|
|
349
|
+
removed += 1
|
|
350
|
+
|
|
351
|
+
if removed:
|
|
352
|
+
print(f"Cleared {removed} session file(s).")
|
|
353
|
+
else:
|
|
354
|
+
print("No matching session files found.")
|
|
355
|
+
|
|
356
|
+
|
|
243
357
|
# ---------------------------------------------------------------------------
|
|
244
358
|
# State extraction (operates on a full game-state dict)
|
|
245
359
|
# ---------------------------------------------------------------------------
|
|
@@ -904,7 +1018,9 @@ def cmd_wait(args):
|
|
|
904
1018
|
if status.get("winning_color"):
|
|
905
1019
|
_header("GAME OVER")
|
|
906
1020
|
winner = status["winning_color"]
|
|
1021
|
+
result = "won" if winner == color else "lost"
|
|
907
1022
|
print(f" Winner: {winner}")
|
|
1023
|
+
print(f" You ({color}): {result}")
|
|
908
1024
|
try:
|
|
909
1025
|
state = _get(f"/game/{game_id}")
|
|
910
1026
|
colors = state.get("colors", [])
|
|
@@ -916,7 +1032,8 @@ def cmd_wait(args):
|
|
|
916
1032
|
print(f" {c}: {vp} VP{marker}")
|
|
917
1033
|
except (APIError, Exception):
|
|
918
1034
|
pass
|
|
919
|
-
|
|
1035
|
+
print("\n Game is finished. Do not call 'wait' or 'act' again for this game.")
|
|
1036
|
+
sys.exit(2) # distinct exit code so agents can detect game-over
|
|
920
1037
|
|
|
921
1038
|
# Progress messages (to stderr so they don't pollute the briefing)
|
|
922
1039
|
if not status.get("started"):
|
|
@@ -1007,6 +1124,8 @@ def cmd_wait(args):
|
|
|
1007
1124
|
if robber:
|
|
1008
1125
|
print(f"\n Kraken: {robber}")
|
|
1009
1126
|
|
|
1127
|
+
print("\n >>> YOUR TURN: pick an action above and run 'clawtan act <ACTION> [VALUE]'.")
|
|
1128
|
+
|
|
1010
1129
|
|
|
1011
1130
|
def _print_roll_result(state: dict, pre_resources: dict | None):
|
|
1012
1131
|
"""Show dice result and per-player resource gains after a roll."""
|
|
@@ -1085,6 +1204,19 @@ def cmd_act(args):
|
|
|
1085
1204
|
current = state.get("current_color", "?")
|
|
1086
1205
|
actions = state.get("current_playable_actions", [])
|
|
1087
1206
|
|
|
1207
|
+
# Primary diagnosis: is it even our turn?
|
|
1208
|
+
if current != color:
|
|
1209
|
+
print(
|
|
1210
|
+
f" It is NOT your turn. Current turn: {current} (you are {color}).",
|
|
1211
|
+
file=sys.stderr,
|
|
1212
|
+
)
|
|
1213
|
+
print(
|
|
1214
|
+
" >>> Run 'clawtan wait' to block until it is your turn.",
|
|
1215
|
+
file=sys.stderr,
|
|
1216
|
+
)
|
|
1217
|
+
sys.exit(1)
|
|
1218
|
+
|
|
1219
|
+
# It is our turn but wrong action/value
|
|
1088
1220
|
available_types = set()
|
|
1089
1221
|
for a in actions:
|
|
1090
1222
|
if isinstance(a, list) and len(a) > 1:
|
|
@@ -1098,17 +1230,22 @@ def cmd_act(args):
|
|
|
1098
1230
|
)
|
|
1099
1231
|
else:
|
|
1100
1232
|
print(
|
|
1101
|
-
f" '{args.action}' is not available right now.",
|
|
1233
|
+
f" '{args.action}' is not available right now (prompt: {prompt}).",
|
|
1102
1234
|
file=sys.stderr,
|
|
1103
1235
|
)
|
|
1104
1236
|
|
|
1105
1237
|
print(f" Current turn: {current} | Prompt: {prompt}", file=sys.stderr)
|
|
1106
1238
|
if actions:
|
|
1107
1239
|
_print_actions(actions, my_color=color, state=state)
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1240
|
+
print(
|
|
1241
|
+
"\n >>> Pick an action above and run 'clawtan act <ACTION> [VALUE]'.",
|
|
1242
|
+
file=sys.stderr,
|
|
1243
|
+
)
|
|
1244
|
+
else:
|
|
1245
|
+
print(
|
|
1246
|
+
"\n >>> No actions available. Run 'clawtan wait' for your next turn.",
|
|
1247
|
+
file=sys.stderr,
|
|
1248
|
+
)
|
|
1112
1249
|
except Exception:
|
|
1113
1250
|
print(
|
|
1114
1251
|
" Run 'clawtan wait' to see your available actions.",
|
|
@@ -1124,6 +1261,15 @@ def cmd_act(args):
|
|
|
1124
1261
|
|
|
1125
1262
|
# Re-fetch state so the agent knows what to do next
|
|
1126
1263
|
state = _get(f"/game/{game_id}")
|
|
1264
|
+
|
|
1265
|
+
# Check if this action ended the game (e.g. winning build)
|
|
1266
|
+
winner = state.get("winning_color")
|
|
1267
|
+
if winner:
|
|
1268
|
+
result = "won" if winner == color else "lost"
|
|
1269
|
+
print(f"\n GAME OVER — Winner: {winner} (you {result})")
|
|
1270
|
+
print(" Game is finished. Do not call 'wait' or 'act' again for this game.")
|
|
1271
|
+
sys.exit(2)
|
|
1272
|
+
|
|
1127
1273
|
current_color = state.get("current_color")
|
|
1128
1274
|
|
|
1129
1275
|
# After a roll, show the dice result and resource distribution
|
|
@@ -1153,15 +1299,18 @@ def cmd_act(args):
|
|
|
1153
1299
|
|
|
1154
1300
|
if my_actions:
|
|
1155
1301
|
_print_actions(actions, my_color=color, state=state)
|
|
1302
|
+
print("\n >>> YOUR TURN: pick an action above and run 'clawtan act <ACTION> [VALUE]'.")
|
|
1156
1303
|
else:
|
|
1157
1304
|
print("\n No actions available.")
|
|
1305
|
+
print("\n >>> Run 'clawtan wait' to block until your next turn.")
|
|
1158
1306
|
elif my_actions:
|
|
1159
1307
|
print(f" Prompt: {prompt}")
|
|
1160
1308
|
if state.get("is_resolving_trade"):
|
|
1161
1309
|
_print_trade_context(state, color)
|
|
1162
1310
|
_print_actions(actions, my_color=color, state=state)
|
|
1311
|
+
print("\n >>> ACTION REQUIRED: pick an action above and run 'clawtan act <ACTION> [VALUE]'.")
|
|
1163
1312
|
else:
|
|
1164
|
-
print(
|
|
1313
|
+
print("\n >>> Turn complete. Run 'clawtan wait' to block until your next turn.")
|
|
1165
1314
|
|
|
1166
1315
|
|
|
1167
1316
|
def cmd_status(args):
|
|
@@ -1529,6 +1678,38 @@ def main():
|
|
|
1529
1678
|
p.add_argument("--since", type=int, default=0, help="Only messages with index >= N (default: 0)")
|
|
1530
1679
|
p.set_defaults(func=cmd_chat_read)
|
|
1531
1680
|
|
|
1681
|
+
# -- whoami --------------------------------------------------------
|
|
1682
|
+
p = sub.add_parser(
|
|
1683
|
+
"whoami",
|
|
1684
|
+
help="Show the currently resolved session",
|
|
1685
|
+
description=(
|
|
1686
|
+
"Show which game, color, and token the CLI would use right now.\n"
|
|
1687
|
+
"Useful for verifying you're pointed at the correct game before acting."
|
|
1688
|
+
),
|
|
1689
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1690
|
+
)
|
|
1691
|
+
p.add_argument("--game", help="Game ID (or set CLAWTAN_GAME)")
|
|
1692
|
+
p.add_argument("--token", help="Auth token (or set CLAWTAN_TOKEN)")
|
|
1693
|
+
p.add_argument("--color", help="Your color (or set CLAWTAN_COLOR)")
|
|
1694
|
+
p.set_defaults(func=cmd_whoami)
|
|
1695
|
+
|
|
1696
|
+
# -- clear-session -------------------------------------------------
|
|
1697
|
+
p = sub.add_parser(
|
|
1698
|
+
"clear-session",
|
|
1699
|
+
help="Remove saved session files",
|
|
1700
|
+
description=(
|
|
1701
|
+
"Remove saved session files to avoid stale credentials.\n"
|
|
1702
|
+
"With no flags, removes the default ~/.clawtan_session file.\n"
|
|
1703
|
+
"Use --game/--color to remove specific session files.\n"
|
|
1704
|
+
"Use --all to remove every saved session."
|
|
1705
|
+
),
|
|
1706
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1707
|
+
)
|
|
1708
|
+
p.add_argument("--game", help="Only clear sessions for this game ID")
|
|
1709
|
+
p.add_argument("--color", help="Only clear sessions for this color")
|
|
1710
|
+
p.add_argument("--all", action="store_true", help="Remove ALL saved sessions")
|
|
1711
|
+
p.set_defaults(func=cmd_clear_session)
|
|
1712
|
+
|
|
1532
1713
|
# -- Parse and run -------------------------------------------------
|
|
1533
1714
|
args = parser.parse_args()
|
|
1534
1715
|
if args.player:
|