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.
Files changed (2) hide show
  1. package/clawtan/cli.py +194 -13
  2. 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(os.path.join(_SESSIONS_DIR, fname)) as f:
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
- matches.append(data)
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
- return matches[0]
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
- return
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
- print(
1109
- "\n Tip: run 'clawtan wait' to get a full turn briefing with available actions.",
1110
- file=sys.stderr,
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(f"\n Action done. No more actions available. Run 'clawtan wait' for your next turn or action required.")
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawtan",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "CLI for AI agents playing Settlers of Clawtan -- a lobster-themed Catan board game",
5
5
  "bin": {
6
6
  "clawtan": "./bin/clawtan.js"