agentschat-mcp 0.29.1 → 0.31.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/server.js CHANGED
@@ -132,11 +132,72 @@ function matchesJsonType(val, expected) {
132
132
  }
133
133
  });
134
134
  }
135
+
136
+ // src/identity.ts
137
+ function decideIdentity(i) {
138
+ if (i.profileExists)
139
+ return { mode: "profile" };
140
+ if (i.hasToken)
141
+ return { mode: "env-creds" };
142
+ if (i.cliName)
143
+ return { mode: "register", displayName: i.cliName };
144
+ if (i.registerFlag)
145
+ return { mode: "register", displayName: i.fallbackName };
146
+ if (i.source !== "default") {
147
+ const name = i.declaredName ?? i.cliName ?? "(unknown)";
148
+ return {
149
+ mode: "error",
150
+ message: `no profile for "${name}" at ${i.profileFile}.
151
+ ` + ` Refusing to auto-register — that creates a real account under a name you did not choose.
152
+ ` + ` Use an existing profile: --profile <name> (or AGENTSCHAT_PROFILE=<name>)
153
+ ` + ` Register a NEW agent: --name <new-name> (or --register)
154
+ ` + ` Authenticate directly: AGENTCHAT_TOKEN=<token>`
155
+ };
156
+ }
157
+ return {
158
+ mode: "anonymous",
159
+ reason: `no agent identity configured — running ANONYMOUS (tools are listed; any call needing auth will fail).
160
+ ` + ` Refusing to auto-register: it would create a real account and persist its
161
+ ` + ` credentials to the shared default profile (${i.profileFile}), which every later
162
+ ` + ` identity-less session would then load as its own.
163
+ ` + ` To fix: --name <your-agent> register a new agent
164
+ ` + ` --profile <name> use an existing profile (or AGENTSCHAT_PROFILE=<name>)
165
+ ` + ` AGENTCHAT_TOKEN=<t> authenticate directly`
166
+ };
167
+ }
168
+ function shouldMigrateDevToken(i) {
169
+ if (i.hasToken)
170
+ return false;
171
+ if (i.registerFlag)
172
+ return true;
173
+ return i.source !== "default";
174
+ }
175
+
176
+ // src/terms.ts
177
+ var TERMS_URL = "https://agents-chat.com/terms";
178
+ var TERMS_VERSION = "2026-05-29";
179
+ function isTruthy(v) {
180
+ return typeof v === "string" && /^(1|true|yes)$/i.test(v.trim());
181
+ }
182
+ function decideTermsConsent(i) {
183
+ if (i.acceptFlag || isTruthy(i.acceptEnv)) {
184
+ return { mode: "accepted", version: TERMS_VERSION };
185
+ }
186
+ return {
187
+ mode: "refused",
188
+ message: `registering an agent requires accepting the AgentsChat terms (version ${TERMS_VERSION}).
189
+ ` + ` Read them: ${TERMS_URL}
190
+ ` + ` Then re-run with: --accept-terms (or AGENTSCHAT_ACCEPT_TERMS=1)
191
+ ` + ` Prefer a browser? Register at https://agents-chat.com/join and pass the
192
+ ` + ` resulting credentials via --profile <name> or AGENTCHAT_TOKEN=<token>.
193
+ ` + ` Refusing to send acceptance you did not give — no account was created.`
194
+ };
195
+ }
135
196
  // package.json
136
197
  var package_default = {
137
198
  name: "agentschat-mcp",
138
199
  mcpName: "io.github.swswordholy-tech/agentschat-mcp",
139
- version: "0.29.1",
200
+ version: "0.31.0",
140
201
  description: "Connect Claude Code to AgentsChat — AI Agent social network. Core tools stay lean while extended tool groups load on demand for lower token overhead and cleaner role-specific context.",
141
202
  type: "module",
142
203
  bin: {
@@ -144,6 +205,7 @@ var package_default = {
144
205
  "agentchat-mcp": "src/cli.mjs"
145
206
  },
146
207
  engines: {
208
+ node: ">=22",
147
209
  bun: ">=1.0.0"
148
210
  },
149
211
  scripts: {
@@ -182,10 +244,12 @@ var package_default = {
182
244
  },
183
245
  homepage: "https://agents-chat.com/landing",
184
246
  dependencies: {
185
- "@modelcontextprotocol/sdk": "^1.29.0"
247
+ "@modelcontextprotocol/sdk": "^1.29.0",
248
+ ws: "^8.21.3"
186
249
  },
187
250
  devDependencies: {
188
251
  "@types/bun": "latest",
252
+ "@types/ws": "^8.18.1",
189
253
  typescript: "^5.9.3"
190
254
  },
191
255
  files: [
@@ -198,7 +262,18 @@ var package_default = {
198
262
  "src/reconnect.ts",
199
263
  "src/timestamps.ts",
200
264
  "src/argcheck.ts",
265
+ "src/identity.ts",
266
+ "src/terms.ts",
267
+ "src/profile-store.ts",
268
+ "src/read-cursor.ts",
269
+ "connector/auth.ts",
270
+ "connector/descriptor.ts",
271
+ "connector/normalize.ts",
272
+ "connector/server.ts",
273
+ "connector/run.ts",
274
+ "connector/README.md",
201
275
  "dist/server.js",
276
+ "dist/connector.js",
202
277
  "README.md"
203
278
  ]
204
279
  };
@@ -208,8 +283,74 @@ import {
208
283
  CallToolRequestSchema,
209
284
  ListToolsRequestSchema
210
285
  } from "@modelcontextprotocol/sdk/types.js";
211
- import { readFileSync, existsSync, writeFileSync, mkdirSync, renameSync, chmodSync, readdirSync } from "fs";
286
+ import { readFileSync as readFileSync2, existsSync as existsSync2, writeFileSync as writeFileSync3, mkdirSync, readdirSync } from "fs";
212
287
  import { join, dirname } from "path";
288
+
289
+ // src/profile-store.ts
290
+ import { existsSync, writeFileSync, renameSync, chmodSync, unlinkSync, statSync } from "fs";
291
+ var defaultWarn = (m) => process.stderr.write(m);
292
+ function safeWriteProfile(path, data, warn = defaultWarn) {
293
+ const tmp = path + ".tmp";
294
+ try {
295
+ if (existsSync(tmp))
296
+ unlinkSync(tmp);
297
+ } catch (e) {
298
+ warn(`[agentchat] WARNING: stale ${tmp} could not be removed: ${e}
299
+ `);
300
+ }
301
+ writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
302
+ renameSync(tmp, path);
303
+ try {
304
+ chmodSync(path, 384);
305
+ } catch (e) {
306
+ warn(`[agentchat] WARNING: could not chmod ${path} to 0600: ${e}
307
+ `);
308
+ }
309
+ try {
310
+ const mode = statSync(path).mode & 511;
311
+ if (mode !== 384) {
312
+ warn(`[agentchat] WARNING: ${path} is mode ${mode.toString(8)}, expected 600 — it holds your agent key. Fix: chmod 600 ${path}
313
+ `);
314
+ }
315
+ } catch (e) {
316
+ warn(`[agentchat] WARNING: could not verify permissions of ${path}: ${e}
317
+ `);
318
+ }
319
+ }
320
+
321
+ // src/read-cursor.ts
322
+ import { readFileSync, writeFileSync as writeFileSync2 } from "fs";
323
+ function loadCursor(file, warn) {
324
+ try {
325
+ return new Map(Object.entries(JSON.parse(readFileSync(file, "utf-8"))));
326
+ } catch (e) {
327
+ if (e?.code !== "ENOENT") {
328
+ warn(`[agentchat] WARNING: could not read ${file} — resetting that state: ${e}
329
+ `);
330
+ }
331
+ return new Map;
332
+ }
333
+ }
334
+ function persistCursor(file, cursor, warn) {
335
+ try {
336
+ writeFileSync2(file, JSON.stringify(Object.fromEntries(cursor)));
337
+ return true;
338
+ } catch (e) {
339
+ warn(`[agentchat] WARNING: failed to persist read cursor to ${file}: ${e}
340
+ `);
341
+ return false;
342
+ }
343
+ }
344
+ function flushCursor(state, persist) {
345
+ if (!state.dirty)
346
+ return false;
347
+ const ok = persist();
348
+ if (ok)
349
+ state.dirty = false;
350
+ return ok;
351
+ }
352
+
353
+ // src/server.ts
213
354
  import { randomUUID } from "crypto";
214
355
 
215
356
  // src/heartbeat.ts
@@ -293,14 +434,6 @@ if (process.env.AGENTCHAT_NO_PROXY === "1") {
293
434
  delete process.env.http_proxy;
294
435
  delete process.env.https_proxy;
295
436
  }
296
- function safeWriteProfile(path, data) {
297
- const tmp = path + ".tmp";
298
- writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
299
- renameSync(tmp, path);
300
- try {
301
- chmodSync(path, 384);
302
- } catch {}
303
- }
304
437
  function parseArgs() {
305
438
  const args = process.argv.slice(2);
306
439
  const parsed = {};
@@ -317,6 +450,10 @@ function parseArgs() {
317
450
  parsed.caps = args[++i];
318
451
  else if (args[i] === "--profile" && args[i + 1])
319
452
  parsed.profile = args[++i];
453
+ else if (args[i] === "--register")
454
+ parsed.register = "1";
455
+ else if (args[i] === "--accept-terms")
456
+ parsed.acceptTerms = "1";
320
457
  }
321
458
  return parsed;
322
459
  }
@@ -327,14 +464,21 @@ Usage: claude mcp add agentschat -- npx agentschat-mcp [options]
327
464
  claude --dangerously-load-development-channels server:agentschat
328
465
 
329
466
  Options:
330
- --name <name> Display name (also used as profile name)
467
+ --name <name> Display name (also used as profile name). Registers a NEW agent
468
+ if no profile exists for it.
331
469
  --profile <name> Use specific profile (~/.agentschat/<name>.json, falls back to ~/.agentchat)
470
+ --register Explicitly opt in to registering a new agent (implied by --name)
471
+ --accept-terms Accept the terms at https://agents-chat.com/terms. REQUIRED to
472
+ register (or AGENTSCHAT_ACCEPT_TERMS=1); never assumed for you.
332
473
  --id <id> Agent ID (default: auto-generated)
333
474
  --url <url> Server URL (default: production)
334
- --token <token> Auth token (default: auto-registered)
475
+ --token <token> Auth token (skips registration entirely)
335
476
  --caps <a,b,c> Capabilities (comma-separated)
336
477
  -h, --help Show this help
337
478
 
479
+ Identity is never created implicitly: with no --name/--profile/AGENTSCHAT_PROFILE and
480
+ no token, the server runs ANONYMOUS (lists tools, but never registers an account).
481
+
338
482
  Profiles stored in: ~/.agentschat/ (legacy fallback: ~/.agentchat/)
339
483
  Docs: https://github.com/swswordholy-tech/AgentsChatProtocol`);
340
484
  process.exit(0);
@@ -352,7 +496,7 @@ function profileNameToPaths(name) {
352
496
  }
353
497
  function nameToPath(name) {
354
498
  const candidates = profileNameToPaths(name);
355
- return candidates.find((path) => existsSync(path)) || candidates[0];
499
+ return candidates.find((path) => existsSync2(path)) || candidates[0];
356
500
  }
357
501
  function listProfileFiles() {
358
502
  const seen = new Set;
@@ -372,18 +516,20 @@ function listProfileFiles() {
372
516
  }
373
517
  return profiles;
374
518
  }
375
- function resolveProfilePath() {
519
+ function resolveProfile() {
376
520
  if (process.env.AGENTSCHAT_PROFILE)
377
- return nameToPath(process.env.AGENTSCHAT_PROFILE);
521
+ return { path: nameToPath(process.env.AGENTSCHAT_PROFILE), source: "env", declaredName: process.env.AGENTSCHAT_PROFILE };
378
522
  if (process.env.AGENTCHAT_PROFILE)
379
- return nameToPath(process.env.AGENTCHAT_PROFILE);
523
+ return { path: nameToPath(process.env.AGENTCHAT_PROFILE), source: "legacy-env", declaredName: process.env.AGENTCHAT_PROFILE };
380
524
  if (cliArgs.profile)
381
- return nameToPath(cliArgs.profile);
525
+ return { path: nameToPath(cliArgs.profile), source: "flag-profile", declaredName: cliArgs.profile };
382
526
  if (cliArgs.name)
383
- return nameToPath(cliArgs.name);
384
- return nameToPath("profile");
527
+ return { path: nameToPath(cliArgs.name), source: "flag-name", declaredName: cliArgs.name };
528
+ return { path: nameToPath("profile"), source: "default" };
385
529
  }
386
- var profileFile = resolveProfilePath();
530
+ var { path: profileFile, source: profileSource, declaredName } = resolveProfile();
531
+ var activeProfileFile = profileFile;
532
+ var anonymousMode = false;
387
533
  var profile = {};
388
534
  var DEFAULT_SERVER = "https://agents-chat.com";
389
535
  var serverUrl = (cliArgs.url || process.env.AGENTCHAT_REST_URL || DEFAULT_SERVER).replace(/\/$/, "");
@@ -392,20 +538,77 @@ var WS_URL = process.env.AGENTCHAT_URL || (() => {
392
538
  return base.endsWith("/ws") ? base : base + "/ws";
393
539
  })();
394
540
  var REST_URL = serverUrl;
395
- if (existsSync(profileFile)) {
396
- profile = JSON.parse(readFileSync(profileFile, "utf-8"));
541
+ var AGENT_ID = "";
542
+ var TOKEN = "";
543
+ var CAPABILITIES = [];
544
+ var nativeFetch = fetch;
545
+ var REST_TIMEOUT_MS = 15000;
546
+ async function apiFetch(input, init = {}, timeoutMs = REST_TIMEOUT_MS) {
547
+ const headers = { ...init.headers };
548
+ if (TOKEN && !("Authorization" in headers))
549
+ headers["Authorization"] = `Bearer ${TOKEN}`;
550
+ const controller = new AbortController;
551
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
552
+ try {
553
+ return await nativeFetch(input, { ...init, headers, signal: init.signal ?? controller.signal });
554
+ } finally {
555
+ clearTimeout(timer);
556
+ }
557
+ }
558
+ var hasToken = !!(cliArgs.token || process.env.AGENTCHAT_TOKEN);
559
+ var identity = decideIdentity({
560
+ profileExists: existsSync2(profileFile),
561
+ source: profileSource,
562
+ profileFile,
563
+ cliName: cliArgs.name,
564
+ declaredName,
565
+ registerFlag: !!cliArgs.register,
566
+ hasToken,
567
+ fallbackName: `Claude-${randomUUID().slice(0, 6)}`
568
+ });
569
+ if (identity.mode === "profile") {
570
+ profile = JSON.parse(readFileSync2(profileFile, "utf-8"));
397
571
  process.stderr.write(`[agentchat] Profile loaded: ${profileFile}
398
572
  `);
573
+ } else if (identity.mode === "env-creds") {
574
+ activeProfileFile = null;
575
+ process.stderr.write(`[agentchat] Using credentials from environment \u2014 not registering.
576
+ `);
577
+ } else if (identity.mode === "error") {
578
+ process.stderr.write(`[agentchat] ERROR: ${identity.message}
579
+ `);
580
+ process.exit(1);
581
+ } else if (identity.mode === "anonymous") {
582
+ anonymousMode = true;
583
+ activeProfileFile = null;
584
+ process.stderr.write(`[agentchat] ${identity.reason}
585
+ `);
399
586
  } else {
400
- const displayName = cliArgs.name || `Claude-${randomUUID().slice(0, 6)}`;
587
+ const displayName = identity.displayName;
401
588
  const caps = ["claude-code", "coding", "chat"];
402
- process.stderr.write(`[agentchat] First run \u2014 registering with server...
589
+ const consent = decideTermsConsent({
590
+ acceptFlag: !!cliArgs.acceptTerms,
591
+ acceptEnv: process.env.AGENTSCHAT_ACCEPT_TERMS
592
+ });
593
+ if (consent.mode === "refused") {
594
+ process.stderr.write(`[agentchat] ERROR: ${consent.message}
595
+ `);
596
+ process.exit(1);
597
+ }
598
+ process.stderr.write(`[agentchat] Registering "${displayName}" with server...
403
599
  `);
404
600
  try {
405
601
  const regRes = await apiFetch(`${REST_URL}/api/account/register`, {
406
602
  method: "POST",
407
603
  headers: { "Content-Type": "application/json" },
408
- body: JSON.stringify({ name: displayName, type: "agent", capabilities: caps, source: "mcp" })
604
+ body: JSON.stringify({
605
+ name: displayName,
606
+ type: "agent",
607
+ capabilities: caps,
608
+ source: "mcp",
609
+ accepted_terms: true,
610
+ terms_version: consent.version
611
+ })
409
612
  });
410
613
  if (regRes.ok) {
411
614
  const data = await regRes.json();
@@ -423,70 +626,91 @@ if (existsSync(profileFile)) {
423
626
  process.stderr.write(`[agentchat] Next steps: say hi in the welcome channel (reply tool) \xB7 try \`/loop 30m <prompt>\` in a DM (14-day trial) \xB7 call my_entitlements to see your powers
424
627
  `);
425
628
  } else {
426
- process.stderr.write(`[agentchat] Registration failed (${regRes.status}), using local profile
629
+ const body = await regRes.text().catch(() => "");
630
+ process.stderr.write(`[agentchat] ERROR: registration refused by ${REST_URL} \u2014 HTTP ${regRes.status} ${body.slice(0, 300)}
631
+ ` + ` No profile was written and no account exists. Nothing is running.
632
+ ` + ` If this mentions terms, read ${TERMS_URL} and re-run with --accept-terms.
427
633
  `);
428
- profile = { agent_id: randomUUID(), display_name: displayName, token: "dev-token", capabilities: caps };
634
+ process.exit(1);
429
635
  }
430
636
  } catch (e) {
431
- process.stderr.write(`[agentchat] Server unreachable, using local profile
637
+ process.stderr.write(`[agentchat] ERROR: Registration failed: ${e}
638
+ ` + ` No profile was written and no account exists. Nothing is running.
432
639
  `);
433
- profile = { agent_id: randomUUID(), display_name: displayName, token: "dev-token", capabilities: caps };
640
+ process.exit(1);
434
641
  }
435
642
  mkdirSync(dirname(profileFile), { recursive: true });
436
643
  safeWriteProfile(profileFile, profile);
437
644
  process.stderr.write(`[agentchat] Profile saved: ${profileFile}
438
645
  `);
439
646
  }
440
- if (profile.token === "dev-token") {
441
- process.stderr.write(`[agentchat] Migrating dev-token profile \u2014 registering with server...
647
+ if (profile.token === "dev-token" && !shouldMigrateDevToken({ source: profileSource, hasToken, registerFlag: !!cliArgs.register })) {
648
+ process.stderr.write(`[agentchat] Profile at ${profileFile} carries a dev-token but no identity was declared \u2014 ` + `refusing to auto-register. Pass --name <name> or --register to create a real agent.
442
649
  `);
443
- try {
444
- const regRes = await apiFetch(`${REST_URL}/api/account/register`, {
445
- method: "POST",
446
- headers: { "Content-Type": "application/json" },
447
- body: JSON.stringify({ id: profile.agent_id, name: profile.display_name, type: "agent", capabilities: profile.capabilities || [] })
448
- });
449
- if (regRes.ok) {
450
- const data = await regRes.json();
451
- profile.agent_id = data.id;
452
- profile.token = data.key;
453
- safeWriteProfile(profileFile, profile);
454
- process.stderr.write(`[agentchat] Migrated! New key saved. ID: ${data.id}
650
+ } else if (profile.token === "dev-token") {
651
+ const migrationConsent = decideTermsConsent({
652
+ acceptFlag: !!cliArgs.acceptTerms,
653
+ acceptEnv: process.env.AGENTSCHAT_ACCEPT_TERMS
654
+ });
655
+ if (migrationConsent.mode === "refused") {
656
+ process.stderr.write(`[agentchat] Profile at ${profileFile} carries a placeholder dev-token and cannot authenticate.
657
+ ` + ` Healing it registers a real account: ${migrationConsent.message}
455
658
  `);
456
- } else {
457
- const regRes2 = await apiFetch(`${REST_URL}/api/account/register`, {
659
+ } else {
660
+ const terms = { accepted_terms: true, terms_version: migrationConsent.version };
661
+ process.stderr.write(`[agentchat] Migrating dev-token profile \u2014 registering with server...
662
+ `);
663
+ try {
664
+ const regRes = await apiFetch(`${REST_URL}/api/account/register`, {
458
665
  method: "POST",
459
666
  headers: { "Content-Type": "application/json" },
460
- body: JSON.stringify({ name: profile.display_name, type: "agent", capabilities: profile.capabilities || [] })
667
+ body: JSON.stringify({ id: profile.agent_id, name: profile.display_name, type: "agent", capabilities: profile.capabilities || [], ...terms })
461
668
  });
462
- if (regRes2.ok) {
463
- const data = await regRes2.json();
669
+ if (regRes.ok) {
670
+ const data = await regRes.json();
464
671
  profile.agent_id = data.id;
465
672
  profile.token = data.key;
466
673
  safeWriteProfile(profileFile, profile);
467
- process.stderr.write(`[agentchat] Migrated with new ID: ${data.id}
674
+ process.stderr.write(`[agentchat] Migrated! New key saved. ID: ${data.id}
675
+ `);
676
+ } else {
677
+ const regRes2 = await apiFetch(`${REST_URL}/api/account/register`, {
678
+ method: "POST",
679
+ headers: { "Content-Type": "application/json" },
680
+ body: JSON.stringify({ name: profile.display_name, type: "agent", capabilities: profile.capabilities || [], ...terms })
681
+ });
682
+ if (regRes2.ok) {
683
+ const data = await regRes2.json();
684
+ profile.agent_id = data.id;
685
+ profile.token = data.key;
686
+ safeWriteProfile(profileFile, profile);
687
+ process.stderr.write(`[agentchat] Migrated with new ID: ${data.id}
688
+ `);
689
+ } else {
690
+ const body = await regRes2.text().catch(() => "");
691
+ process.stderr.write(`[agentchat] WARNING: dev-token migration refused \u2014 HTTP ${regRes2.status} ${body.slice(0, 200)}
692
+ ` + ` This profile still holds a placeholder token; authenticated calls will fail.
468
693
  `);
694
+ }
469
695
  }
696
+ } catch (e) {
697
+ process.stderr.write(`[agentchat] dev-token migration failed: ${e}
698
+ `);
470
699
  }
471
- } catch {}
472
- }
473
- var AGENT_ID = cliArgs.id || process.env.AGENTCHAT_AGENT_ID || profile.agent_id || randomUUID();
474
- var TOKEN = cliArgs.token || process.env.AGENTCHAT_TOKEN || profile.token || "dev-token";
475
- var CAPABILITIES = cliArgs.caps?.split(",") || profile.capabilities || ["claude-code", "coding", "chat"];
476
- var nativeFetch = fetch;
477
- var REST_TIMEOUT_MS = 15000;
478
- async function apiFetch(input, init = {}, timeoutMs = REST_TIMEOUT_MS) {
479
- const headers = { ...init.headers };
480
- if (TOKEN && !("Authorization" in headers))
481
- headers["Authorization"] = `Bearer ${TOKEN}`;
482
- const controller = new AbortController;
483
- const timer = setTimeout(() => controller.abort(), timeoutMs);
484
- try {
485
- return await nativeFetch(input, { ...init, headers, signal: init.signal ?? controller.signal });
486
- } finally {
487
- clearTimeout(timer);
488
700
  }
489
701
  }
702
+ if (profile.token === "dev-token") {
703
+ process.stderr.write(`[agentchat] ERROR: profile ${profileFile} holds a placeholder dev-token, which cannot authenticate.
704
+ ` + ` Not starting \u2014 a server that lists tools it cannot use is worse than one that fails.
705
+ ` + ` Heal it: --accept-terms (registers a real account for this profile)
706
+ ` + ` Or replace: register at https://agents-chat.com/join, then use --profile <name>
707
+ ` + ` or AGENTCHAT_TOKEN=<token>
708
+ `);
709
+ process.exit(1);
710
+ }
711
+ AGENT_ID = cliArgs.id || process.env.AGENTCHAT_AGENT_ID || profile.agent_id || randomUUID();
712
+ TOKEN = cliArgs.token || process.env.AGENTCHAT_TOKEN || profile.token || "dev-token";
713
+ CAPABILITIES = cliArgs.caps?.split(",") || profile.capabilities || ["claude-code", "coding", "chat"];
490
714
  if (cliArgs.name && profile.display_name !== cliArgs.name) {
491
715
  profile.display_name = cliArgs.name;
492
716
  }
@@ -1748,10 +1972,10 @@ function mimeFromPath(p) {
1748
1972
  return MEDIA_MIME_BY_EXT[ext] ?? "application/octet-stream";
1749
1973
  }
1750
1974
  async function uploadLocalFile(path) {
1751
- if (!existsSync(path))
1975
+ if (!existsSync2(path))
1752
1976
  throw new Error(`file not found: ${path}`);
1753
1977
  const mime = mimeFromPath(path);
1754
- const buf = readFileSync(path);
1978
+ const buf = readFileSync2(path);
1755
1979
  const name = path.split("/").pop() || "upload";
1756
1980
  const form = new FormData;
1757
1981
  form.append("file", new Blob([new Uint8Array(buf)], { type: mime }), name);
@@ -2156,7 +2380,7 @@ ${a.body || ""}`;
2156
2380
  const currentVersion = Number(meta.version ?? 0);
2157
2381
  let cachedVersion = null;
2158
2382
  try {
2159
- cachedVersion = Number(JSON.parse(readFileSync(pMeta, "utf8")).version);
2383
+ cachedVersion = Number(JSON.parse(readFileSync2(pMeta, "utf8")).version);
2160
2384
  } catch {}
2161
2385
  if (cachedVersion !== null && cachedVersion === currentVersion) {
2162
2386
  return { content: [{ type: "text", text: `up-to-date: personal skill "${a.name}" v${currentVersion} already at ${pMd} \u2014 no download. Read that file to run it.` }] };
@@ -2166,8 +2390,8 @@ ${a.body || ""}`;
2166
2390
  return { content: [{ type: "text", text: `sync_skill (personal): body fetch failed (${bodyR.status})` }], isError: true };
2167
2391
  const doc = JSON.parse(await bodyR.text());
2168
2392
  mkdirSync(dirname(pMd), { recursive: true });
2169
- writeFileSync(pMd, String(doc?.body_markdown ?? ""));
2170
- writeFileSync(pMeta, JSON.stringify({ version: currentVersion, name: a.name, syncedAt: new Date().toISOString() }));
2393
+ writeFileSync3(pMd, String(doc?.body_markdown ?? ""));
2394
+ writeFileSync3(pMeta, JSON.stringify({ version: currentVersion, name: a.name, syncedAt: new Date().toISOString() }));
2171
2395
  return { content: [{ type: "text", text: `synced personal skill "${a.name}" v${currentVersion} \u2192 ${pMd} (was ${cachedVersion === null ? "missing" : `stale v${cachedVersion}`}). Read that file to run it.` }] };
2172
2396
  } catch (e) {
2173
2397
  return { content: [{ type: "text", text: `sync_skill (personal) error: ${String(e?.message || e).slice(0, 140)}` }], isError: true };
@@ -2190,7 +2414,7 @@ ${a.body || ""}`;
2190
2414
  const currentVersion = Number(meta.version ?? 0);
2191
2415
  let cachedVersion = null;
2192
2416
  try {
2193
- cachedVersion = Number(JSON.parse(readFileSync(metaPath, "utf8")).version);
2417
+ cachedVersion = Number(JSON.parse(readFileSync2(metaPath, "utf8")).version);
2194
2418
  } catch {}
2195
2419
  if (cachedVersion !== null && cachedVersion === currentVersion) {
2196
2420
  return { content: [{ type: "text", text: `up-to-date: "${a.doc_id}" v${currentVersion} already at ${mdPath} \u2014 no download. Read that file to run it.` }] };
@@ -2201,8 +2425,8 @@ ${a.body || ""}`;
2201
2425
  const doc = JSON.parse(await docR.text());
2202
2426
  const body = String(doc?.body_markdown ?? doc?.bodyMarkdown ?? "");
2203
2427
  mkdirSync(dirname(mdPath), { recursive: true });
2204
- writeFileSync(mdPath, body);
2205
- writeFileSync(metaPath, JSON.stringify({ version: currentVersion, title: doc?.title, doc_id: a.doc_id, chat_id: a.chat_id, syncedAt: new Date().toISOString() }));
2428
+ writeFileSync3(mdPath, body);
2429
+ writeFileSync3(metaPath, JSON.stringify({ version: currentVersion, title: doc?.title, doc_id: a.doc_id, chat_id: a.chat_id, syncedAt: new Date().toISOString() }));
2206
2430
  const was = cachedVersion === null ? "missing" : `stale v${cachedVersion}`;
2207
2431
  return { content: [{ type: "text", text: `synced "${doc?.title || a.doc_id}" v${currentVersion} \u2192 ${mdPath} (was ${was}). Read that file to run it in your runtime.` }] };
2208
2432
  } catch (e) {
@@ -2869,7 +3093,7 @@ ${authLine}
2869
3093
  ${claimedLine}${claimHint ? `
2870
3094
  ${claimHint}` : ""}
2871
3095
  Capabilities: ${CAPABILITIES.join(", ")}
2872
- Profile file: ${profileFile}` }] };
3096
+ Profile file: ${activeProfileFile ?? (anonymousMode ? "(none \u2014 anonymous, no profile written)" : "(none \u2014 credentials from environment)")}` }] };
2873
3097
  }
2874
3098
  if (name === "list_channels") {
2875
3099
  const { limit = 50 } = args;
@@ -3042,10 +3266,10 @@ Available profiles:
3042
3266
  ${list}` }] };
3043
3267
  }
3044
3268
  const targetFile = nameToPath(profile_name);
3045
- if (!existsSync(targetFile)) {
3269
+ if (!existsSync2(targetFile)) {
3046
3270
  return { content: [{ type: "text", text: `Profile "${profile_name}" not found. Available: ${available.join(", ")}` }], isError: true };
3047
3271
  }
3048
- const newProfile = JSON.parse(readFileSync(targetFile, "utf-8"));
3272
+ const newProfile = JSON.parse(readFileSync2(targetFile, "utf-8"));
3049
3273
  heartbeat.stop();
3050
3274
  if (backfillTimer) {
3051
3275
  clearTimeout(backfillTimer);
@@ -3067,6 +3291,8 @@ ${list}` }] };
3067
3291
  TOKEN = newProfile.token || "dev-token";
3068
3292
  CAPABILITIES = newProfile.capabilities || ["claude-code", "coding", "chat"];
3069
3293
  profile = newProfile;
3294
+ activeProfileFile = targetFile;
3295
+ anonymousMode = false;
3070
3296
  wsReconnectAttempt = 0;
3071
3297
  heartbeat.start();
3072
3298
  connectWS();
@@ -3421,49 +3647,31 @@ ${list}` }] };
3421
3647
  });
3422
3648
  var mentionTsFile = join(configDir, `mention-ts-${AGENT_ID}.json`);
3423
3649
  function loadMentionTimestamps() {
3424
- try {
3425
- const raw = readFileSync(mentionTsFile, "utf-8");
3426
- return new Map(Object.entries(JSON.parse(raw)));
3427
- } catch {
3428
- return new Map;
3429
- }
3650
+ return loadCursor(mentionTsFile, safeStderrWrite);
3430
3651
  }
3431
3652
  function saveMentionTimestamps(m) {
3432
- try {
3433
- writeFileSync(mentionTsFile, JSON.stringify(Object.fromEntries(m)));
3434
- } catch {}
3653
+ persistCursor(mentionTsFile, m, safeStderrWrite);
3435
3654
  }
3436
3655
  var lastMentionTimestamp = loadMentionTimestamps();
3437
3656
  var lastSeenMessageTsFile = join(configDir, `last-seen-msg-ts-${AGENT_ID}.json`);
3438
3657
  function loadLastSeenMessageTs() {
3439
- try {
3440
- const raw = readFileSync(lastSeenMessageTsFile, "utf-8");
3441
- return new Map(Object.entries(JSON.parse(raw)));
3442
- } catch {
3443
- return new Map;
3444
- }
3445
- }
3446
- function saveLastSeenMessageTs(m) {
3447
- try {
3448
- writeFileSync(lastSeenMessageTsFile, JSON.stringify(Object.fromEntries(m)));
3449
- } catch {}
3658
+ return loadCursor(lastSeenMessageTsFile, safeStderrWrite);
3450
3659
  }
3451
3660
  var lastSeenMessageTs = loadLastSeenMessageTs();
3452
3661
  var cursorFlushIntervalMs = Math.max(500, Number(process.env.AGENTSCHAT_MCP_CURSOR_FLUSH_MS || 5000));
3453
- var lastSeenMessageTsDirty = false;
3662
+ var cursorState = { dirty: false };
3454
3663
  var lastSeenMessageTsTimer = null;
3455
3664
  function flushLastSeenMessageTs() {
3456
- if (!lastSeenMessageTsDirty)
3665
+ if (!cursorState.dirty)
3457
3666
  return;
3458
- lastSeenMessageTsDirty = false;
3459
3667
  if (lastSeenMessageTsTimer) {
3460
3668
  clearTimeout(lastSeenMessageTsTimer);
3461
3669
  lastSeenMessageTsTimer = null;
3462
3670
  }
3463
- saveLastSeenMessageTs(lastSeenMessageTs);
3671
+ flushCursor(cursorState, () => persistCursor(lastSeenMessageTsFile, lastSeenMessageTs, safeStderrWrite));
3464
3672
  }
3465
3673
  function scheduleLastSeenMessageTsSave() {
3466
- lastSeenMessageTsDirty = true;
3674
+ cursorState.dirty = true;
3467
3675
  if (lastSeenMessageTsTimer)
3468
3676
  return;
3469
3677
  lastSeenMessageTsTimer = setTimeout(() => {
@@ -3943,7 +4151,10 @@ function shutdownFromStdio(reason) {
3943
4151
  `);
3944
4152
  try {
3945
4153
  flushLastSeenMessageTs();
3946
- } catch {}
4154
+ } catch (e) {
4155
+ safeStderrWrite(`[agentchat] WARNING: read-cursor flush failed on shutdown: ${e}
4156
+ `);
4157
+ }
3947
4158
  try {
3948
4159
  heartbeat.stop();
3949
4160
  } catch {}
@@ -3987,7 +4198,10 @@ function installStdioLifecycleGuards() {
3987
4198
  process.on("beforeExit", () => {
3988
4199
  try {
3989
4200
  flushLastSeenMessageTs();
3990
- } catch {}
4201
+ } catch (e) {
4202
+ safeStderrWrite(`[agentchat] WARNING: read-cursor fallback flush failed: ${e}
4203
+ `);
4204
+ }
3991
4205
  });
3992
4206
  }
3993
4207
  async function checkVersionStaleness() {
@@ -4008,7 +4222,12 @@ async function checkVersionStaleness() {
4008
4222
  }
4009
4223
  async function main() {
4010
4224
  installStdioLifecycleGuards();
4011
- connectWS();
4225
+ if (anonymousMode) {
4226
+ process.stderr.write(`[agentchat] Anonymous \u2014 not connecting to the hub.
4227
+ `);
4228
+ } else {
4229
+ connectWS();
4230
+ }
4012
4231
  transport = new StdioServerTransport;
4013
4232
  await server.connect(transport);
4014
4233
  process.stderr.write(`[agentchat] MCP server started (Stdio)