@wangzhizhi/remi 0.1.224 → 0.1.225

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
@@ -3,7 +3,7 @@
3
3
  Remi AI coding agent CLI.
4
4
 
5
5
  ```bash
6
- npm install -g @wangzhizhi/remi@0.1.224
6
+ npm install -g @wangzhizhi/remi@0.1.225
7
7
  remi setup
8
8
  remi
9
9
  ```
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t } from './i18n.js';
2
- export const version = '0.1.224';
2
+ export const version = '0.1.225';
3
3
  export function formatVersionMessage(language) {
4
4
  return t(language, 'version.message', { version });
5
5
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remi/compact",
3
- "version": "0.1.224",
3
+ "version": "0.1.225",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remi/config",
3
- "version": "0.1.224",
3
+ "version": "0.1.225",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remi/core",
3
- "version": "0.1.224",
3
+ "version": "0.1.225",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remi/hooks",
3
- "version": "0.1.224",
3
+ "version": "0.1.225",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remi/llm",
3
- "version": "0.1.224",
3
+ "version": "0.1.225",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"
@@ -0,0 +1,643 @@
1
+ import { evaluateToolPermission } from '@remi/permissions';
2
+ import { createToolPermissionRequest, createToolRegistry, } from '@remi/tools';
3
+ import { spawn } from 'node:child_process';
4
+ import { resolve } from 'node:path';
5
+ export const mcpPackageName = '@remi/mcp';
6
+ const mcpProtocolVersion = '2025-11-25';
7
+ const defaultMcpRequestTimeoutMs = 15_000;
8
+ const maxMcpListPages = 20;
9
+ export class StdioMcpClient {
10
+ serverName;
11
+ config;
12
+ options;
13
+ child;
14
+ buffer = '';
15
+ nextRequestId = 1;
16
+ pending = new Map();
17
+ closed = false;
18
+ constructor(serverName, config, options) {
19
+ this.serverName = serverName;
20
+ this.config = config;
21
+ this.options = options;
22
+ }
23
+ async connect() {
24
+ if (this.child) {
25
+ return;
26
+ }
27
+ const command = this.config.command.trim();
28
+ if (!command) {
29
+ throw new Error('MCP stdio server command must not be empty.');
30
+ }
31
+ const args = Array.isArray(this.config.args) ? this.config.args : [];
32
+ const cwd = this.config.cwd ? resolve(this.options.cwd, this.config.cwd) : this.options.cwd;
33
+ const child = spawn(command, args, {
34
+ cwd,
35
+ env: mcpProcessEnv(this.options.env ?? process.env, this.config.env),
36
+ stdio: 'pipe',
37
+ shell: false,
38
+ });
39
+ this.child = child;
40
+ child.stdout.setEncoding('utf8');
41
+ child.stderr.setEncoding('utf8');
42
+ child.stdout.on('data', chunk => this.handleStdout(String(chunk)));
43
+ child.on('error', error => this.closeWithError(error instanceof Error ? error : new Error(String(error))));
44
+ child.on('close', () => this.closeWithError(new Error(`MCP stdio server ${this.serverName} exited.`)));
45
+ if (this.options.signal) {
46
+ this.options.signal.addEventListener('abort', () => void this.close(), { once: true });
47
+ }
48
+ await this.initialize();
49
+ }
50
+ async initialize() {
51
+ const result = await this.request('initialize', {
52
+ protocolVersion: mcpProtocolVersion,
53
+ capabilities: {},
54
+ clientInfo: {
55
+ name: 'Remi',
56
+ version: '0.1',
57
+ },
58
+ });
59
+ this.notify('notifications/initialized');
60
+ return result;
61
+ }
62
+ async listTools() {
63
+ return await this.listToolsPage();
64
+ }
65
+ async listToolsPage(cursor) {
66
+ return normalizeToolsListResult(await this.request('tools/list', cursor ? { cursor } : undefined));
67
+ }
68
+ async callTool(name, args) {
69
+ return await this.request('tools/call', { name, arguments: args });
70
+ }
71
+ async close() {
72
+ this.closed = true;
73
+ for (const [id, pending] of this.pending) {
74
+ clearTimeout(pending.timer);
75
+ pending.reject(new Error(`MCP request ${id} was cancelled because the client closed.`));
76
+ }
77
+ this.pending.clear();
78
+ if (this.child && !this.child.killed) {
79
+ this.child.kill();
80
+ }
81
+ this.child = undefined;
82
+ }
83
+ request(method, params) {
84
+ const child = this.requireChild();
85
+ const id = this.nextRequestId++;
86
+ const timeoutMs = normalizedTimeoutMs(this.config.timeoutMs);
87
+ const request = {
88
+ jsonrpc: '2.0',
89
+ id,
90
+ method,
91
+ ...(params !== undefined ? { params } : {}),
92
+ };
93
+ return new Promise((resolve, reject) => {
94
+ const timer = setTimeout(() => {
95
+ this.pending.delete(id);
96
+ reject(new Error(`MCP request timed out: ${method}`));
97
+ }, timeoutMs);
98
+ this.pending.set(id, { resolve, reject, timer });
99
+ child.stdin.write(`${JSON.stringify(request)}\n`);
100
+ });
101
+ }
102
+ notify(method, params) {
103
+ const notification = {
104
+ jsonrpc: '2.0',
105
+ method,
106
+ ...(params !== undefined ? { params } : {}),
107
+ };
108
+ this.requireChild().stdin.write(`${JSON.stringify(notification)}\n`);
109
+ }
110
+ requireChild() {
111
+ if (!this.child || this.closed) {
112
+ throw new Error(`MCP stdio server ${this.serverName} is not connected.`);
113
+ }
114
+ return this.child;
115
+ }
116
+ handleStdout(chunk) {
117
+ this.buffer += chunk;
118
+ while (true) {
119
+ const newline = this.buffer.indexOf('\n');
120
+ if (newline < 0) {
121
+ return;
122
+ }
123
+ const line = this.buffer.slice(0, newline).trim();
124
+ this.buffer = this.buffer.slice(newline + 1);
125
+ if (!line) {
126
+ continue;
127
+ }
128
+ this.handleMessage(line);
129
+ }
130
+ }
131
+ handleMessage(line) {
132
+ let parsed;
133
+ try {
134
+ parsed = JSON.parse(line);
135
+ }
136
+ catch {
137
+ return;
138
+ }
139
+ if (!isJsonRpcResponse(parsed)) {
140
+ return;
141
+ }
142
+ const pending = this.pending.get(parsed.id);
143
+ if (!pending) {
144
+ return;
145
+ }
146
+ this.pending.delete(parsed.id);
147
+ clearTimeout(pending.timer);
148
+ if (parsed.error) {
149
+ pending.reject(new Error(parsed.error.message || `MCP request failed with code ${parsed.error.code ?? 'unknown'}.`));
150
+ return;
151
+ }
152
+ pending.resolve(parsed.result);
153
+ }
154
+ closeWithError(error) {
155
+ if (this.closed) {
156
+ return;
157
+ }
158
+ this.closed = true;
159
+ this.child = undefined;
160
+ for (const [id, pending] of this.pending) {
161
+ clearTimeout(pending.timer);
162
+ pending.reject(new Error(`${error.message} Pending MCP request id=${id} failed.`));
163
+ }
164
+ this.pending.clear();
165
+ }
166
+ }
167
+ export async function createMcpRuntime(config, options, baseExecutor) {
168
+ const sessions = [];
169
+ const diagnostics = [];
170
+ const tools = [];
171
+ const usedNames = new Set();
172
+ for (const [serverName, serverConfig] of enabledMcpServers(config)) {
173
+ const client = new StdioMcpClient(serverName, serverConfig, options);
174
+ try {
175
+ await client.connect();
176
+ sessions.push(client);
177
+ const discovered = await discoverToolsForClient(serverName, client, usedNames);
178
+ tools.push(...discovered);
179
+ }
180
+ catch (error) {
181
+ diagnostics.push({
182
+ server: serverName,
183
+ level: 'error',
184
+ message: error instanceof Error ? error.message : 'MCP server discovery failed.',
185
+ });
186
+ await client.close();
187
+ }
188
+ }
189
+ const registry = createToolRegistry(tools.map(tool => tool.definition));
190
+ return {
191
+ tools,
192
+ diagnostics,
193
+ registry,
194
+ executor: createMcpToolExecutor(tools, baseExecutor),
195
+ close: async () => {
196
+ await Promise.all(sessions.map(session => session.close()));
197
+ },
198
+ };
199
+ }
200
+ export function createMcpAwareToolRegistry(base, mcpTools) {
201
+ const registry = createToolRegistry(base.list());
202
+ for (const tool of mcpTools) {
203
+ registry.register(tool.definition);
204
+ }
205
+ return registry;
206
+ }
207
+ export function createMcpToolExecutor(mcpTools, baseExecutor) {
208
+ const byName = new Map(mcpTools.map(tool => [tool.remiName, tool]));
209
+ return {
210
+ async execute(request) {
211
+ const tool = byName.get(request.call.toolName);
212
+ if (!tool) {
213
+ if (baseExecutor) {
214
+ return await baseExecutor.execute(request);
215
+ }
216
+ return unknownMcpToolOutcome(request);
217
+ }
218
+ return await executeMcpTool(tool, request);
219
+ },
220
+ };
221
+ }
222
+ async function discoverToolsForClient(serverName, client, usedNames) {
223
+ const tools = [];
224
+ let cursor;
225
+ for (let page = 0; page < maxMcpListPages; page += 1) {
226
+ const result = await client.listToolsPage(cursor);
227
+ for (const rawTool of result.tools ?? []) {
228
+ const remoteTool = normalizeRemoteTool(rawTool);
229
+ if (!remoteTool) {
230
+ continue;
231
+ }
232
+ const remiName = uniqueToolName(mcpToolName(serverName, remoteTool.name), usedNames);
233
+ const definition = createMcpToolDefinition(serverName, remoteTool.name, remiName, remoteTool);
234
+ tools.push({
235
+ serverName,
236
+ remoteName: remoteTool.name,
237
+ remiName,
238
+ description: remoteTool.description,
239
+ inputSchema: definition.inputSchema,
240
+ ...(remoteTool.rawInputSchema ? { rawInputSchema: remoteTool.rawInputSchema } : {}),
241
+ ...(remoteTool.annotations ? { annotations: remoteTool.annotations } : {}),
242
+ client,
243
+ definition,
244
+ });
245
+ }
246
+ cursor = result.nextCursor;
247
+ if (!cursor) {
248
+ break;
249
+ }
250
+ }
251
+ return tools;
252
+ }
253
+ function normalizeRemoteTool(rawTool) {
254
+ if (!rawTool || typeof rawTool !== 'object' || Array.isArray(rawTool)) {
255
+ return undefined;
256
+ }
257
+ const record = rawTool;
258
+ const name = typeof record['name'] === 'string' ? record['name'].trim() : '';
259
+ if (!name) {
260
+ return undefined;
261
+ }
262
+ const description = typeof record['description'] === 'string' && record['description'].trim() ? record['description'].trim() : `MCP tool ${name}.`;
263
+ const inputSchema = normalizeMcpInputSchema(record['inputSchema']);
264
+ const annotations = normalizeMcpToolAnnotations(record['annotations']);
265
+ return {
266
+ name,
267
+ description,
268
+ inputSchema,
269
+ ...(isRecord(record['inputSchema']) ? { rawInputSchema: record['inputSchema'] } : {}),
270
+ ...(annotations ? { annotations } : {}),
271
+ };
272
+ }
273
+ function createMcpToolDefinition(serverName, remoteName, remiName, remoteTool) {
274
+ const riskLevel = mcpToolRiskLevel(remoteTool.annotations);
275
+ return {
276
+ name: remiName,
277
+ description: `Call MCP server "${serverName}" tool "${remoteName}". ${remoteTool.description}`,
278
+ inputSchema: remoteTool.inputSchema,
279
+ outputSchema: mcpOutputSchema,
280
+ riskLevel,
281
+ permissionPolicy: {
282
+ mode: 'ask',
283
+ requirements: mcpPermissionRequirements(riskLevel),
284
+ reason: `Calls external MCP tool ${serverName}/${remoteName}. Requires MCP permission review before execution.`,
285
+ },
286
+ outputLimit: { maxBytes: 128 * 1024, truncate: 'tail' },
287
+ };
288
+ }
289
+ async function executeMcpTool(tool, request) {
290
+ const permissionRequest = createToolPermissionRequest(tool.definition, request.call, request.context);
291
+ const initialDecision = evaluateToolPermission(permissionRequest);
292
+ let permissionDecision = applyMcpPermissionMode(request.context, initialDecision);
293
+ if (permissionDecision.status === 'ask' && request.context.requestPermission) {
294
+ try {
295
+ permissionDecision = await request.context.requestPermission(permissionRequest, permissionDecision);
296
+ }
297
+ catch (error) {
298
+ permissionDecision = {
299
+ status: 'deny',
300
+ reason: error instanceof Error ? error.message : 'Permission request was rejected.',
301
+ requirements: permissionDecision.requirements,
302
+ };
303
+ }
304
+ }
305
+ const callEvent = {
306
+ type: 'tool_call',
307
+ callId: request.call.id,
308
+ toolName: tool.definition.name,
309
+ input: request.call.input,
310
+ riskLevel: tool.definition.riskLevel,
311
+ permissionPolicy: tool.definition.permissionPolicy,
312
+ status: permissionDecision.status === 'deny' ? 'denied' : 'running',
313
+ };
314
+ if (permissionDecision.status !== 'allow') {
315
+ const result = {
316
+ ok: false,
317
+ callId: request.call.id,
318
+ toolName: tool.definition.name,
319
+ error: {
320
+ code: permissionDecision.status === 'ask' ? 'PERMISSION_REQUIRED' : 'PERMISSION_DENIED',
321
+ message: permissionDecision.reason,
322
+ },
323
+ };
324
+ return {
325
+ dryRun: false,
326
+ callId: request.call.id,
327
+ toolName: tool.definition.name,
328
+ permissionRequest,
329
+ permissionDecision,
330
+ transcriptEvents: [{ ...callEvent, status: 'denied' }, toolResultTranscriptEvent(result)],
331
+ result,
332
+ };
333
+ }
334
+ const result = await callMcpTool(tool, request);
335
+ return {
336
+ dryRun: false,
337
+ callId: request.call.id,
338
+ toolName: tool.definition.name,
339
+ permissionRequest,
340
+ permissionDecision,
341
+ transcriptEvents: [callEvent, toolResultTranscriptEvent(result)],
342
+ result,
343
+ };
344
+ }
345
+ async function callMcpTool(tool, request) {
346
+ try {
347
+ const rawResult = await tool.client.callTool(tool.remoteName, request.call.input);
348
+ const normalized = normalizeMcpCallResult(tool.serverName, tool.remoteName, rawResult);
349
+ if (normalized.isError) {
350
+ return {
351
+ ok: false,
352
+ callId: request.call.id,
353
+ toolName: tool.definition.name,
354
+ error: {
355
+ code: 'MCP_TOOL_ERROR',
356
+ message: normalized.text || `MCP tool ${tool.serverName}/${tool.remoteName} returned an error result.`,
357
+ },
358
+ };
359
+ }
360
+ return boundedJsonResult(request, normalized);
361
+ }
362
+ catch (error) {
363
+ return {
364
+ ok: false,
365
+ callId: request.call.id,
366
+ toolName: tool.definition.name,
367
+ error: {
368
+ code: 'MCP_TOOL_CALL_FAILED',
369
+ message: error instanceof Error ? error.message : 'MCP tool call failed.',
370
+ },
371
+ };
372
+ }
373
+ }
374
+ function applyMcpPermissionMode(context, decision) {
375
+ if (decision.status === 'ask' && context.permissionMode === 'bypass') {
376
+ return {
377
+ status: 'allow',
378
+ reason: 'full-access permission profile allows MCP tool execution without prompting.',
379
+ requirements: decision.requirements,
380
+ };
381
+ }
382
+ return decision;
383
+ }
384
+ function enabledMcpServers(config) {
385
+ return Object.entries(config?.servers ?? {})
386
+ .filter(([, server]) => server.enabled !== false)
387
+ .filter(([, server]) => server.type === undefined || server.type === 'stdio');
388
+ }
389
+ function mcpToolName(serverName, toolName) {
390
+ return `mcp__${safeToolNameSegment(serverName)}__${safeToolNameSegment(toolName)}`;
391
+ }
392
+ function safeToolNameSegment(value) {
393
+ const normalized = value.toLowerCase().replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 48);
394
+ const segment = normalized || 'tool';
395
+ return /^[a-z]/.test(segment) ? segment : `x_${segment}`;
396
+ }
397
+ function uniqueToolName(baseName, usedNames) {
398
+ let candidate = baseName;
399
+ let suffix = 2;
400
+ while (usedNames.has(candidate)) {
401
+ candidate = `${baseName}_${suffix}`;
402
+ suffix += 1;
403
+ }
404
+ usedNames.add(candidate);
405
+ return candidate;
406
+ }
407
+ function mcpToolRiskLevel(annotations) {
408
+ if (annotations?.openWorldHint) {
409
+ return 'network';
410
+ }
411
+ if (annotations?.destructiveHint) {
412
+ return 'write';
413
+ }
414
+ if (annotations?.readOnlyHint) {
415
+ return 'read';
416
+ }
417
+ return 'execute';
418
+ }
419
+ function mcpPermissionRequirements(riskLevel) {
420
+ return riskLevel === 'network' ? ['mcp', 'network'] : ['mcp'];
421
+ }
422
+ const mcpOutputSchema = {
423
+ type: 'object',
424
+ properties: {
425
+ server: { type: 'string', description: 'MCP server name.' },
426
+ tool: { type: 'string', description: 'Remote MCP tool name.' },
427
+ text: { type: 'string', description: 'Text extracted from MCP content blocks.' },
428
+ content: {
429
+ type: 'array',
430
+ description: 'Bounded MCP content blocks.',
431
+ items: {
432
+ type: 'object',
433
+ description: 'One MCP content block.',
434
+ additionalProperties: true,
435
+ },
436
+ },
437
+ structuredContent: {
438
+ type: 'object',
439
+ description: 'Structured MCP tool output when provided by the server.',
440
+ additionalProperties: true,
441
+ },
442
+ isError: { type: 'boolean', description: 'Whether the MCP server marked the result as an error.' },
443
+ },
444
+ required: ['server', 'tool', 'text', 'content', 'isError'],
445
+ additionalProperties: false,
446
+ };
447
+ function normalizeMcpInputSchema(rawSchema) {
448
+ if (!isRecord(rawSchema) || rawSchema['type'] !== 'object') {
449
+ return {
450
+ type: 'object',
451
+ properties: {},
452
+ additionalProperties: true,
453
+ };
454
+ }
455
+ const rawProperties = isRecord(rawSchema['properties']) ? rawSchema['properties'] : {};
456
+ const properties = {};
457
+ for (const [name, property] of Object.entries(rawProperties)) {
458
+ properties[name] = normalizeMcpSchemaProperty(property);
459
+ }
460
+ return {
461
+ type: 'object',
462
+ ...(typeof rawSchema['description'] === 'string' ? { description: rawSchema['description'] } : {}),
463
+ properties,
464
+ ...(Array.isArray(rawSchema['required']) ? { required: rawSchema['required'].filter((item) => typeof item === 'string') } : {}),
465
+ additionalProperties: rawSchema['additionalProperties'] === false ? false : true,
466
+ };
467
+ }
468
+ function normalizeMcpSchemaProperty(rawProperty) {
469
+ const record = isRecord(rawProperty) ? rawProperty : {};
470
+ const type = normalizeMcpSchemaType(record);
471
+ const description = typeof record['description'] === 'string' && record['description'].trim() ? record['description'] : `MCP ${type} value.`;
472
+ const property = { type, description };
473
+ const enumValues = Array.isArray(record['enum']) ? record['enum'].filter((item) => typeof item === 'string') : [];
474
+ if (enumValues.length > 0) {
475
+ property.enum = enumValues;
476
+ }
477
+ if (type === 'array') {
478
+ property.items = normalizeMcpSchemaProperty(record['items']);
479
+ }
480
+ if (type === 'object') {
481
+ const rawProperties = isRecord(record['properties']) ? record['properties'] : {};
482
+ property.properties = Object.fromEntries(Object.entries(rawProperties).map(([name, nested]) => [name, normalizeMcpSchemaProperty(nested)]));
483
+ const required = Array.isArray(record['required']) ? record['required'].filter((item) => typeof item === 'string') : [];
484
+ if (required.length > 0) {
485
+ property.required = required;
486
+ }
487
+ property.additionalProperties = record['additionalProperties'] === false ? false : true;
488
+ }
489
+ return property;
490
+ }
491
+ function normalizeMcpSchemaType(record) {
492
+ const rawType = Array.isArray(record['type']) ? record['type'].find(item => item !== 'null') : record['type'];
493
+ if (rawType === 'number' || rawType === 'integer' || rawType === 'boolean' || rawType === 'object' || rawType === 'array' || rawType === 'string') {
494
+ return rawType;
495
+ }
496
+ if (isRecord(record['properties'])) {
497
+ return 'object';
498
+ }
499
+ if (record['items']) {
500
+ return 'array';
501
+ }
502
+ if (Array.isArray(record['enum']) && record['enum'].every(item => typeof item === 'string')) {
503
+ return 'string';
504
+ }
505
+ return 'string';
506
+ }
507
+ function normalizeMcpToolAnnotations(value) {
508
+ if (!isRecord(value)) {
509
+ return undefined;
510
+ }
511
+ const annotations = {};
512
+ if (typeof value['readOnlyHint'] === 'boolean')
513
+ annotations.readOnlyHint = value['readOnlyHint'];
514
+ if (typeof value['destructiveHint'] === 'boolean')
515
+ annotations.destructiveHint = value['destructiveHint'];
516
+ if (typeof value['idempotentHint'] === 'boolean')
517
+ annotations.idempotentHint = value['idempotentHint'];
518
+ if (typeof value['openWorldHint'] === 'boolean')
519
+ annotations.openWorldHint = value['openWorldHint'];
520
+ return Object.keys(annotations).length > 0 ? annotations : undefined;
521
+ }
522
+ function normalizeMcpCallResult(server, tool, rawResult) {
523
+ const record = isRecord(rawResult) ? rawResult : {};
524
+ const content = Array.isArray(record['content']) ? record['content'].map(normalizeMcpContentBlock) : [];
525
+ const text = content
526
+ .map(block => (isRecord(block) && typeof block['text'] === 'string' ? block['text'] : ''))
527
+ .filter(Boolean)
528
+ .join('\n');
529
+ return {
530
+ server,
531
+ tool,
532
+ text,
533
+ content,
534
+ ...(record['structuredContent'] !== undefined ? { structuredContent: record['structuredContent'] } : {}),
535
+ isError: record['isError'] === true,
536
+ };
537
+ }
538
+ function normalizeMcpContentBlock(block) {
539
+ if (!isRecord(block)) {
540
+ return { type: 'unknown' };
541
+ }
542
+ return block;
543
+ }
544
+ function normalizeToolsListResult(result) {
545
+ if (!isRecord(result)) {
546
+ return {};
547
+ }
548
+ return {
549
+ ...(Array.isArray(result['tools']) ? { tools: result['tools'] } : {}),
550
+ ...(typeof result['nextCursor'] === 'string' && result['nextCursor'] ? { nextCursor: result['nextCursor'] } : {}),
551
+ };
552
+ }
553
+ function boundedJsonResult(request, payload) {
554
+ const raw = JSON.stringify(payload);
555
+ const maxBytes = Math.max(1, Math.min(request.context.maxOutputBytes, 128 * 1024));
556
+ const bounded = truncateStringBytes(raw, maxBytes);
557
+ return {
558
+ ok: true,
559
+ callId: request.call.id,
560
+ toolName: request.call.toolName,
561
+ output: bounded.text,
562
+ truncated: bounded.truncated,
563
+ bytes: Buffer.byteLength(bounded.text, 'utf8'),
564
+ };
565
+ }
566
+ function toolResultTranscriptEvent(result) {
567
+ if (result.ok) {
568
+ return {
569
+ type: 'tool_result',
570
+ callId: result.callId,
571
+ toolName: result.toolName,
572
+ ok: true,
573
+ summary: result.output,
574
+ bytes: result.bytes,
575
+ truncated: result.truncated,
576
+ };
577
+ }
578
+ return {
579
+ type: 'tool_result',
580
+ callId: result.callId,
581
+ toolName: result.toolName,
582
+ ok: false,
583
+ summary: result.error.message,
584
+ errorCode: result.error.code,
585
+ };
586
+ }
587
+ function unknownMcpToolOutcome(request) {
588
+ const decision = {
589
+ status: 'deny',
590
+ reason: `Unknown tool: ${request.call.toolName}`,
591
+ requirements: [],
592
+ };
593
+ const result = {
594
+ ok: false,
595
+ callId: request.call.id,
596
+ toolName: request.call.toolName,
597
+ error: {
598
+ code: 'UNKNOWN_TOOL',
599
+ message: decision.reason,
600
+ },
601
+ };
602
+ return {
603
+ dryRun: false,
604
+ callId: request.call.id,
605
+ toolName: request.call.toolName,
606
+ permissionDecision: decision,
607
+ transcriptEvents: [toolResultTranscriptEvent(result)],
608
+ result,
609
+ };
610
+ }
611
+ function isJsonRpcResponse(value) {
612
+ return isRecord(value) && value['jsonrpc'] === '2.0' && typeof value['id'] === 'number';
613
+ }
614
+ function isRecord(value) {
615
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
616
+ }
617
+ function normalizedTimeoutMs(value) {
618
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? Math.min(value, 120_000) : defaultMcpRequestTimeoutMs;
619
+ }
620
+ function mcpProcessEnv(baseEnv, extraEnv) {
621
+ const env = { ...baseEnv };
622
+ for (const [name, value] of Object.entries(extraEnv ?? {})) {
623
+ env[name] = interpolateEnvValue(value, baseEnv);
624
+ }
625
+ return env;
626
+ }
627
+ function interpolateEnvValue(value, env) {
628
+ const match = /^\$\{([A-Z0-9_]+)\}$/.exec(value);
629
+ if (!match) {
630
+ return value;
631
+ }
632
+ return env[match[1] ?? ''] ?? '';
633
+ }
634
+ function truncateStringBytes(text, maxBytes) {
635
+ const buffer = Buffer.from(text, 'utf8');
636
+ if (buffer.byteLength <= maxBytes) {
637
+ return { text, truncated: false };
638
+ }
639
+ return {
640
+ text: buffer.subarray(0, maxBytes).toString('utf8'),
641
+ truncated: true,
642
+ };
643
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@remi/mcp",
3
+ "version": "0.1.225",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./dist/index.js"
7
+ }
8
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remi/memory",
3
- "version": "0.1.224",
3
+ "version": "0.1.225",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remi/permissions",
3
- "version": "0.1.224",
3
+ "version": "0.1.225",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remi/sessions",
3
- "version": "0.1.224",
3
+ "version": "0.1.225",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remi/skills",
3
- "version": "0.1.224",
3
+ "version": "0.1.225",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remi/terminal-markdown",
3
- "version": "0.1.224",
3
+ "version": "0.1.225",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remi/tools",
3
- "version": "0.1.224",
3
+ "version": "0.1.225",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wangzhizhi/remi",
3
- "version": "0.1.224",
3
+ "version": "0.1.225",
4
4
  "description": "Remi AI coding agent CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,17 +10,18 @@
10
10
  "node": ">=24 <25"
11
11
  },
12
12
  "dependencies": {
13
- "@remi/compact": "0.1.224",
14
- "@remi/config": "0.1.224",
15
- "@remi/core": "0.1.224",
16
- "@remi/hooks": "0.1.224",
17
- "@remi/llm": "0.1.224",
18
- "@remi/memory": "0.1.224",
19
- "@remi/permissions": "0.1.224",
20
- "@remi/sessions": "0.1.224",
21
- "@remi/skills": "0.1.224",
22
- "@remi/terminal-markdown": "0.1.224",
23
- "@remi/tools": "0.1.224",
13
+ "@remi/compact": "0.1.225",
14
+ "@remi/config": "0.1.225",
15
+ "@remi/core": "0.1.225",
16
+ "@remi/hooks": "0.1.225",
17
+ "@remi/llm": "0.1.225",
18
+ "@remi/memory": "0.1.225",
19
+ "@remi/mcp": "0.1.225",
20
+ "@remi/permissions": "0.1.225",
21
+ "@remi/sessions": "0.1.225",
22
+ "@remi/skills": "0.1.225",
23
+ "@remi/terminal-markdown": "0.1.225",
24
+ "@remi/tools": "0.1.225",
24
25
  "highlight.js": "11.11.1",
25
26
  "ink": "7.0.4",
26
27
  "marked": "17.0.1",
@@ -33,6 +34,7 @@
33
34
  "@remi/hooks",
34
35
  "@remi/llm",
35
36
  "@remi/memory",
37
+ "@remi/mcp",
36
38
  "@remi/permissions",
37
39
  "@remi/sessions",
38
40
  "@remi/skills",