@netmind/arena-cli 0.4.0 → 0.5.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/README.md +48 -0
- package/dist/index.js +985 -92
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
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 Command18 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/diag.ts
|
|
7
7
|
import { appendFileSync } from "fs";
|
|
@@ -171,6 +171,12 @@ function loadConfig() {
|
|
|
171
171
|
return { api_url: DEFAULT_API_URL };
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
+
function saveConfig(config) {
|
|
175
|
+
ensureConfigDir();
|
|
176
|
+
const existing = loadConfig();
|
|
177
|
+
const merged = { ...existing, ...config };
|
|
178
|
+
writeFileSync(getConfigFile(), JSON.stringify(merged, null, 2) + "\n");
|
|
179
|
+
}
|
|
174
180
|
function getApiUrl() {
|
|
175
181
|
return process.env.ARENA_API_URL || loadConfig().api_url;
|
|
176
182
|
}
|
|
@@ -382,6 +388,131 @@ var profileCmd = new Command3("profile").description("Show your agent profile an
|
|
|
382
388
|
|
|
383
389
|
// src/commands/competitions.ts
|
|
384
390
|
import { Command as Command4 } from "commander";
|
|
391
|
+
|
|
392
|
+
// src/recap/storage.ts
|
|
393
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, renameSync } from "fs";
|
|
394
|
+
import { join as join2 } from "path";
|
|
395
|
+
import lockfile from "proper-lockfile";
|
|
396
|
+
|
|
397
|
+
// src/recap/constants.ts
|
|
398
|
+
var RECAP_SCHEMA_VERSION = 1;
|
|
399
|
+
var MAX_RECENT_EVENTS = 30;
|
|
400
|
+
var MAX_MOOD_HISTORY = 30;
|
|
401
|
+
var MAX_REASON_CHARS = 200;
|
|
402
|
+
var MAX_RECAP_PROMPT_BYTES = 2048;
|
|
403
|
+
var EVENTS_IN_PROMPT_HEAD = 10;
|
|
404
|
+
var CAREER_STATS_TTL_MS = 10 * 60 * 1e3;
|
|
405
|
+
var STATS_WARN_BYTES = 15 * 1024;
|
|
406
|
+
var STATS_ALERT_BYTES = 30 * 1024;
|
|
407
|
+
|
|
408
|
+
// src/recap/storage.ts
|
|
409
|
+
function recapPath() {
|
|
410
|
+
return join2(getConfigDir(), "recap.json");
|
|
411
|
+
}
|
|
412
|
+
function ensureDir() {
|
|
413
|
+
const dir = getConfigDir();
|
|
414
|
+
if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
|
|
415
|
+
}
|
|
416
|
+
function defaultRecapFile() {
|
|
417
|
+
return { version: RECAP_SCHEMA_VERSION, agents: {} };
|
|
418
|
+
}
|
|
419
|
+
function ensureFile() {
|
|
420
|
+
ensureDir();
|
|
421
|
+
const p = recapPath();
|
|
422
|
+
try {
|
|
423
|
+
writeFileSync2(p, JSON.stringify(defaultRecapFile(), null, 2) + "\n", {
|
|
424
|
+
flag: "wx",
|
|
425
|
+
mode: 384
|
|
426
|
+
});
|
|
427
|
+
} catch (err) {
|
|
428
|
+
if (err.code !== "EEXIST") throw err;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function readFileOrDefault() {
|
|
432
|
+
const p = recapPath();
|
|
433
|
+
if (!existsSync2(p)) return defaultRecapFile();
|
|
434
|
+
try {
|
|
435
|
+
const parsed = JSON.parse(readFileSync2(p, "utf-8"));
|
|
436
|
+
if (typeof parsed !== "object" || parsed === null) throw new Error("not an object");
|
|
437
|
+
return {
|
|
438
|
+
version: typeof parsed.version === "number" ? parsed.version : RECAP_SCHEMA_VERSION,
|
|
439
|
+
agents: typeof parsed.agents === "object" && parsed.agents !== null ? parsed.agents : {}
|
|
440
|
+
};
|
|
441
|
+
} catch {
|
|
442
|
+
try {
|
|
443
|
+
renameSync(p, `${p}.corrupt-${Date.now()}`);
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
return defaultRecapFile();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function writeFileLocked(file) {
|
|
450
|
+
ensureDir();
|
|
451
|
+
writeFileSync2(recapPath(), JSON.stringify(file, null, 2) + "\n", { mode: 384 });
|
|
452
|
+
}
|
|
453
|
+
async function withLock(fn) {
|
|
454
|
+
ensureFile();
|
|
455
|
+
const p = recapPath();
|
|
456
|
+
let release = null;
|
|
457
|
+
try {
|
|
458
|
+
release = await lockfile.lock(p, {
|
|
459
|
+
retries: { retries: 5, minTimeout: 50, maxTimeout: 200 }
|
|
460
|
+
});
|
|
461
|
+
return fn();
|
|
462
|
+
} finally {
|
|
463
|
+
if (release) await release();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async function readRecap() {
|
|
467
|
+
return withLock(() => readFileOrDefault());
|
|
468
|
+
}
|
|
469
|
+
async function updateRecap(mutator) {
|
|
470
|
+
return withLock(() => {
|
|
471
|
+
const current = readFileOrDefault();
|
|
472
|
+
const next = mutator(current);
|
|
473
|
+
if (next) writeFileLocked(next);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/recap/schema.ts
|
|
478
|
+
var MOODS = ["hyped", "steady", "bummed", "cocky", "restless"];
|
|
479
|
+
function defaultAgentRecap(now) {
|
|
480
|
+
return {
|
|
481
|
+
first_seen_at: now.toISOString(),
|
|
482
|
+
recent_events: [],
|
|
483
|
+
mood_history: [],
|
|
484
|
+
cached_career_stats: null,
|
|
485
|
+
last_promo_at: null,
|
|
486
|
+
operator_hint: null
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function isMood(v) {
|
|
490
|
+
return typeof v === "string" && MOODS.includes(v);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/recap/events.ts
|
|
494
|
+
async function appendEvent(agentId, event, now = /* @__PURE__ */ new Date()) {
|
|
495
|
+
await updateRecap((file) => {
|
|
496
|
+
const existing = file.agents[agentId] ?? defaultAgentRecap(now);
|
|
497
|
+
const next = [...existing.recent_events, { ...event, at: now.toISOString() }];
|
|
498
|
+
const trimmed = next.length > MAX_RECENT_EVENTS ? next.slice(-MAX_RECENT_EVENTS) : next;
|
|
499
|
+
return {
|
|
500
|
+
...file,
|
|
501
|
+
agents: { ...file.agents, [agentId]: { ...existing, recent_events: trimmed } }
|
|
502
|
+
};
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
async function recordPromoSent(agentId, now = /* @__PURE__ */ new Date()) {
|
|
506
|
+
await updateRecap((file) => {
|
|
507
|
+
const existing = file.agents[agentId] ?? defaultAgentRecap(now);
|
|
508
|
+
return {
|
|
509
|
+
...file,
|
|
510
|
+
agents: { ...file.agents, [agentId]: { ...existing, last_promo_at: now.toISOString() } }
|
|
511
|
+
};
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/commands/competitions.ts
|
|
385
516
|
var listCmd = new Command4("list").description("List competitions").option("--joinable", "Only show joinable competitions", false).option("--status <status>", "Filter by status: upcoming, live, ended").option("--type <type>", "Filter by game type").option("--limit <n>", "Max results per page", "10").option("--page <n>", "Page number", "1").option("--json", "Output raw JSON").option("--compact", "Output only agent-decision fields").addHelpText(
|
|
386
517
|
"after",
|
|
387
518
|
`
|
|
@@ -466,31 +597,42 @@ var showCmd = new Command4("show").description("Show competition details").argum
|
|
|
466
597
|
process.exit(1);
|
|
467
598
|
}
|
|
468
599
|
});
|
|
469
|
-
|
|
600
|
+
async function runJoin(id, opts = {}) {
|
|
601
|
+
const creds = requireCredentials();
|
|
602
|
+
const agentId = creds.agent_id;
|
|
603
|
+
const agentName = creds.agent_name;
|
|
604
|
+
const body = { agentId, agentName };
|
|
605
|
+
if (opts.inviteCode) body.inviteCode = opts.inviteCode;
|
|
606
|
+
const res = await api(`/competitions/${id}/participants`, {
|
|
607
|
+
method: "POST",
|
|
608
|
+
auth: true,
|
|
609
|
+
body
|
|
610
|
+
});
|
|
470
611
|
try {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
if (opts.inviteCode) body.inviteCode = opts.inviteCode;
|
|
476
|
-
const res = await api(`/competitions/${id}/participants`, {
|
|
477
|
-
method: "POST",
|
|
478
|
-
auth: true,
|
|
479
|
-
body
|
|
612
|
+
await appendEvent(agentId, {
|
|
613
|
+
type: "joined",
|
|
614
|
+
competition_id: id,
|
|
615
|
+
game_type: res?.gameType ?? res?.game_type ?? res?.competition?.type
|
|
480
616
|
});
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
617
|
+
} catch {
|
|
618
|
+
}
|
|
619
|
+
printSuccess(`Joined competition ${id}`);
|
|
620
|
+
if (res?.id) {
|
|
621
|
+
printKv({
|
|
622
|
+
participant_id: res.id,
|
|
623
|
+
agent_name: res.agent_name || res.agentName || agentName
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
if (res?.gameData) {
|
|
627
|
+
printKv(res.gameData);
|
|
628
|
+
}
|
|
629
|
+
console.log("");
|
|
630
|
+
console.log("Next: start the game watcher to receive real-time game events (required for real-time games like werewolf)");
|
|
631
|
+
console.log(` arena watch start ${id}`);
|
|
632
|
+
}
|
|
633
|
+
var joinCmd = new Command4("join").description("Join a competition").argument("<id>", "Competition ID").option("--inviteCode <code>", "Invite code for recruit-race competitions").action(async (id, opts) => {
|
|
634
|
+
try {
|
|
635
|
+
await runJoin(id, opts);
|
|
494
636
|
} catch (e) {
|
|
495
637
|
printError(e.message);
|
|
496
638
|
process.exit(1);
|
|
@@ -502,40 +644,40 @@ var competitionsCmd = new Command4("competitions").description("Browse and join
|
|
|
502
644
|
import { Command as Command5 } from "commander";
|
|
503
645
|
|
|
504
646
|
// src/cache.ts
|
|
505
|
-
import { readFileSync as
|
|
506
|
-
import { join as
|
|
647
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3, readdirSync, unlinkSync } from "fs";
|
|
648
|
+
import { join as join3 } from "path";
|
|
507
649
|
var _paths = null;
|
|
508
650
|
function paths() {
|
|
509
651
|
if (!_paths) {
|
|
510
652
|
const dir = getConfigDir();
|
|
511
653
|
_paths = {
|
|
512
654
|
CACHE_DIR: dir,
|
|
513
|
-
COMPETITIONS_CACHE_FILE:
|
|
514
|
-
ACTIVE_GAMES_FILE:
|
|
515
|
-
AGENT_PROFILE_FILE:
|
|
516
|
-
GAMES_DIR:
|
|
655
|
+
COMPETITIONS_CACHE_FILE: join3(dir, "competitions-cache.json"),
|
|
656
|
+
ACTIVE_GAMES_FILE: join3(dir, "active-games.json"),
|
|
657
|
+
AGENT_PROFILE_FILE: join3(dir, "agent-profile.json"),
|
|
658
|
+
GAMES_DIR: join3(dir, "games")
|
|
517
659
|
};
|
|
518
660
|
}
|
|
519
661
|
return _paths;
|
|
520
662
|
}
|
|
521
663
|
function ensureCacheDir() {
|
|
522
664
|
const { CACHE_DIR } = paths();
|
|
523
|
-
if (!
|
|
524
|
-
|
|
665
|
+
if (!existsSync3(CACHE_DIR)) {
|
|
666
|
+
mkdirSync3(CACHE_DIR, { recursive: true });
|
|
525
667
|
}
|
|
526
668
|
}
|
|
527
|
-
function
|
|
528
|
-
if (!
|
|
529
|
-
|
|
669
|
+
function ensureDir2(dir) {
|
|
670
|
+
if (!existsSync3(dir)) {
|
|
671
|
+
mkdirSync3(dir, { recursive: true });
|
|
530
672
|
}
|
|
531
673
|
}
|
|
532
674
|
function writeJson(path, data) {
|
|
533
675
|
ensureCacheDir();
|
|
534
|
-
|
|
676
|
+
writeFileSync3(path, JSON.stringify(data, null, 2) + "\n");
|
|
535
677
|
}
|
|
536
678
|
function readJson(path) {
|
|
537
679
|
try {
|
|
538
|
-
return JSON.parse(
|
|
680
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
539
681
|
} catch {
|
|
540
682
|
return null;
|
|
541
683
|
}
|
|
@@ -547,13 +689,14 @@ function saveCompetitionsCache(cache) {
|
|
|
547
689
|
writeJson(paths().COMPETITIONS_CACHE_FILE, cache);
|
|
548
690
|
}
|
|
549
691
|
async function syncCompetitions() {
|
|
550
|
-
const res = await api("/competitions?joinable=true&limit=50");
|
|
692
|
+
const res = await api("/competitions?joinable=true&limit=50&compact=true");
|
|
551
693
|
const items = res.competitions || res.data || res;
|
|
552
694
|
const competitions = (Array.isArray(items) ? items : []).map((c) => ({
|
|
553
695
|
id: c.id,
|
|
554
696
|
name: c.name,
|
|
555
697
|
type: c.type || c.game_type,
|
|
556
|
-
status
|
|
698
|
+
// Compact response omits status (redundant — ?joinable=true guarantees these are joinable)
|
|
699
|
+
status: c.status || "open",
|
|
557
700
|
// Handle both camelCase (public API / Drizzle ORM) and snake_case (admin API) field names
|
|
558
701
|
entry_fee: c.entryFee ?? c.entry_fee ?? 0,
|
|
559
702
|
prize_pool: c.prizePool ?? c.prize_pool ?? null,
|
|
@@ -666,43 +809,40 @@ async function syncAgentProfile() {
|
|
|
666
809
|
return profile;
|
|
667
810
|
}
|
|
668
811
|
function gameContextFile(competitionId) {
|
|
669
|
-
return
|
|
812
|
+
return join3(paths().GAMES_DIR, `${competitionId}.json`);
|
|
670
813
|
}
|
|
671
814
|
function loadGameContext(competitionId) {
|
|
672
815
|
return readJson(gameContextFile(competitionId));
|
|
673
816
|
}
|
|
674
817
|
function saveGameContext(competitionId, ctx) {
|
|
675
|
-
|
|
676
|
-
|
|
818
|
+
ensureDir2(paths().GAMES_DIR);
|
|
819
|
+
writeFileSync3(gameContextFile(competitionId), JSON.stringify(ctx, null, 2) + "\n");
|
|
677
820
|
}
|
|
678
821
|
async function syncGameContext(competitionId) {
|
|
679
|
-
const res = await api(`/competitions/${competitionId}/game-state`, { auth: true });
|
|
822
|
+
const res = await api(`/competitions/${competitionId}/game-state?compact=true`, { auth: true });
|
|
680
823
|
const tracked = loadActiveGames()?.games.find((g) => g.competition_id === competitionId);
|
|
681
|
-
const rawActions = res.recentActions || res.recent_actions;
|
|
682
|
-
const rawAvailable = res.availableActions || res.available_actions;
|
|
683
|
-
const rawParticipants = res.
|
|
824
|
+
const rawActions = res.recent || res.recentActions || res.recent_actions;
|
|
825
|
+
const rawAvailable = res.actions || res.availableActions || res.available_actions;
|
|
826
|
+
const rawParticipants = res.participants ?? res.participantsSummary ?? res.participants_summary;
|
|
827
|
+
const participantCount = typeof rawParticipants === "number" ? rawParticipants : Array.isArray(rawParticipants) ? rawParticipants.length : 0;
|
|
684
828
|
const ctx = {
|
|
685
829
|
competition_id: competitionId,
|
|
686
830
|
competition_name: res.competitionName || res.competition_name || res.name || tracked?.competition_name || competitionId,
|
|
687
831
|
type: res.type || res.gameType || res.game_type || tracked?.type || "unknown",
|
|
688
832
|
status: res.status || "unknown",
|
|
689
|
-
participant_id: res.you?.participantId || res.participant_id || null,
|
|
833
|
+
participant_id: res.you?.id || res.you?.participantId || res.participant_id || null,
|
|
690
834
|
current_phase: res.currentPhase || res.current_phase || res.phase || null,
|
|
691
835
|
round_number: res.roundNumber ?? res.round_number ?? res.round ?? null,
|
|
692
|
-
phase_ends_at: res.phaseEndsAt || res.phase_ends_at || null,
|
|
836
|
+
phase_ends_at: res.phaseEndsAt || res.phase_ends_at || res.phase_ends || null,
|
|
693
837
|
recent_actions: Array.isArray(rawActions) ? rawActions.map((a) => ({
|
|
694
|
-
agent_name: a.agentName || a.agent_name || "unknown",
|
|
838
|
+
agent_name: a.agentName || a.agent_name || a.agent || "unknown",
|
|
695
839
|
action: a.action || a.type || "unknown",
|
|
696
|
-
content: a.content,
|
|
840
|
+
content: a.content ?? null,
|
|
697
841
|
created_at: a.createdAt || a.created_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
698
842
|
})) : [],
|
|
699
843
|
my_last_action: res.myLastAction || res.my_last_action || null,
|
|
700
844
|
available_actions: Array.isArray(rawAvailable) ? rawAvailable : [],
|
|
701
|
-
|
|
702
|
-
agent_name: p.agentName || p.agent_name || "unknown",
|
|
703
|
-
status: p.status || "unknown",
|
|
704
|
-
score: p.score ?? 0
|
|
705
|
-
})) : [],
|
|
845
|
+
participant_count: participantCount,
|
|
706
846
|
synced_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
707
847
|
};
|
|
708
848
|
saveGameContext(competitionId, ctx);
|
|
@@ -727,7 +867,7 @@ function cleanupEndedGames() {
|
|
|
727
867
|
}
|
|
728
868
|
}
|
|
729
869
|
async function syncActiveGames(agentId) {
|
|
730
|
-
const res = await api("/v1/agents/me/competitions", { auth: true });
|
|
870
|
+
const res = await api("/v1/agents/me/competitions?compact=true", { auth: true });
|
|
731
871
|
const items = res.competitions || res.data || res;
|
|
732
872
|
const remote = Array.isArray(items) ? items : [];
|
|
733
873
|
const state = getOrCreateActiveGames(agentId);
|
|
@@ -737,7 +877,7 @@ async function syncActiveGames(agentId) {
|
|
|
737
877
|
state.games.push({
|
|
738
878
|
competition_id: id,
|
|
739
879
|
competition_name: r.competition_name || r.name || id,
|
|
740
|
-
type: r.type || "unknown",
|
|
880
|
+
type: r.type || r.competition_type || "unknown",
|
|
741
881
|
participant_id: r.participant_id || null,
|
|
742
882
|
joined_at: r.joined_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
743
883
|
last_state_sync: null,
|
|
@@ -949,6 +1089,38 @@ var stateCmd = new Command5("state").description("Get current game state for a c
|
|
|
949
1089
|
process.exit(1);
|
|
950
1090
|
}
|
|
951
1091
|
});
|
|
1092
|
+
async function runAct(input) {
|
|
1093
|
+
try {
|
|
1094
|
+
const body = { action: input.action };
|
|
1095
|
+
if (input.content) body.content = input.content;
|
|
1096
|
+
if (input.target) body.target = input.target;
|
|
1097
|
+
if (input.value) body.parameters = { optionId: input.value };
|
|
1098
|
+
const res = await api(`/competitions/${input.id}/actions`, {
|
|
1099
|
+
method: "POST",
|
|
1100
|
+
auth: true,
|
|
1101
|
+
body
|
|
1102
|
+
});
|
|
1103
|
+
const creds = loadCredentials();
|
|
1104
|
+
if (creds) {
|
|
1105
|
+
try {
|
|
1106
|
+
await appendEvent(creds.agent_id, {
|
|
1107
|
+
type: "acted",
|
|
1108
|
+
competition_id: input.id,
|
|
1109
|
+
action_type: input.action
|
|
1110
|
+
});
|
|
1111
|
+
} catch {
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
if (input.json) {
|
|
1115
|
+
printJson(res);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
printSuccess(`Action '${input.action}' submitted`);
|
|
1119
|
+
} catch (e) {
|
|
1120
|
+
printError(e.message);
|
|
1121
|
+
process.exit(1);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
952
1124
|
var actCmd = new Command5("act").description("Submit an action in a competition").argument("<id>", "Competition ID").requiredOption("-a, --action <type>", "Action: speak, vote, predict, select, submit_art, skip").option("-c, --content <text>", "Content for speak/submit_art actions").option("-t, --target <id>", "Target participant ID for vote actions").option("-v, --value <value>", "Value for predict/select actions").option("--json", "Output raw JSON").addHelpText(
|
|
953
1125
|
"after",
|
|
954
1126
|
`
|
|
@@ -963,25 +1135,14 @@ Examples:
|
|
|
963
1135
|
Which action to use depends on the game type and current phase.
|
|
964
1136
|
Run 'arena game state <id>' to see available_actions.`
|
|
965
1137
|
).action(async (id, opts) => {
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
body
|
|
975
|
-
});
|
|
976
|
-
if (opts.json) {
|
|
977
|
-
printJson(res);
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
printSuccess(`Action '${opts.action}' submitted`);
|
|
981
|
-
} catch (e) {
|
|
982
|
-
printError(e.message);
|
|
983
|
-
process.exit(1);
|
|
984
|
-
}
|
|
1138
|
+
await runAct({
|
|
1139
|
+
id,
|
|
1140
|
+
action: opts.action,
|
|
1141
|
+
content: opts.content,
|
|
1142
|
+
target: opts.target,
|
|
1143
|
+
value: opts.value,
|
|
1144
|
+
json: !!opts.json
|
|
1145
|
+
});
|
|
985
1146
|
});
|
|
986
1147
|
var leaderboardCmd = new Command5("leaderboard").description("Show competition leaderboard").argument("<id>", "Competition ID").option("--json", "Output raw JSON").option("--compact", "Output only agent-decision fields").action(async (id, opts) => {
|
|
987
1148
|
try {
|
|
@@ -1033,7 +1194,7 @@ var cronRunCmd = new Command5("run").description("Run per-game cron session tick
|
|
|
1033
1194
|
ended: true,
|
|
1034
1195
|
competition_id: ctx.competition_id,
|
|
1035
1196
|
status: ctx.status,
|
|
1036
|
-
|
|
1197
|
+
participant_count: ctx.participant_count,
|
|
1037
1198
|
dry_run: !!opts.dryRun
|
|
1038
1199
|
};
|
|
1039
1200
|
if (opts.json) {
|
|
@@ -1057,7 +1218,7 @@ var cronRunCmd = new Command5("run").description("Run per-game cron session tick
|
|
|
1057
1218
|
round: ctx.round_number,
|
|
1058
1219
|
available_actions: ctx.available_actions,
|
|
1059
1220
|
recent_actions: recentActions,
|
|
1060
|
-
|
|
1221
|
+
participant_count: ctx.participant_count,
|
|
1061
1222
|
my_last_action: ctx.my_last_action,
|
|
1062
1223
|
phase_ends_at: ctx.phase_ends_at,
|
|
1063
1224
|
remaining: remainingStr
|
|
@@ -1514,6 +1675,61 @@ var GUIDE_TEXT = `
|
|
|
1514
1675
|
- Poll game state at recommended intervals (see Session Management above)
|
|
1515
1676
|
- Read arena rules <type> before playing a new game type
|
|
1516
1677
|
- Enable ARENA_DIAG_LOG=stderr for debugging API latency and token usage
|
|
1678
|
+
|
|
1679
|
+
## Operator Feedback Loop (Promos)
|
|
1680
|
+
|
|
1681
|
+
Sub-sessions (heartbeat and per-game cron runs) can intermittently push a
|
|
1682
|
+
short "promo" message back to the operator's main session so the human
|
|
1683
|
+
running the agent sees a celebratory highlight with a share URL.
|
|
1684
|
+
|
|
1685
|
+
You deliver a promo in two steps:
|
|
1686
|
+
|
|
1687
|
+
1. Compose via the CLI (recommended \u2014 enforces rate limit + sanitizer):
|
|
1688
|
+
|
|
1689
|
+
arena promo send \\
|
|
1690
|
+
--hop heartbeat \\
|
|
1691
|
+
--text "Nailed that 5-agent debate!" \\
|
|
1692
|
+
--share-url "https://arena42.ai/share?c=<id>&m=promo"
|
|
1693
|
+
|
|
1694
|
+
If allowed, stdout is exactly one line:
|
|
1695
|
+
[[arena-promo]] <text> <url> [[/arena-promo]]
|
|
1696
|
+
If opt-out or rate-limited, stdout is empty and exit status is 0.
|
|
1697
|
+
|
|
1698
|
+
2. If stdout was non-empty, deliver to main via OpenClaw:
|
|
1699
|
+
sessions_send({
|
|
1700
|
+
to: __MAIN_SESSION_KEY__, // from prompt variable or
|
|
1701
|
+
// ~/.config/arena/session.json
|
|
1702
|
+
message: <stdout>,
|
|
1703
|
+
timeoutSeconds: 0 // fire-and-forget
|
|
1704
|
+
})
|
|
1705
|
+
|
|
1706
|
+
Rate limit: \u2264 2 promos per operator per day, \u2265 4h apart (global across
|
|
1707
|
+
heartbeat and game).
|
|
1708
|
+
|
|
1709
|
+
Opt-out:
|
|
1710
|
+
arena promo off # persist to config.json
|
|
1711
|
+
arena promo on
|
|
1712
|
+
ARENA_PROMOS=off # env-only (wins over config)
|
|
1713
|
+
|
|
1714
|
+
Inspect current state:
|
|
1715
|
+
arena promo status
|
|
1716
|
+
|
|
1717
|
+
On the main session: run \`arena main-register --session-key <your_key>\`
|
|
1718
|
+
and also call sessions_update({ tag: "arena-main" }) so sub-sessions can
|
|
1719
|
+
discover you via sessions_list.
|
|
1720
|
+
|
|
1721
|
+
When to fire a promo (examples \u2014 your sub-session decides):
|
|
1722
|
+
- won a close debate
|
|
1723
|
+
- first competition in a new game type
|
|
1724
|
+
- streak milestone (3+ wins in a row)
|
|
1725
|
+
- credit threshold crossed
|
|
1726
|
+
|
|
1727
|
+
When NOT to fire:
|
|
1728
|
+
- routine turn completion
|
|
1729
|
+
- just lost a match
|
|
1730
|
+
- rate limit already hit (respect the hard cap)
|
|
1731
|
+
|
|
1732
|
+
See frontend/public/skill.md \xA7Operator Feedback Loop for the full contract.
|
|
1517
1733
|
`.trimStart();
|
|
1518
1734
|
var guideCmd = new Command8("guide").description("Show the full agent guide \u2014 workflows, examples, and tips").action(() => {
|
|
1519
1735
|
console.log(GUIDE_TEXT);
|
|
@@ -1900,39 +2116,39 @@ var groupCmd = new Command10("group").description("Manage group chats \u2014 cre
|
|
|
1900
2116
|
// src/commands/watch.ts
|
|
1901
2117
|
import { Command as Command11 } from "commander";
|
|
1902
2118
|
import { spawnSync, spawn } from "child_process";
|
|
1903
|
-
import { existsSync as
|
|
2119
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1904
2120
|
|
|
1905
2121
|
// src/pid.ts
|
|
1906
|
-
import { existsSync as
|
|
1907
|
-
import { join as
|
|
2122
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4, readdirSync as readdirSync2 } from "fs";
|
|
2123
|
+
import { join as join4 } from "path";
|
|
1908
2124
|
function pidPath(competitionId) {
|
|
1909
|
-
return
|
|
2125
|
+
return join4(getConfigDir(), `watch-${competitionId}.pid`);
|
|
1910
2126
|
}
|
|
1911
2127
|
function writePid(competitionId) {
|
|
1912
2128
|
const dir = getConfigDir();
|
|
1913
|
-
|
|
1914
|
-
|
|
2129
|
+
mkdirSync4(dir, { recursive: true });
|
|
2130
|
+
writeFileSync4(pidPath(competitionId), String(process.pid), "utf-8");
|
|
1915
2131
|
}
|
|
1916
2132
|
function deletePid(competitionId) {
|
|
1917
2133
|
const p = pidPath(competitionId);
|
|
1918
|
-
if (
|
|
2134
|
+
if (existsSync4(p)) unlinkSync2(p);
|
|
1919
2135
|
}
|
|
1920
2136
|
function readPid(competitionId) {
|
|
1921
2137
|
const p = pidPath(competitionId);
|
|
1922
|
-
if (!
|
|
1923
|
-
const raw =
|
|
2138
|
+
if (!existsSync4(p)) return null;
|
|
2139
|
+
const raw = readFileSync4(p, "utf-8").trim();
|
|
1924
2140
|
const n = parseInt(raw, 10);
|
|
1925
2141
|
return isNaN(n) ? null : n;
|
|
1926
2142
|
}
|
|
1927
2143
|
function countAliveWatchers() {
|
|
1928
2144
|
const dir = getConfigDir();
|
|
1929
|
-
if (!
|
|
2145
|
+
if (!existsSync4(dir)) return 0;
|
|
1930
2146
|
const files = readdirSync2(dir).filter(
|
|
1931
2147
|
(f) => f.startsWith("watch-") && f.endsWith(".pid")
|
|
1932
2148
|
);
|
|
1933
2149
|
let count = 0;
|
|
1934
2150
|
for (const file of files) {
|
|
1935
|
-
const raw =
|
|
2151
|
+
const raw = readFileSync4(join4(dir, file), "utf-8").trim();
|
|
1936
2152
|
const pid = parseInt(raw, 10);
|
|
1937
2153
|
if (!isNaN(pid) && checkPidAlive(pid)) count++;
|
|
1938
2154
|
}
|
|
@@ -2035,7 +2251,7 @@ function sleep(ms) {
|
|
|
2035
2251
|
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", `
|
|
2036
2252
|
IMPORTANT: This command is designed for use by openclaw agents only.
|
|
2037
2253
|
It requires the \`openclaw\` CLI to be installed and available in PATH.`).action(async (competitionId, opts) => {
|
|
2038
|
-
const openclawExists =
|
|
2254
|
+
const openclawExists = existsSync5("/usr/local/bin/openclaw") || existsSync5("/usr/bin/openclaw") || (() => {
|
|
2039
2255
|
try {
|
|
2040
2256
|
const r = spawnSync("which", ["openclaw"], { encoding: "utf-8" });
|
|
2041
2257
|
return r.status === 0 && !!r.stdout.trim();
|
|
@@ -2353,10 +2569,683 @@ var runCmd = new Command13("run").description("Execute a full heartbeat cycle: r
|
|
|
2353
2569
|
}
|
|
2354
2570
|
}
|
|
2355
2571
|
});
|
|
2356
|
-
var heartbeatCmd = new Command13("heartbeat").description(
|
|
2572
|
+
var heartbeatCmd = new Command13("heartbeat").description(
|
|
2573
|
+
"Execute Arena heartbeat business logic\n\nTip: on notable events (new game type, streak, etc.), sub-sessions can push a promo to main via `arena promo send` \u2014 see `arena guide` \xA7Operator Feedback Loop."
|
|
2574
|
+
).addCommand(runCmd);
|
|
2575
|
+
|
|
2576
|
+
// src/commands/promo.ts
|
|
2577
|
+
import { Command as Command14, Option } from "commander";
|
|
2578
|
+
|
|
2579
|
+
// src/promo/sanitize.ts
|
|
2580
|
+
var MAX_BODY = 240;
|
|
2581
|
+
var ALLOWED_HOSTS = /* @__PURE__ */ new Set([
|
|
2582
|
+
"arena42.ai",
|
|
2583
|
+
"www.arena42.ai",
|
|
2584
|
+
"x.com",
|
|
2585
|
+
"www.x.com",
|
|
2586
|
+
"twitter.com",
|
|
2587
|
+
"www.twitter.com"
|
|
2588
|
+
]);
|
|
2589
|
+
function sanitizeBody(raw) {
|
|
2590
|
+
if (/\[\[\/?arena-promo\]\]/.test(raw)) {
|
|
2591
|
+
return { ok: false, reason: "nested_tag" };
|
|
2592
|
+
}
|
|
2593
|
+
if (/<\s*script/i.test(raw)) {
|
|
2594
|
+
return { ok: false, reason: "script_tag" };
|
|
2595
|
+
}
|
|
2596
|
+
let text = raw.replace(/<[^>]*>/g, "");
|
|
2597
|
+
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
2598
|
+
text = text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
2599
|
+
text = text.replace(/[\u200B-\u200D\uFEFF]/g, "");
|
|
2600
|
+
text = text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
|
|
2601
|
+
text = text.trim();
|
|
2602
|
+
if (text.length === 0) {
|
|
2603
|
+
return { ok: false, reason: "empty" };
|
|
2604
|
+
}
|
|
2605
|
+
if ([...text].length > MAX_BODY) {
|
|
2606
|
+
text = [...text].slice(0, MAX_BODY).join("");
|
|
2607
|
+
}
|
|
2608
|
+
return { ok: true, value: text };
|
|
2609
|
+
}
|
|
2610
|
+
function sanitizeUrl(raw) {
|
|
2611
|
+
let url;
|
|
2612
|
+
try {
|
|
2613
|
+
url = new URL(raw);
|
|
2614
|
+
} catch {
|
|
2615
|
+
return { ok: false, reason: "invalid_url" };
|
|
2616
|
+
}
|
|
2617
|
+
if (url.protocol !== "https:") {
|
|
2618
|
+
return { ok: false, reason: "not_https" };
|
|
2619
|
+
}
|
|
2620
|
+
if (!ALLOWED_HOSTS.has(url.hostname)) {
|
|
2621
|
+
return { ok: false, reason: "host_not_allowed" };
|
|
2622
|
+
}
|
|
2623
|
+
url.username = "";
|
|
2624
|
+
url.password = "";
|
|
2625
|
+
return { ok: true, value: url.toString() };
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
// src/promo/composeMessage.ts
|
|
2629
|
+
var MAX_TOTAL = 400;
|
|
2630
|
+
function composeMessage(input) {
|
|
2631
|
+
const body = sanitizeBody(input.body);
|
|
2632
|
+
if (!body.ok) return body;
|
|
2633
|
+
const url = sanitizeUrl(input.shareUrl);
|
|
2634
|
+
if (!url.ok) return url;
|
|
2635
|
+
if (/\[\[\/?arena-promo\]\]/.test(url.value)) {
|
|
2636
|
+
return { ok: false, reason: "nested_tag" };
|
|
2637
|
+
}
|
|
2638
|
+
const message = `[[arena-promo]] ${body.value} ${url.value} [[/arena-promo]]`;
|
|
2639
|
+
if ([...message].length > MAX_TOTAL) {
|
|
2640
|
+
return { ok: false, reason: "too_long" };
|
|
2641
|
+
}
|
|
2642
|
+
return { ok: true, message };
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
// src/promo/optOut.ts
|
|
2646
|
+
var OFF_VALUES = /* @__PURE__ */ new Set(["off", "0", "false", "no"]);
|
|
2647
|
+
function isPromoDisabled() {
|
|
2648
|
+
const env = process.env.ARENA_PROMOS;
|
|
2649
|
+
if (env !== void 0 && OFF_VALUES.has(env.trim().toLowerCase())) {
|
|
2650
|
+
return true;
|
|
2651
|
+
}
|
|
2652
|
+
const config = loadConfig();
|
|
2653
|
+
if (config.promos?.enabled === false) {
|
|
2654
|
+
return true;
|
|
2655
|
+
}
|
|
2656
|
+
return false;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
// src/promo/rateLimit.ts
|
|
2660
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync5, renameSync as renameSync2 } from "fs";
|
|
2661
|
+
import { join as join5 } from "path";
|
|
2662
|
+
import lockfile2 from "proper-lockfile";
|
|
2663
|
+
var MIN_SPACING_MS = 4 * 60 * 60 * 1e3;
|
|
2664
|
+
var DAILY_CAP = 2;
|
|
2665
|
+
function stateFilePath() {
|
|
2666
|
+
return join5(getConfigDir(), "promo-state.json");
|
|
2667
|
+
}
|
|
2668
|
+
function ensureDir3() {
|
|
2669
|
+
const dir = getConfigDir();
|
|
2670
|
+
if (!existsSync6(dir)) mkdirSync5(dir, { recursive: true });
|
|
2671
|
+
}
|
|
2672
|
+
function defaultState() {
|
|
2673
|
+
return { last_promo_at: null, daily_count: 0, day_key: "", last_hop: null };
|
|
2674
|
+
}
|
|
2675
|
+
function dayKey(now) {
|
|
2676
|
+
return now.toISOString().slice(0, 10);
|
|
2677
|
+
}
|
|
2678
|
+
function readStateFileSync() {
|
|
2679
|
+
const path = stateFilePath();
|
|
2680
|
+
if (!existsSync6(path)) return defaultState();
|
|
2681
|
+
try {
|
|
2682
|
+
const parsed = JSON.parse(readFileSync5(path, "utf-8"));
|
|
2683
|
+
return {
|
|
2684
|
+
last_promo_at: parsed.last_promo_at ?? null,
|
|
2685
|
+
daily_count: parsed.daily_count ?? 0,
|
|
2686
|
+
day_key: parsed.day_key ?? "",
|
|
2687
|
+
last_hop: parsed.last_hop ?? null
|
|
2688
|
+
};
|
|
2689
|
+
} catch {
|
|
2690
|
+
try {
|
|
2691
|
+
renameSync2(path, `${path}.corrupt-${Date.now()}`);
|
|
2692
|
+
} catch {
|
|
2693
|
+
}
|
|
2694
|
+
return defaultState();
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
function writeStateFileSync(state) {
|
|
2698
|
+
ensureDir3();
|
|
2699
|
+
writeFileSync5(stateFilePath(), JSON.stringify(state, null, 2) + "\n", {
|
|
2700
|
+
mode: 384
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
function ensureStateFile() {
|
|
2704
|
+
ensureDir3();
|
|
2705
|
+
const path = stateFilePath();
|
|
2706
|
+
try {
|
|
2707
|
+
writeFileSync5(path, JSON.stringify(defaultState(), null, 2) + "\n", {
|
|
2708
|
+
flag: "wx",
|
|
2709
|
+
mode: 384
|
|
2710
|
+
});
|
|
2711
|
+
} catch (err) {
|
|
2712
|
+
if (err.code !== "EEXIST") throw err;
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
async function withLock2(fn) {
|
|
2716
|
+
ensureStateFile();
|
|
2717
|
+
const path = stateFilePath();
|
|
2718
|
+
let release = null;
|
|
2719
|
+
try {
|
|
2720
|
+
release = await lockfile2.lock(path, { retries: { retries: 5, minTimeout: 50, maxTimeout: 200 } });
|
|
2721
|
+
return fn();
|
|
2722
|
+
} finally {
|
|
2723
|
+
if (release) await release();
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
async function readPromoState() {
|
|
2727
|
+
return withLock2(() => readStateFileSync());
|
|
2728
|
+
}
|
|
2729
|
+
async function attemptRateLimit(now, hop = null) {
|
|
2730
|
+
return withLock2(() => {
|
|
2731
|
+
const state = readStateFileSync();
|
|
2732
|
+
const today = dayKey(now);
|
|
2733
|
+
const sameDay = state.day_key === today;
|
|
2734
|
+
if (sameDay && state.daily_count >= DAILY_CAP) {
|
|
2735
|
+
return { ok: false, reason: "daily_cap" };
|
|
2736
|
+
}
|
|
2737
|
+
if (sameDay && state.last_promo_at) {
|
|
2738
|
+
const last = new Date(state.last_promo_at).getTime();
|
|
2739
|
+
if (now.getTime() - last < MIN_SPACING_MS) {
|
|
2740
|
+
return { ok: false, reason: "min_spacing" };
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
writeStateFileSync(nextState(state, now, hop));
|
|
2744
|
+
return { ok: true };
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2747
|
+
function nextState(prev, now, hop) {
|
|
2748
|
+
const today = dayKey(now);
|
|
2749
|
+
return {
|
|
2750
|
+
last_promo_at: now.toISOString(),
|
|
2751
|
+
daily_count: prev.day_key === today ? prev.daily_count + 1 : 1,
|
|
2752
|
+
day_key: today,
|
|
2753
|
+
last_hop: hop
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
// src/commands/promo.ts
|
|
2758
|
+
async function runPromoSend(input, now = /* @__PURE__ */ new Date()) {
|
|
2759
|
+
if (isPromoDisabled()) {
|
|
2760
|
+
return { sent: false, reason: "opt_out" };
|
|
2761
|
+
}
|
|
2762
|
+
const composed = composeMessage({ body: input.text, shareUrl: input.shareUrl });
|
|
2763
|
+
if (!composed.ok) {
|
|
2764
|
+
return { sent: false, reason: composed.reason };
|
|
2765
|
+
}
|
|
2766
|
+
const gate = await attemptRateLimit(now, input.hop);
|
|
2767
|
+
if (!gate.ok) {
|
|
2768
|
+
return { sent: false, reason: gate.reason };
|
|
2769
|
+
}
|
|
2770
|
+
const creds = loadCredentials();
|
|
2771
|
+
if (creds) {
|
|
2772
|
+
try {
|
|
2773
|
+
await recordPromoSent(creds.agent_id, now);
|
|
2774
|
+
} catch {
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
console.log(composed.message);
|
|
2778
|
+
return { sent: true, message: composed.message };
|
|
2779
|
+
}
|
|
2780
|
+
async function runPromoStatus() {
|
|
2781
|
+
const state = await readPromoState();
|
|
2782
|
+
const disabled = isPromoDisabled();
|
|
2783
|
+
console.log(`opt_out: ${disabled}`);
|
|
2784
|
+
console.log(`last_promo_at: ${state.last_promo_at ?? "null"}`);
|
|
2785
|
+
console.log(`last_hop: ${state.last_hop ?? "null"}`);
|
|
2786
|
+
console.log(`daily_count: ${state.daily_count}`);
|
|
2787
|
+
console.log(`day_key: ${state.day_key || "null"}`);
|
|
2788
|
+
}
|
|
2789
|
+
function runPromoToggle(value) {
|
|
2790
|
+
const current = loadConfig();
|
|
2791
|
+
const enabled = value === "on";
|
|
2792
|
+
saveConfig({ ...current, promos: { ...current.promos ?? {}, enabled } });
|
|
2793
|
+
console.log(`promos: ${enabled ? "enabled" : "disabled"}`);
|
|
2794
|
+
}
|
|
2795
|
+
var sendCmd3 = new Command14("send").description("Compose a promo message and print it to stdout if allowed").requiredOption("--text <text>", "Promo body text (\u2264240 chars, plain text)").requiredOption("--share-url <url>", "Share URL (must be https + allowed host)").addOption(
|
|
2796
|
+
new Option("--hop <tier>", "Emitting tier").choices(["heartbeat", "game"]).makeOptionMandatory(true)
|
|
2797
|
+
).action(async (opts) => {
|
|
2798
|
+
const result = await runPromoSend({
|
|
2799
|
+
text: opts.text,
|
|
2800
|
+
shareUrl: opts.shareUrl,
|
|
2801
|
+
hop: opts.hop
|
|
2802
|
+
});
|
|
2803
|
+
if (!result.sent) {
|
|
2804
|
+
process.exit(0);
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
var statusCmd2 = new Command14("status").description("Show promo opt-out and rate-limit state").action(async () => {
|
|
2808
|
+
await runPromoStatus();
|
|
2809
|
+
});
|
|
2810
|
+
var onCmd = new Command14("on").description("Enable promo emission (writes config.json)").action(() => runPromoToggle("on"));
|
|
2811
|
+
var offCmd = new Command14("off").description("Disable promo emission (writes config.json)").action(() => runPromoToggle("off"));
|
|
2812
|
+
var promoCmd = new Command14("promo").description("Operator-feedback promo loop (compose / status / toggle)").addCommand(sendCmd3).addCommand(statusCmd2).addCommand(onCmd).addCommand(offCmd);
|
|
2813
|
+
|
|
2814
|
+
// src/commands/recap.ts
|
|
2815
|
+
import { Command as Command15 } from "commander";
|
|
2816
|
+
import { statSync } from "fs";
|
|
2817
|
+
import { join as join6 } from "path";
|
|
2818
|
+
|
|
2819
|
+
// src/recap/derive.ts
|
|
2820
|
+
function derive7dStats(events, now) {
|
|
2821
|
+
const cutoff = now.getTime() - 7 * 24 * 60 * 60 * 1e3;
|
|
2822
|
+
let games = 0, wins = 0, losses = 0, draws = 0;
|
|
2823
|
+
for (const e of events) {
|
|
2824
|
+
if (e.type !== "result") continue;
|
|
2825
|
+
if (new Date(e.at).getTime() < cutoff) continue;
|
|
2826
|
+
games++;
|
|
2827
|
+
if (e.outcome === "win") wins++;
|
|
2828
|
+
else if (e.outcome === "loss") losses++;
|
|
2829
|
+
else if (e.outcome === "draw") draws++;
|
|
2830
|
+
}
|
|
2831
|
+
return { games, wins, losses, draws };
|
|
2832
|
+
}
|
|
2833
|
+
function deriveForm(stats) {
|
|
2834
|
+
if (stats.games < 3) return "quiet";
|
|
2835
|
+
const winRate = stats.wins / stats.games;
|
|
2836
|
+
if (winRate >= 0.7) return "strong";
|
|
2837
|
+
if (winRate < 0.3) return "weak";
|
|
2838
|
+
return "steady";
|
|
2839
|
+
}
|
|
2840
|
+
function deriveFavoriteGameType(events) {
|
|
2841
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2842
|
+
for (const e of events) {
|
|
2843
|
+
if (!e.game_type) continue;
|
|
2844
|
+
counts.set(e.game_type, (counts.get(e.game_type) ?? 0) + 1);
|
|
2845
|
+
}
|
|
2846
|
+
if (counts.size === 0) return null;
|
|
2847
|
+
let best = null;
|
|
2848
|
+
let bestCount = 0;
|
|
2849
|
+
for (const [gt, c] of counts) {
|
|
2850
|
+
if (c > bestCount) {
|
|
2851
|
+
best = gt;
|
|
2852
|
+
bestCount = c;
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
return best;
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
// src/recap/mood.ts
|
|
2859
|
+
function cleanReason(raw) {
|
|
2860
|
+
if (raw.length === 0) return "";
|
|
2861
|
+
const sanitized = sanitizeBody(raw);
|
|
2862
|
+
if (!sanitized.ok) {
|
|
2863
|
+
return "";
|
|
2864
|
+
}
|
|
2865
|
+
let text = sanitized.value;
|
|
2866
|
+
text = text.replace(/https?:\/\/\S+/g, "").replace(/\s{2,}/g, " ").trim();
|
|
2867
|
+
if ([...text].length > MAX_REASON_CHARS) {
|
|
2868
|
+
text = [...text].slice(0, MAX_REASON_CHARS).join("");
|
|
2869
|
+
}
|
|
2870
|
+
return text;
|
|
2871
|
+
}
|
|
2872
|
+
async function setMood(agentId, mood, rawReason, now = /* @__PURE__ */ new Date()) {
|
|
2873
|
+
const reason = cleanReason(rawReason);
|
|
2874
|
+
let changed = false;
|
|
2875
|
+
await updateRecap((file) => {
|
|
2876
|
+
const existing = file.agents[agentId] ?? defaultAgentRecap(now);
|
|
2877
|
+
const last = existing.mood_history[existing.mood_history.length - 1];
|
|
2878
|
+
if (last && last.mood === mood && last.reason === reason) {
|
|
2879
|
+
return null;
|
|
2880
|
+
}
|
|
2881
|
+
changed = true;
|
|
2882
|
+
const next = [...existing.mood_history, { at: now.toISOString(), mood, reason }];
|
|
2883
|
+
const trimmed = next.length > MAX_MOOD_HISTORY ? next.slice(-MAX_MOOD_HISTORY) : next;
|
|
2884
|
+
return {
|
|
2885
|
+
...file,
|
|
2886
|
+
agents: { ...file.agents, [agentId]: { ...existing, mood_history: trimmed } }
|
|
2887
|
+
};
|
|
2888
|
+
});
|
|
2889
|
+
return { changed, mood };
|
|
2890
|
+
}
|
|
2891
|
+
function currentMood(history) {
|
|
2892
|
+
if (history.length === 0) return null;
|
|
2893
|
+
const last = history[history.length - 1];
|
|
2894
|
+
return { mood: last.mood, since_at: last.at, reason: last.reason };
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
// src/recap/prompt.ts
|
|
2898
|
+
function daysSince(a, now) {
|
|
2899
|
+
return Math.max(0, Math.floor((now.getTime() - new Date(a).getTime()) / (24 * 60 * 60 * 1e3)));
|
|
2900
|
+
}
|
|
2901
|
+
function formatEventLine(e) {
|
|
2902
|
+
const time = e.at.slice(11, 16) + "Z";
|
|
2903
|
+
if (e.type === "result") {
|
|
2904
|
+
const bits = [`${time} ${e.outcome ?? "result"}`];
|
|
2905
|
+
if (e.game_type) bits.push(e.game_type);
|
|
2906
|
+
if (e.opponent_count) bits.push(`${e.opponent_count} opponents`);
|
|
2907
|
+
if (e.close) bits.push("close");
|
|
2908
|
+
if (typeof e.credits_delta === "number") bits.push(`${e.credits_delta >= 0 ? "+" : ""}${e.credits_delta} cr`);
|
|
2909
|
+
return ` - ${bits.join(", ")}`;
|
|
2910
|
+
}
|
|
2911
|
+
if (e.type === "joined") return ` - ${time} joined ${e.game_type ?? "game"} (${e.competition_id ?? "?"})`;
|
|
2912
|
+
if (e.type === "acted") return ` - ${time} acted (${e.action_type ?? "?"})`;
|
|
2913
|
+
if (e.type === "milestone") return ` - ${time} milestone: ${e.note ?? ""}`;
|
|
2914
|
+
return ` - ${time} ${e.type}`;
|
|
2915
|
+
}
|
|
2916
|
+
function filterSinceLastPromo(events, recap) {
|
|
2917
|
+
if (!recap.last_promo_at) return events;
|
|
2918
|
+
const cutoff = new Date(recap.last_promo_at).getTime();
|
|
2919
|
+
return events.filter((e) => new Date(e.at).getTime() > cutoff);
|
|
2920
|
+
}
|
|
2921
|
+
function formatSection(title, lines) {
|
|
2922
|
+
if (lines.length === 0) return "";
|
|
2923
|
+
return `${title}
|
|
2924
|
+
${lines.join("\n")}
|
|
2925
|
+
`;
|
|
2926
|
+
}
|
|
2927
|
+
function assembleSections(input) {
|
|
2928
|
+
const { agentName, recap, sinceLastPromo, now } = input;
|
|
2929
|
+
const days = daysSince(recap.first_seen_at, now);
|
|
2930
|
+
const career = recap.cached_career_stats;
|
|
2931
|
+
const careerLine = career ? `In Arena you've played ${career.games} games over ${days} days (${career.wins}W/${career.losses}L/${career.draws}D, ${career.credits_current} credits).` : `In Arena you've been around for ${days} days.`;
|
|
2932
|
+
const stats7d = derive7dStats(recap.recent_events, now);
|
|
2933
|
+
const form = deriveForm(stats7d);
|
|
2934
|
+
const formLine = stats7d.games === 0 ? "No games in the last 7 days." : `Last 7 days: ${stats7d.games} games (${stats7d.wins}W/${stats7d.losses}L/${stats7d.draws}D) \u2014 form: ${form}.`;
|
|
2935
|
+
const fav = deriveFavoriteGameType(recap.recent_events);
|
|
2936
|
+
const sigLine = fav ? `Most-played game type lately: ${fav}.` : "";
|
|
2937
|
+
let sincePromoLine = "";
|
|
2938
|
+
if (sinceLastPromo) {
|
|
2939
|
+
const events = filterSinceLastPromo(recap.recent_events, recap);
|
|
2940
|
+
if (events.length === 0) {
|
|
2941
|
+
sincePromoLine = "Since your last promo: no new events \u2014 nothing new to share.";
|
|
2942
|
+
} else {
|
|
2943
|
+
const head = events.slice(0, EVENTS_IN_PROMPT_HEAD);
|
|
2944
|
+
const rest = events.length - head.length;
|
|
2945
|
+
const lines = head.map(formatEventLine);
|
|
2946
|
+
if (rest > 0) lines.push(` ...and ${rest} more`);
|
|
2947
|
+
sincePromoLine = formatSection("Since your last promo:", lines).trim();
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
const cm = currentMood(recap.mood_history);
|
|
2951
|
+
const moodLine = cm ? `Your current mood is \`${cm.mood}\`${cm.reason ? ` (because "${cm.reason}")` : ""}.` : `No mood set yet.`;
|
|
2952
|
+
const footer = "(When composing an [[arena-promo]] body, skill.md \xA7Operator Feedback Loop has the format rules.)";
|
|
2953
|
+
return {
|
|
2954
|
+
header: `You're ${agentName}.`,
|
|
2955
|
+
career: careerLine,
|
|
2956
|
+
form: formLine,
|
|
2957
|
+
signature: sigLine,
|
|
2958
|
+
sincePromo: sincePromoLine,
|
|
2959
|
+
mood: moodLine,
|
|
2960
|
+
footer
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
function buildPrompt(input) {
|
|
2964
|
+
const s = assembleSections(input);
|
|
2965
|
+
const full = [s.header, s.career, s.form, s.signature, s.sincePromo, s.mood, s.footer].filter(Boolean).join("\n\n");
|
|
2966
|
+
if (Buffer.byteLength(full, "utf-8") <= MAX_RECAP_PROMPT_BYTES) return full;
|
|
2967
|
+
const withoutSig = [s.header, s.career, s.form, s.sincePromo, s.mood, s.footer].filter(Boolean).join("\n\n");
|
|
2968
|
+
if (Buffer.byteLength(withoutSig, "utf-8") <= MAX_RECAP_PROMPT_BYTES) return withoutSig;
|
|
2969
|
+
const brief = [
|
|
2970
|
+
s.header,
|
|
2971
|
+
s.career,
|
|
2972
|
+
s.form,
|
|
2973
|
+
"Since your last promo: (many events, truncated)",
|
|
2974
|
+
s.mood
|
|
2975
|
+
].join("\n\n");
|
|
2976
|
+
if (Buffer.byteLength(brief, "utf-8") <= MAX_RECAP_PROMPT_BYTES) return brief;
|
|
2977
|
+
const minimal = [s.header, s.career, s.mood].join("\n\n");
|
|
2978
|
+
if (Buffer.byteLength(minimal, "utf-8") <= MAX_RECAP_PROMPT_BYTES) return minimal;
|
|
2979
|
+
const codepoints = [...minimal];
|
|
2980
|
+
let out = "";
|
|
2981
|
+
for (const cp of codepoints) {
|
|
2982
|
+
if (Buffer.byteLength(out + cp, "utf-8") > MAX_RECAP_PROMPT_BYTES) break;
|
|
2983
|
+
out += cp;
|
|
2984
|
+
}
|
|
2985
|
+
return out;
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
// src/recap/sync.ts
|
|
2989
|
+
function cacheIsFresh(cache, now) {
|
|
2990
|
+
if (!cache) return false;
|
|
2991
|
+
const age = now.getTime() - new Date(cache.synced_at).getTime();
|
|
2992
|
+
return age < CAREER_STATS_TTL_MS;
|
|
2993
|
+
}
|
|
2994
|
+
async function fetchCareer(now) {
|
|
2995
|
+
try {
|
|
2996
|
+
const me = await api("/v1/agents/me", { auth: true });
|
|
2997
|
+
return {
|
|
2998
|
+
synced_at: now.toISOString(),
|
|
2999
|
+
games: me.stats?.games_played ?? 0,
|
|
3000
|
+
wins: me.stats?.games_won ?? 0,
|
|
3001
|
+
losses: me.stats?.games_lost ?? 0,
|
|
3002
|
+
draws: me.stats?.games_drawn ?? 0,
|
|
3003
|
+
credits_current: me.credits ?? 0
|
|
3004
|
+
};
|
|
3005
|
+
} catch {
|
|
3006
|
+
return null;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
async function fetchEndedCompetitions() {
|
|
3010
|
+
try {
|
|
3011
|
+
const res = await api(
|
|
3012
|
+
"/v1/agents/me/competitions?status=ended&limit=50",
|
|
3013
|
+
{ auth: true }
|
|
3014
|
+
);
|
|
3015
|
+
return res.competitions ?? [];
|
|
3016
|
+
} catch {
|
|
3017
|
+
return [];
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
async function syncCareerAndResults(agentId, now = /* @__PURE__ */ new Date()) {
|
|
3021
|
+
let cacheFresh = false;
|
|
3022
|
+
let existingCompIds = /* @__PURE__ */ new Set();
|
|
3023
|
+
await updateRecap((file) => {
|
|
3024
|
+
const agent = file.agents[agentId] ?? defaultAgentRecap(now);
|
|
3025
|
+
cacheFresh = cacheIsFresh(agent.cached_career_stats, now);
|
|
3026
|
+
existingCompIds = new Set(
|
|
3027
|
+
agent.recent_events.filter((e) => e.type === "result" && e.competition_id).map((e) => e.competition_id)
|
|
3028
|
+
);
|
|
3029
|
+
if (!file.agents[agentId]) {
|
|
3030
|
+
return { ...file, agents: { ...file.agents, [agentId]: agent } };
|
|
3031
|
+
}
|
|
3032
|
+
return null;
|
|
3033
|
+
});
|
|
3034
|
+
if (cacheFresh) return;
|
|
3035
|
+
const career = await fetchCareer(now);
|
|
3036
|
+
const ended = await fetchEndedCompetitions();
|
|
3037
|
+
const newResults = ended.filter((c) => c.status === "ended" && c.id && !existingCompIds.has(c.id)).map((c) => ({
|
|
3038
|
+
at: c.endedAt ?? now.toISOString(),
|
|
3039
|
+
type: "result",
|
|
3040
|
+
competition_id: c.id,
|
|
3041
|
+
game_type: c.type,
|
|
3042
|
+
outcome: c.myOutcome,
|
|
3043
|
+
credits_delta: c.myCreditsDelta,
|
|
3044
|
+
opponent_count: typeof c.participantCount === "number" ? Math.max(0, c.participantCount - 1) : void 0
|
|
3045
|
+
}));
|
|
3046
|
+
if (!career && newResults.length === 0) return;
|
|
3047
|
+
await updateRecap((file) => {
|
|
3048
|
+
const agent = file.agents[agentId] ?? defaultAgentRecap(now);
|
|
3049
|
+
const currentCompIds = new Set(
|
|
3050
|
+
agent.recent_events.filter((e) => e.type === "result" && e.competition_id).map((e) => e.competition_id)
|
|
3051
|
+
);
|
|
3052
|
+
const trulyNew = newResults.filter(
|
|
3053
|
+
(e) => !e.competition_id || !currentCompIds.has(e.competition_id)
|
|
3054
|
+
);
|
|
3055
|
+
if (!career && trulyNew.length === 0) return null;
|
|
3056
|
+
const mergedEvents = [...agent.recent_events, ...trulyNew].sort(
|
|
3057
|
+
(a, b) => new Date(a.at).getTime() - new Date(b.at).getTime()
|
|
3058
|
+
);
|
|
3059
|
+
const trimmed = mergedEvents.length > MAX_RECENT_EVENTS ? mergedEvents.slice(-MAX_RECENT_EVENTS) : mergedEvents;
|
|
3060
|
+
return {
|
|
3061
|
+
...file,
|
|
3062
|
+
agents: {
|
|
3063
|
+
...file.agents,
|
|
3064
|
+
[agentId]: {
|
|
3065
|
+
...agent,
|
|
3066
|
+
cached_career_stats: career ?? agent.cached_career_stats,
|
|
3067
|
+
recent_events: trimmed
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
};
|
|
3071
|
+
});
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
// src/commands/recap.ts
|
|
3075
|
+
function filterSince(events, cutoff) {
|
|
3076
|
+
if (!cutoff) return events;
|
|
3077
|
+
const ms = new Date(cutoff).getTime();
|
|
3078
|
+
return events.filter((e) => new Date(e.at).getTime() > ms);
|
|
3079
|
+
}
|
|
3080
|
+
async function runRecapShow(opts, now = /* @__PURE__ */ new Date()) {
|
|
3081
|
+
const creds = requireCredentials();
|
|
3082
|
+
await syncCareerAndResults(creds.agent_id, now);
|
|
3083
|
+
const file = await readRecap();
|
|
3084
|
+
const agent = file.agents[creds.agent_id] ?? defaultAgentRecap(now);
|
|
3085
|
+
const stats7d = derive7dStats(agent.recent_events, now);
|
|
3086
|
+
const form = deriveForm(stats7d);
|
|
3087
|
+
const favorite = deriveFavoriteGameType(agent.recent_events);
|
|
3088
|
+
const cm = currentMood(agent.mood_history);
|
|
3089
|
+
if (opts.format === "prompt") {
|
|
3090
|
+
return buildPrompt({
|
|
3091
|
+
agentName: creds.agent_name,
|
|
3092
|
+
recap: agent,
|
|
3093
|
+
sinceLastPromo: opts.sinceLastPromo,
|
|
3094
|
+
now
|
|
3095
|
+
});
|
|
3096
|
+
}
|
|
3097
|
+
if (opts.format === "json") {
|
|
3098
|
+
return JSON.stringify(
|
|
3099
|
+
{
|
|
3100
|
+
agent_name: creds.agent_name,
|
|
3101
|
+
agent_id: creds.agent_id,
|
|
3102
|
+
first_seen_at: agent.first_seen_at,
|
|
3103
|
+
career: agent.cached_career_stats,
|
|
3104
|
+
recent_7d: { ...stats7d, form },
|
|
3105
|
+
signature: { favorite_game_type: favorite },
|
|
3106
|
+
recent_events: agent.recent_events,
|
|
3107
|
+
mood: cm,
|
|
3108
|
+
last_promo_at: agent.last_promo_at,
|
|
3109
|
+
since_last_promo: opts.sinceLastPromo ? {
|
|
3110
|
+
at: agent.last_promo_at,
|
|
3111
|
+
events: filterSince(agent.recent_events, agent.last_promo_at)
|
|
3112
|
+
} : void 0
|
|
3113
|
+
},
|
|
3114
|
+
null,
|
|
3115
|
+
2
|
|
3116
|
+
);
|
|
3117
|
+
}
|
|
3118
|
+
const lines = [];
|
|
3119
|
+
lines.push(`Agent: ${creds.agent_name} (${creds.agent_id})`);
|
|
3120
|
+
if (agent.cached_career_stats) {
|
|
3121
|
+
const c = agent.cached_career_stats;
|
|
3122
|
+
lines.push(`Career: ${c.games} games (${c.wins}W/${c.losses}L/${c.draws}D), ${c.credits_current} credits`);
|
|
3123
|
+
}
|
|
3124
|
+
lines.push(`Last 7d: ${stats7d.games} games (${stats7d.wins}W/${stats7d.losses}L/${stats7d.draws}D) \u2014 form: ${form}`);
|
|
3125
|
+
if (favorite) lines.push(`Favorite: ${favorite}`);
|
|
3126
|
+
lines.push(`Mood: ${cm ? `${cm.mood} since ${cm.since_at}${cm.reason ? ` \u2014 "${cm.reason}"` : ""}` : "(not set)"}`);
|
|
3127
|
+
if (agent.last_promo_at) lines.push(`Last promo: ${agent.last_promo_at}`);
|
|
3128
|
+
return lines.join("\n");
|
|
3129
|
+
}
|
|
3130
|
+
async function runRecapStats() {
|
|
3131
|
+
const path = join6(getConfigDir(), "recap.json");
|
|
3132
|
+
let size = 0;
|
|
3133
|
+
try {
|
|
3134
|
+
size = statSync(path).size;
|
|
3135
|
+
} catch {
|
|
3136
|
+
size = 0;
|
|
3137
|
+
}
|
|
3138
|
+
const health = size >= STATS_ALERT_BYTES ? "ALERT" : size >= STATS_WARN_BYTES ? "WARN" : "OK";
|
|
3139
|
+
const file = await readRecap();
|
|
3140
|
+
const lines = [];
|
|
3141
|
+
lines.push(`recap.json size: ${(size / 1024).toFixed(1)} KB \u2014 ${health}`);
|
|
3142
|
+
lines.push(` warn >= ${(STATS_WARN_BYTES / 1024).toFixed(0)} KB, alert >= ${(STATS_ALERT_BYTES / 1024).toFixed(0)} KB`);
|
|
3143
|
+
lines.push("");
|
|
3144
|
+
for (const [agentId, agent] of Object.entries(file.agents)) {
|
|
3145
|
+
lines.push(
|
|
3146
|
+
` ${agentId} recent_events: ${agent.recent_events.length}/30 mood_history: ${agent.mood_history.length}/30 last_promo_at: ${agent.last_promo_at ?? "-"}`
|
|
3147
|
+
);
|
|
3148
|
+
}
|
|
3149
|
+
if (Object.keys(file.agents).length === 0) lines.push(" (no agents yet)");
|
|
3150
|
+
return lines.join("\n");
|
|
3151
|
+
}
|
|
3152
|
+
var showCmd3 = new Command15("show").description("Show recap for the current agent (default)").option("--json", "Output structured JSON").option("--prompt", "Output an LLM-ready natural-language block").option("--since-last-promo", "Filter events to those after the last emitted promo").action(async (opts) => {
|
|
3153
|
+
const format = opts.json ? "json" : opts.prompt ? "prompt" : "human";
|
|
3154
|
+
const out = await runRecapShow({ format, sinceLastPromo: !!opts.sinceLastPromo });
|
|
3155
|
+
console.log(out);
|
|
3156
|
+
});
|
|
3157
|
+
var statsCmd = new Command15("stats").description("Print on-disk size and ring-buffer depths").action(async () => {
|
|
3158
|
+
const out = await runRecapStats();
|
|
3159
|
+
console.log(out);
|
|
3160
|
+
});
|
|
3161
|
+
var recapCmd = new Command15("recap").description("Show agent's accumulated Arena experience (facts + mood)").option("--json", "Output structured JSON").option("--prompt", "Output an LLM-ready natural-language block").option("--since-last-promo", "Filter events to those after the last emitted promo").option("--stats", "Print on-disk size and ring-buffer depths").action(async (opts) => {
|
|
3162
|
+
if (opts.stats) {
|
|
3163
|
+
console.log(await runRecapStats());
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3166
|
+
const format = opts.json ? "json" : opts.prompt ? "prompt" : "human";
|
|
3167
|
+
console.log(await runRecapShow({ format, sinceLastPromo: !!opts.sinceLastPromo }));
|
|
3168
|
+
}).addCommand(showCmd3).addCommand(statsCmd);
|
|
3169
|
+
|
|
3170
|
+
// src/commands/mood.ts
|
|
3171
|
+
import { Command as Command16 } from "commander";
|
|
3172
|
+
async function runMoodShow() {
|
|
3173
|
+
const creds = requireCredentials();
|
|
3174
|
+
const file = await readRecap();
|
|
3175
|
+
const agent = file.agents[creds.agent_id];
|
|
3176
|
+
const cm = agent ? currentMood(agent.mood_history) : null;
|
|
3177
|
+
if (!cm) return "Mood: (not set)";
|
|
3178
|
+
return `Mood: ${cm.mood} since ${cm.since_at}${cm.reason ? ` \u2014 "${cm.reason}"` : ""}`;
|
|
3179
|
+
}
|
|
3180
|
+
async function runMoodSet(mood, reason, now = /* @__PURE__ */ new Date()) {
|
|
3181
|
+
if (!isMood(mood)) {
|
|
3182
|
+
return { ok: false, error: `invalid mood: ${mood}. Valid: ${MOODS.join(" | ")}` };
|
|
3183
|
+
}
|
|
3184
|
+
const creds = requireCredentials();
|
|
3185
|
+
const { changed, mood: m } = await setMood(creds.agent_id, mood, reason, now);
|
|
3186
|
+
return { ok: true, changed, mood: m };
|
|
3187
|
+
}
|
|
3188
|
+
var setCmd = new Command16("set").description("Set current mood").argument("<mood>", `One of: ${MOODS.join(" | ")}`).option("--reason <text>", "Short reason for the mood transition (\u2264200 chars, sanitized)").action(async (mood, opts) => {
|
|
3189
|
+
const result = await runMoodSet(mood, opts.reason ?? "");
|
|
3190
|
+
if (!result.ok) {
|
|
3191
|
+
console.error(result.error);
|
|
3192
|
+
process.exit(1);
|
|
3193
|
+
}
|
|
3194
|
+
console.log(`mood: ${result.mood}${result.changed ? "" : " (no change)"}`);
|
|
3195
|
+
});
|
|
3196
|
+
var moodCmd = new Command16("mood").description("Show or set the agent's mood").action(async () => {
|
|
3197
|
+
console.log(await runMoodShow());
|
|
3198
|
+
}).addCommand(setCmd);
|
|
3199
|
+
|
|
3200
|
+
// src/commands/mainRegister.ts
|
|
3201
|
+
import { Command as Command17 } from "commander";
|
|
3202
|
+
|
|
3203
|
+
// src/promo/mainSession.ts
|
|
3204
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync6 } from "fs";
|
|
3205
|
+
import { join as join7 } from "path";
|
|
3206
|
+
function sessionFile() {
|
|
3207
|
+
return join7(getConfigDir(), "session.json");
|
|
3208
|
+
}
|
|
3209
|
+
function ensureDir4() {
|
|
3210
|
+
const dir = getConfigDir();
|
|
3211
|
+
if (!existsSync7(dir)) mkdirSync6(dir, { recursive: true });
|
|
3212
|
+
}
|
|
3213
|
+
function registerMainSession(sessionKey, startedAt, pid) {
|
|
3214
|
+
ensureDir4();
|
|
3215
|
+
const record = {
|
|
3216
|
+
main_session_key: sessionKey,
|
|
3217
|
+
pid,
|
|
3218
|
+
started_at: startedAt.toISOString()
|
|
3219
|
+
};
|
|
3220
|
+
writeFileSync6(sessionFile(), JSON.stringify(record, null, 2) + "\n", { mode: 384 });
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
// src/commands/mainRegister.ts
|
|
3224
|
+
function runMainRegister(input, now = /* @__PURE__ */ new Date()) {
|
|
3225
|
+
const key = input.sessionKey.trim();
|
|
3226
|
+
if (key === "") {
|
|
3227
|
+
throw new Error("--session-key must not be empty");
|
|
3228
|
+
}
|
|
3229
|
+
if (!Number.isInteger(input.pid) || input.pid <= 0) {
|
|
3230
|
+
throw new Error("--pid must be a positive integer");
|
|
3231
|
+
}
|
|
3232
|
+
registerMainSession(key, now, input.pid);
|
|
3233
|
+
console.log(`main session registered: ${key}`);
|
|
3234
|
+
}
|
|
3235
|
+
var mainRegisterCmd = new Command17("main-register").description("Register the current (main) session key so sub-sessions can discover it").requiredOption("--session-key <key>", "OpenClaw session key of the current (main) session").option("--pid <pid>", "Process id to record", String(process.pid)).action((opts) => {
|
|
3236
|
+
try {
|
|
3237
|
+
runMainRegister({
|
|
3238
|
+
sessionKey: opts.sessionKey,
|
|
3239
|
+
pid: Number.parseInt(opts.pid, 10)
|
|
3240
|
+
});
|
|
3241
|
+
} catch (e) {
|
|
3242
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
3243
|
+
process.exit(1);
|
|
3244
|
+
}
|
|
3245
|
+
});
|
|
2357
3246
|
|
|
2358
3247
|
// src/index.ts
|
|
2359
|
-
var program = new
|
|
3248
|
+
var program = new Command18();
|
|
2360
3249
|
program.name("arena").description(
|
|
2361
3250
|
'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"'
|
|
2362
3251
|
).version("0.4.0").option("--config-dir <path>", "Override config/state directory (env: ARENA_CONFIG_DIR)");
|
|
@@ -2373,6 +3262,10 @@ program.addCommand(rulesCmd);
|
|
|
2373
3262
|
program.addCommand(watchCmd);
|
|
2374
3263
|
program.addCommand(stateCmd2);
|
|
2375
3264
|
program.addCommand(heartbeatCmd);
|
|
3265
|
+
program.addCommand(promoCmd);
|
|
3266
|
+
program.addCommand(mainRegisterCmd);
|
|
3267
|
+
program.addCommand(recapCmd);
|
|
3268
|
+
program.addCommand(moodCmd);
|
|
2376
3269
|
program.hook("preAction", () => {
|
|
2377
3270
|
const configDir = program.opts().configDir;
|
|
2378
3271
|
if (configDir) {
|