Skip to content

Commit aac4d1f

Browse files
committed
refactor: migrate runtime managers to typescript
1 parent 1ce7374 commit aac4d1f

8 files changed

Lines changed: 554 additions & 141 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ npm run healthcheck
104104
Telegram Message
105105
-> src/bot/handlers.js
106106
-> src/orchestrator/router.ts
107-
-> src/runner/ptyManager.js (coding tasks -> Codex CLI)
107+
-> src/runner/ptyManager.ts (coding tasks -> Codex CLI)
108108
-> src/orchestrator/skills/*.js (general tasks -> MCP/GitHub subagents)
109109
-> src/bot/formatter.js
110110
-> Telegram sendMessage/editMessageText
@@ -116,7 +116,7 @@ Core modules:
116116
- `src/config.ts`: env parsing and validation
117117
- `src/bot/`: auth middleware, formatting, command handlers
118118
- `src/orchestrator/`: routing + MCP client + skills
119-
- `src/runner/ptyManager.js`: Codex PTY process + streaming
119+
- `src/runner/ptyManager.ts`: Codex PTY process + streaming
120120
- `src/cron/scheduler.js`: proactive scheduled push
121121

122122
Enterprise target architecture: [docs/enterprise-architecture.md](/Users/ding/Documents/Code/Github/codex-telegram-claws/docs/enterprise-architecture.md)

docs/enterprise-architecture.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,9 @@ For enterprise rollout, migrate the following first:
110110
- `src/config.ts`
111111
- `src/orchestrator/router.ts`
112112
- `src/orchestrator/skillRegistry.ts`
113-
- `src/orchestrator/mcpClient.js`
114-
- `src/runner/ptyManager.js`
115-
- `src/runner/shellManager.js`
113+
- `src/orchestrator/mcpClient.ts`
114+
- `src/runner/ptyManager.ts`
115+
- `src/runner/shellManager.ts`
116116

117117
TypeScript matters here because config shape, skill contracts, worker RPC payloads, and audit event schemas must remain stable across teams and releases.
118118

docs/phase-1-roadmap.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ Scope:
2020
- Migrate `src/config.ts`
2121
- Migrate `src/orchestrator/router.ts`
2222
- Migrate `src/orchestrator/skillRegistry.ts`
23-
- Migrate `src/orchestrator/mcpClient.js`
24-
- Migrate `src/runner/ptyManager.js`
25-
- Migrate `src/runner/shellManager.js`
23+
- Migrate `src/orchestrator/mcpClient.ts`
24+
- Migrate `src/runner/ptyManager.ts`
25+
- Migrate `src/runner/shellManager.ts`
2626

2727
Deliverables:
2828

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
},
4545
"devDependencies": {
4646
"@eslint/js": "^9.17.0",
47+
"@types/lodash.throttle": "^4.1.9",
4748
"@types/node": "^25.5.0",
4849
"@typescript-eslint/eslint-plugin": "^8.57.0",
4950
"@typescript-eslint/parser": "^8.57.0",
Lines changed: 111 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,57 @@
1+
import process from "node:process";
12
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
23
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4+
import type { AppConfig, McpServerConfig } from "../config.js";
35

4-
function normalizeToolContent(result) {
6+
interface ToolTextItem {
7+
text?: string;
8+
json?: unknown;
9+
}
10+
11+
interface ToolResultLike {
12+
content?: string | ToolTextItem[];
13+
}
14+
15+
interface McpConnection {
16+
client: {
17+
connect: (transport: unknown) => Promise<void>;
18+
listTools: () => Promise<{ tools?: Array<{ name?: string }> }>;
19+
callTool: (input: {
20+
name: string;
21+
arguments: Record<string, unknown>;
22+
}) => Promise<unknown>;
23+
};
24+
transport: {
25+
close?: () => Promise<void>;
26+
};
27+
}
28+
29+
export interface McpServerStatus {
30+
name: string;
31+
command: string;
32+
args: string[];
33+
cwd: string;
34+
enabled: boolean;
35+
connected: boolean;
36+
}
37+
38+
export interface McpClientSnapshot {
39+
disabledServers: string[];
40+
}
41+
42+
interface McpClientOptions {
43+
onChange?: (snapshot: McpClientSnapshot) => void;
44+
}
45+
46+
function normalizeToolContent(result: unknown): string {
547
if (!result) return "";
648

749
if (typeof result === "string") return result;
850

9-
if (Array.isArray(result.content)) {
10-
return result.content
51+
const content = (result as ToolResultLike & { content?: unknown }).content;
52+
53+
if (Array.isArray(content)) {
54+
return content
1155
.map((item) => {
1256
if (typeof item === "string") return item;
1357
if (item?.text) return item.text;
@@ -18,44 +62,60 @@ function normalizeToolContent(result) {
1862
.join("\n");
1963
}
2064

21-
if (result?.content?.text) return String(result.content.text);
22-
if (result?.content?.json) return JSON.stringify(result.content.json);
65+
if (content && typeof content === "object") {
66+
const record = content as ToolTextItem;
67+
if (record.text) {
68+
return String(record.text);
69+
}
70+
71+
if (record.json !== undefined) {
72+
return JSON.stringify(record.json);
73+
}
74+
}
2375

2476
return JSON.stringify(result);
2577
}
2678

2779
export class McpClient {
28-
constructor(config, { onChange } = {}) {
80+
readonly config: Pick<AppConfig, "mcp">;
81+
readonly connections: Map<string, McpConnection>;
82+
disabledServers: Set<string>;
83+
private readonly onChange?: (snapshot: McpClientSnapshot) => void;
84+
85+
constructor(
86+
config: Pick<AppConfig, "mcp">,
87+
{ onChange }: McpClientOptions = {}
88+
) {
2989
this.config = config;
3090
this.connections = new Map();
3191
this.disabledServers = new Set();
3292
this.onChange = onChange;
3393
}
3494

35-
hasServers() {
95+
hasServers(): boolean {
3696
return this.config.mcp.servers.length > 0;
3797
}
3898

39-
getServerConfig(serverName) {
99+
getServerConfig(serverName: string): McpServerConfig | null {
40100
return (
41101
this.config.mcp.servers.find((server) => server.name === serverName) ||
42102
null
43103
);
44104
}
45105

46-
hasServer(serverName) {
106+
hasServer(serverName: string): boolean {
47107
return Boolean(this.getServerConfig(serverName));
48108
}
49109

50-
isServerEnabled(serverName) {
110+
isServerEnabled(serverName: string): boolean {
51111
return this.hasServer(serverName) && !this.disabledServers.has(serverName);
52112
}
53113

54-
isServerConnected(serverName) {
114+
isServerConnected(serverName: string): boolean {
55115
return this.connections.has(serverName);
56116
}
57117

58-
listServers() {
118+
listServers(): McpServerStatus[] {
59119
return this.config.mcp.servers.map((server) => ({
60120
name: server.name,
61121
command: server.command,
@@ -66,27 +126,31 @@ export class McpClient {
66126
}));
67127
}
68128

69-
async connectAll() {
129+
async connectAll(): Promise<void> {
70130
for (const server of this.config.mcp.servers) {
71131
await this.connectServer(server);
72132
}
73133
}
74134

75-
async connectServer(server) {
135+
async connectServer(server: McpServerConfig): Promise<void> {
76136
if (this.disabledServers.has(server.name)) {
77137
return;
78138
}
79139

80140
if (this.connections.has(server.name)) return;
81141

142+
const env = Object.fromEntries(
143+
Object.entries({
144+
...process.env,
145+
...server.env
146+
}).filter(([, value]) => value !== undefined)
147+
) as Record<string, string>;
148+
82149
const transport = new StdioClientTransport({
83150
command: server.command,
84151
args: server.args,
85152
cwd: server.cwd,
86-
env: {
87-
...process.env,
88-
...server.env
89-
}
153+
env
90154
});
91155

92156
const client = new Client(
@@ -97,13 +161,13 @@ export class McpClient {
97161
{
98162
capabilities: {}
99163
}
100-
);
164+
) as McpConnection["client"];
101165

102166
await client.connect(transport);
103167
this.connections.set(server.name, { client, transport });
104168
}
105169

106-
async connectServerByName(serverName) {
170+
async connectServerByName(serverName: string): Promise<void> {
107171
const server = this.getServerConfig(serverName);
108172
if (!server) {
109173
throw new Error(`Unknown MCP server: ${serverName}`);
@@ -116,7 +180,7 @@ export class McpClient {
116180
await this.connectServer(server);
117181
}
118182

119-
async disconnectServer(serverName) {
183+
async disconnectServer(serverName: string): Promise<boolean> {
120184
const conn = this.connections.get(serverName);
121185
if (!conn) return false;
122186

@@ -130,7 +194,7 @@ export class McpClient {
130194
return true;
131195
}
132196

133-
async reconnectServer(serverName) {
197+
async reconnectServer(serverName: string): Promise<McpServerStatus | null> {
134198
if (!this.hasServer(serverName)) {
135199
throw new Error(`Unknown MCP server: ${serverName}`);
136200
}
@@ -146,7 +210,9 @@ export class McpClient {
146210
);
147211
}
148212

149-
async disableServer(serverName) {
213+
async disableServer(
214+
serverName: string
215+
): Promise<(McpServerStatus & { changed: boolean }) | null> {
150216
if (!this.hasServer(serverName)) {
151217
throw new Error(`Unknown MCP server: ${serverName}`);
152218
}
@@ -165,7 +231,9 @@ export class McpClient {
165231
return current ? { ...current, changed: true } : null;
166232
}
167233

168-
async enableServer(serverName) {
234+
async enableServer(
235+
serverName: string
236+
): Promise<(McpServerStatus & { changed: boolean }) | null> {
169237
if (!this.hasServer(serverName)) {
170238
throw new Error(`Unknown MCP server: ${serverName}`);
171239
}
@@ -188,13 +256,13 @@ export class McpClient {
188256
return current ? { ...current, changed } : null;
189257
}
190258

191-
exportState() {
259+
exportState(): McpClientSnapshot {
192260
return {
193261
disabledServers: [...this.disabledServers].sort()
194262
};
195263
}
196264

197-
restoreState(snapshot = {}) {
265+
restoreState(snapshot: Partial<McpClientSnapshot> = {}): void {
198266
const disabledServers = Array.isArray(snapshot?.disabledServers)
199267
? snapshot.disabledServers.filter((serverName) =>
200268
this.hasServer(serverName)
@@ -204,14 +272,22 @@ export class McpClient {
204272
this.disabledServers = new Set(disabledServers);
205273
}
206274

207-
async listTools(serverName) {
275+
async listTools(serverName: string): Promise<Array<{ name?: string }>> {
208276
const conn = this.connections.get(serverName);
209277
if (!conn) throw new Error(`MCP server not connected: ${serverName}`);
210278
const res = await conn.client.listTools();
211279
return res.tools || [];
212280
}
213281

214-
async callTool({ serverName, toolName, args = {} }) {
282+
async callTool({
283+
serverName,
284+
toolName,
285+
args = {}
286+
}: {
287+
serverName: string;
288+
toolName: string;
289+
args?: Record<string, unknown>;
290+
}): Promise<string> {
215291
const conn = this.connections.get(serverName);
216292
if (!conn) throw new Error(`MCP server not connected: ${serverName}`);
217293

@@ -223,12 +299,12 @@ export class McpClient {
223299
return normalizeToolContent(result);
224300
}
225301

226-
async gatherContextForTask(taskText) {
302+
async gatherContextForTask(taskText: string): Promise<string> {
227303
if (!this.connections.size || !taskText.trim()) {
228304
return "";
229305
}
230306

231-
const contextBlocks = [];
307+
const contextBlocks: string[] = [];
232308
const toolNameHints = [
233309
"search",
234310
"query",
@@ -249,7 +325,7 @@ export class McpClient {
249325
return toolNameHints.some((hint) => name.includes(hint));
250326
});
251327

252-
if (!preferredTool) continue;
328+
if (!preferredTool?.name) continue;
253329

254330
const result = await conn.client.callTool({
255331
name: preferredTool.name,
@@ -265,19 +341,18 @@ export class McpClient {
265341

266342
contextBlocks.push(`[${serverName}/${preferredTool.name}]\n${text}`);
267343
} catch (error) {
268-
contextBlocks.push(
269-
`[${serverName}] MCP query failed: ${error.message}`
270-
);
344+
const message = error instanceof Error ? error.message : String(error);
345+
contextBlocks.push(`[${serverName}] MCP query failed: ${message}`);
271346
}
272347
}
273348

274349
return contextBlocks.join("\n\n");
275350
}
276351

277-
async closeAll() {
352+
async closeAll(): Promise<void> {
278353
for (const { transport } of this.connections.values()) {
279354
try {
280-
await transport.close();
355+
await transport.close?.();
281356
} catch {
282357
// Ignore close errors on shutdown.
283358
}

0 commit comments

Comments
 (0)