@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.
- package/package.json +1 -1
- package/src/config/src/index.ts +2 -1
- package/src/config/src/manager.ts +1 -0
- package/src/core/src/providers/resolver.ts +3 -2
- package/src/core/src/tools/bin-manager.ts +88 -33
- package/src/core/src/tools/builtin/fs/edit.txt +1 -1
- package/src/core/src/tools/builtin/fs/index.ts +2 -0
- package/src/core/src/tools/builtin/fs/multiedit.txt +1 -1
- package/src/core/src/tools/builtin/glob.txt +4 -0
- package/src/core/src/tools/builtin/patch/indentation.ts +8 -1
- package/src/core/src/tools/builtin/patch/normalize.ts +4 -0
- package/src/core/src/tools/builtin/patch/repair.ts +42 -0
- package/src/core/src/tools/builtin/patch.txt +2 -0
- package/src/core/src/tools/builtin/search.txt +4 -0
- package/src/core/src/tools/builtin/shell.ts +161 -22
- package/src/core/src/tools/builtin/shell.txt +15 -1
- package/src/core/src/tools/loader.ts +8 -4
- package/src/index.ts +2 -5
- package/src/prompts/src/agents/build.txt +2 -2
- package/src/prompts/src/providers/{moonshot.txt → kimi.txt} +1 -1
- package/src/prompts/src/providers.ts +5 -5
- package/src/providers/src/catalog-manual.ts +101 -9
- package/src/providers/src/catalog.ts +74 -34
- package/src/providers/src/env.ts +4 -7
- package/src/providers/src/index.ts +3 -9
- package/src/providers/src/{moonshot-client.ts → kimi-client.ts} +131 -15
- package/src/providers/src/model-merge.ts +7 -1
- package/src/providers/src/pricing.ts +1 -1
- package/src/providers/src/registry.ts +8 -19
- package/src/providers/src/utils.ts +7 -8
- package/src/providers/src/zai-client.ts +3 -0
- package/src/types/src/config.ts +1 -0
- package/src/types/src/provider.ts +4 -4
package/package.json
CHANGED
package/src/config/src/index.ts
CHANGED
|
@@ -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
|
-
|
|
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',
|
|
@@ -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 === '
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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: [
|
|
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
|
|
135
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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"`.
|