@ottocode/sdk 0.1.314 → 0.1.316

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.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/src/config/src/index.ts +2 -1
  3. package/src/config/src/manager.ts +1 -0
  4. package/src/core/src/providers/resolver.ts +3 -2
  5. package/src/core/src/tools/bin-manager.ts +88 -33
  6. package/src/core/src/tools/builtin/fs/edit.txt +1 -1
  7. package/src/core/src/tools/builtin/fs/index.ts +2 -0
  8. package/src/core/src/tools/builtin/fs/multiedit.txt +1 -1
  9. package/src/core/src/tools/builtin/glob.txt +4 -0
  10. package/src/core/src/tools/builtin/patch/indentation.ts +8 -1
  11. package/src/core/src/tools/builtin/patch/normalize.ts +4 -0
  12. package/src/core/src/tools/builtin/patch/repair.ts +42 -0
  13. package/src/core/src/tools/builtin/patch.txt +2 -0
  14. package/src/core/src/tools/builtin/search.txt +4 -0
  15. package/src/core/src/tools/builtin/shell.ts +161 -22
  16. package/src/core/src/tools/builtin/shell.txt +15 -1
  17. package/src/core/src/tools/loader.ts +8 -4
  18. package/src/index.ts +2 -5
  19. package/src/prompts/src/agents/build.txt +2 -2
  20. package/src/prompts/src/providers/{moonshot.txt → kimi.txt} +1 -1
  21. package/src/prompts/src/providers.ts +5 -5
  22. package/src/providers/src/catalog-manual.ts +101 -9
  23. package/src/providers/src/catalog.ts +74 -34
  24. package/src/providers/src/env.ts +4 -7
  25. package/src/providers/src/index.ts +3 -9
  26. package/src/providers/src/{moonshot-client.ts → kimi-client.ts} +131 -15
  27. package/src/providers/src/model-merge.ts +7 -1
  28. package/src/providers/src/pricing.ts +1 -1
  29. package/src/providers/src/registry.ts +8 -19
  30. package/src/providers/src/utils.ts +7 -8
  31. package/src/providers/src/zai-client.ts +3 -0
  32. package/src/types/src/config.ts +1 -0
  33. package/src/types/src/provider.ts +4 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.314",
3
+ "version": "0.1.316",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -25,7 +25,7 @@ const DEFAULT_PROVIDER_SETTINGS: OttoConfig['providers'] = {
25
25
  xai: { enabled: false },
26
26
  zai: { enabled: false },
27
27
  'zai-coding': { enabled: false },
28
- moonshot: { enabled: false },
28
+ kimi: { enabled: false },
29
29
  minimax: { enabled: false },
30
30
  };
31
31
 
@@ -42,6 +42,7 @@ const DEFAULTS: {
42
42
  reasoningText: true,
43
43
  reasoningLevel: 'high',
44
44
  theme: 'dark',
45
+ tuiTheme: 'tokyo-night',
45
46
  vimMode: false,
46
47
  compactThread: true,
47
48
  fontFamily: 'IBM Plex Mono',
@@ -86,6 +86,7 @@ export async function writeDefaults(
86
86
  reasoningText: boolean;
87
87
  reasoningLevel: 'minimal' | 'low' | 'medium' | 'high' | 'max' | 'xhigh';
88
88
  theme: string;
89
+ tuiTheme: string;
89
90
  vimMode: boolean;
90
91
  compactThread: boolean;
91
92
  fontFamily: string;
@@ -33,7 +33,6 @@ export type ProviderName =
33
33
  | 'xai'
34
34
  | 'zai'
35
35
  | 'zai-coding'
36
- | 'moonshot'
37
36
  | 'kimi'
38
37
  | 'minimax';
39
38
 
@@ -212,6 +211,7 @@ export async function resolveModel(
212
211
  return createZaiModel(model, {
213
212
  apiKey: config.apiKey,
214
213
  baseURL: config.baseURL,
214
+ fetch: config.customFetch,
215
215
  });
216
216
  }
217
217
 
@@ -219,10 +219,11 @@ export async function resolveModel(
219
219
  return createZaiCodingModel(model, {
220
220
  apiKey: config.apiKey,
221
221
  baseURL: config.baseURL,
222
+ fetch: config.customFetch,
222
223
  });
223
224
  }
224
225
 
225
- if (provider === 'moonshot' || provider === 'kimi') {
226
+ if (provider === 'kimi') {
226
227
  return createKimiModel(model, {
227
228
  apiKey: config.apiKey,
228
229
  baseURL: config.baseURL,
@@ -15,6 +15,16 @@ let cachedLoginPath: {
15
15
  path: string | null;
16
16
  } | null = null;
17
17
 
18
+ let cachedLoginEnv: {
19
+ key: string;
20
+ env: NodeJS.ProcessEnv | null;
21
+ } | null = null;
22
+
23
+ export type ShellEnvMode = 'fast' | 'login-cache' | 'login-fresh';
24
+
25
+ const ENV_JSON_START = '___OTTO_ENV_JSON_START___';
26
+ const ENV_JSON_END = '___OTTO_ENV_JSON_END___';
27
+
18
28
  export { getAgiBinDir } from './bin-manager/paths.ts';
19
29
 
20
30
  async function whichBinary(name: string): Promise<string | null> {
@@ -72,29 +82,35 @@ export function getUserShell(): string {
72
82
  return process.env.SHELL || '/bin/bash';
73
83
  }
74
84
 
75
- function getShellRcBootstrap(shell: string): string {
76
- const shellName = shell.split('/').pop() || '';
77
- if (shellName.includes('zsh')) {
78
- return 'if [ -f "$HOME/.zshrc" ]; then . "$HOME/.zshrc"; fi';
79
- }
80
- if (shellName.includes('bash')) {
81
- return 'if [ -f "$HOME/.bashrc" ]; then . "$HOME/.bashrc"; fi';
82
- }
83
- return '';
84
- }
85
-
86
- function getInteractiveShellFlag(shell: string): string {
87
- const shellName = shell.split('/').pop() || '';
88
- if (shellName.includes('bash')) return '-ic';
89
- return '-ilc';
90
- }
91
-
92
- export function getShellExecutionConfig(cmd: string): {
85
+ export function getShellExecutionConfig(
86
+ cmd: string,
87
+ options?: { envMode?: ShellEnvMode },
88
+ ): {
89
+ command: string;
90
+ args: string[];
91
+ env: NodeJS.ProcessEnv;
92
+ };
93
+ export function getShellExecutionConfig(
94
+ cmd: string,
95
+ options: { envMode?: ShellEnvMode } = {},
96
+ ): {
93
97
  command: string;
94
98
  args: string[];
95
99
  env: NodeJS.ProcessEnv;
96
100
  } {
97
- const env = { ...process.env, PATH: getAugmentedPath() };
101
+ const envMode = options.envMode ?? 'fast';
102
+ const loginEnv =
103
+ envMode === 'fast' ? null : getLoginShellEnv(envMode === 'login-fresh');
104
+ const env = {
105
+ ...process.env,
106
+ ...(loginEnv ?? {}),
107
+ PATH: mergePaths([
108
+ getAgiBinDir(),
109
+ loginEnv?.PATH,
110
+ getLoginShellPath(),
111
+ process.env.PATH,
112
+ ]),
113
+ };
98
114
  if (process.platform === 'win32') {
99
115
  return {
100
116
  command: getUserShell(),
@@ -106,11 +122,56 @@ export function getShellExecutionConfig(cmd: string): {
106
122
  const command = getUserShell();
107
123
  return {
108
124
  command,
109
- args: [getInteractiveShellFlag(command), 'eval "$OTTO_SHELL_COMMAND"'],
125
+ args: ['-c', 'eval "$OTTO_SHELL_COMMAND"'],
110
126
  env: { ...env, OTTO_SHELL_COMMAND: cmd },
111
127
  };
112
128
  }
113
129
 
130
+ function getLoginShellEnv(refresh: boolean): NodeJS.ProcessEnv | null {
131
+ const home = process.env.HOME || homedir();
132
+ const userShell = getUserShell();
133
+ const cacheKey = [home, userShell, process.env.PATH || ''].join('\0');
134
+ if (!refresh && cachedLoginEnv?.key === cacheKey) return cachedLoginEnv.env;
135
+
136
+ if (process.platform === 'win32') {
137
+ cachedLoginEnv = { key: cacheKey, env: { ...process.env } };
138
+ return cachedLoginEnv.env;
139
+ }
140
+ try {
141
+ const output = execFileSync(
142
+ userShell,
143
+ [
144
+ '-ic',
145
+ `printf '%s\n' ${JSON.stringify(ENV_JSON_START)}; env; printf '%s\n' ${JSON.stringify(ENV_JSON_END)}`,
146
+ ],
147
+ {
148
+ timeout: 10000,
149
+ stdio: ['ignore', 'pipe', 'ignore'],
150
+ env: {
151
+ ...process.env,
152
+ HOME: home,
153
+ USER: process.env.USER || '',
154
+ SHELL: userShell,
155
+ },
156
+ },
157
+ ).toString();
158
+ const start = output.indexOf(ENV_JSON_START);
159
+ const end = output.indexOf(ENV_JSON_END, start + ENV_JSON_START.length);
160
+ if (start >= 0 && end > start) {
161
+ const env: NodeJS.ProcessEnv = {};
162
+ const body = output.slice(start + ENV_JSON_START.length, end).trim();
163
+ for (const line of body.split('\n')) {
164
+ const separator = line.indexOf('=');
165
+ if (separator <= 0) continue;
166
+ env[line.slice(0, separator)] = line.slice(separator + 1);
167
+ }
168
+ cachedLoginEnv = { key: cacheKey, env };
169
+ return env;
170
+ }
171
+ } catch {}
172
+ return null;
173
+ }
174
+
114
175
  function getLoginShellPath(): string | null {
115
176
  const home = process.env.HOME || homedir();
116
177
  const userShell = getUserShell();
@@ -131,10 +192,8 @@ function getLoginShellPath(): string | null {
131
192
 
132
193
  for (const shell of shellCandidates) {
133
194
  try {
134
- const rcBootstrap = getShellRcBootstrap(shell);
135
- const pathCommand = `${rcBootstrap ? `${rcBootstrap}\n` : ''}echo "___PATH___:$PATH"`;
136
- const result = execFileSync(shell, ['-ilc', pathCommand], {
137
- timeout: 5000,
195
+ const result = execFileSync(shell, ['-lc', 'echo "___PATH___:$PATH"'], {
196
+ timeout: 1500,
138
197
  stdio: ['ignore', 'pipe', 'ignore'],
139
198
  env: {
140
199
  ...process.env,
@@ -157,19 +216,15 @@ function getLoginShellPath(): string | null {
157
216
  }
158
217
 
159
218
  export function getAugmentedPath(): string {
160
- const sep = process.platform === 'win32' ? ';' : ':';
161
- const binDir = getAgiBinDir();
162
- const current = process.env.PATH || '';
163
- const loginPath = getLoginShellPath();
219
+ return mergePaths([getAgiBinDir(), getLoginShellPath(), process.env.PATH]);
220
+ }
164
221
 
222
+ function mergePaths(paths: Array<string | null | undefined>): string {
223
+ const sep = process.platform === 'win32' ? ';' : ':';
165
224
  const seen = new Set<string>();
166
225
  const parts: string[] = [];
167
226
 
168
- for (const p of [
169
- binDir,
170
- ...(loginPath ? loginPath.split(sep) : []),
171
- ...current.split(sep),
172
- ]) {
227
+ for (const p of paths.flatMap((path) => (path ? path.split(sep) : []))) {
173
228
  if (p && !seen.has(p)) {
174
229
  seen.add(p);
175
230
  parts.push(p);
@@ -1,6 +1,6 @@
1
1
  Replace an exact text block in an existing file.
2
2
 
3
- Use this for targeted edits instead of structural patch-style editing whenever possible.
3
+ Prefer `apply_patch` for most code/text edits when it is available, because it produces the clearest diff preview. Use `edit` when you have one precise old/new text replacement, when a patch would be awkward, or after patch attempts fail.
4
4
 
5
5
  Rules:
6
6
  - You must read the file first in the current session before editing it.
@@ -9,6 +9,8 @@ import { buildTreeTool } from './tree.ts';
9
9
  import { buildPwdTool } from './pwd.ts';
10
10
  import { buildCdTool } from './cd.ts';
11
11
 
12
+ export { rememberFileRead } from './read-tracker.ts';
13
+
12
14
  export function buildFsTools(
13
15
  projectRoot: string,
14
16
  ): Array<{ name: string; tool: Tool }> {
@@ -1,6 +1,6 @@
1
1
  Apply multiple exact text replacements to a single existing file atomically.
2
2
 
3
- Use this when you need several edits in one file.
3
+ Prefer `apply_patch` for most code/text edits when it is available, especially when a diff is easy to express. Use `multiedit` when you have several precise old/new replacements in one file, when a patch would be awkward, or after patch attempts fail.
4
4
 
5
5
  Rules:
6
6
  - Read the file first before editing.
@@ -6,8 +6,12 @@
6
6
 
7
7
  **Use `glob` first to discover files** before reading them, unless you already know exact paths.
8
8
 
9
+ Think of `glob` as the repository's fast local `find` replacement for filenames and paths. Use it before shelling out to `find`, `fd`, or `ls **`.
10
+
9
11
  ## Usage tips
10
12
 
11
13
  - Use `glob` for filename patterns; use `search` for file contents.
12
14
  - Combine with `path` to restrict the search to a subdirectory.
13
15
  - Prefer reading a known file directly over globbing to "find" it (check the `<project>` listing in the system prompt first).
16
+ - Instead of `find packages -name "*.ts"`, call `glob` with `pattern: "packages/**/*.ts"`.
17
+ - Instead of `find apps -name package.json`, call `glob` with `pattern: "apps/**/package.json"`.
@@ -23,6 +23,7 @@ export function adjustReplacementIndentation(
23
23
  let fileIndentChar: 'tab' | 'space' = 'space';
24
24
  const deltas: number[] = [];
25
25
  let hasAddStyleMismatch = false;
26
+ let hasContextContentMismatch = false;
26
27
  let fileIndentDetected = false;
27
28
 
28
29
  for (const fl of matchedFileLines) {
@@ -81,6 +82,7 @@ export function adjustReplacementIndentation(
81
82
  if (line.kind === 'context') {
82
83
  const fileLine = matchedFileLines[expectedIdx];
83
84
  if (fileLine !== undefined) {
85
+ if (line.content !== fileLine) hasContextContentMismatch = true;
84
86
  lastDelta = computeIndentDelta(line.content, fileLine, tabSize);
85
87
  lastFileIndentExpanded = expandWhitespace(
86
88
  getLeadingWhitespace(fileLine),
@@ -152,7 +154,12 @@ export function adjustReplacementIndentation(
152
154
  }
153
155
  }
154
156
 
155
- if (!hasDelta && !hasStyleMismatch && !hasAddStyleMismatch) {
157
+ if (
158
+ !hasDelta &&
159
+ !hasStyleMismatch &&
160
+ !hasAddStyleMismatch &&
161
+ !hasContextContentMismatch
162
+ ) {
156
163
  return hunk.lines.filter((l) => l.kind !== 'remove').map((l) => l.content);
157
164
  }
158
165
 
@@ -3,6 +3,7 @@ enum NormalizationLevel {
3
3
  TABS_ONLY = 'tabs',
4
4
  WHITESPACE = 'whitespace',
5
5
  AGGRESSIVE = 'aggressive',
6
+ COLLAPSED = 'collapsed',
6
7
  }
7
8
 
8
9
  const DEFAULT_TAB_SIZE = 2;
@@ -22,6 +23,8 @@ export function normalizeWhitespace(
22
23
  return line.replace(/\t/g, tabReplacement).replace(/\s+$/, '');
23
24
  case NormalizationLevel.AGGRESSIVE:
24
25
  return line.replace(/\t/g, tabReplacement).trim();
26
+ case NormalizationLevel.COLLAPSED:
27
+ return line.replace(/\t/g, tabReplacement).trim().replace(/\s+/g, ' ');
25
28
  default:
26
29
  return line;
27
30
  }
@@ -32,6 +35,7 @@ export const NORMALIZATION_LEVELS: NormalizationLevel[] = [
32
35
  NormalizationLevel.TABS_ONLY,
33
36
  NormalizationLevel.WHITESPACE,
34
37
  NormalizationLevel.AGGRESSIVE,
38
+ NormalizationLevel.COLLAPSED,
35
39
  ];
36
40
 
37
41
  export function getLeadingWhitespace(line: string): string {
@@ -13,10 +13,30 @@ import {
13
13
 
14
14
  export function repairPatchContent(patch: string): string {
15
15
  patch = extractPatchFromWrappedJson(patch);
16
+ patch = extractEnvelopedPatchFromText(patch);
17
+ patch = stripTrailingMarkdownFenceBeforeMissingEndMarker(patch);
16
18
  patch = appendMissingEndMarker(patch);
19
+ patch = trimAfterEndMarker(patch);
17
20
  return patch;
18
21
  }
19
22
 
23
+ function looksLikeUnifiedPatch(patch: string): boolean {
24
+ const trimmed = patch.trimStart();
25
+ return (
26
+ trimmed.startsWith('diff --git ') ||
27
+ trimmed.startsWith('--- ') ||
28
+ trimmed.startsWith('Index: ')
29
+ );
30
+ }
31
+
32
+ function extractEnvelopedPatchFromText(patch: string): string {
33
+ const beginIndex = patch.indexOf(PATCH_BEGIN_MARKER);
34
+ if (beginIndex === -1) return patch;
35
+ if (beginIndex === patch.search(/\S/)) return patch;
36
+ if (looksLikeUnifiedPatch(patch)) return patch;
37
+ return patch.slice(beginIndex);
38
+ }
39
+
20
40
  function extractPatchFromWrappedJson(patch: string): string {
21
41
  if (patch.includes(PATCH_BEGIN_MARKER)) return patch;
22
42
 
@@ -61,3 +81,25 @@ function appendMissingEndMarker(patch: string): string {
61
81
 
62
82
  return patch;
63
83
  }
84
+
85
+ function stripTrailingMarkdownFenceBeforeMissingEndMarker(
86
+ patch: string,
87
+ ): string {
88
+ const trimmed = patch.trimEnd();
89
+ if (!trimmed.trimStart().startsWith(PATCH_BEGIN_MARKER)) return patch;
90
+ if (trimmed.includes(PATCH_END_MARKER)) return patch;
91
+ const lines = trimmed.split('\n');
92
+ const last = lines.at(-1)?.trim();
93
+ if (last !== '```') return patch;
94
+ return lines.slice(0, -1).join('\n');
95
+ }
96
+
97
+ function trimAfterEndMarker(patch: string): string {
98
+ if (!patch.trimStart().startsWith(PATCH_BEGIN_MARKER)) return patch;
99
+ const endIndex = patch.indexOf(PATCH_END_MARKER);
100
+ if (endIndex === -1) return patch;
101
+ const endOfMarker = endIndex + PATCH_END_MARKER.length;
102
+ const suffix = patch.slice(endOfMarker);
103
+ if (suffix.trim().length === 0) return patch;
104
+ return patch.slice(0, endOfMarker);
105
+ }
@@ -1,5 +1,7 @@
1
1
  Apply a patch to modify one or more files.
2
2
 
3
+ Prefer this as the first-choice editing tool for code and text changes when it is available. Use exact-replacement tools (`edit`/`multiedit`) when a patch would be awkward, when you only need a precise old/new string replacement, or after patch attempts fail.
4
+
3
5
  Use the **enveloped format** by default. Standard unified diffs (`---` / `+++`) are also accepted.
4
6
 
5
7
  ## Fastest / safest mode (recommended): Replace
@@ -5,6 +5,8 @@
5
5
 
6
6
  Use this for text/code search across the codebase. It is the primary tool for repository content discovery.
7
7
 
8
+ Think of `search` as the repository's fast local `rg`/`grep` replacement: use it for exact string and regex searches before reaching for shell commands.
9
+
8
10
  ## Usage tips
9
11
 
10
12
  - Narrow broad searches with `path` and `glob` values.
@@ -12,3 +14,5 @@ Use this for text/code search across the codebase. It is the primary tool for re
12
14
  - Batch independent searches (e.g. multiple function names) in a single turn for parallel execution.
13
15
  - Use `ignoreCase: true` for case-insensitive matching; pass `glob` patterns (e.g. `["*.ts", "*.tsx"]`) to limit file types.
14
16
  - For filename/path discovery, use `glob` first when you already know the file pattern.
17
+ - Instead of `grep -r "workspace:" packages apps`, call `search` with `query: "workspace:"`, `path: "."`, and `glob: ["**/package.json"]`.
18
+ - Instead of `rg "function foo" src`, call `search` with `query: "function foo"` and `path: "src"`.