@ottocode/sdk 0.1.298 → 0.1.300

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.298",
3
+ "version": "0.1.300",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -102,7 +102,7 @@
102
102
  "@modelcontextprotocol/sdk": "^1.12",
103
103
  "@openauthjs/openauth": "^0.4.3",
104
104
  "@openrouter/ai-sdk-provider": "^1.2.0",
105
- "@ottorouter/ai-sdk": "0.2.5",
105
+ "@ottorouter/ai-sdk": "0.2.6",
106
106
  "@solana/web3.js": "^1.98.0",
107
107
  "ai": "^6.0.170",
108
108
  "ai-sdk-ollama": "^3.8.3",
@@ -56,6 +56,7 @@ export {
56
56
  buildLazyToolsRecord,
57
57
  buildLoadFirstPartyToolsTool,
58
58
  buildSimulatorTool,
59
+ getLazyToolDefinitions,
59
60
  } from './tools/lazy/index';
60
61
 
61
62
  // =======================
@@ -13,6 +13,7 @@ import {
13
13
  createXaiModel,
14
14
  createZaiCodingModel,
15
15
  createZaiModel,
16
+ isXaiGrokCliModel,
16
17
  normalizeOllamaBaseURL,
17
18
  resolveOpenAIResponsesModel,
18
19
  shouldUseOpenAIResponsesApi,
@@ -195,10 +196,14 @@ export async function resolveModel(
195
196
  }
196
197
 
197
198
  if (provider === 'xai') {
199
+ if (isXaiGrokCliModel(model) && !config.oauth) {
200
+ throw new Error('Grok Build and Grok Composer 2.5 require xAI OAuth.');
201
+ }
198
202
  return createXaiModel(model, {
199
203
  apiKey: config.oauth?.access ?? config.apiKey,
200
204
  baseURL: config.baseURL,
201
205
  useResponses: !!config.oauth,
206
+ useGrokCliProxy: !!config.oauth && isXaiGrokCliModel(model),
202
207
  });
203
208
  }
204
209
 
@@ -81,15 +81,25 @@ export function buildReadTool(projectRoot: string): {
81
81
  .describe(
82
82
  'Ending line number (1-indexed, inclusive). Required if startLine is provided.',
83
83
  ),
84
+ maxLines: z
85
+ .number()
86
+ .int()
87
+ .min(1)
88
+ .optional()
89
+ .describe(
90
+ 'Number of lines to read starting at startLine. Ignored when endLine is provided.',
91
+ ),
84
92
  }),
85
93
  async execute({
86
94
  path,
87
95
  startLine,
88
96
  endLine,
97
+ maxLines,
89
98
  }: {
90
99
  path: string;
91
100
  startLine?: number;
92
101
  endLine?: number;
102
+ maxLines?: number;
93
103
  }): Promise<ToolResponse<ReadResult>> {
94
104
  if (!path || path.trim().length === 0) {
95
105
  return createToolError(
@@ -111,10 +121,12 @@ export function buildReadTool(projectRoot: string): {
111
121
  const indent = detectIndentation(content);
112
122
  await rememberFileRead(projectRoot, abs);
113
123
 
114
- if (startLine !== undefined && endLine !== undefined) {
124
+ if (startLine !== undefined) {
115
125
  const lines = content.split('\n');
126
+ const requestedEndLine =
127
+ endLine ?? startLine + Math.max(1, maxLines ?? 1) - 1;
116
128
  const start = Math.max(1, startLine) - 1;
117
- const end = Math.min(lines.length, endLine);
129
+ const end = Math.min(lines.length, requestedEndLine);
118
130
  const selectedLines = lines.slice(start, end);
119
131
  content = selectedLines.join('\n');
120
132
  const result: ReadResult = {
@@ -122,7 +134,7 @@ export function buildReadTool(projectRoot: string): {
122
134
  path: req,
123
135
  content,
124
136
  size: content.length,
125
- lineRange: `@${startLine}-${endLine}`,
137
+ lineRange: `@${startLine}-${requestedEndLine}`,
126
138
  totalLines: lines.length,
127
139
  };
128
140
  if (indent) result.indentation = indent;
@@ -6,3 +6,5 @@
6
6
  Usage tips:
7
7
  - Prefer relative project paths when possible (more portable)
8
8
  - For large files or searches, use the Grep or Ripgrep tool
9
+ - Use startLine/endLine or startLine/maxLines for targeted reads of large files
10
+ - If startLine is provided without endLine or maxLines, only that one line is read
@@ -1,4 +1,15 @@
1
- import { PATCH_BEGIN_MARKER, PATCH_END_MARKER } from './constants.ts';
1
+ import {
2
+ PATCH_ADD_PREFIX,
3
+ PATCH_BEGIN_MARKER,
4
+ PATCH_DELETE_LINES_PREFIX,
5
+ PATCH_DELETE_PREFIX,
6
+ PATCH_END_MARKER,
7
+ PATCH_INSERT_AFTER_PREFIX,
8
+ PATCH_INSERT_BEFORE_PREFIX,
9
+ PATCH_REPLACE_LINES_PREFIX,
10
+ PATCH_REPLACE_PREFIX,
11
+ PATCH_UPDATE_PREFIX,
12
+ } from './constants.ts';
2
13
 
3
14
  export function repairPatchContent(patch: string): string {
4
15
  patch = extractPatchFromWrappedJson(patch);
@@ -35,12 +46,16 @@ function appendMissingEndMarker(patch: string): string {
35
46
  if (trimmed.includes(PATCH_END_MARKER)) return patch;
36
47
 
37
48
  const hasContent =
38
- trimmed.includes('*** Update File:') ||
39
- trimmed.includes('*** Add File:') ||
40
- trimmed.includes('*** Delete File:') ||
41
- trimmed.includes('*** Replace in:');
42
-
43
- if (hasContent) {
49
+ trimmed.includes(PATCH_UPDATE_PREFIX) ||
50
+ trimmed.includes(PATCH_ADD_PREFIX) ||
51
+ trimmed.includes(PATCH_DELETE_PREFIX) ||
52
+ trimmed.includes(PATCH_REPLACE_PREFIX) ||
53
+ trimmed.includes(PATCH_DELETE_LINES_PREFIX) ||
54
+ trimmed.includes(PATCH_REPLACE_LINES_PREFIX) ||
55
+ trimmed.includes(PATCH_INSERT_BEFORE_PREFIX) ||
56
+ trimmed.includes(PATCH_INSERT_AFTER_PREFIX);
57
+
58
+ if (hasContent || trimmed.trim() === PATCH_BEGIN_MARKER) {
44
59
  return `${trimmed}\n${PATCH_END_MARKER}`;
45
60
  }
46
61
 
@@ -110,11 +110,24 @@ export function buildApplyPatchTool(projectRoot: string): {
110
110
  const message = error instanceof Error ? error.message : String(error);
111
111
  return createToolError(message, 'validation', {
112
112
  parameter: 'patch',
113
- suggestion:
114
- 'Provide patch content using the enveloped format (*** Begin Patch ... *** End Patch) or standard unified diff format (---/+++ headers).',
113
+ suggestion: message.includes('Missing "*** End Patch"')
114
+ ? 'The patch tool automatically appends a missing end marker when real patch operations are present. This input appears incomplete; resend the full patch body, not just the begin marker.'
115
+ : 'Provide patch content using the enveloped format (*** Begin Patch ... *** End Patch) or standard unified diff format (---/+++ headers).',
115
116
  });
116
117
  }
117
118
 
119
+ if (operations.length === 0) {
120
+ return createToolError(
121
+ 'Patch contains no operations. Do not retry the same empty patch; include at least one Add/Update/Delete/Replace directive between the begin and end markers.',
122
+ 'validation',
123
+ {
124
+ parameter: 'patch',
125
+ suggestion:
126
+ 'Provide a complete patch body such as *** Replace in: path, *** Find:, *** With:, then *** End Patch.',
127
+ },
128
+ );
129
+ }
130
+
118
131
  try {
119
132
  const result = await applyPatchOperations(projectRoot, operations, {
120
133
  useFuzzy: fuzzyMatch,
@@ -42,6 +42,9 @@ export function buildRipgrepTool(projectRoot: string): {
42
42
  ToolResponse<{
43
43
  count: number;
44
44
  matches: Array<{ file: string; line: number; text: string }>;
45
+ truncated?: boolean;
46
+ shownMatches?: number;
47
+ files?: Array<{ file: string; matches: number }>;
45
48
  }>
46
49
  > {
47
50
  function expandTilde(p: string) {
@@ -54,7 +57,14 @@ export function buildRipgrepTool(projectRoot: string): {
54
57
  const p = expandTilde(String(path ?? '.')).trim();
55
58
  const isAbs = p.startsWith('/') || /^[A-Za-z]:[\\/]/.test(p);
56
59
  const target = p ? (isAbs ? p : join(projectRoot, p)) : projectRoot;
57
- const args = ['--no-heading', '--line-number', '--color=never'];
60
+ const args = [
61
+ '--no-heading',
62
+ '--line-number',
63
+ '--color=never',
64
+ '--max-columns',
65
+ '240',
66
+ '--max-columns-preview',
67
+ ];
58
68
  if (ignoreCase) args.push('-i');
59
69
  if (Array.isArray(glob)) for (const g of glob) args.push('-g', g);
60
70
  args.push('--max-count', String(maxResults));
@@ -65,11 +75,72 @@ export function buildRipgrepTool(projectRoot: string): {
65
75
  const rgBin = await resolveBinary('rg');
66
76
  return await new Promise((resolve) => {
67
77
  const proc = spawn(rgBin, args, { cwd: projectRoot });
68
- let stdout = '';
69
78
  let stderr = '';
79
+ let pendingLine = '';
80
+ let truncated = false;
81
+ let settled = false;
82
+ const TEXT_MAX = 200;
83
+ const matches: Array<{ file: string; line: number; text: string }> =
84
+ [];
85
+ const fileCounts = new Map<string, number>();
86
+
87
+ const parseLine = (lineText: string) => {
88
+ if (!lineText || matches.length >= maxResults) return;
89
+ const m = lineText.match(/^(.+?):(\d+):(.*)$/s);
90
+ const match = (() => {
91
+ if (!m) {
92
+ return {
93
+ file: '',
94
+ line: 0,
95
+ text:
96
+ lineText.length > TEXT_MAX
97
+ ? `${lineText.slice(0, TEXT_MAX)}…`
98
+ : lineText,
99
+ };
100
+ }
101
+ const file = m[1];
102
+ const line = Number.parseInt(m[2], 10);
103
+ const raw = m[3];
104
+ const text =
105
+ raw.length > TEXT_MAX ? `${raw.slice(0, TEXT_MAX)}…` : raw;
106
+ return { file, line, text };
107
+ })();
108
+ matches.push(match);
109
+ if (match.file) {
110
+ fileCounts.set(match.file, (fileCounts.get(match.file) ?? 0) + 1);
111
+ }
112
+ if (matches.length >= maxResults) {
113
+ truncated = true;
114
+ proc.kill('SIGTERM');
115
+ }
116
+ };
117
+
118
+ const resolveSuccess = () => {
119
+ if (settled) return;
120
+ settled = true;
121
+ const files = Array.from(fileCounts.entries()).map(
122
+ ([file, count]) => ({ file, matches: count }),
123
+ );
124
+ resolve({
125
+ ok: true,
126
+ count: matches.length,
127
+ matches,
128
+ ...(truncated
129
+ ? { truncated: true, shownMatches: matches.length }
130
+ : {}),
131
+ ...(files.length ? { files } : {}),
132
+ });
133
+ };
70
134
 
71
135
  proc.stdout.on('data', (data) => {
72
- stdout += data.toString();
136
+ if (matches.length >= maxResults) return;
137
+ pendingLine += data.toString();
138
+ const lines = pendingLine.split('\n');
139
+ pendingLine = lines.pop() ?? '';
140
+ for (const line of lines) {
141
+ parseLine(line);
142
+ if (matches.length >= maxResults) break;
143
+ }
73
144
  });
74
145
 
75
146
  proc.stderr.on('data', (data) => {
@@ -77,7 +148,9 @@ export function buildRipgrepTool(projectRoot: string): {
77
148
  });
78
149
 
79
150
  proc.on('close', (code) => {
80
- if (code !== 0 && code !== 1) {
151
+ if (pendingLine && matches.length < maxResults)
152
+ parseLine(pendingLine);
153
+ if (!truncated && code !== 0 && code !== 1) {
81
154
  resolve(
82
155
  createToolError(
83
156
  stderr.trim() || 'ripgrep failed',
@@ -91,30 +164,12 @@ export function buildRipgrepTool(projectRoot: string): {
91
164
  return;
92
165
  }
93
166
 
94
- const lines = stdout
95
- .split('\n')
96
- .filter(Boolean)
97
- .slice(0, maxResults);
98
- const TEXT_MAX = 200;
99
- const matches = lines.map((l) => {
100
- const m = l.match(/^(.+?):(\d+):(.*)$/s);
101
- if (!m)
102
- return {
103
- file: '',
104
- line: 0,
105
- text: l.length > TEXT_MAX ? `${l.slice(0, TEXT_MAX)}…` : l,
106
- };
107
- const file = m[1];
108
- const line = Number.parseInt(m[2], 10);
109
- const raw = m[3];
110
- const text =
111
- raw.length > TEXT_MAX ? `${raw.slice(0, TEXT_MAX)}…` : raw;
112
- return { file, line, text };
113
- });
114
- resolve({ ok: true, count: matches.length, matches });
167
+ resolveSuccess();
115
168
  });
116
169
 
117
170
  proc.on('error', (err) => {
171
+ if (settled) return;
172
+ settled = true;
118
173
  resolve(
119
174
  createToolError(String(err), 'execution', {
120
175
  suggestion: 'Ensure ripgrep (rg) is installed',
@@ -8,5 +8,7 @@ This is the only content-search tool available. Use it for any text search acros
8
8
  ## Usage tips
9
9
 
10
10
  - Narrow the search set with `glob` first if the pattern may match too broadly.
11
+ - Prefer narrow `path` and `glob` values over repo-wide searches.
12
+ - Keep `maxResults` low for broad searches; the tool stops after the global limit.
11
13
  - Batch independent searches (e.g. multiple function names) in a single turn for parallel execution.
12
14
  - Use `ignoreCase: true` for case-insensitive matching; pass `glob` patterns (e.g. `["*.ts"]`) to limit file types.
@@ -42,9 +42,44 @@ function killProcessTree(pid: number) {
42
42
  }
43
43
  }
44
44
 
45
- export type ShellOutputMode = 'full' | 'tail';
45
+ export type ShellOutputMode = 'auto' | 'full' | 'tail';
46
46
 
47
47
  const DEFAULT_TAIL_LINES = 100;
48
+ const DEFAULT_MAX_OUTPUT_BYTES = 128_000;
49
+
50
+ type CompactTextResult = {
51
+ text: string;
52
+ truncated: boolean;
53
+ originalBytes: number;
54
+ shownBytes: number;
55
+ };
56
+
57
+ function compactTextByBytes(
58
+ text: string,
59
+ maxBytes: number,
60
+ label: string,
61
+ ): CompactTextResult {
62
+ const originalBytes = Buffer.byteLength(text, 'utf8');
63
+ if (maxBytes <= 0 || originalBytes <= maxBytes) {
64
+ return { text, truncated: false, originalBytes, shownBytes: originalBytes };
65
+ }
66
+
67
+ const marker = `\n… omitted ${originalBytes - maxBytes} bytes from ${label} …\n`;
68
+ const markerBytes = Buffer.byteLength(marker, 'utf8');
69
+ const budget = Math.max(0, maxBytes - markerBytes);
70
+ const headBytes = Math.floor(budget / 2);
71
+ const tailBytes = budget - headBytes;
72
+ const buffer = Buffer.from(text, 'utf8');
73
+ const compacted = `${buffer.subarray(0, headBytes).toString('utf8')}${marker}${buffer
74
+ .subarray(buffer.byteLength - tailBytes)
75
+ .toString('utf8')}`;
76
+ return {
77
+ text: compacted,
78
+ truncated: true,
79
+ originalBytes,
80
+ shownBytes: Buffer.byteLength(compacted, 'utf8'),
81
+ };
82
+ }
48
83
 
49
84
  export function appendTailLines(
50
85
  current: string,
@@ -66,6 +101,13 @@ type ShellResult = ToolResponse<{
66
101
  stderr: string;
67
102
  outputMode?: ShellOutputMode;
68
103
  tailLines?: number;
104
+ maxOutputBytes?: number;
105
+ stdoutTruncated?: boolean;
106
+ stdoutOriginalBytes?: number;
107
+ stdoutShownBytes?: number;
108
+ stderrTruncated?: boolean;
109
+ stderrOriginalBytes?: number;
110
+ stderrShownBytes?: number;
69
111
  }>;
70
112
 
71
113
  type ShellStreamChunk =
@@ -110,11 +152,11 @@ const shellInputSchema = z
110
152
  .default(300000)
111
153
  .describe('Timeout in milliseconds (default: 300000 = 5 minutes)'),
112
154
  outputMode: z
113
- .enum(['full', 'tail'])
155
+ .enum(['auto', 'full', 'tail'])
114
156
  .optional()
115
- .default('full')
157
+ .default('auto')
116
158
  .describe(
117
- 'Output capture mode. Use "full" for complete stdout/stderr, or "tail" to keep only the last tailLines lines and avoid huge tool results.',
159
+ 'Output capture mode. Use "auto" for bounded output, "full" for full output up to maxOutputBytes, or "tail" to keep only the last tailLines lines.',
118
160
  ),
119
161
  tailLines: z
120
162
  .number()
@@ -126,6 +168,16 @@ const shellInputSchema = z
126
168
  .describe(
127
169
  'Number of trailing stdout/stderr lines to keep when outputMode is "tail"',
128
170
  ),
171
+ maxOutputBytes: z
172
+ .number()
173
+ .int()
174
+ .min(0)
175
+ .max(10_000_000)
176
+ .optional()
177
+ .default(DEFAULT_MAX_OUTPUT_BYTES)
178
+ .describe(
179
+ 'Maximum bytes to keep per stdout/stderr in the final tool result. Use 0 to disable byte capping.',
180
+ ),
129
181
  })
130
182
  .strict();
131
183
 
@@ -154,8 +206,9 @@ export function buildShellTool(projectRoot: string): {
154
206
  cwd,
155
207
  allowNonZeroExit,
156
208
  timeout = 300000,
157
- outputMode = 'full',
209
+ outputMode = 'auto',
158
210
  tailLines = DEFAULT_TAIL_LINES,
211
+ maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES,
159
212
  }: ShellInput,
160
213
  options?: { abortSignal?: AbortSignal },
161
214
  ): AsyncIterable<ShellStreamChunk> | ShellResult {
@@ -179,6 +232,7 @@ export function buildShellTool(projectRoot: string): {
179
232
  timeout,
180
233
  outputMode,
181
234
  tailLines,
235
+ maxOutputBytes,
182
236
  },
183
237
  options,
184
238
  ) as AsyncIterable<ShellStreamChunk> | ShellResult;
@@ -217,6 +271,49 @@ export function buildShellTool(projectRoot: string): {
217
271
  const settle = (result: ShellResult) => {
218
272
  if (settled) return;
219
273
  settled = true;
274
+ if ('stdout' in result && 'stderr' in result) {
275
+ const stdoutCompact = compactTextByBytes(
276
+ result.stdout,
277
+ maxOutputBytes,
278
+ 'shell stdout',
279
+ );
280
+ const stderrCompact = compactTextByBytes(
281
+ result.stderr,
282
+ maxOutputBytes,
283
+ 'shell stderr',
284
+ );
285
+ result.stdout = stdoutCompact.text;
286
+ result.stderr = stderrCompact.text;
287
+ result.maxOutputBytes = maxOutputBytes;
288
+ if (stdoutCompact.truncated) {
289
+ result.stdoutTruncated = true;
290
+ result.stdoutOriginalBytes = stdoutCompact.originalBytes;
291
+ result.stdoutShownBytes = stdoutCompact.shownBytes;
292
+ }
293
+ if (stderrCompact.truncated) {
294
+ result.stderrTruncated = true;
295
+ result.stderrOriginalBytes = stderrCompact.originalBytes;
296
+ result.stderrShownBytes = stderrCompact.shownBytes;
297
+ }
298
+ } else if ('details' in result && result.details) {
299
+ const details = result.details;
300
+ for (const field of ['stdout', 'stderr'] as const) {
301
+ const value = details[field];
302
+ if (typeof value !== 'string') continue;
303
+ const compact = compactTextByBytes(
304
+ value,
305
+ maxOutputBytes,
306
+ `shell ${field}`,
307
+ );
308
+ details[field] = compact.text;
309
+ if (compact.truncated) {
310
+ details[`${field}Truncated`] = true;
311
+ details[`${field}OriginalBytes`] = compact.originalBytes;
312
+ details[`${field}ShownBytes`] = compact.shownBytes;
313
+ }
314
+ }
315
+ details.maxOutputBytes = maxOutputBytes;
316
+ }
220
317
  if (timeoutId) clearTimeout(timeoutId);
221
318
  if (abortSignal) {
222
319
  abortSignal.removeEventListener('abort', onAbort);
@@ -248,7 +345,7 @@ export function buildShellTool(projectRoot: string): {
248
345
  proc.stdout?.on('data', (chunk) => {
249
346
  const text = chunk.toString();
250
347
  stdout =
251
- outputMode === 'tail'
348
+ outputMode === 'tail' || outputMode === 'auto'
252
349
  ? appendTailLines(stdout, text, tailLines)
253
350
  : `${stdout}${text}`;
254
351
  pushDelta(text);
@@ -257,7 +354,7 @@ export function buildShellTool(projectRoot: string): {
257
354
  proc.stderr?.on('data', (chunk) => {
258
355
  const text = chunk.toString();
259
356
  stderr =
260
- outputMode === 'tail'
357
+ outputMode === 'tail' || outputMode === 'auto'
261
358
  ? appendTailLines(stderr, text, tailLines)
262
359
  : `${stderr}${text}`;
263
360
  pushDelta(text);
@@ -270,7 +367,9 @@ export function buildShellTool(projectRoot: string): {
270
367
  cmd,
271
368
  stdout,
272
369
  stderr,
273
- ...(outputMode === 'tail' ? { outputMode, tailLines } : {}),
370
+ ...(outputMode === 'tail' || outputMode === 'auto'
371
+ ? { outputMode, tailLines, maxOutputBytes }
372
+ : { outputMode, maxOutputBytes }),
274
373
  }),
275
374
  );
276
375
  return;
@@ -286,7 +385,9 @@ export function buildShellTool(projectRoot: string): {
286
385
  value: timeout,
287
386
  stdout,
288
387
  stderr,
289
- ...(outputMode === 'tail' ? { outputMode, tailLines } : {}),
388
+ ...(outputMode === 'tail' || outputMode === 'auto'
389
+ ? { outputMode, tailLines, maxOutputBytes }
390
+ : { outputMode, maxOutputBytes }),
290
391
  suggestion: 'Increase timeout or optimize the command',
291
392
  },
292
393
  ),
@@ -303,7 +404,9 @@ export function buildShellTool(projectRoot: string): {
303
404
  stdout,
304
405
  stderr,
305
406
  cmd,
306
- ...(outputMode === 'tail' ? { outputMode, tailLines } : {}),
407
+ ...(outputMode === 'tail' || outputMode === 'auto'
408
+ ? { outputMode, tailLines, maxOutputBytes }
409
+ : { outputMode, maxOutputBytes }),
307
410
  suggestion: 'Check command syntax or use allowNonZeroExit: true',
308
411
  }),
309
412
  );
@@ -315,7 +418,9 @@ export function buildShellTool(projectRoot: string): {
315
418
  exitCode: exitCode ?? 0,
316
419
  stdout,
317
420
  stderr,
318
- ...(outputMode === 'tail' ? { outputMode, tailLines } : {}),
421
+ ...(outputMode === 'tail' || outputMode === 'auto'
422
+ ? { outputMode, tailLines, maxOutputBytes }
423
+ : { outputMode, maxOutputBytes }),
319
424
  });
320
425
  });
321
426
 
@@ -7,8 +7,10 @@
7
7
  ## Usage tips
8
8
 
9
9
  - Chain commands with `&&` to fail-fast.
10
- - For long outputs, redirect to a file and `read` it back.
11
- - For verbose commands like builds or tests, set `outputMode: "tail"` and optionally `tailLines` to return only the last N stdout/stderr lines in the final tool result while still streaming full output to the UI. Use `outputMode: "full"` when you need complete output in the result.
10
+ - For long outputs, redirect to a file, inspect `wc -c`, then read a small range or tail.
11
+ - `outputMode: "auto"` is the default and keeps bounded tail output. Use `outputMode: "tail"` with `tailLines` for verbose builds/tests, or `outputMode: "full"` only when complete output is truly needed.
12
+ - Final `stdout`/`stderr` are capped by `maxOutputBytes` per stream to avoid huge tool results. Set `maxOutputBytes: 0` only when you intentionally need uncapped output.
13
+ - For binary, minified, or `strings` searches, cap line width explicitly, e.g. `rg -o '.{0,80}needle.{0,120}' | head -50`.
12
14
  - For long-running non-interactive commands, set an appropriate `timeout` and ensure the command exits on its own.
13
15
  - Batch independent checks (e.g. `git status && git diff`) in parallel tool calls rather than sequential shell chains when you need results separately.
14
16
  - Never use `shell` with `sed`/`awk` for programmatic file editing — use the dedicated file-editing tools instead.
@@ -42,10 +42,16 @@ export function buildLazyToolsRecord(
42
42
  return record;
43
43
  }
44
44
 
45
- export function buildLoadFirstPartyToolsTool(): { name: string; tool: Tool } {
46
- const briefs = getLazyToolDefinitions().map(({ name, description }) => ({
47
- name,
48
- description,
49
- }));
45
+ export function buildLoadFirstPartyToolsTool(allowedNames?: Iterable<string>): {
46
+ name: string;
47
+ tool: Tool;
48
+ } {
49
+ const allowed = allowedNames ? new Set(allowedNames) : null;
50
+ const briefs = getLazyToolDefinitions()
51
+ .filter(({ name }) => !allowed || allowed.has(name))
52
+ .map(({ name, description }) => ({
53
+ name,
54
+ description,
55
+ }));
50
56
  return buildLoadToolsTool(briefs);
51
57
  }
package/src/index.ts CHANGED
@@ -144,7 +144,12 @@ export {
144
144
  export type { AnthropicOAuthConfig } from './providers/src/index.ts';
145
145
  export { createGoogleModel } from './providers/src/index.ts';
146
146
  export type { GoogleProviderConfig } from './providers/src/index.ts';
147
- export { createXaiModel } from './providers/src/index.ts';
147
+ export {
148
+ createXaiModel,
149
+ getGrokCliHeaders,
150
+ isXaiGrokCliModel,
151
+ XAI_GROK_CLI_MODEL_IDS,
152
+ } from './providers/src/index.ts';
148
153
  export type { XaiProviderConfig } from './providers/src/index.ts';
149
154
  export { createZaiModel, createZaiCodingModel } from './providers/src/index.ts';
150
155
  export type { ZaiProviderConfig } from './providers/src/index.ts';
@@ -306,6 +311,7 @@ export {
306
311
  buildLazyToolsRecord,
307
312
  buildLoadFirstPartyToolsTool,
308
313
  buildSimulatorTool,
314
+ getLazyToolDefinitions,
309
315
  } from './core/src/index.ts';
310
316
  export {
311
317
  appendCoAuthorTrailer,