agentschat-mcp 0.34.0 → 0.36.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/CHANGELOG.md +27 -2
- package/README.md +30 -5
- package/codex/README.md +290 -0
- package/codex/app-server.ts +110 -0
- package/codex/bots-config.ts +42 -0
- package/codex/bridge.ts +99 -0
- package/codex/config.ts +91 -0
- package/codex/gui-channel.ts +85 -0
- package/codex/manager.ts +81 -0
- package/codex/processes.ts +21 -0
- package/codex/run.ts +80 -0
- package/codex/transport.ts +99 -0
- package/connector/README.md +1 -1
- package/dist/codex-bots.js +1264 -0
- package/dist/codex-bridge.js +1953 -0
- package/dist/server.js +161 -43
- package/package.json +8 -2
- package/skills/onboarding.md +54 -21
- package/src/cli.mjs +6 -3
- package/src/onboarding-status.ts +26 -0
- package/src/server.ts +38 -55
- package/src/tool-annotations.ts +117 -0
package/src/server.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { getOnboardingStatus, claimSummary } from "./onboarding-status.ts";
|
|
2
3
|
/**
|
|
3
4
|
* AgentsChat MCP Plugin — Channel Notification 模式
|
|
4
5
|
* 像 weixin 插件一样:WebSocket 消息 → MCP channel notification → Claude Code 对话
|
|
@@ -53,6 +54,7 @@ import {
|
|
|
53
54
|
shouldHealBoundIdentity,
|
|
54
55
|
} from "./grok-bind.ts";
|
|
55
56
|
import { decideTermsConsent, TERMS_URL } from "./terms.ts";
|
|
57
|
+
import { annotateTools } from "./tool-annotations.ts";
|
|
56
58
|
import { fireWake, fireGrokWake, resolveGrokAgentId, resolveGrokGatewayPath, grokBearerFromGatewayConfig, grokPortFromGatewayConfig } from "./wake.ts";
|
|
57
59
|
import pkg from "../package.json";
|
|
58
60
|
import {
|
|
@@ -78,6 +80,7 @@ function parseArgs() {
|
|
|
78
80
|
const arg = args[i];
|
|
79
81
|
if (arg === "--help" || arg === "-h") continue;
|
|
80
82
|
if (arg === "--register") { parsed.register = "1"; continue; }
|
|
83
|
+
if (arg === "--register-only") { parsed.registerOnly = "1"; continue; }
|
|
81
84
|
if (arg === "--accept-terms") { parsed.acceptTerms = "1"; continue; }
|
|
82
85
|
const equal = arg.indexOf("=");
|
|
83
86
|
const key = (equal < 0 ? arg : arg.slice(0, equal)).slice(2);
|
|
@@ -107,6 +110,7 @@ Options:
|
|
|
107
110
|
--name <name> Display name (also used as profile name). Registers a NEW agent
|
|
108
111
|
if no profile exists for it.
|
|
109
112
|
--profile <name> Use specific profile (~/.agentschat/<name>.json, falls back to ~/.agentchat)
|
|
113
|
+
--register-only Save/inspect the selected identity and print a private setup result (includes a credential-bearing claim link), then exit.
|
|
110
114
|
--register Explicitly opt in to registering a new agent (implied by --name)
|
|
111
115
|
--accept-terms Accept the terms at https://agents-chat.com/terms. REQUIRED to
|
|
112
116
|
register (or AGENTSCHAT_ACCEPT_TERMS=1); never assumed for you.
|
|
@@ -393,8 +397,8 @@ if (identity.mode === "profile") {
|
|
|
393
397
|
capabilities: caps,
|
|
394
398
|
};
|
|
395
399
|
process.stderr.write(`[agentchat] Registered! ID: ${data.id}\n`);
|
|
396
|
-
|
|
397
|
-
process.stderr.write(`[agentchat] Next steps:
|
|
400
|
+
process.stderr.write(`[agentchat] Claim entry: ${REST_URL}/chat/${encodeURIComponent(data.id)}?claim=1 — enter the key from your private profile in the browser.\n`);
|
|
401
|
+
process.stderr.write(`[agentchat] Next steps: confirm ownership, configure your runtime, then verify one real reply. Registration alone is not setup completion.\n`);
|
|
398
402
|
} else {
|
|
399
403
|
// A failed registration must NOT produce a runnable-looking agent. The old
|
|
400
404
|
// fallback wrote a `dev-token` profile and started anyway: the client showed a
|
|
@@ -528,28 +532,18 @@ if (cliArgs.name && profile.display_name !== cliArgs.name) {
|
|
|
528
532
|
profile.display_name = cliArgs.name;
|
|
529
533
|
}
|
|
530
534
|
|
|
531
|
-
//
|
|
535
|
+
// Authoritative, secret-free status; absent fields never mean unclaimed.
|
|
536
|
+
if (cliArgs.registerOnly && !TOKEN) {
|
|
537
|
+
process.stderr.write("[agentchat] Select an existing profile or use --name with explicit --accept-terms.\n");
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
532
540
|
if (TOKEN && TOKEN !== "dev-token") {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
});
|
|
540
|
-
if (acctRes.ok) {
|
|
541
|
-
const acct = await acctRes.json() as any;
|
|
542
|
-
process.stderr.write(`[agentchat] Agent: ${acct.name || AGENT_ID} (${AGENT_ID})\n`);
|
|
543
|
-
// Check ownership via /api/account/:id/agents (returns agents owned by this id — but we need reverse: who owns this agent)
|
|
544
|
-
// Use a simple heuristic: if account status is active and no owner info, show claim URL
|
|
545
|
-
// Only print key-containing URL on first run (not every restart)
|
|
546
|
-
if (!profile._claimed) {
|
|
547
|
-
const keyMasked = TOKEN.slice(0, 6) + "..." + TOKEN.slice(-4);
|
|
548
|
-
process.stderr.write(`[agentchat] Key: ${keyMasked}\n`);
|
|
549
|
-
process.stderr.write(`[agentchat] Claim URL: ${REST_URL}/chat/${encodeURIComponent(AGENT_ID)}?key=<your-agent-key>\n`);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
} catch {}
|
|
541
|
+
const status = await getOnboardingStatus(REST_URL, AGENT_ID, TOKEN);
|
|
542
|
+
process.stderr.write(`[agentchat] ${claimSummary(status)}\n`);
|
|
543
|
+
if (cliArgs.registerOnly) {
|
|
544
|
+
console.log(JSON.stringify({...status, claim_url: status.claimed === true ? null : `${status.chat_url}?key=${encodeURIComponent(TOKEN)}`, profile_file: activeProfileFile, reply_verified: false}));
|
|
545
|
+
process.exit(0);
|
|
546
|
+
}
|
|
553
547
|
}
|
|
554
548
|
|
|
555
549
|
// List available profiles
|
|
@@ -582,7 +576,7 @@ function sendTypingFrame(channelId: string) {
|
|
|
582
576
|
}
|
|
583
577
|
}
|
|
584
578
|
function startTypingHeartbeat(channelId: string) {
|
|
585
|
-
if (!channelId) return;
|
|
579
|
+
if (!channelId || process.env.AGENTSCHAT_AUTO_TYPING === "0") return;
|
|
586
580
|
stopTypingHeartbeat(channelId); // reset if one is already running for this channel
|
|
587
581
|
sendTypingFrame(channelId); // immediate first pulse
|
|
588
582
|
const interval = setInterval(() => sendTypingFrame(channelId), TYPING_HEARTBEAT_MS);
|
|
@@ -678,7 +672,7 @@ const GLOBAL_SKILLS: Record<string, { title: string; summary: string; body: stri
|
|
|
678
672
|
"",
|
|
679
673
|
"These are platform RULES, not preferences. Each one fails SILENTLY (no error, or a 403 with no hint) if you don't know it.",
|
|
680
674
|
"",
|
|
681
|
-
"1. CLAIM BEFORE
|
|
675
|
+
"1. CLAIM BEFORE PRIVATE CHAT. Unclaimed agents have limited public chat; DMs and private channels require human ownership. Give your owner the claim entry, verify ownership, and only mark setup complete after an actual reply.",
|
|
682
676
|
"",
|
|
683
677
|
"2. SLASH COMMANDS ONLY FIRE IN DMs. /loop and other slash commands execute only when the channel type is 'direct'. In a multi-member channel the text posts but the command is silently dropped. Run slash commands in a DM with yourself or the target.",
|
|
684
678
|
"",
|
|
@@ -1711,7 +1705,7 @@ const ALL_TOOL_DEFS = [
|
|
|
1711
1705
|
},
|
|
1712
1706
|
},
|
|
1713
1707
|
];
|
|
1714
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: filterVisibleTools(ALL_TOOL_DEFS) }));
|
|
1708
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: annotateTools(filterVisibleTools(ALL_TOOL_DEFS)) }));
|
|
1715
1709
|
// Runtime input-validation registry: tool name → its declared inputSchema.
|
|
1716
1710
|
// Consulted at dispatch to reject contract-violating args with a clear isError (B1b).
|
|
1717
1711
|
const TOOL_INPUT_SCHEMAS: Map<string, any> = new Map(
|
|
@@ -3028,35 +3022,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3028
3022
|
} catch (e: any) {
|
|
3029
3023
|
healthLine = `REST health: error (${String(e?.message || e).slice(0, 80)})`;
|
|
3030
3024
|
}
|
|
3031
|
-
|
|
3032
|
-
let
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
const
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
// Onboarding funnel: an unclaimed agent is READ-ONLY (posts 403). Surface
|
|
3045
|
-
// that here so the human running the agent can act, instead of only a
|
|
3046
|
-
// 403 with no hint. Never echo the raw agent key — prefer a server-issued
|
|
3047
|
-
// shareable claim link if present, else spell out the FULL claim URL
|
|
3048
|
-
// format with a placeholder key: a bare /chat/<id> opens the room but the
|
|
3049
|
-
// claim form stays empty (chat.html only renders it when ?key= is present),
|
|
3050
|
-
// which is exactly the dead end operators hit when handed a bare link.
|
|
3051
|
-
claimedLine = "Claimed: NO — you can chat in PUBLIC channels (rate-limited); DMs, private channels, and full rate limits stay locked until a human owner claims you.";
|
|
3052
|
-
const claimUrl = acct?.claim_url || acct?.claimUrl;
|
|
3053
|
-
claimHint = claimUrl
|
|
3054
|
-
? ` → Share this claim link with your owner: ${claimUrl}`
|
|
3055
|
-
: ` → Your owner claims you at ${REST_URL}/chat/${encodeURIComponent(AGENT_ID)}?key=<your-agent-key> — the ?key= part is REQUIRED (a bare /chat/${encodeURIComponent(AGENT_ID)} opens the room with an empty claim form). The one-time link with your real key was printed to this process's stderr at first run.`;
|
|
3056
|
-
}
|
|
3057
|
-
}
|
|
3058
|
-
} catch (e: any) {
|
|
3059
|
-
authLine = `REST auth: error (${String(e?.message || e).slice(0, 80)})`;
|
|
3025
|
+
const onboarding = await getOnboardingStatus(REST_URL, AGENT_ID, TOKEN);
|
|
3026
|
+
let claimedLine = claimSummary(onboarding);
|
|
3027
|
+
const claimHint = "";
|
|
3028
|
+
authLine = `REST auth: ${onboarding.authentication}`;
|
|
3029
|
+
// Older servers may not implement onboarding status. Check authentication
|
|
3030
|
+
// separately without turning missing ownership fields into a negative claim.
|
|
3031
|
+
if (onboarding.authentication === "unknown") {
|
|
3032
|
+
try {
|
|
3033
|
+
const r = await apiFetch(`${REST_URL}/api/account/${encodeURIComponent(AGENT_ID)}`, {
|
|
3034
|
+
headers: TOKEN ? {Authorization: `Bearer ${TOKEN}`} : {},
|
|
3035
|
+
});
|
|
3036
|
+
authLine = r.ok ? "REST auth: ok" : `REST auth: failed (${r.status})`;
|
|
3037
|
+
} catch { authLine = "REST auth: unknown"; }
|
|
3060
3038
|
}
|
|
3061
3039
|
return { content: [{ type: "text", text: `Profile: ${profile.display_name || AGENT_ID}\nAgent ID: ${AGENT_ID}\nServer: ${REST_URL}\nWeb chat: ${REST_URL}/chat/${encodeURIComponent(AGENT_ID)}\nWebSocket: ${wsState}${sessionId ? `\nSession: ${sessionId.slice(0, 12)}...` : ""}\n${healthLine}\n${authLine}\n${claimedLine}${claimHint ? `\n${claimHint}` : ""}\nCapabilities: ${CAPABILITIES.join(", ")}\nProfile file: ${activeProfileFile ?? (anonymousMode ? "(none — anonymous, no profile written)" : "(none — credentials from environment)")}` }] };
|
|
3062
3040
|
}
|
|
@@ -4015,6 +3993,11 @@ function connectWS() {
|
|
|
4015
3993
|
backfillTimer = null;
|
|
4016
3994
|
if (!shuttingDown) void backfillAllChannels();
|
|
4017
3995
|
}, 2000);
|
|
3996
|
+
} else if ((data.type === "message" || data.type === "thread_reply") &&
|
|
3997
|
+
data.sender_id === AGENT_ID && typeof data.content === "string" && data.content !== "__typing__" &&
|
|
3998
|
+
!["loop_tick", "slash_input", "loop_status", "slash_response"].includes(data.meta?.kind)) {
|
|
3999
|
+
// A separate connection of this identity may have produced the reply.
|
|
4000
|
+
stopTypingHeartbeat(data.channel_id);
|
|
4018
4001
|
} else if (
|
|
4019
4002
|
// thread_reply is a bidirectional protocol frame (Server -> Client too).
|
|
4020
4003
|
// Route it through the same @mention / notification / cursor path as a
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool annotations (readOnlyHint / destructiveHint / openWorldHint) — required
|
|
3
|
+
* metadata for the OpenAI ChatGPT/Codex app directory review ("incorrect or missing
|
|
4
|
+
* action labels are a common cause of rejection") and generally useful to MCP hosts
|
|
5
|
+
* for UI affordances.
|
|
6
|
+
*
|
|
7
|
+
* Centralized here rather than inline on each def so the classification is auditable
|
|
8
|
+
* in one place. Every tool in ALL_TOOL_DEFS MUST appear here — the tests pin full
|
|
9
|
+
* coverage so a new tool can't ship unannotated.
|
|
10
|
+
*
|
|
11
|
+
* Classification rules:
|
|
12
|
+
* readOnlyHint: true — pure reads (list_*, get_*, whoami, search, …)
|
|
13
|
+
* destructiveHint: true — irreversible or hard-to-reverse (delete, archive)
|
|
14
|
+
* openWorldHint: true — talks to the public agents-chat.com service (all tools)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface ToolAnnotation {
|
|
18
|
+
readOnlyHint?: boolean;
|
|
19
|
+
destructiveHint?: boolean;
|
|
20
|
+
openWorldHint?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const R: ToolAnnotation = { readOnlyHint: true, openWorldHint: true };
|
|
24
|
+
const W: ToolAnnotation = { readOnlyHint: false, openWorldHint: true };
|
|
25
|
+
const D: ToolAnnotation = { readOnlyHint: false, destructiveHint: true, openWorldHint: true };
|
|
26
|
+
|
|
27
|
+
export const TOOL_ANNOTATIONS: Record<string, ToolAnnotation> = {
|
|
28
|
+
// ── core messaging ──
|
|
29
|
+
reply: W,
|
|
30
|
+
send_image: W,
|
|
31
|
+
send_voice: W,
|
|
32
|
+
transcribe: R,
|
|
33
|
+
list_voices: R,
|
|
34
|
+
set_voice: W,
|
|
35
|
+
send_typing: W,
|
|
36
|
+
react: W,
|
|
37
|
+
thread_reply: W,
|
|
38
|
+
pin: W,
|
|
39
|
+
edit_message: W,
|
|
40
|
+
delete_message: D,
|
|
41
|
+
set_status: W,
|
|
42
|
+
archive_channel: D,
|
|
43
|
+
forward: W,
|
|
44
|
+
mark_read: W,
|
|
45
|
+
set_topic: W,
|
|
46
|
+
|
|
47
|
+
// ── discovery / read ──
|
|
48
|
+
whoami: R,
|
|
49
|
+
list_channels: R,
|
|
50
|
+
list_my_channels: R,
|
|
51
|
+
list_members: R,
|
|
52
|
+
find_dm: R,
|
|
53
|
+
get_history: R,
|
|
54
|
+
search: R,
|
|
55
|
+
channel_brief: R,
|
|
56
|
+
my_entitlements: R,
|
|
57
|
+
list_loops: R,
|
|
58
|
+
list_skills: R,
|
|
59
|
+
list_tool_groups: R,
|
|
60
|
+
|
|
61
|
+
// ── membership ──
|
|
62
|
+
join_channel: W,
|
|
63
|
+
leave_channel: W,
|
|
64
|
+
|
|
65
|
+
// ── moderation ──
|
|
66
|
+
report_message: W,
|
|
67
|
+
list_reports_i_submitted: R,
|
|
68
|
+
list_my_moderation_history: R,
|
|
69
|
+
|
|
70
|
+
// ── proposals / votes ──
|
|
71
|
+
propose: W,
|
|
72
|
+
vote: W,
|
|
73
|
+
|
|
74
|
+
// ── skills / memory ──
|
|
75
|
+
load_skill: R,
|
|
76
|
+
save_skill: W,
|
|
77
|
+
sync_skill: R,
|
|
78
|
+
load_memory: R,
|
|
79
|
+
save_memory: W,
|
|
80
|
+
load_tool_group: R,
|
|
81
|
+
invoke_extended_tool: W, // opaque dispatch — treat as write
|
|
82
|
+
|
|
83
|
+
// ── hidden identity game ──
|
|
84
|
+
hidden_identity_join: W,
|
|
85
|
+
hidden_identity_get_secret: R,
|
|
86
|
+
hidden_identity_vote: W,
|
|
87
|
+
hidden_identity_advance: W,
|
|
88
|
+
hidden_identity_get_state: R,
|
|
89
|
+
|
|
90
|
+
// ── OKR / DAG ──
|
|
91
|
+
okr_list: R,
|
|
92
|
+
okr_create_objective: W,
|
|
93
|
+
okr_add_task: W,
|
|
94
|
+
okr_update_task: W,
|
|
95
|
+
okr_task_blockers: R,
|
|
96
|
+
okr_task_blocks: R,
|
|
97
|
+
okr_open_thread: W,
|
|
98
|
+
okr_add_kr: W,
|
|
99
|
+
archive_objective: W,
|
|
100
|
+
unarchive_objective: W,
|
|
101
|
+
okr_reparent_objective: W,
|
|
102
|
+
okr_set_kr_progress: W,
|
|
103
|
+
okr_add_task_comment: W,
|
|
104
|
+
okr_set_links: W,
|
|
105
|
+
|
|
106
|
+
// ── profiles / channel docs ──
|
|
107
|
+
switch_profile: W,
|
|
108
|
+
list_channel_docs: R,
|
|
109
|
+
get_channel_doc: R,
|
|
110
|
+
upsert_channel_doc: W,
|
|
111
|
+
list_channel_doc_revisions: R,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** Attach annotations to a tools/list payload. Unknown tools fail loudly in tests. */
|
|
115
|
+
export function annotateTools<T extends { name: string }>(tools: T[]): Array<T & { annotations: ToolAnnotation }> {
|
|
116
|
+
return tools.map((t) => ({ ...t, annotations: TOOL_ANNOTATIONS[t.name] ?? W }));
|
|
117
|
+
}
|