praxis-agent 0.50.0 → 0.52.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/README.md CHANGED
@@ -215,7 +215,13 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
215
215
  disconnect recovery that never replays an already-dispatched call. Default
216
216
  tool selection defers `mcp__*` schemas behind a turn-scoped `ToolSearch`;
217
217
  each query activates at most eight deterministic matches for the next model
218
- request. Explicit concrete `--tools` selections load selected tools directly, while
218
+ request, and published MCP tool descriptions are capped at 2,048 Unicode
219
+ code points. Text-only MCP results above 100,000 UTF-8 bytes are redacted
220
+ into mode-`0600` `.txt` files under the session-scoped `tool-results`
221
+ directory; providers and transcripts receive only a bounded instruction with
222
+ the absolute file path. Results at or below the limit remain inline, while
223
+ structured, mixed-media, and binary-resource handling is unchanged. Explicit
224
+ concrete `--tools` selections load selected tools directly, while
219
225
  `--disallowedTools ToolSearch` restores the complete tool list.
220
226
  - **Provider-neutral models** — native Provider Registry/Vault routing, API
221
227
  adapters, an experimental Codex OAuth adapter, explicit capability checks,
@@ -1,7 +1,7 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { mkdir, mkdtemp, open, rm } from 'node:fs/promises';
3
3
  import { homedir, tmpdir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { join, resolve as resolvePath } from 'node:path';
5
5
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
6
6
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
7
7
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
@@ -21,7 +21,12 @@ function sanitizeMcpUnicode(value) {
21
21
  .replace(/[\p{Cf}\p{Co}\p{Cn}]/gu, '')
22
22
  .replace(/[\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF\uE000-\uF8FF]/gu, '');
23
23
  }
24
+ const MAX_MCP_TOOL_DESCRIPTION_CODE_POINTS = 2_048;
25
+ function capMcpToolDescription(value) {
26
+ return [...value].slice(0, MAX_MCP_TOOL_DESCRIPTION_CODE_POINTS).join('');
27
+ }
24
28
  const MAX_RESOURCE_BYTES = 25 * 1024 * 1024;
29
+ const MAX_INLINE_MCP_TEXT_RESULT_BYTES = 100_000;
25
30
  const NO_MCP_RESOURCES = 'No resources found. MCP servers may still provide tools even if they have no resources.';
26
31
  const MCP_RESOURCE_TOOL_DEFINITIONS = [
27
32
  {
@@ -454,11 +459,21 @@ async function mcpBinaryText(options, sensitiveValues, createdFiles) {
454
459
  : `Resource from ${options.serverName} at ${options.uri}`;
455
460
  return mcpTextBlock(`[${origin}] Binary content (${options.mimeType}, ${options.bytes.length} bytes) saved to ${filePath}`, sensitiveValues);
456
461
  }
462
+ async function externalizeMcpTextResult(serverName, content, context, sensitiveValues, createdFiles) {
463
+ if (!context.toolResultDirectory) {
464
+ throw new Error('MCP large text tool result output directory is unavailable');
465
+ }
466
+ const bytes = Buffer.from(content, 'utf8');
467
+ const filePath = await writeToolBlob(resolvePath(context.cwd, context.toolResultDirectory), serverName, bytes, '.txt');
468
+ createdFiles.push(filePath);
469
+ return mcpTextBlock(`[MCP tool result from ${serverName}] Text content (${bytes.length} bytes) saved to ${filePath}`, sensitiveValues);
470
+ }
457
471
  async function mcpToolResultUnchecked(serverName, result, context, sensitiveValues, createdFiles) {
458
472
  if (!Array.isArray(result.content)) {
459
473
  throw new Error('MCP tool result content must be an array');
460
474
  }
461
475
  const blocks = [];
476
+ let textOnly = result.structuredContent === undefined;
462
477
  let resultBytes = 0;
463
478
  const consumeBytes = (bytes) => {
464
479
  resultBytes += bytes;
@@ -483,6 +498,7 @@ async function mcpToolResultUnchecked(serverName, result, context, sensitiveValu
483
498
  }
484
499
  continue;
485
500
  }
501
+ textOnly = false;
486
502
  if (item.type === 'image') {
487
503
  if (typeof item.mimeType !== 'string' ||
488
504
  !MCP_IMAGE_MEDIA_TYPES.has(item.mimeType) ||
@@ -569,6 +585,15 @@ async function mcpToolResultUnchecked(serverName, result, context, sensitiveValu
569
585
  if (Buffer.byteLength(content) > MAX_RESOURCE_BYTES) {
570
586
  throw new Error(`MCP tool result exceeded ${MAX_RESOURCE_BYTES} bytes`);
571
587
  }
588
+ if (textOnly &&
589
+ Buffer.byteLength(content) > MAX_INLINE_MCP_TEXT_RESULT_BYTES) {
590
+ const pointerBlock = await externalizeMcpTextResult(serverName, content, context, sensitiveValues, createdFiles);
591
+ return {
592
+ content: pointerBlock.text,
593
+ contentBlocks: [pointerBlock],
594
+ isError: result.isError === true,
595
+ };
596
+ }
572
597
  const images = blocks.filter((block) => block.type === 'image');
573
598
  return {
574
599
  content,
@@ -1250,7 +1275,7 @@ export class ClaudeMcpToolRegistry {
1250
1275
  sensitiveValues,
1251
1276
  definition: {
1252
1277
  name,
1253
- description: redactSensitiveText(tool.description ?? `MCP tool ${tool.name} from ${serverName}`, sensitiveValues),
1278
+ description: capMcpToolDescription(redactSensitiveText(tool.description ?? `MCP tool ${tool.name} from ${serverName}`, sensitiveValues)),
1254
1279
  inputSchema: redactSensitiveValue(tool.inputSchema, sensitiveValues),
1255
1280
  },
1256
1281
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.50.0",
3
+ "version": "0.52.0",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",