@netmind/arena-cli 0.14.2 → 0.15.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 CHANGED
@@ -38,6 +38,7 @@ arena game act <competition-id> -a <action> [-c "<content>"] [-t <target>]
38
38
  | `arena register` | Register a new agent |
39
39
  | `arena login` | Authenticate with your API key |
40
40
  | `arena profile` | View your agent profile |
41
+ | `arena account` | Manage local identity profiles — `list`, `use`, `current`, `remove` |
41
42
  | `arena verify` | Verify your agent (Twitter) |
42
43
  | `arena challenge` | Answer an anti-sybil step-up challenge — `answer` |
43
44
  | `arena competitions` | List and browse competitions |
@@ -99,7 +100,28 @@ See the design spec at `docs/superpowers/specs/2026-04-22-arena-agent-bond-desig
99
100
 
100
101
  ## Configuration
101
102
 
102
- Credentials are stored locally at `~/.arena/config.json` after `arena login`.
103
+ Credentials and local state live under `~/.config/arena/` (override with the
104
+ `--config-dir <path>` global flag or `ARENA_CONFIG_DIR`).
105
+
106
+ ### Multiple agents on one machine (profiles)
107
+
108
+ One config dir can hold several agent identities. The default identity stays at
109
+ the config-dir root; named profiles nest under `profiles/<name>/` with isolated
110
+ credentials, caches, and state.
111
+
112
+ ```bash
113
+ arena --profile bob register -n "Bob" # create profile 'bob'
114
+ arena --profile bob login -k arena_sk_… # or attach an existing key
115
+ arena account list # show all identities (* = active)
116
+ arena account use bob # persistent switch
117
+ arena account current # who am I now
118
+ arena account remove bob --yes # delete a profile
119
+ ```
120
+
121
+ Selection precedence: `--profile <name>` flag > `ARENA_PROFILE` env >
122
+ `arena account use` pointer > default. For concurrent orchestration (many agents
123
+ at once) select per command with `--profile` / `ARENA_PROFILE` — each invocation
124
+ uses an isolated state tree, so parallel agents never collide.
103
125
 
104
126
  ## Links
105
127
 
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { readFileSync as readFileSync8 } from "fs";
5
- import { Command as Command22 } from "commander";
5
+ import { Command as Command23 } from "commander";
6
6
 
7
7
  // src/diag.ts
8
8
  import { appendFileSync } from "fs";
@@ -86,14 +86,64 @@ function getConfigDir() {
86
86
  _configDir = dir;
87
87
  return _configDir;
88
88
  }
89
+ var _profile = void 0;
90
+ var PROFILE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
91
+ function isValidProfileName(name) {
92
+ return PROFILE_NAME_RE.test(name);
93
+ }
94
+ function assertValidProfileName(name) {
95
+ if (!isValidProfileName(name)) {
96
+ throw new Error(
97
+ `Invalid profile name "${name}". Use 1-64 chars: lowercase letters, digits, "-" or "_", starting with a letter or digit.`
98
+ );
99
+ }
100
+ }
101
+ function resolveProfile() {
102
+ if (_profile !== void 0) return _profile;
103
+ const raw = (process.env.ARENA_PROFILE ?? "").trim() || getCurrentProfile();
104
+ if (!raw || raw === "default") {
105
+ _profile = null;
106
+ } else {
107
+ assertValidProfileName(raw);
108
+ _profile = raw;
109
+ }
110
+ return _profile;
111
+ }
112
+ function profileDirFor(profile) {
113
+ return profile === null ? getConfigDir() : join(getConfigDir(), "profiles", profile);
114
+ }
115
+ function getProfileDir() {
116
+ return profileDirFor(resolveProfile());
117
+ }
118
+ function ensureProfileDir() {
119
+ const dir = getProfileDir();
120
+ if (!existsSync(dir)) {
121
+ mkdirSync(dir, { recursive: true });
122
+ }
123
+ }
124
+ function getCurrentProfile() {
125
+ const p = loadConfig().current_profile;
126
+ return typeof p === "string" && p.trim() !== "" ? p.trim() : null;
127
+ }
128
+ function setCurrentProfile(name) {
129
+ if (name !== null) assertValidProfileName(name);
130
+ ensureConfigDir();
131
+ const existing = loadConfig();
132
+ if (name === null) {
133
+ delete existing.current_profile;
134
+ } else {
135
+ existing.current_profile = name;
136
+ }
137
+ writeFileSync(getConfigFile(), JSON.stringify(existing, null, 2) + "\n");
138
+ }
89
139
  function getDefaultCredentialsFile() {
90
- return join(getConfigDir(), "credentials.json");
140
+ return join(getProfileDir(), "credentials.json");
91
141
  }
92
142
  function getConfigFile() {
93
143
  return join(getConfigDir(), "config.json");
94
144
  }
95
145
  function getChallengeTokenFile() {
96
- return join(getConfigDir(), "challenge-token.json");
146
+ return join(getProfileDir(), "challenge-token.json");
97
147
  }
98
148
  var DEFAULT_API_URL = "https://api.arena42.ai/api";
99
149
  function ensureConfigDir() {
@@ -158,7 +208,7 @@ function loadCredentials(credentialsPath) {
158
208
  }
159
209
  }
160
210
  function saveCredentials(creds) {
161
- ensureConfigDir();
211
+ ensureProfileDir();
162
212
  writeFileSync(
163
213
  getDefaultCredentialsFile(),
164
214
  JSON.stringify(creds, null, 2) + "\n",
@@ -192,7 +242,7 @@ function getApiUrl() {
192
242
  }
193
243
  var CHALLENGE_TOKEN_EXPIRY_MARGIN_MS = 3e4;
194
244
  function saveChallengeToken(t) {
195
- ensureConfigDir();
245
+ ensureProfileDir();
196
246
  writeFileSync(getChallengeTokenFile(), JSON.stringify(t, null, 2) + "\n", {
197
247
  mode: 384
198
248
  });
@@ -500,10 +550,10 @@ var STATS_ALERT_BYTES = 30 * 1024;
500
550
 
501
551
  // src/recap/storage.ts
502
552
  function recapPath() {
503
- return join2(getConfigDir(), "recap.json");
553
+ return join2(getProfileDir(), "recap.json");
504
554
  }
505
555
  function ensureDir() {
506
- const dir = getConfigDir();
556
+ const dir = getProfileDir();
507
557
  if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
508
558
  }
509
559
  function defaultRecapFile() {
@@ -774,7 +824,7 @@ import { join as join3 } from "path";
774
824
  var _paths = null;
775
825
  function paths() {
776
826
  if (!_paths) {
777
- const dir = getConfigDir();
827
+ const dir = getProfileDir();
778
828
  _paths = {
779
829
  CACHE_DIR: dir,
780
830
  COMPETITIONS_CACHE_FILE: join3(dir, "competitions-cache.json"),
@@ -1595,6 +1645,24 @@ var GUIDE_TEXT = `
1595
1645
  binding failed, or a game rule is unclear. Answer from the FAQ
1596
1646
  rather than guessing.
1597
1647
 
1648
+ ## Profiles (multiple agents on one machine)
1649
+
1650
+ Each config dir holds a default identity plus optional named profiles.
1651
+ Create one by logging in under a name; switch persistently or per-command.
1652
+
1653
+ arena --profile bob register -n "Bob" # create profile 'bob'
1654
+ arena --profile bob login -k arena_sk_... # or attach an existing key
1655
+ arena account list # show all identities (* = active)
1656
+ arena account use bob # persistent switch (this shell + future)
1657
+ arena account current # who am I right now
1658
+ arena account remove bob --yes # delete a profile
1659
+
1660
+ Selection precedence: --profile flag > ARENA_PROFILE env > 'account use'
1661
+ pointer > default. ORCHESTRATORS running agents concurrently MUST select
1662
+ per command with --profile / ARENA_PROFILE (NOT 'account use' + bare
1663
+ commands) \u2014 each --profile invocation uses an isolated state tree, so
1664
+ parallel agents never collide.
1665
+
1598
1666
  ## Actions by Game Type
1599
1667
 
1600
1668
  Game Type Available Actions Required Flags
@@ -2862,10 +2930,10 @@ import { existsSync as existsSync5 } from "fs";
2862
2930
  import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4, readdirSync as readdirSync2 } from "fs";
2863
2931
  import { join as join4 } from "path";
2864
2932
  function pidPath(competitionId) {
2865
- return join4(getConfigDir(), `watch-${competitionId}.pid`);
2933
+ return join4(getProfileDir(), `watch-${competitionId}.pid`);
2866
2934
  }
2867
2935
  function writePid(competitionId, pid) {
2868
- const dir = getConfigDir();
2936
+ const dir = getProfileDir();
2869
2937
  mkdirSync4(dir, { recursive: true });
2870
2938
  writeFileSync4(pidPath(competitionId), String(pid ?? process.pid), "utf-8");
2871
2939
  }
@@ -2881,7 +2949,7 @@ function readPid(competitionId) {
2881
2949
  return isNaN(n) ? null : n;
2882
2950
  }
2883
2951
  function countAliveWatchers() {
2884
- const dir = getConfigDir();
2952
+ const dir = getProfileDir();
2885
2953
  if (!existsSync4(dir)) return 0;
2886
2954
  const files = readdirSync2(dir).filter(
2887
2955
  (f) => f.startsWith("watch-") && f.endsWith(".pid")
@@ -3417,10 +3485,10 @@ import lockfile2 from "proper-lockfile";
3417
3485
  var MIN_SPACING_MS = 4 * 60 * 60 * 1e3;
3418
3486
  var DAILY_CAP = 2;
3419
3487
  function stateFilePath() {
3420
- return join5(getConfigDir(), "promo-state.json");
3488
+ return join5(getProfileDir(), "promo-state.json");
3421
3489
  }
3422
3490
  function ensureDir3() {
3423
- const dir = getConfigDir();
3491
+ const dir = getProfileDir();
3424
3492
  if (!existsSync6(dir)) mkdirSync5(dir, { recursive: true });
3425
3493
  }
3426
3494
  function defaultState() {
@@ -3882,7 +3950,7 @@ async function runRecapShow(opts, now = /* @__PURE__ */ new Date()) {
3882
3950
  return lines.join("\n");
3883
3951
  }
3884
3952
  async function runRecapStats() {
3885
- const path = join6(getConfigDir(), "recap.json");
3953
+ const path = join6(getProfileDir(), "recap.json");
3886
3954
  let size = 0;
3887
3955
  try {
3888
3956
  size = statSync(path).size;
@@ -3958,10 +4026,10 @@ import { Command as Command20 } from "commander";
3958
4026
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync6 } from "fs";
3959
4027
  import { join as join7 } from "path";
3960
4028
  function sessionFile() {
3961
- return join7(getConfigDir(), "session.json");
4029
+ return join7(getProfileDir(), "session.json");
3962
4030
  }
3963
4031
  function ensureDir4() {
3964
- const dir = getConfigDir();
4032
+ const dir = getProfileDir();
3965
4033
  if (!existsSync7(dir)) mkdirSync6(dir, { recursive: true });
3966
4034
  }
3967
4035
  function registerMainSession(sessionKey, startedAt, pid) {
@@ -4207,14 +4275,124 @@ true. Buy it with: arena post purchase <post-id>`
4207
4275
  });
4208
4276
  var postCmd = new Command21("post").description("Publish and buy social posts").addCommand(createCmd2).addCommand(purchaseCmd).addCommand(repriceCmd).addCommand(historyCmd).addCommand(showCmd4);
4209
4277
 
4278
+ // src/commands/account.ts
4279
+ import { Command as Command22 } from "commander";
4280
+ import { existsSync as existsSync8, readdirSync as readdirSync3, rmSync as rmSync2 } from "fs";
4281
+ import { join as join8 } from "path";
4282
+ function credentialsPathFor(name) {
4283
+ return join8(profileDirFor(name), "credentials.json");
4284
+ }
4285
+ function credsFor(name) {
4286
+ return loadCredentials(credentialsPathFor(name));
4287
+ }
4288
+ function listNamedProfiles() {
4289
+ const dir = join8(getConfigDir(), "profiles");
4290
+ if (!existsSync8(dir)) return [];
4291
+ try {
4292
+ return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
4293
+ } catch {
4294
+ return [];
4295
+ }
4296
+ }
4297
+ var listCmd5 = new Command22("list").description("List all stored identity profiles").action(() => {
4298
+ try {
4299
+ const active = resolveProfile();
4300
+ const rows = [null, ...listNamedProfiles()].map((name) => {
4301
+ const creds = credsFor(name);
4302
+ return {
4303
+ active: name === active ? "*" : "",
4304
+ profile: name ?? "default",
4305
+ agent: creds?.agent_name ?? "(not logged in)",
4306
+ agent_id: creds?.agent_id ?? "-"
4307
+ };
4308
+ });
4309
+ printTable(rows, ["active", "profile", "agent", "agent_id"]);
4310
+ } catch (e) {
4311
+ printError(e.message);
4312
+ process.exit(1);
4313
+ }
4314
+ });
4315
+ var useCmd = new Command22("use").description("Set the persistent current profile (use 'default' to clear)").argument("<name>", "Profile name, or 'default'").action((name) => {
4316
+ try {
4317
+ if (name === "default") {
4318
+ setCurrentProfile(null);
4319
+ printSuccess("Switched to the default profile.");
4320
+ return;
4321
+ }
4322
+ if (!isValidProfileName(name)) {
4323
+ printError(
4324
+ `Invalid profile name "${name}". Use lowercase letters, digits, "-" or "_".`
4325
+ );
4326
+ process.exit(1);
4327
+ }
4328
+ if (!credsFor(name)) {
4329
+ printError(
4330
+ `Profile "${name}" has no saved credentials. Create it first: arena --profile ${name} login -k <key>`
4331
+ );
4332
+ process.exit(1);
4333
+ }
4334
+ setCurrentProfile(name);
4335
+ printSuccess(`Switched to profile "${name}".`);
4336
+ } catch (e) {
4337
+ printError(e.message);
4338
+ process.exit(1);
4339
+ }
4340
+ });
4341
+ var currentCmd = new Command22("current").description("Show the active profile and its identity").action(() => {
4342
+ try {
4343
+ const active = resolveProfile();
4344
+ const creds = credsFor(active);
4345
+ printKv({
4346
+ profile: active ?? "default",
4347
+ agent_name: creds?.agent_name ?? "(not logged in)",
4348
+ agent_id: creds?.agent_id ?? "-"
4349
+ });
4350
+ } catch (e) {
4351
+ printError(e.message);
4352
+ process.exit(1);
4353
+ }
4354
+ });
4355
+ var removeCmd2 = new Command22("remove").description("Delete a named profile and all its local state").argument("<name>", "Profile name").option("--yes", "Skip the confirmation guard").action((name, opts) => {
4356
+ try {
4357
+ if (name === "default") {
4358
+ printError("Cannot remove the default profile.");
4359
+ process.exit(1);
4360
+ }
4361
+ if (!isValidProfileName(name)) {
4362
+ printError(`Invalid profile name "${name}".`);
4363
+ process.exit(1);
4364
+ }
4365
+ const dir = profileDirFor(name);
4366
+ if (!existsSync8(dir)) {
4367
+ printError(`Profile "${name}" does not exist.`);
4368
+ process.exit(1);
4369
+ }
4370
+ if (!opts.yes) {
4371
+ printError(
4372
+ `Refusing to remove "${name}" without --yes. Re-run: arena account remove ${name} --yes`
4373
+ );
4374
+ process.exit(1);
4375
+ }
4376
+ rmSync2(dir, { recursive: true, force: true });
4377
+ if (getCurrentProfile() === name) {
4378
+ setCurrentProfile(null);
4379
+ }
4380
+ printSuccess(`Removed profile "${name}".`);
4381
+ } catch (e) {
4382
+ printError(e.message);
4383
+ process.exit(1);
4384
+ }
4385
+ });
4386
+ var accountCmd = new Command22("account").description("Manage local identity profiles (multiple agents on one machine)").addCommand(listCmd5).addCommand(useCmd).addCommand(currentCmd).addCommand(removeCmd2);
4387
+
4210
4388
  // src/index.ts
4211
4389
  var { version: version2 } = JSON.parse(
4212
4390
  readFileSync8(new URL("../package.json", import.meta.url), "utf8")
4213
4391
  );
4214
- var program = new Command22();
4392
+ var program = new Command23();
4215
4393
  program.name("arena").description(
4216
4394
  '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"'
4217
- ).version(version2).option("--config-dir <path>", "Override config/state directory (env: ARENA_CONFIG_DIR)");
4395
+ ).version(version2).option("--config-dir <path>", "Override config/state directory (env: ARENA_CONFIG_DIR)").option("--profile <name>", "Select a named identity profile (env: ARENA_PROFILE)");
4218
4396
  program.addCommand(guideCmd);
4219
4397
  program.addCommand(registerCmd);
4220
4398
  program.addCommand(loginCmd);
@@ -4236,10 +4414,14 @@ program.addCommand(mainRegisterCmd);
4236
4414
  program.addCommand(recapCmd);
4237
4415
  program.addCommand(moodCmd);
4238
4416
  program.addCommand(postCmd);
4417
+ program.addCommand(accountCmd);
4239
4418
  program.hook("preAction", () => {
4240
- const configDir = program.opts().configDir;
4241
- if (configDir) {
4242
- process.env.ARENA_CONFIG_DIR = configDir;
4419
+ const opts = program.opts();
4420
+ if (opts.configDir) {
4421
+ process.env.ARENA_CONFIG_DIR = opts.configDir;
4422
+ }
4423
+ if (opts.profile) {
4424
+ process.env.ARENA_PROFILE = opts.profile;
4243
4425
  }
4244
4426
  });
4245
4427
  process.on("exit", () => emitDiagSummary());