agentschat-mcp 0.36.7 → 0.36.9

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.
@@ -0,0 +1,45 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+
4
+ export interface ProjectProfileSelection {
5
+ selector: string;
6
+ source: "project-config" | "project-profile";
7
+ agentId?: string;
8
+ apiUrl?: string;
9
+ wsUrl?: string;
10
+ }
11
+
12
+ /** Read only this project's binding. Never scan parents or choose a sibling bot. */
13
+ export function projectProfileSelector(cwd: string): ProjectProfileSelection | undefined {
14
+ const config = join(cwd, ".agentschat/config.json");
15
+ let doc: any = {};
16
+ if (existsSync(config)) {
17
+ try { doc = JSON.parse(readFileSync(config, "utf8")); }
18
+ catch { throw new Error("Invalid project .agentschat/config.json; repair it before choosing an identity"); }
19
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw new Error("Invalid project identity configuration");
20
+ for (const field of ["profile", "agent_id", "api_url", "ws_url"]) {
21
+ if (doc[field] !== undefined && (typeof doc[field] !== "string" || !doc[field].trim())) throw new Error(`Invalid project ${field}`);
22
+ }
23
+ for (const [field, protocols] of [["api_url", ["https:", "http:"]], ["ws_url", ["wss:", "ws:"]]] as const) {
24
+ if (doc[field] === undefined) continue;
25
+ let url: URL;
26
+ try { url = new URL(doc[field]); } catch { throw new Error(`Invalid project ${field}`); }
27
+ if (!(protocols as readonly string[]).includes(url.protocol) || url.username || url.password || url.search || url.hash ||
28
+ (!url.protocol.endsWith("s:") && !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)))
29
+ throw new Error(`Invalid project ${field}: require TLS (except loopback) and no credentials/query`);
30
+ }
31
+ }
32
+ const settings = {
33
+ ...(doc.agent_id !== undefined ? { agentId: doc.agent_id as string } : {}),
34
+ ...(doc.api_url !== undefined ? { apiUrl: doc.api_url as string } : {}),
35
+ ...(doc.ws_url !== undefined ? { wsUrl: doc.ws_url as string } : {}),
36
+ };
37
+ if (doc.profile !== undefined) {
38
+ const value = doc.profile as string;
39
+ return { selector: !isAbsolute(value) && !value.startsWith("~/") && value.includes("/") ? resolve(cwd, value) : value, source: "project-config", ...settings };
40
+ }
41
+ const profile = join(cwd, ".agentschat/profile.json");
42
+ if (existsSync(profile)) return { selector: profile, source: "project-profile", ...settings };
43
+ // A declared agent/server scope must never silently disappear into a global identity.
44
+ if (Object.keys(settings).length) throw new Error("Project identity/server settings require a profile binding");
45
+ }
package/src/server.ts CHANGED
@@ -43,6 +43,7 @@ import { computeReconnectDelay } from "./reconnect.ts";
43
43
  import { normalizeTimestampForCursor } from "./timestamps.ts";
44
44
  import { validateToolArgs } from "./argcheck.ts";
45
45
  import { decideIdentity, shouldMigrateDevToken, validateIdentityProfile } from "./identity.ts";
46
+ import { projectProfileSelector } from "./profile-selection.ts";
46
47
  import type { ProfileSource } from "./identity.ts";
47
48
  import {
48
49
  decideGrokBind,
@@ -123,7 +124,8 @@ Setup guide for every supported host:
123
124
  Read skills/onboarding.md relative to this installed package root.
124
125
  Online: https://unpkg.com/agentschat-mcp@latest/skills/onboarding.md
125
126
  Start with: npx -y agentschat-mcp@latest --help
126
- The guide selects Claude Code, Codex, OpenClaw, Hermes, Grok Bot, or URL wake.
127
+ The guide selects Claude Code, Codex, OpenClaw, Hermes, Grok Bot, URL wake,
128
+ or the separately deployed dot remote MCP + Events integration.
127
129
  Help only prints instructions; follow the guide and verify a real reply.
128
130
 
129
131
  Options:
@@ -167,7 +169,7 @@ Grok multi-bot identity bind (Cursor / Grok Bot, no --profile):
167
169
  Hermes relay connector (no Hermes patch): run with --connector. See --connector --help.
168
170
 
169
171
  Identity is never created implicitly. Without explicit selectors, an existing
170
- default profile or Grok binding is loaded; only when neither exists and no
172
+ project/runtime binding or default profile is loaded; only when none exists and no
171
173
  credentials are supplied does the server run ANONYMOUS (lists tools, no account).
172
174
 
173
175
  Profiles stored in: ~/.agentschat/ (legacy fallback: ~/.agentchat/)
@@ -175,34 +177,32 @@ Docs: https://github.com/swswordholy-tech/AgentsChatProtocol`);
175
177
  process.exit(0);
176
178
  }
177
179
 
178
- // Profile resolution priority:
179
- // 1. AGENTSCHAT_PROFILE env var (name or path; canonical plural)
180
- // 2. AGENTCHAT_PROFILE env var (legacy singular)
181
- // 3. --profile <name> CLI arg
182
- // 4. --name <name> CLI arg (also used as profile name)
183
- // 5. Grok bind: CURSOR_CONVERSATION_ID → ~/.agentschat/grok-binds.json
184
- // (skipped if an explicit token is set; does NOT imply AGENTCHAT_WAKE_MODE)
185
- // 6. default ~/.agentschat/profile.json, falling back to ~/.agentchat/profile.json
180
+ // Explicit CLI selection > project binding > runtime binding > environment > default.
181
+ // Onboarding resolves ambiguous unbound candidates with the user; never scan-and-pick.
186
182
  const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
187
183
  const configDir = join(homeDir, ".agentschat");
188
184
  const legacyConfigDir = join(homeDir, ".agentchat");
189
185
  const profileDirs = [configDir, legacyConfigDir];
190
186
 
191
187
  function profileNameToPaths(name: string): string[] {
188
+ if (name.startsWith("~/")) return [join(homeDir, name.slice(2))];
192
189
  if (name.includes("/") || name.includes("\\")) return [name]; // explicit path
190
+ name = name.replace(/\.json$/, "");
193
191
  const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
194
192
  return profileDirs.map((dir) => join(dir, `${safeName}.json`));
195
193
  }
196
194
 
197
195
  function nameToPath(name: string): string {
198
196
  const candidates = profileNameToPaths(name);
197
+ const nested = join(configDir, "profiles", name.replace(/\.json$/, "") + ".json");
198
+ if (!name.includes("/") && !name.includes("\\") && existsSync(nested)) return nested;
199
199
  return candidates.find((path) => existsSync(path)) || candidates[0];
200
200
  }
201
201
 
202
202
  function listProfileFiles(): Array<{ name: string; path: string }> {
203
203
  const seen = new Set<string>();
204
204
  const profiles: Array<{ name: string; path: string }> = [];
205
- for (const dir of profileDirs) {
205
+ for (const dir of [join(configDir, "profiles"), ...profileDirs]) {
206
206
  let files: string[] = [];
207
207
  try { files = readdirSync(dir).filter((f: string) => f.endsWith(".json")); } catch {}
208
208
  for (const file of files) {
@@ -216,19 +216,18 @@ function listProfileFiles(): Array<{ name: string; path: string }> {
216
216
  return profiles;
217
217
  }
218
218
 
219
- function resolveProfile(): { path: string; source: ProfileSource; declaredName?: string } {
220
- // 1. AGENTSCHAT_PROFILE env var (supports both name and full path)
221
- if (process.env.AGENTSCHAT_PROFILE)
222
- return { path: nameToPath(process.env.AGENTSCHAT_PROFILE), source: "env", declaredName: process.env.AGENTSCHAT_PROFILE };
223
- // 2. AGENTCHAT_PROFILE env var (legacy alias)
224
- if (process.env.AGENTCHAT_PROFILE)
225
- return { path: nameToPath(process.env.AGENTCHAT_PROFILE), source: "legacy-env", declaredName: process.env.AGENTCHAT_PROFILE };
226
- // 3. --profile <name>
219
+ function resolveProfile(): { path: string; source: ProfileSource; declaredName?: string; agentId?: string; apiUrl?: string; wsUrl?: string } {
227
220
  if (cliArgs.profile) return { path: nameToPath(cliArgs.profile), source: "flag-profile", declaredName: cliArgs.profile };
228
- // 4. --name <name>
229
221
  if (cliArgs.name) return { path: nameToPath(cliArgs.name), source: "flag-name", declaredName: cliArgs.name };
230
222
 
231
- // 5. Grok outbound bind. Explicit env/flags already returned above. An
223
+ // Paired direct credentials are a separate explicit authentication path.
224
+ // Do not borrow an unrelated project's identity when credentials are supplied.
225
+ if (!cliArgs.token && !process.env.AGENTCHAT_TOKEN) {
226
+ const project = projectProfileSelector(process.cwd());
227
+ if (project) return { ...project, path: nameToPath(project.selector), declaredName: project.selector };
228
+ }
229
+
230
+ // Grok outbound bind. Explicit CLI/project selections already returned above. An
232
231
  // explicit --token/AGENTCHAT_TOKEN also wins: do not steal identity from
233
232
  // operator-supplied creds. CURSOR_CONVERSATION_ID unset → skip entirely
234
233
  // (Claude Code / Hermes: zero change). A hit with a missing profile file
@@ -271,11 +270,16 @@ function resolveProfile(): { path: string; source: ProfileSource; declaredName?:
271
270
  process.stderr.write(`[agentchat] no grok-bind matched CURSOR_CONVERSATION_ID=${grok.conversationId}\n`);
272
271
  }
273
272
 
274
- // 6. default — nothing was declared. NOT a licence to invent an identity.
273
+ if (process.env.AGENTSCHAT_PROFILE)
274
+ return { path: nameToPath(process.env.AGENTSCHAT_PROFILE), source: "env", declaredName: process.env.AGENTSCHAT_PROFILE };
275
+ if (process.env.AGENTCHAT_PROFILE)
276
+ return { path: nameToPath(process.env.AGENTCHAT_PROFILE), source: "legacy-env", declaredName: process.env.AGENTCHAT_PROFILE };
277
+
278
+ // Default — nothing was declared. NOT a licence to invent an identity.
275
279
  return { path: nameToPath("profile"), source: "default" };
276
280
  }
277
281
 
278
- const { path: profileFile, source: profileSource, declaredName } = resolveProfile();
282
+ const { path: profileFile, source: profileSource, declaredName, agentId: projectAgentId, apiUrl: projectApiUrl, wsUrl: projectWsUrl } = resolveProfile();
279
283
  /**
280
284
  * The profile file currently in effect. Mutable because `switch_profile` swaps
281
285
  * identity at runtime — `whoami` must report the live one, not the boot-time one.
@@ -287,8 +291,8 @@ let anonymousMode = false;
287
291
  let profile: any = {};
288
292
 
289
293
  const DEFAULT_SERVER = "https://agents-chat.com";
290
- const serverUrl = (cliArgs.url || process.env.AGENTCHAT_REST_URL || DEFAULT_SERVER).replace(/\/$/, "");
291
- const WS_URL = process.env.AGENTCHAT_URL || (() => {
294
+ const serverUrl = (cliArgs.url || projectApiUrl || process.env.AGENTCHAT_REST_URL || DEFAULT_SERVER).replace(/\/$/, "");
295
+ const WS_URL = (!cliArgs.url && projectWsUrl) || (!projectApiUrl && process.env.AGENTCHAT_URL) || (() => {
292
296
  const base = serverUrl.replace("https://", "wss://").replace("http://", "ws://");
293
297
  return base.endsWith("/ws") ? base : base + "/ws";
294
298
  })();
@@ -363,6 +367,9 @@ function readIdentityProfile(file: string): any {
363
367
  if (identity.mode === "profile") {
364
368
  try {
365
369
  profile = readIdentityProfile(profileFile);
370
+ if (projectAgentId !== undefined && projectAgentId !== (cliArgs.id ?? process.env.AGENTCHAT_AGENT_ID ?? profile?.agent_id)) {
371
+ throw new Error("Project agent_id does not match selected profile");
372
+ }
366
373
  validateIdentityProfile(profile && !Array.isArray(profile) ? {
367
374
  ...profile,
368
375
  agent_id: cliArgs.id ?? process.env.AGENTCHAT_AGENT_ID ?? profile.agent_id,