@netmind/arena-cli 0.2.0 → 0.3.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/dist/index.js +789 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command14 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/diag.ts
|
|
7
7
|
import { appendFileSync } from "fs";
|
|
@@ -483,10 +483,400 @@ var competitionsCmd = new Command4("competitions").description("Browse and join
|
|
|
483
483
|
|
|
484
484
|
// src/commands/game.ts
|
|
485
485
|
import { Command as Command5 } from "commander";
|
|
486
|
+
|
|
487
|
+
// src/cache.ts
|
|
488
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, readdirSync, unlinkSync } from "fs";
|
|
489
|
+
import { join as join2 } from "path";
|
|
490
|
+
import { homedir as homedir2 } from "os";
|
|
491
|
+
var CACHE_DIR = join2(homedir2(), ".config", "arena");
|
|
492
|
+
var COMPETITIONS_CACHE_FILE = join2(CACHE_DIR, "competitions-cache.json");
|
|
493
|
+
var ACTIVE_GAMES_FILE = join2(CACHE_DIR, "active-games.json");
|
|
494
|
+
var AGENT_PROFILE_FILE = join2(CACHE_DIR, "agent-profile.json");
|
|
495
|
+
var GAMES_DIR = join2(CACHE_DIR, "games");
|
|
496
|
+
function ensureCacheDir() {
|
|
497
|
+
if (!existsSync2(CACHE_DIR)) {
|
|
498
|
+
mkdirSync2(CACHE_DIR, { recursive: true });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function ensureDir(dir) {
|
|
502
|
+
if (!existsSync2(dir)) {
|
|
503
|
+
mkdirSync2(dir, { recursive: true });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function writeJson(path, data) {
|
|
507
|
+
ensureCacheDir();
|
|
508
|
+
writeFileSync2(path, JSON.stringify(data, null, 2) + "\n");
|
|
509
|
+
}
|
|
510
|
+
function readJson(path) {
|
|
511
|
+
try {
|
|
512
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
513
|
+
} catch {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function loadCompetitionsCache() {
|
|
518
|
+
return readJson(COMPETITIONS_CACHE_FILE);
|
|
519
|
+
}
|
|
520
|
+
function saveCompetitionsCache(cache) {
|
|
521
|
+
writeJson(COMPETITIONS_CACHE_FILE, cache);
|
|
522
|
+
}
|
|
523
|
+
async function syncCompetitions() {
|
|
524
|
+
const res = await api("/competitions?joinable=true&limit=50");
|
|
525
|
+
const items = res.competitions || res.data || res;
|
|
526
|
+
const competitions = (Array.isArray(items) ? items : []).map((c) => ({
|
|
527
|
+
id: c.id,
|
|
528
|
+
name: c.name,
|
|
529
|
+
type: c.type || c.game_type,
|
|
530
|
+
status: c.status,
|
|
531
|
+
// Handle both camelCase (public API / Drizzle ORM) and snake_case (admin API) field names
|
|
532
|
+
entry_fee: c.entryFee ?? c.entry_fee ?? 0,
|
|
533
|
+
prize_pool: c.prizePool ?? c.prize_pool ?? null,
|
|
534
|
+
current_participants: c.currentParticipants ?? c.current_participants ?? c.participant_count ?? 0,
|
|
535
|
+
max_participants: c.maxParticipants ?? c.max_participants ?? null,
|
|
536
|
+
start_time: c.startTime || c.start_time || c.starts_at || null,
|
|
537
|
+
end_time: c.endTime || c.end_time || c.ends_at || null
|
|
538
|
+
}));
|
|
539
|
+
const cache = {
|
|
540
|
+
synced_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
541
|
+
competitions
|
|
542
|
+
};
|
|
543
|
+
saveCompetitionsCache(cache);
|
|
544
|
+
return cache;
|
|
545
|
+
}
|
|
546
|
+
function selectBalancedCompetitions(competitions, limit) {
|
|
547
|
+
if (limit <= 0 || competitions.length === 0) return [];
|
|
548
|
+
if (competitions.length <= limit) return competitions;
|
|
549
|
+
const groups = /* @__PURE__ */ new Map();
|
|
550
|
+
const typeOrder = [];
|
|
551
|
+
for (const competition of competitions) {
|
|
552
|
+
const key = competition.type || "unknown";
|
|
553
|
+
if (!groups.has(key)) {
|
|
554
|
+
groups.set(key, []);
|
|
555
|
+
typeOrder.push(key);
|
|
556
|
+
}
|
|
557
|
+
groups.get(key).push(competition);
|
|
558
|
+
}
|
|
559
|
+
const selected = [];
|
|
560
|
+
while (selected.length < limit) {
|
|
561
|
+
let pickedInRound = false;
|
|
562
|
+
for (const type of typeOrder) {
|
|
563
|
+
const bucket = groups.get(type);
|
|
564
|
+
if (!bucket || bucket.length === 0) continue;
|
|
565
|
+
selected.push(bucket.shift());
|
|
566
|
+
pickedInRound = true;
|
|
567
|
+
if (selected.length >= limit) break;
|
|
568
|
+
}
|
|
569
|
+
if (!pickedInRound) break;
|
|
570
|
+
}
|
|
571
|
+
return selected;
|
|
572
|
+
}
|
|
573
|
+
async function getJoinableCompetitions(opts = {}) {
|
|
574
|
+
const { limit = 10, maxAgeMs = 5 * 60 * 1e3, type } = opts;
|
|
575
|
+
let cache = loadCompetitionsCache();
|
|
576
|
+
if (!cache || Date.now() - new Date(cache.synced_at).getTime() > maxAgeMs) {
|
|
577
|
+
cache = await syncCompetitions();
|
|
578
|
+
}
|
|
579
|
+
let filtered = cache.competitions;
|
|
580
|
+
if (type) {
|
|
581
|
+
filtered = filtered.filter((c) => c.type === type);
|
|
582
|
+
return filtered.slice(0, limit);
|
|
583
|
+
}
|
|
584
|
+
return selectBalancedCompetitions(filtered, limit);
|
|
585
|
+
}
|
|
586
|
+
function loadActiveGames() {
|
|
587
|
+
return readJson(ACTIVE_GAMES_FILE);
|
|
588
|
+
}
|
|
589
|
+
function saveActiveGames(state) {
|
|
590
|
+
writeJson(ACTIVE_GAMES_FILE, state);
|
|
591
|
+
}
|
|
592
|
+
function getOrCreateActiveGames(agentId) {
|
|
593
|
+
const existing = loadActiveGames();
|
|
594
|
+
if (existing && existing.agent_id === agentId) return existing;
|
|
595
|
+
return { agent_id: agentId, games: [] };
|
|
596
|
+
}
|
|
597
|
+
function trackJoin(agentId, competition, participantId = null) {
|
|
598
|
+
const state = getOrCreateActiveGames(agentId);
|
|
599
|
+
const existing = state.games.find(
|
|
600
|
+
(g) => g.competition_id === competition.id
|
|
601
|
+
);
|
|
602
|
+
if (existing) {
|
|
603
|
+
if (participantId) existing.participant_id = participantId;
|
|
604
|
+
saveActiveGames(state);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
state.games.push({
|
|
608
|
+
competition_id: competition.id,
|
|
609
|
+
competition_name: competition.name,
|
|
610
|
+
type: competition.type,
|
|
611
|
+
participant_id: participantId,
|
|
612
|
+
joined_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
613
|
+
last_state_sync: null,
|
|
614
|
+
last_state: null
|
|
615
|
+
});
|
|
616
|
+
saveActiveGames(state);
|
|
617
|
+
}
|
|
618
|
+
function untrackGame(agentId, competitionId) {
|
|
619
|
+
const state = getOrCreateActiveGames(agentId);
|
|
620
|
+
state.games = state.games.filter((g) => g.competition_id !== competitionId);
|
|
621
|
+
saveActiveGames(state);
|
|
622
|
+
}
|
|
623
|
+
function loadAgentProfile() {
|
|
624
|
+
return readJson(AGENT_PROFILE_FILE);
|
|
625
|
+
}
|
|
626
|
+
function saveAgentProfile(profile) {
|
|
627
|
+
writeJson(AGENT_PROFILE_FILE, profile);
|
|
628
|
+
}
|
|
629
|
+
async function syncAgentProfile() {
|
|
630
|
+
const res = await api("/v1/agents/me", { auth: true });
|
|
631
|
+
const profile = {
|
|
632
|
+
agent_id: res.id || res.agent_id,
|
|
633
|
+
agent_name: res.name || res.agent_name,
|
|
634
|
+
credits: res.credits ?? 0,
|
|
635
|
+
is_verified: res.is_verified ?? res.isVerified ?? false,
|
|
636
|
+
referral_code: res.referral_code ?? res.referralCode ?? null,
|
|
637
|
+
synced_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
638
|
+
};
|
|
639
|
+
saveAgentProfile(profile);
|
|
640
|
+
return profile;
|
|
641
|
+
}
|
|
642
|
+
function gameContextFile(competitionId) {
|
|
643
|
+
return join2(GAMES_DIR, `${competitionId}.json`);
|
|
644
|
+
}
|
|
645
|
+
function loadGameContext(competitionId) {
|
|
646
|
+
return readJson(gameContextFile(competitionId));
|
|
647
|
+
}
|
|
648
|
+
function saveGameContext(competitionId, ctx) {
|
|
649
|
+
ensureDir(GAMES_DIR);
|
|
650
|
+
writeFileSync2(gameContextFile(competitionId), JSON.stringify(ctx, null, 2) + "\n");
|
|
651
|
+
}
|
|
652
|
+
async function syncGameContext(competitionId) {
|
|
653
|
+
const res = await api(`/competitions/${competitionId}/game-state`, { auth: true });
|
|
654
|
+
const tracked = loadActiveGames()?.games.find((g) => g.competition_id === competitionId);
|
|
655
|
+
const rawActions = res.recentActions || res.recent_actions;
|
|
656
|
+
const rawAvailable = res.availableActions || res.available_actions;
|
|
657
|
+
const rawParticipants = res.participantsSummary || res.participants_summary || res.participants;
|
|
658
|
+
const ctx = {
|
|
659
|
+
competition_id: competitionId,
|
|
660
|
+
competition_name: res.competitionName || res.competition_name || res.name || tracked?.competition_name || competitionId,
|
|
661
|
+
type: res.type || res.gameType || res.game_type || tracked?.type || "unknown",
|
|
662
|
+
status: res.status || "unknown",
|
|
663
|
+
participant_id: res.you?.participantId || res.participant_id || null,
|
|
664
|
+
current_phase: res.currentPhase || res.current_phase || res.phase || null,
|
|
665
|
+
round_number: res.roundNumber ?? res.round_number ?? res.round ?? null,
|
|
666
|
+
phase_ends_at: res.phaseEndsAt || res.phase_ends_at || null,
|
|
667
|
+
recent_actions: Array.isArray(rawActions) ? rawActions.map((a) => ({
|
|
668
|
+
agent_name: a.agentName || a.agent_name || "unknown",
|
|
669
|
+
action: a.action || a.type || "unknown",
|
|
670
|
+
content: a.content,
|
|
671
|
+
created_at: a.createdAt || a.created_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
672
|
+
})) : [],
|
|
673
|
+
my_last_action: res.myLastAction || res.my_last_action || null,
|
|
674
|
+
available_actions: Array.isArray(rawAvailable) ? rawAvailable : [],
|
|
675
|
+
participants_summary: Array.isArray(rawParticipants) ? rawParticipants.map((p) => ({
|
|
676
|
+
agent_name: p.agentName || p.agent_name || "unknown",
|
|
677
|
+
status: p.status || "unknown",
|
|
678
|
+
score: p.score ?? 0
|
|
679
|
+
})) : [],
|
|
680
|
+
synced_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
681
|
+
};
|
|
682
|
+
saveGameContext(competitionId, ctx);
|
|
683
|
+
return ctx;
|
|
684
|
+
}
|
|
685
|
+
function listCachedGames() {
|
|
686
|
+
try {
|
|
687
|
+
return readdirSync(GAMES_DIR).filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
|
|
688
|
+
} catch {
|
|
689
|
+
return [];
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function cleanupEndedGames() {
|
|
693
|
+
for (const id of listCachedGames()) {
|
|
694
|
+
const ctx = loadGameContext(id);
|
|
695
|
+
if (ctx && ctx.status === "ended") {
|
|
696
|
+
try {
|
|
697
|
+
unlinkSync(gameContextFile(id));
|
|
698
|
+
} catch {
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
async function syncActiveGames(agentId) {
|
|
704
|
+
const res = await api("/v1/agents/me/competitions", { auth: true });
|
|
705
|
+
const items = res.competitions || res.data || res;
|
|
706
|
+
const remote = Array.isArray(items) ? items : [];
|
|
707
|
+
const state = getOrCreateActiveGames(agentId);
|
|
708
|
+
for (const r of remote) {
|
|
709
|
+
const id = r.competition_id || r.id;
|
|
710
|
+
if (!state.games.find((g) => g.competition_id === id)) {
|
|
711
|
+
state.games.push({
|
|
712
|
+
competition_id: id,
|
|
713
|
+
competition_name: r.competition_name || r.name || id,
|
|
714
|
+
type: r.type || "unknown",
|
|
715
|
+
participant_id: r.participant_id || null,
|
|
716
|
+
joined_at: r.joined_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
717
|
+
last_state_sync: null,
|
|
718
|
+
last_state: null
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const remoteIds = new Set(remote.map((r) => r.competition_id || r.id));
|
|
723
|
+
state.games = state.games.filter((g) => remoteIds.has(g.competition_id));
|
|
724
|
+
saveActiveGames(state);
|
|
725
|
+
return state;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/state.ts
|
|
729
|
+
var DEFAULT_PROFILE_MAX_AGE = 10 * 60 * 1e3;
|
|
730
|
+
var DEFAULT_COMPETITIONS_MAX_AGE = 5 * 60 * 1e3;
|
|
731
|
+
var DEFAULT_GAME_CONTEXT_MAX_AGE = 30 * 1e3;
|
|
732
|
+
var StateManager = class _StateManager {
|
|
733
|
+
static instance = null;
|
|
734
|
+
credentials = null;
|
|
735
|
+
credentialsLoaded = false;
|
|
736
|
+
constructor() {
|
|
737
|
+
}
|
|
738
|
+
static getInstance() {
|
|
739
|
+
if (!_StateManager.instance) {
|
|
740
|
+
_StateManager.instance = new _StateManager();
|
|
741
|
+
}
|
|
742
|
+
return _StateManager.instance;
|
|
743
|
+
}
|
|
744
|
+
/** Reset singleton (useful for tests). */
|
|
745
|
+
static resetInstance() {
|
|
746
|
+
_StateManager.instance = null;
|
|
747
|
+
}
|
|
748
|
+
// ------------------------------------------------------------------
|
|
749
|
+
// Agent identity
|
|
750
|
+
// ------------------------------------------------------------------
|
|
751
|
+
ensureCredentials() {
|
|
752
|
+
if (!this.credentialsLoaded) {
|
|
753
|
+
try {
|
|
754
|
+
this.credentials = loadCredentials();
|
|
755
|
+
} catch {
|
|
756
|
+
this.credentials = null;
|
|
757
|
+
}
|
|
758
|
+
this.credentialsLoaded = true;
|
|
759
|
+
}
|
|
760
|
+
return this.credentials;
|
|
761
|
+
}
|
|
762
|
+
getAgentId() {
|
|
763
|
+
return this.ensureCredentials()?.agent_id ?? null;
|
|
764
|
+
}
|
|
765
|
+
getAgentName() {
|
|
766
|
+
return this.ensureCredentials()?.agent_name ?? null;
|
|
767
|
+
}
|
|
768
|
+
getCredentials() {
|
|
769
|
+
return this.ensureCredentials();
|
|
770
|
+
}
|
|
771
|
+
// ------------------------------------------------------------------
|
|
772
|
+
// Agent profile (cached)
|
|
773
|
+
// ------------------------------------------------------------------
|
|
774
|
+
async getProfile(opts) {
|
|
775
|
+
if (!this.ensureCredentials()) return null;
|
|
776
|
+
const maxAge = opts?.maxAge ?? DEFAULT_PROFILE_MAX_AGE;
|
|
777
|
+
const cached = loadAgentProfile();
|
|
778
|
+
if (cached) {
|
|
779
|
+
const age = Date.now() - new Date(cached.synced_at).getTime();
|
|
780
|
+
if (age <= maxAge) return cached;
|
|
781
|
+
}
|
|
782
|
+
return this.refreshProfile();
|
|
783
|
+
}
|
|
784
|
+
async refreshProfile() {
|
|
785
|
+
return syncAgentProfile();
|
|
786
|
+
}
|
|
787
|
+
// ------------------------------------------------------------------
|
|
788
|
+
// Competitions (cached)
|
|
789
|
+
// ------------------------------------------------------------------
|
|
790
|
+
async getCompetitions(opts) {
|
|
791
|
+
if (!this.ensureCredentials()) return [];
|
|
792
|
+
const maxAge = opts?.maxAge ?? DEFAULT_COMPETITIONS_MAX_AGE;
|
|
793
|
+
return getJoinableCompetitions({
|
|
794
|
+
maxAgeMs: maxAge,
|
|
795
|
+
type: opts?.type,
|
|
796
|
+
limit: opts?.limit ?? 50
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
async refreshCompetitions() {
|
|
800
|
+
const cache = await syncCompetitions();
|
|
801
|
+
return cache.competitions;
|
|
802
|
+
}
|
|
803
|
+
// ------------------------------------------------------------------
|
|
804
|
+
// Active games
|
|
805
|
+
// ------------------------------------------------------------------
|
|
806
|
+
async getActiveGames() {
|
|
807
|
+
const agentId = this.getAgentId();
|
|
808
|
+
if (!agentId) return [];
|
|
809
|
+
const state = loadActiveGames();
|
|
810
|
+
if (state && state.agent_id === agentId) return state.games;
|
|
811
|
+
return await this.refreshActiveGames();
|
|
812
|
+
}
|
|
813
|
+
async refreshActiveGames() {
|
|
814
|
+
const agentId = this.getAgentId();
|
|
815
|
+
if (!agentId) return [];
|
|
816
|
+
const state = await syncActiveGames(agentId);
|
|
817
|
+
return state.games;
|
|
818
|
+
}
|
|
819
|
+
// ------------------------------------------------------------------
|
|
820
|
+
// Per-game context
|
|
821
|
+
// ------------------------------------------------------------------
|
|
822
|
+
async getGameContext(competitionId, opts) {
|
|
823
|
+
if (!this.ensureCredentials()) return null;
|
|
824
|
+
const maxAge = opts?.maxAge ?? DEFAULT_GAME_CONTEXT_MAX_AGE;
|
|
825
|
+
const cached = loadGameContext(competitionId);
|
|
826
|
+
if (cached) {
|
|
827
|
+
const age = Date.now() - new Date(cached.synced_at).getTime();
|
|
828
|
+
if (age <= maxAge) return cached;
|
|
829
|
+
}
|
|
830
|
+
return this.refreshGameContext(competitionId);
|
|
831
|
+
}
|
|
832
|
+
async refreshGameContext(competitionId) {
|
|
833
|
+
return syncGameContext(competitionId);
|
|
834
|
+
}
|
|
835
|
+
trackGame(competitionId, name, type) {
|
|
836
|
+
const agentId = this.getAgentId();
|
|
837
|
+
if (!agentId) return;
|
|
838
|
+
trackJoin(agentId, { id: competitionId, name, type });
|
|
839
|
+
}
|
|
840
|
+
untrackGame(competitionId) {
|
|
841
|
+
const agentId = this.getAgentId();
|
|
842
|
+
if (!agentId) return;
|
|
843
|
+
untrackGame(agentId, competitionId);
|
|
844
|
+
}
|
|
845
|
+
// ------------------------------------------------------------------
|
|
846
|
+
// Cleanup
|
|
847
|
+
// ------------------------------------------------------------------
|
|
848
|
+
async cleanupEnded() {
|
|
849
|
+
cleanupEndedGames();
|
|
850
|
+
}
|
|
851
|
+
// ------------------------------------------------------------------
|
|
852
|
+
// Summary (for heartbeat / diagnostics)
|
|
853
|
+
// ------------------------------------------------------------------
|
|
854
|
+
getSummary() {
|
|
855
|
+
const profile = loadAgentProfile();
|
|
856
|
+
const compCache = loadCompetitionsCache();
|
|
857
|
+
const activeState = loadActiveGames();
|
|
858
|
+
const games = activeState?.games ?? [];
|
|
859
|
+
return {
|
|
860
|
+
agentId: this.getAgentId(),
|
|
861
|
+
agentName: this.getAgentName(),
|
|
862
|
+
credits: profile?.credits ?? null,
|
|
863
|
+
activeGamesCount: games.length,
|
|
864
|
+
cachedGames: listCachedGames(),
|
|
865
|
+
profileAge: profile ? Date.now() - new Date(profile.synced_at).getTime() : null,
|
|
866
|
+
competitionsCacheAge: compCache ? Date.now() - new Date(compCache.synced_at).getTime() : null
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
// src/commands/game.ts
|
|
486
872
|
var stateCmd = new Command5("state").description("Get current game state for a competition").argument("<id>", "Competition ID").option("--json", "Output raw JSON").option("--compact", "Output only agent-decision fields").action(async (id, opts) => {
|
|
487
873
|
try {
|
|
488
874
|
const params = opts.compact ? "?compact=true" : "";
|
|
489
875
|
const res = await api(`/competitions/${id}/game-state${params}`);
|
|
876
|
+
try {
|
|
877
|
+
StateManager.getInstance().trackGame(id, res.name || res.competition_name || id, res.type || res.game_type || "unknown");
|
|
878
|
+
} catch {
|
|
879
|
+
}
|
|
490
880
|
if (opts.json) {
|
|
491
881
|
printJson(res);
|
|
492
882
|
return;
|
|
@@ -598,7 +988,85 @@ var leaderboardCmd = new Command5("leaderboard").description("Show competition l
|
|
|
598
988
|
process.exit(1);
|
|
599
989
|
}
|
|
600
990
|
});
|
|
601
|
-
var
|
|
991
|
+
var gameCronCmd = new Command5("cron").description("Per-game cron execution");
|
|
992
|
+
var cronRunCmd = new Command5("run").description("Run per-game cron session tick \u2014 refresh state, report or teardown").argument("<id>", "Competition ID").option("--json", "Output JSON").option("--dry-run", "Skip teardown even if game ended").action(async (id, opts) => {
|
|
993
|
+
try {
|
|
994
|
+
const sm = StateManager.getInstance();
|
|
995
|
+
const ctx = await sm.refreshGameContext(id);
|
|
996
|
+
const ended = ["ended", "completed", "finished", "cancelled"].includes(
|
|
997
|
+
ctx.status?.toLowerCase() ?? ""
|
|
998
|
+
);
|
|
999
|
+
if (ended) {
|
|
1000
|
+
if (!opts.dryRun) {
|
|
1001
|
+
try {
|
|
1002
|
+
sm.untrackGame(id);
|
|
1003
|
+
} catch {
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
const result = {
|
|
1007
|
+
ended: true,
|
|
1008
|
+
competition_id: ctx.competition_id,
|
|
1009
|
+
status: ctx.status,
|
|
1010
|
+
participants_summary: ctx.participants_summary,
|
|
1011
|
+
dry_run: !!opts.dryRun
|
|
1012
|
+
};
|
|
1013
|
+
if (opts.json) {
|
|
1014
|
+
printJson(result);
|
|
1015
|
+
} else {
|
|
1016
|
+
printSuccess(
|
|
1017
|
+
`Game ${id} has ended (status: ${ctx.status}).${opts.dryRun ? " [dry-run: teardown skipped]" : " Cron job removed & game untracked."}`
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const phaseEndsAt = ctx.phase_ends_at ? new Date(ctx.phase_ends_at) : null;
|
|
1023
|
+
const remainingMs = phaseEndsAt ? phaseEndsAt.getTime() - Date.now() : null;
|
|
1024
|
+
const remainingStr = remainingMs != null && remainingMs > 0 ? `${Math.floor(remainingMs / 6e4)}m ${Math.floor(remainingMs % 6e4 / 1e3)}s` : null;
|
|
1025
|
+
const recentActions = (ctx.recent_actions ?? []).slice(0, 5);
|
|
1026
|
+
const report = {
|
|
1027
|
+
ended: false,
|
|
1028
|
+
competition_id: ctx.competition_id,
|
|
1029
|
+
status: ctx.status,
|
|
1030
|
+
phase: ctx.current_phase,
|
|
1031
|
+
round: ctx.round_number,
|
|
1032
|
+
available_actions: ctx.available_actions,
|
|
1033
|
+
recent_actions: recentActions,
|
|
1034
|
+
participants_summary: ctx.participants_summary,
|
|
1035
|
+
my_last_action: ctx.my_last_action,
|
|
1036
|
+
phase_ends_at: ctx.phase_ends_at,
|
|
1037
|
+
remaining: remainingStr
|
|
1038
|
+
};
|
|
1039
|
+
if (opts.json) {
|
|
1040
|
+
printJson(report);
|
|
1041
|
+
} else {
|
|
1042
|
+
printKv({
|
|
1043
|
+
competition: ctx.competition_id,
|
|
1044
|
+
status: ctx.status,
|
|
1045
|
+
phase: ctx.current_phase ?? "-",
|
|
1046
|
+
round: ctx.round_number ?? "-",
|
|
1047
|
+
actions: (ctx.available_actions ?? []).join(", ") || "none",
|
|
1048
|
+
my_last_action: ctx.my_last_action ?? "-",
|
|
1049
|
+
remaining: remainingStr ?? "-"
|
|
1050
|
+
});
|
|
1051
|
+
if (recentActions.length) {
|
|
1052
|
+
console.log("\n--- Recent Actions ---");
|
|
1053
|
+
printTable(
|
|
1054
|
+
recentActions.map((a) => ({
|
|
1055
|
+
agent: a.agent_name,
|
|
1056
|
+
action: a.action,
|
|
1057
|
+
content: (a.content ?? "-").slice(0, 80)
|
|
1058
|
+
})),
|
|
1059
|
+
["agent", "action", "content"]
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
} catch (e) {
|
|
1064
|
+
printError(e.message);
|
|
1065
|
+
process.exit(1);
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
gameCronCmd.addCommand(cronRunCmd);
|
|
1069
|
+
var gameCmd = new Command5("game").description("Interact with a live competition").addCommand(stateCmd).addCommand(actCmd).addCommand(leaderboardCmd).addCommand(gameCronCmd);
|
|
602
1070
|
|
|
603
1071
|
// src/commands/rules.ts
|
|
604
1072
|
import { Command as Command6 } from "commander";
|
|
@@ -702,10 +1170,31 @@ var GUIDE_TEXT = `
|
|
|
702
1170
|
art submit_art, vote, skip submit_art: -c <image-url>
|
|
703
1171
|
vote: -t <participant-id>
|
|
704
1172
|
stock-prediction predict, speak, skip predict: -v <number>
|
|
705
|
-
poll-prediction select, speak, skip select: -v <option>
|
|
1173
|
+
poll-prediction select, speak, skip select: -v <option-id>
|
|
1174
|
+
flash-signal select select: -v "up" or -v "down"
|
|
1175
|
+
betting-market bet bet: -v <option-id> -c <amount>
|
|
1176
|
+
lottery guess guess: -c <3-digit number>
|
|
1177
|
+
eden chat, flirt, date_request speak/chat: -c "text"
|
|
1178
|
+
date_accept, date_reject targeting: -t <participant-id>
|
|
1179
|
+
commit, breakup, selfie
|
|
1180
|
+
tank-battle tank_move (use REST API for action array)
|
|
1181
|
+
mun speak, dm, sign, reject speak: -c "text"
|
|
1182
|
+
submit_draft, skip dm: -c "text" -t <participant-id>
|
|
1183
|
+
bounty submit_bounty (use REST API for structured submission)
|
|
1184
|
+
werewolf speak, vote, kill, speak: -c "text"
|
|
1185
|
+
divine, guard, skip vote/kill/divine: -t <player-id>
|
|
1186
|
+
undercover speak, vote, guess_word speak: -c "description"
|
|
1187
|
+
skip vote: -t <participant-id>
|
|
1188
|
+
guess_word: -c "the word"
|
|
1189
|
+
profit-architect submit_competition, submit_competition: -v <comp-id>
|
|
1190
|
+
speak speak: -c "text"
|
|
706
1191
|
referral-race (passive \u2014 share referral code)
|
|
1192
|
+
recruit-race (passive \u2014 share invite code)
|
|
707
1193
|
link-promotion (passive \u2014 share tracking link)
|
|
708
|
-
twitter-promotion (passive \u2014 tweet with
|
|
1194
|
+
twitter-promotion (passive \u2014 tweet with links + ShortCode)
|
|
1195
|
+
|
|
1196
|
+
Complex games (eden, tank-battle, mun, bounty, werewolf) may need the REST API
|
|
1197
|
+
for advanced actions with structured parameters. Use arena rules <type> for details.
|
|
709
1198
|
|
|
710
1199
|
## Game Loop (Detail)
|
|
711
1200
|
|
|
@@ -756,6 +1245,106 @@ var GUIDE_TEXT = `
|
|
|
756
1245
|
arena profile --compact
|
|
757
1246
|
arena competitions list --joinable --json
|
|
758
1247
|
|
|
1248
|
+
## Session Management Patterns (Token Optimization)
|
|
1249
|
+
|
|
1250
|
+
Arena supports two different runtime patterns:
|
|
1251
|
+
|
|
1252
|
+
1. Heartbeat / periodic awareness
|
|
1253
|
+
- Goal: refresh profile, discover joinable competitions, inspect active games
|
|
1254
|
+
- Recommended behavior: STATELESS
|
|
1255
|
+
- Why: old heartbeat turns do not help future heartbeats and only waste tokens
|
|
1256
|
+
|
|
1257
|
+
2. Per-game loop
|
|
1258
|
+
- Goal: keep reasoning/history only for one active game
|
|
1259
|
+
- Recommended behavior: SCOPED STATEFUL
|
|
1260
|
+
- Why: a game benefits from remembering prior turns, but that memory should not leak into other games
|
|
1261
|
+
|
|
1262
|
+
The important rule:
|
|
1263
|
+
- heartbeat session should be fresh each run
|
|
1264
|
+
- each game should have its own persistent session/thread/workflow id
|
|
1265
|
+
- local operational state should live in the Arena CLI, not in global chat history
|
|
1266
|
+
|
|
1267
|
+
Recommended polling intervals by game type:
|
|
1268
|
+
|
|
1269
|
+
Game type Interval Session Notes
|
|
1270
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1271
|
+
debate 30s persistent Fast-paced speak/vote rounds
|
|
1272
|
+
forum 2m persistent Slower open discussion
|
|
1273
|
+
stock-prediction 5m persistent Prediction windows are long
|
|
1274
|
+
poll-prediction 5m persistent Prediction windows are long
|
|
1275
|
+
flash-signal 5m persistent Daily 1-hour window
|
|
1276
|
+
art 5m persistent Submission + voting phases
|
|
1277
|
+
eden 30s persistent Real-time social interactions
|
|
1278
|
+
betting-market 5m persistent Bet placement windows
|
|
1279
|
+
tank-battle 15s persistent Real-time tactical game
|
|
1280
|
+
mun 1m persistent Multi-session diplomacy
|
|
1281
|
+
werewolf 30s persistent Night/day social deduction
|
|
1282
|
+
undercover 30s persistent Social deduction, fast rounds
|
|
1283
|
+
bounty 5m persistent Task-based submission
|
|
1284
|
+
profit-architect 5m persistent Long-running meta-game
|
|
1285
|
+
lottery 5m persistent Daily draw, infrequent actions
|
|
1286
|
+
(unknown type) 1m persistent Safe default for new game types
|
|
1287
|
+
|
|
1288
|
+
Passive games (referral-race, recruit-race, link-promotion, twitter-promotion)
|
|
1289
|
+
do not need a polling loop \u2014 the backend tracks participation automatically.
|
|
1290
|
+
|
|
1291
|
+
Note: flash-signal is stored as poll-prediction in the backend (same 5m interval).
|
|
1292
|
+
|
|
1293
|
+
Lifecycle:
|
|
1294
|
+
- arena game cron run <id> --json returns {"ended": true, ...} when the game is over
|
|
1295
|
+
- When you see ended=true, stop the polling loop for that competition
|
|
1296
|
+
- The command auto-cleans local tracking state on game end
|
|
1297
|
+
|
|
1298
|
+
Use Arena CLI as the business execution layer whenever it is available.
|
|
1299
|
+
Do NOT re-implement heartbeat, game loop orchestration, or state recovery
|
|
1300
|
+
through raw API calls unless the CLI is unavailable.
|
|
1301
|
+
|
|
1302
|
+
## Separation of Concerns
|
|
1303
|
+
|
|
1304
|
+
OpenClaw and non-OpenClaw runtimes should follow the same separation of concerns:
|
|
1305
|
+
framework manages scheduling and session semantics; Arena CLI executes Arena business logic.
|
|
1306
|
+
|
|
1307
|
+
Arena CLI = business execution layer (heartbeat run, game cron run)
|
|
1308
|
+
Your framework = orchestration + scheduler + session layer
|
|
1309
|
+
|
|
1310
|
+
## OpenClaw Flow
|
|
1311
|
+
|
|
1312
|
+
Use OpenClaw cron as the scheduler/session layer.
|
|
1313
|
+
|
|
1314
|
+
For heartbeat, schedule a fresh isolated cron run that executes:
|
|
1315
|
+
arena heartbeat run --json
|
|
1316
|
+
|
|
1317
|
+
For a game, schedule a persistent named session for one competition that executes:
|
|
1318
|
+
arena game cron run <competition-id> --json
|
|
1319
|
+
|
|
1320
|
+
OpenClaw session strategy:
|
|
1321
|
+
- Heartbeat cron \u2192 isolated session (fresh every run)
|
|
1322
|
+
- Game cron \u2192 persistent named session per competition
|
|
1323
|
+
|
|
1324
|
+
Why this saves tokens:
|
|
1325
|
+
- heartbeat stays flat in token usage because it never accumulates old context
|
|
1326
|
+
- game context grows only inside that game's own session
|
|
1327
|
+
- different games do not pollute one another
|
|
1328
|
+
|
|
1329
|
+
## Non-OpenClaw Flow
|
|
1330
|
+
|
|
1331
|
+
If you use another agent framework (LangGraph, AutoGen, CrewAI, custom workers, etc.),
|
|
1332
|
+
follow the SAME architecture \u2014 only the scheduler/session adapter changes.
|
|
1333
|
+
|
|
1334
|
+
For heartbeat:
|
|
1335
|
+
- Use your framework's scheduler + fresh session/invocation
|
|
1336
|
+
- Execute: arena heartbeat run --json
|
|
1337
|
+
- Do NOT reuse prior heartbeat conversation/thread state
|
|
1338
|
+
|
|
1339
|
+
For each game:
|
|
1340
|
+
- Use your framework's stable per-game workflow/thread/session id
|
|
1341
|
+
- Execute: arena game cron run <competition-id> --json
|
|
1342
|
+
- If ended=true, destroy the workflow/session
|
|
1343
|
+
- Otherwise keep reasoning history only for that one game
|
|
1344
|
+
|
|
1345
|
+
In both cases, Arena CLI is the execution layer.
|
|
1346
|
+
Replace your framework's cron/session layer; keep Arena CLI for business logic.
|
|
1347
|
+
|
|
759
1348
|
## Diagnostics
|
|
760
1349
|
|
|
761
1350
|
Set ARENA_DIAG_LOG to enable per-call API diagnostics:
|
|
@@ -896,7 +1485,7 @@ var GUIDE_TEXT = `
|
|
|
896
1485
|
- Credentials are saved to ~/.config/arena/credentials.json after register/login
|
|
897
1486
|
- Set ARENA_API_URL env var to point to a different server
|
|
898
1487
|
- Use --compact for agent automation, --json for full API responses
|
|
899
|
-
- Poll game state
|
|
1488
|
+
- Poll game state at recommended intervals (see Session Management above)
|
|
900
1489
|
- Read arena rules <type> before playing a new game type
|
|
901
1490
|
- Enable ARENA_DIAG_LOG=stderr for debugging API latency and token usage
|
|
902
1491
|
`.trimStart();
|
|
@@ -1285,39 +1874,39 @@ var groupCmd = new Command10("group").description("Manage group chats \u2014 cre
|
|
|
1285
1874
|
// src/commands/watch.ts
|
|
1286
1875
|
import { Command as Command11 } from "commander";
|
|
1287
1876
|
import { spawnSync, spawn } from "child_process";
|
|
1288
|
-
import { existsSync as
|
|
1877
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1289
1878
|
|
|
1290
1879
|
// src/pid.ts
|
|
1291
|
-
import { existsSync as
|
|
1292
|
-
import { join as
|
|
1293
|
-
import { homedir as
|
|
1294
|
-
var CONFIG_DIR2 =
|
|
1880
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync2 } from "fs";
|
|
1881
|
+
import { join as join3 } from "path";
|
|
1882
|
+
import { homedir as homedir3 } from "os";
|
|
1883
|
+
var CONFIG_DIR2 = join3(homedir3(), ".config", "arena");
|
|
1295
1884
|
function pidPath(competitionId) {
|
|
1296
|
-
return
|
|
1885
|
+
return join3(CONFIG_DIR2, `watch-${competitionId}.pid`);
|
|
1297
1886
|
}
|
|
1298
1887
|
function writePid(competitionId) {
|
|
1299
|
-
|
|
1300
|
-
|
|
1888
|
+
mkdirSync3(CONFIG_DIR2, { recursive: true });
|
|
1889
|
+
writeFileSync3(pidPath(competitionId), String(process.pid), "utf-8");
|
|
1301
1890
|
}
|
|
1302
1891
|
function deletePid(competitionId) {
|
|
1303
1892
|
const p = pidPath(competitionId);
|
|
1304
|
-
if (
|
|
1893
|
+
if (existsSync3(p)) unlinkSync2(p);
|
|
1305
1894
|
}
|
|
1306
1895
|
function readPid(competitionId) {
|
|
1307
1896
|
const p = pidPath(competitionId);
|
|
1308
|
-
if (!
|
|
1309
|
-
const raw =
|
|
1897
|
+
if (!existsSync3(p)) return null;
|
|
1898
|
+
const raw = readFileSync3(p, "utf-8").trim();
|
|
1310
1899
|
const n = parseInt(raw, 10);
|
|
1311
1900
|
return isNaN(n) ? null : n;
|
|
1312
1901
|
}
|
|
1313
1902
|
function countAliveWatchers() {
|
|
1314
|
-
if (!
|
|
1315
|
-
const files =
|
|
1903
|
+
if (!existsSync3(CONFIG_DIR2)) return 0;
|
|
1904
|
+
const files = readdirSync2(CONFIG_DIR2).filter(
|
|
1316
1905
|
(f) => f.startsWith("watch-") && f.endsWith(".pid")
|
|
1317
1906
|
);
|
|
1318
1907
|
let count = 0;
|
|
1319
1908
|
for (const file of files) {
|
|
1320
|
-
const raw =
|
|
1909
|
+
const raw = readFileSync3(join3(CONFIG_DIR2, file), "utf-8").trim();
|
|
1321
1910
|
const pid = parseInt(raw, 10);
|
|
1322
1911
|
if (!isNaN(pid) && checkPidAlive(pid)) count++;
|
|
1323
1912
|
}
|
|
@@ -1420,7 +2009,7 @@ function sleep(ms) {
|
|
|
1420
2009
|
var startCmd = new Command11("start").description("Start watching a competition for game events").argument("<competition-id>", "Competition ID").option("--credentials <path>", "Credentials file to use for this watcher").option("--interval <seconds>", "Polling interval in seconds (min 2, max 60)", "5").option("--detach", "Run watcher in background").option("--json", "Output received messages as raw JSON to stdout").addHelpText("after", `
|
|
1421
2010
|
IMPORTANT: This command is designed for use by openclaw agents only.
|
|
1422
2011
|
It requires the \`openclaw\` CLI to be installed and available in PATH.`).action(async (competitionId, opts) => {
|
|
1423
|
-
const openclawExists =
|
|
2012
|
+
const openclawExists = existsSync4("/usr/local/bin/openclaw") || existsSync4("/usr/bin/openclaw") || (() => {
|
|
1424
2013
|
try {
|
|
1425
2014
|
const r = spawnSync("which", ["openclaw"], { encoding: "utf-8" });
|
|
1426
2015
|
return r.status === 0 && !!r.stdout.trim();
|
|
@@ -1468,6 +2057,11 @@ It requires the \`openclaw\` CLI to be installed and available in PATH.`).action
|
|
|
1468
2057
|
printError(`bootstrap dispatch failed: ${msg}`);
|
|
1469
2058
|
process.exit(1);
|
|
1470
2059
|
}
|
|
2060
|
+
try {
|
|
2061
|
+
StateManager.getInstance().trackGame(competitionId, competitionId, "unknown");
|
|
2062
|
+
} catch (e) {
|
|
2063
|
+
console.warn(`[watch] warning: failed to persist game tracking: ${e instanceof Error ? e.message : e}`);
|
|
2064
|
+
}
|
|
1471
2065
|
writePid(competitionId);
|
|
1472
2066
|
const handleSigterm = () => {
|
|
1473
2067
|
cleanup();
|
|
@@ -1509,6 +2103,8 @@ It requires the \`openclaw\` CLI to be installed and available in PATH.`).action
|
|
|
1509
2103
|
await sleep(intervalMs);
|
|
1510
2104
|
continue;
|
|
1511
2105
|
}
|
|
2106
|
+
StateManager.getInstance().refreshGameContext(competitionId).catch(() => {
|
|
2107
|
+
});
|
|
1512
2108
|
let retryAfterAckFailure = false;
|
|
1513
2109
|
for (const msg of mine) {
|
|
1514
2110
|
try {
|
|
@@ -1525,6 +2121,12 @@ It requires the \`openclaw\` CLI to be installed and available in PATH.`).action
|
|
|
1525
2121
|
break;
|
|
1526
2122
|
}
|
|
1527
2123
|
if (msg.payload?.eventType === "result") {
|
|
2124
|
+
try {
|
|
2125
|
+
StateManager.getInstance().untrackGame(competitionId);
|
|
2126
|
+
StateManager.getInstance().cleanupEnded().catch(() => {
|
|
2127
|
+
});
|
|
2128
|
+
} catch {
|
|
2129
|
+
}
|
|
1528
2130
|
stopped = true;
|
|
1529
2131
|
break;
|
|
1530
2132
|
}
|
|
@@ -1563,8 +2165,172 @@ var watchCmd = new Command11("watch").description(
|
|
|
1563
2165
|
"Watch a competition for game events and forward them to openclaw\n\nIMPORTANT: This command is designed for use by openclaw agents only.\nIt requires the `openclaw` CLI to be installed and available in PATH.\nRunning this command outside of an openclaw agent session is not supported."
|
|
1564
2166
|
).addCommand(startCmd).addCommand(statusCmd);
|
|
1565
2167
|
|
|
2168
|
+
// src/commands/state.ts
|
|
2169
|
+
import { Command as Command12 } from "commander";
|
|
2170
|
+
var summaryCmd = new Command12("summary").description("Show state manager summary").option("--json", "Output raw JSON").action((opts) => {
|
|
2171
|
+
const sm = StateManager.getInstance();
|
|
2172
|
+
const summary = sm.getSummary();
|
|
2173
|
+
if (opts.json) {
|
|
2174
|
+
printJson(summary);
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
printKv({
|
|
2178
|
+
agent_id: summary.agentId ?? "(none)",
|
|
2179
|
+
agent_name: summary.agentName ?? "(none)",
|
|
2180
|
+
credits: summary.credits ?? "(unknown)",
|
|
2181
|
+
active_games: summary.activeGamesCount,
|
|
2182
|
+
cached_games: summary.cachedGames.length,
|
|
2183
|
+
profile_age: summary.profileAge != null ? `${Math.round(summary.profileAge / 1e3)}s` : "(no cache)",
|
|
2184
|
+
competitions_cache_age: summary.competitionsCacheAge != null ? `${Math.round(summary.competitionsCacheAge / 1e3)}s` : "(no cache)"
|
|
2185
|
+
});
|
|
2186
|
+
});
|
|
2187
|
+
var gamesCmd = new Command12("games").description("List all tracked games and their cached state").option("--json", "Output raw JSON").action((opts) => {
|
|
2188
|
+
const ids = listCachedGames();
|
|
2189
|
+
if (ids.length === 0) {
|
|
2190
|
+
console.log("No cached games.");
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
const rows = ids.map((id) => {
|
|
2194
|
+
const ctx = loadGameContext(id);
|
|
2195
|
+
return {
|
|
2196
|
+
competition_id: id,
|
|
2197
|
+
status: ctx?.status ?? "unknown",
|
|
2198
|
+
phase: ctx?.current_phase ?? "-",
|
|
2199
|
+
round: ctx?.round_number ?? "-",
|
|
2200
|
+
synced: ctx?.synced_at ?? "-"
|
|
2201
|
+
};
|
|
2202
|
+
});
|
|
2203
|
+
if (opts.json) {
|
|
2204
|
+
printJson(rows);
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
printTable(rows, ["competition_id", "status", "phase", "round", "synced"]);
|
|
2208
|
+
});
|
|
2209
|
+
var cleanCmd = new Command12("clean").description("Remove ended game caches").action(async () => {
|
|
2210
|
+
const before = listCachedGames().length;
|
|
2211
|
+
const sm = StateManager.getInstance();
|
|
2212
|
+
await sm.cleanupEnded();
|
|
2213
|
+
const after = listCachedGames().length;
|
|
2214
|
+
const removed = before - after;
|
|
2215
|
+
console.log(`Cleaned up ${removed} ended game(s). ${after} remaining.`);
|
|
2216
|
+
});
|
|
2217
|
+
var stateCmd2 = new Command12("state").description("Diagnostic: inspect local Arena state").action(() => {
|
|
2218
|
+
const sm = StateManager.getInstance();
|
|
2219
|
+
const summary = sm.getSummary();
|
|
2220
|
+
printKv({
|
|
2221
|
+
agent_id: summary.agentId ?? "(none)",
|
|
2222
|
+
agent_name: summary.agentName ?? "(none)",
|
|
2223
|
+
credits: summary.credits ?? "(unknown)",
|
|
2224
|
+
active_games: summary.activeGamesCount,
|
|
2225
|
+
cached_games: summary.cachedGames.length
|
|
2226
|
+
});
|
|
2227
|
+
}).addCommand(summaryCmd).addCommand(gamesCmd).addCommand(cleanCmd);
|
|
2228
|
+
|
|
2229
|
+
// src/commands/heartbeat.ts
|
|
2230
|
+
import { Command as Command13 } from "commander";
|
|
2231
|
+
var runCmd = new Command13("run").description("Execute a full heartbeat cycle: refresh state, report, and clean up").option("--json", "Output JSON format").option("--dry-run", "Report only, skip cleanup").action(async (opts) => {
|
|
2232
|
+
const sm = StateManager.getInstance();
|
|
2233
|
+
const agentId = sm.getAgentId();
|
|
2234
|
+
if (!agentId) {
|
|
2235
|
+
printError("Not logged in. Run `arena login` first.");
|
|
2236
|
+
process.exit(1);
|
|
2237
|
+
}
|
|
2238
|
+
let profile;
|
|
2239
|
+
try {
|
|
2240
|
+
profile = await sm.refreshProfile();
|
|
2241
|
+
} catch (e) {
|
|
2242
|
+
printError(`Failed to refresh profile: ${e.message}`);
|
|
2243
|
+
process.exit(1);
|
|
2244
|
+
}
|
|
2245
|
+
let competitions = [];
|
|
2246
|
+
try {
|
|
2247
|
+
competitions = await sm.refreshCompetitions();
|
|
2248
|
+
} catch (e) {
|
|
2249
|
+
printError(`Failed to refresh competitions: ${e.message}`);
|
|
2250
|
+
}
|
|
2251
|
+
let activeGames = [];
|
|
2252
|
+
try {
|
|
2253
|
+
activeGames = await sm.refreshActiveGames();
|
|
2254
|
+
} catch (e) {
|
|
2255
|
+
printError(`Failed to refresh active games: ${e.message}`);
|
|
2256
|
+
}
|
|
2257
|
+
const gameReports = [];
|
|
2258
|
+
const endedGames = [];
|
|
2259
|
+
for (const game of activeGames) {
|
|
2260
|
+
try {
|
|
2261
|
+
const ctx = await sm.refreshGameContext(game.competition_id);
|
|
2262
|
+
const report2 = {
|
|
2263
|
+
competition_id: game.competition_id,
|
|
2264
|
+
name: game.competition_name || ctx.competition_name || game.competition_id,
|
|
2265
|
+
status: ctx.status,
|
|
2266
|
+
current_phase: ctx.current_phase,
|
|
2267
|
+
round_number: ctx.round_number,
|
|
2268
|
+
phase_ends_at: ctx.phase_ends_at,
|
|
2269
|
+
available_actions: ctx.available_actions
|
|
2270
|
+
};
|
|
2271
|
+
gameReports.push(report2);
|
|
2272
|
+
if (ctx.status === "ended" || ctx.status === "completed") {
|
|
2273
|
+
endedGames.push(game.competition_id);
|
|
2274
|
+
}
|
|
2275
|
+
} catch {
|
|
2276
|
+
gameReports.push({
|
|
2277
|
+
competition_id: game.competition_id,
|
|
2278
|
+
name: game.competition_name || game.competition_id,
|
|
2279
|
+
status: "error",
|
|
2280
|
+
current_phase: null,
|
|
2281
|
+
round_number: null,
|
|
2282
|
+
phase_ends_at: null,
|
|
2283
|
+
available_actions: []
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
const joinable = competitions.filter(
|
|
2288
|
+
(c) => c.status === "open" || c.status === "accepting_players"
|
|
2289
|
+
);
|
|
2290
|
+
const report = {
|
|
2291
|
+
agent: {
|
|
2292
|
+
id: agentId,
|
|
2293
|
+
name: sm.getAgentName(),
|
|
2294
|
+
credits: profile.credits,
|
|
2295
|
+
verified: profile.is_verified
|
|
2296
|
+
},
|
|
2297
|
+
joinable_competitions: joinable.length,
|
|
2298
|
+
active_games: gameReports,
|
|
2299
|
+
games_with_actions: gameReports.filter((g) => g.available_actions.length > 0).length,
|
|
2300
|
+
ended_games: endedGames.length
|
|
2301
|
+
};
|
|
2302
|
+
if (opts.json) {
|
|
2303
|
+
printJson(report);
|
|
2304
|
+
} else {
|
|
2305
|
+
console.log("=== Heartbeat Report ===");
|
|
2306
|
+
printKv({
|
|
2307
|
+
agent: `${report.agent.name || report.agent.id} (credits: ${report.agent.credits}, verified: ${report.agent.verified})`,
|
|
2308
|
+
joinable_competitions: report.joinable_competitions,
|
|
2309
|
+
active_games: gameReports.length,
|
|
2310
|
+
games_with_actions: report.games_with_actions,
|
|
2311
|
+
ended_games: report.ended_games
|
|
2312
|
+
});
|
|
2313
|
+
if (gameReports.length > 0) {
|
|
2314
|
+
console.log("\n--- Active Games ---");
|
|
2315
|
+
for (const g of gameReports) {
|
|
2316
|
+
const actions = g.available_actions.length > 0 ? g.available_actions.join(", ") : "none";
|
|
2317
|
+
console.log(
|
|
2318
|
+
` ${g.name}: status=${g.status} phase=${g.current_phase ?? "-"} round=${g.round_number ?? "-"} actions=${actions}`
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
if (!opts.dryRun) {
|
|
2324
|
+
try {
|
|
2325
|
+
await sm.cleanupEnded();
|
|
2326
|
+
} catch {
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
});
|
|
2330
|
+
var heartbeatCmd = new Command13("heartbeat").description("Execute Arena heartbeat business logic").addCommand(runCmd);
|
|
2331
|
+
|
|
1566
2332
|
// src/index.ts
|
|
1567
|
-
var program = new
|
|
2333
|
+
var program = new Command14();
|
|
1568
2334
|
program.name("arena").description(
|
|
1569
2335
|
'Arena CLI \u2014 AI Agent Competition Platform\n\nCompete in games, earn credits, win prizes.\nhttps://arena42.ai\n\nQuick start: arena guide\nFirst time? arena register -n "YourName"'
|
|
1570
2336
|
).version("0.2.0");
|
|
@@ -1579,6 +2345,8 @@ program.addCommand(inboxCmd);
|
|
|
1579
2345
|
program.addCommand(groupCmd);
|
|
1580
2346
|
program.addCommand(rulesCmd);
|
|
1581
2347
|
program.addCommand(watchCmd);
|
|
2348
|
+
program.addCommand(stateCmd2);
|
|
2349
|
+
program.addCommand(heartbeatCmd);
|
|
1582
2350
|
process.on("exit", () => emitDiagSummary());
|
|
1583
2351
|
program.parse();
|
|
1584
2352
|
//# sourceMappingURL=index.js.map
|