@ottocode/sdk 0.1.245 → 0.1.247
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 +7 -2
- package/src/config/src/index.ts +5 -0
- package/src/config/src/manager.ts +106 -30
- package/src/core/src/providers/resolver.ts +28 -1
- package/src/core/src/tools/builtin/bash.ts +1 -266
- package/src/core/src/tools/builtin/fs/edit-shared.ts +1 -1
- package/src/core/src/tools/builtin/fs/edit.txt +2 -2
- package/src/core/src/tools/builtin/fs/write.txt +1 -1
- package/src/core/src/tools/builtin/shell.ts +273 -0
- package/src/core/src/tools/builtin/shell.txt +13 -0
- package/src/core/src/tools/builtin/terminal.txt +9 -6
- package/src/core/src/tools/loader.ts +134 -82
- package/src/index.ts +33 -0
- package/src/prompts/src/agents/build.txt +5 -6
- package/src/prompts/src/modes/guided.txt +2 -2
- package/src/prompts/src/providers/anthropic.txt +2 -2
- package/src/prompts/src/providers/default.txt +2 -2
- package/src/prompts/src/providers/glm.txt +2 -2
- package/src/prompts/src/providers/google.txt +9 -9
- package/src/prompts/src/providers/moonshot.txt +2 -2
- package/src/prompts/src/providers/openai.txt +3 -3
- package/src/prompts/src/providers.ts +15 -0
- package/src/providers/src/authorization.ts +26 -1
- package/src/providers/src/catalog-manual.ts +21 -6
- package/src/providers/src/catalog-merged.ts +2 -2
- package/src/providers/src/catalog.ts +10462 -10283
- package/src/providers/src/env.ts +10 -5
- package/src/providers/src/index.ts +26 -0
- package/src/providers/src/oauth-models.ts +1 -0
- package/src/providers/src/ollama-discovery.ts +149 -0
- package/src/providers/src/pricing.ts +3 -0
- package/src/providers/src/registry.ts +258 -0
- package/src/providers/src/utils.ts +10 -3
- package/src/providers/src/validate.ts +63 -2
- package/src/skills/index.ts +3 -0
- package/src/skills/tool.ts +28 -36
- package/src/types/src/config.ts +34 -8
- package/src/types/src/index.ts +4 -0
- package/src/types/src/provider.ts +33 -3
- package/src/core/src/tools/builtin/bash.txt +0 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.247",
|
|
4
4
|
"description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
|
|
5
5
|
"author": "nitishxyz",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
"import": "./src/core/src/tools/builtin/bash.ts",
|
|
38
38
|
"types": "./src/core/src/tools/builtin/bash.ts"
|
|
39
39
|
},
|
|
40
|
+
"./tools/builtin/shell": {
|
|
41
|
+
"import": "./src/core/src/tools/builtin/shell.ts",
|
|
42
|
+
"types": "./src/core/src/tools/builtin/shell.ts"
|
|
43
|
+
},
|
|
40
44
|
"./tools/builtin/finish": {
|
|
41
45
|
"import": "./src/core/src/tools/builtin/finish.ts",
|
|
42
46
|
"types": "./src/core/src/tools/builtin/finish.ts"
|
|
@@ -97,9 +101,10 @@
|
|
|
97
101
|
"@modelcontextprotocol/sdk": "^1.12",
|
|
98
102
|
"@openauthjs/openauth": "^0.4.3",
|
|
99
103
|
"@openrouter/ai-sdk-provider": "^1.2.0",
|
|
100
|
-
"@ottorouter/ai-sdk": "0.2.
|
|
104
|
+
"@ottorouter/ai-sdk": "0.2.1",
|
|
101
105
|
"@solana/web3.js": "^1.98.0",
|
|
102
106
|
"ai": "^6.0.0",
|
|
107
|
+
"ai-sdk-ollama": "^3.8.3",
|
|
103
108
|
"bs58": "^6.0.0",
|
|
104
109
|
"bun-pty": "^0.3.2",
|
|
105
110
|
"diff": "^8.0.2",
|
package/src/config/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ const DEFAULT_PROVIDER_SETTINGS: OttoConfig['providers'] = {
|
|
|
13
13
|
openai: { enabled: false },
|
|
14
14
|
anthropic: { enabled: false },
|
|
15
15
|
google: { enabled: false },
|
|
16
|
+
'ollama-cloud': { enabled: false, baseURL: 'https://ollama.com' },
|
|
16
17
|
openrouter: { enabled: false },
|
|
17
18
|
opencode: { enabled: false },
|
|
18
19
|
copilot: { enabled: false },
|
|
@@ -64,6 +65,7 @@ export async function loadConfig(
|
|
|
64
65
|
projectRoot,
|
|
65
66
|
defaults: merged.defaults as OttoConfig['defaults'],
|
|
66
67
|
providers: merged.providers as OttoConfig['providers'],
|
|
68
|
+
skills: merged.skills as OttoConfig['skills'],
|
|
67
69
|
paths: {
|
|
68
70
|
dataDir,
|
|
69
71
|
dbPath,
|
|
@@ -129,6 +131,9 @@ export {
|
|
|
129
131
|
isAuthorized,
|
|
130
132
|
ensureEnv,
|
|
131
133
|
writeDefaults,
|
|
134
|
+
writeProviderSettings,
|
|
135
|
+
removeProviderSettings,
|
|
136
|
+
writeSkillSettings,
|
|
132
137
|
writeAuth,
|
|
133
138
|
removeAuth,
|
|
134
139
|
} from './manager.ts';
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
type ProviderId,
|
|
7
7
|
type AuthInfo,
|
|
8
8
|
} from '../../auth/src/index.ts';
|
|
9
|
+
import type {
|
|
10
|
+
ProviderSettingsEntry,
|
|
11
|
+
SkillSettings,
|
|
12
|
+
} from '../../types/src/index.ts';
|
|
9
13
|
import {
|
|
10
14
|
getGlobalConfigDir,
|
|
11
15
|
getGlobalConfigPath,
|
|
@@ -83,30 +87,8 @@ export async function writeDefaults(
|
|
|
83
87
|
}>,
|
|
84
88
|
projectRoot?: string,
|
|
85
89
|
) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
if (scope === 'local') {
|
|
89
|
-
const localDir = getLocalDataDir(root);
|
|
90
|
-
const localPath = joinPath(localDir, 'config.json');
|
|
91
|
-
const existing = await readJsonFile(localPath);
|
|
92
|
-
const prevDefaults =
|
|
93
|
-
existing && typeof existing.defaults === 'object'
|
|
94
|
-
? (existing.defaults as Record<string, unknown>)
|
|
95
|
-
: {};
|
|
96
|
-
const next = {
|
|
97
|
-
...existing,
|
|
98
|
-
defaults: { ...prevDefaults, ...updates },
|
|
99
|
-
};
|
|
100
|
-
try {
|
|
101
|
-
const { promises: fs } = await import('node:fs');
|
|
102
|
-
await fs.mkdir(localDir, { recursive: true }).catch(() => {});
|
|
103
|
-
} catch {}
|
|
104
|
-
await Bun.write(localPath, JSON.stringify(next, null, 2));
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const globalPath = getGlobalConfigPath();
|
|
109
|
-
const existing = await readJsonFile(globalPath);
|
|
90
|
+
const filePath = getConfigFilePath(scope, projectRoot);
|
|
91
|
+
const existing = await readJsonFile(filePath);
|
|
110
92
|
const prevDefaults =
|
|
111
93
|
existing && typeof existing.defaults === 'object'
|
|
112
94
|
? (existing.defaults as Record<string, unknown>)
|
|
@@ -115,12 +97,82 @@ export async function writeDefaults(
|
|
|
115
97
|
...existing,
|
|
116
98
|
defaults: { ...prevDefaults, ...updates },
|
|
117
99
|
};
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
100
|
+
await writeConfigFile(filePath, next);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Persist provider settings for a built-in or custom provider entry.
|
|
105
|
+
*/
|
|
106
|
+
export async function writeProviderSettings(
|
|
107
|
+
scope: Scope,
|
|
108
|
+
provider: string,
|
|
109
|
+
updates: ProviderSettingsEntry,
|
|
110
|
+
projectRoot?: string,
|
|
111
|
+
) {
|
|
112
|
+
const filePath = getConfigFilePath(scope, projectRoot);
|
|
113
|
+
const existing = await readJsonFile(filePath);
|
|
114
|
+
const prevProviders =
|
|
115
|
+
existing && typeof existing.providers === 'object'
|
|
116
|
+
? (existing.providers as Record<string, unknown>)
|
|
117
|
+
: {};
|
|
118
|
+
const previousEntry =
|
|
119
|
+
prevProviders[provider] && typeof prevProviders[provider] === 'object'
|
|
120
|
+
? (prevProviders[provider] as Record<string, unknown>)
|
|
121
|
+
: {};
|
|
122
|
+
const next = {
|
|
123
|
+
...existing,
|
|
124
|
+
providers: {
|
|
125
|
+
...prevProviders,
|
|
126
|
+
[provider]: { ...previousEntry, ...updates },
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
await writeConfigFile(filePath, next);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Remove a provider override or custom provider entry from config.
|
|
134
|
+
*/
|
|
135
|
+
export async function removeProviderSettings(
|
|
136
|
+
scope: Scope,
|
|
137
|
+
provider: string,
|
|
138
|
+
projectRoot?: string,
|
|
139
|
+
) {
|
|
140
|
+
const filePath = getConfigFilePath(scope, projectRoot);
|
|
141
|
+
const existing = await readJsonFile(filePath);
|
|
142
|
+
if (!existing || typeof existing.providers !== 'object') return;
|
|
143
|
+
const providers = { ...(existing.providers as Record<string, unknown>) };
|
|
144
|
+
delete providers[provider];
|
|
145
|
+
const next = { ...existing, providers };
|
|
146
|
+
await writeConfigFile(filePath, next);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function writeSkillSettings(
|
|
150
|
+
scope: Scope,
|
|
151
|
+
updates: SkillSettings,
|
|
152
|
+
projectRoot?: string,
|
|
153
|
+
) {
|
|
154
|
+
const filePath = getConfigFilePath(scope, projectRoot);
|
|
155
|
+
const existing = await readJsonFile(filePath);
|
|
156
|
+
const prevSkills =
|
|
157
|
+
existing && typeof existing.skills === 'object'
|
|
158
|
+
? (existing.skills as Record<string, unknown>)
|
|
159
|
+
: {};
|
|
160
|
+
const prevItems =
|
|
161
|
+
prevSkills.items && typeof prevSkills.items === 'object'
|
|
162
|
+
? (prevSkills.items as Record<string, unknown>)
|
|
163
|
+
: {};
|
|
164
|
+
const next = {
|
|
165
|
+
...existing,
|
|
166
|
+
skills: {
|
|
167
|
+
...prevSkills,
|
|
168
|
+
...updates,
|
|
169
|
+
items: {
|
|
170
|
+
...prevItems,
|
|
171
|
+
...(updates.items ?? {}),
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
await writeConfigFile(filePath, next);
|
|
124
176
|
}
|
|
125
177
|
|
|
126
178
|
export async function readDebugConfig(
|
|
@@ -183,6 +235,30 @@ async function readJsonFile(
|
|
|
183
235
|
}
|
|
184
236
|
}
|
|
185
237
|
|
|
238
|
+
function getConfigFilePath(scope: Scope, projectRoot?: string): string {
|
|
239
|
+
const root = projectRoot ? String(projectRoot) : process.cwd();
|
|
240
|
+
if (scope === 'local') {
|
|
241
|
+
const localDir = getLocalDataDir(root);
|
|
242
|
+
return joinPath(localDir, 'config.json');
|
|
243
|
+
}
|
|
244
|
+
return getGlobalConfigPath();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function writeConfigFile(
|
|
248
|
+
filePath: string,
|
|
249
|
+
value: Record<string, unknown>,
|
|
250
|
+
) {
|
|
251
|
+
const base =
|
|
252
|
+
filePath === getGlobalConfigPath()
|
|
253
|
+
? getGlobalConfigDir()
|
|
254
|
+
: filePath.slice(0, Math.max(0, filePath.lastIndexOf('/')));
|
|
255
|
+
try {
|
|
256
|
+
const { promises: fs } = await import('node:fs');
|
|
257
|
+
await fs.mkdir(base, { recursive: true }).catch(() => {});
|
|
258
|
+
} catch {}
|
|
259
|
+
await Bun.write(filePath, JSON.stringify(value, null, 2));
|
|
260
|
+
}
|
|
261
|
+
|
|
186
262
|
export async function writeAuth(
|
|
187
263
|
provider: ProviderId,
|
|
188
264
|
info: AuthInfo,
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { openai, createOpenAI } from '@ai-sdk/openai';
|
|
2
2
|
import { anthropic, createAnthropic } from '@ai-sdk/anthropic';
|
|
3
3
|
import { google, createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
4
|
+
import { createOllama } from 'ai-sdk-ollama';
|
|
4
5
|
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
|
5
6
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
6
7
|
import {
|
|
7
8
|
catalog,
|
|
8
9
|
createOttoRouterModel,
|
|
9
10
|
createOpenAIOAuthModel,
|
|
11
|
+
normalizeOllamaBaseURL,
|
|
10
12
|
} from '../../../providers/src/index.ts';
|
|
11
13
|
import { createCopilotModel } from '../../../providers/src/copilot-client.ts';
|
|
12
14
|
import type { OAuth } from '../../../types/src/index.ts';
|
|
@@ -32,13 +34,15 @@ export type ProviderName =
|
|
|
32
34
|
| 'openai'
|
|
33
35
|
| 'anthropic'
|
|
34
36
|
| 'google'
|
|
37
|
+
| 'ollama-cloud'
|
|
35
38
|
| 'openrouter'
|
|
36
39
|
| 'opencode'
|
|
37
40
|
| 'copilot'
|
|
38
41
|
| 'ottorouter'
|
|
39
42
|
| 'zai'
|
|
40
43
|
| 'zai-coding'
|
|
41
|
-
| 'moonshot'
|
|
44
|
+
| 'moonshot'
|
|
45
|
+
| 'minimax';
|
|
42
46
|
|
|
43
47
|
export type ModelConfig = {
|
|
44
48
|
apiKey?: string;
|
|
@@ -96,6 +100,20 @@ export async function resolveModel(
|
|
|
96
100
|
return google(model);
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
if (provider === 'ollama-cloud') {
|
|
104
|
+
const entry = catalog[provider];
|
|
105
|
+
const apiKey = config.apiKey || process.env.OLLAMA_API_KEY || '';
|
|
106
|
+
const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined;
|
|
107
|
+
const baseURL = normalizeOllamaBaseURL(
|
|
108
|
+
config.baseURL || entry?.api || 'https://ollama.com',
|
|
109
|
+
);
|
|
110
|
+
const instance = createOllama({
|
|
111
|
+
baseURL,
|
|
112
|
+
headers,
|
|
113
|
+
});
|
|
114
|
+
return instance(model);
|
|
115
|
+
}
|
|
116
|
+
|
|
99
117
|
if (provider === 'openrouter') {
|
|
100
118
|
const apiKey = config.apiKey || process.env.OPENROUTER_API_KEY || '';
|
|
101
119
|
const openrouter = createOpenRouter({ apiKey });
|
|
@@ -233,6 +251,15 @@ export async function resolveModel(
|
|
|
233
251
|
return instance(model);
|
|
234
252
|
}
|
|
235
253
|
|
|
254
|
+
if (provider === 'minimax') {
|
|
255
|
+
const entry = catalog[provider];
|
|
256
|
+
const apiKey = config.apiKey || process.env.MINIMAX_API_KEY || '';
|
|
257
|
+
const baseURL =
|
|
258
|
+
config.baseURL || entry?.api || 'https://api.minimax.io/anthropic/v1';
|
|
259
|
+
const instance = createAnthropic({ apiKey, baseURL });
|
|
260
|
+
return instance(model);
|
|
261
|
+
}
|
|
262
|
+
|
|
236
263
|
throw new Error(`Unsupported provider: ${provider}`);
|
|
237
264
|
}
|
|
238
265
|
|
|
@@ -1,266 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
import { z } from 'zod/v3';
|
|
4
|
-
import DESCRIPTION from './bash.txt' with { type: 'text' };
|
|
5
|
-
import { getAugmentedPath } from '../bin-manager.ts';
|
|
6
|
-
import { createToolError, type ToolResponse } from '../error.ts';
|
|
7
|
-
import { injectCoAuthorIntoGitCommit } from './git-identity.ts';
|
|
8
|
-
|
|
9
|
-
function normalizePath(p: string) {
|
|
10
|
-
const normalized = p.replace(/\\/g, '/');
|
|
11
|
-
const driveMatch = normalized.match(/^([A-Za-z]):\//);
|
|
12
|
-
const drivePrefix = driveMatch ? `${driveMatch[1]}:` : '';
|
|
13
|
-
const rest = driveMatch ? normalized.slice(2) : normalized;
|
|
14
|
-
const parts = rest.split('/');
|
|
15
|
-
const stack: string[] = [];
|
|
16
|
-
for (const part of parts) {
|
|
17
|
-
if (!part || part === '.') continue;
|
|
18
|
-
if (part === '..') stack.pop();
|
|
19
|
-
else stack.push(part);
|
|
20
|
-
}
|
|
21
|
-
if (drivePrefix) return `${drivePrefix}/${stack.join('/')}`;
|
|
22
|
-
return `/${stack.join('/')}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function resolveSafePath(projectRoot: string, p: string) {
|
|
26
|
-
const root = normalizePath(projectRoot);
|
|
27
|
-
const abs = normalizePath(`${root}/${p || '.'}`);
|
|
28
|
-
if (!(abs === root || abs.startsWith(`${root}/`))) {
|
|
29
|
-
throw new Error(`cwd escapes project root: ${p}`);
|
|
30
|
-
}
|
|
31
|
-
return abs;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function killProcessTree(pid: number) {
|
|
35
|
-
try {
|
|
36
|
-
process.kill(-pid, 'SIGTERM');
|
|
37
|
-
} catch {
|
|
38
|
-
try {
|
|
39
|
-
process.kill(pid, 'SIGTERM');
|
|
40
|
-
} catch {}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
type BashResult = ToolResponse<{
|
|
45
|
-
exitCode: number;
|
|
46
|
-
stdout: string;
|
|
47
|
-
stderr: string;
|
|
48
|
-
}>;
|
|
49
|
-
|
|
50
|
-
type BashStreamChunk =
|
|
51
|
-
| {
|
|
52
|
-
channel: 'output';
|
|
53
|
-
delta: string;
|
|
54
|
-
}
|
|
55
|
-
| {
|
|
56
|
-
result: BashResult;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export function buildBashTool(projectRoot: string): {
|
|
60
|
-
name: string;
|
|
61
|
-
tool: Tool;
|
|
62
|
-
} {
|
|
63
|
-
// biome-ignore lint/suspicious/noExplicitAny: AI SDK tool typings do not model async-iterable execute results yet.
|
|
64
|
-
const bash = tool({
|
|
65
|
-
description: DESCRIPTION,
|
|
66
|
-
inputSchema: z
|
|
67
|
-
.object({
|
|
68
|
-
cmd: z.string().describe('Shell command to run (bash -c <cmd>)'),
|
|
69
|
-
cwd: z
|
|
70
|
-
.string()
|
|
71
|
-
.default('.')
|
|
72
|
-
.describe('Working directory relative to project root'),
|
|
73
|
-
allowNonZeroExit: z
|
|
74
|
-
.boolean()
|
|
75
|
-
.optional()
|
|
76
|
-
.default(false)
|
|
77
|
-
.describe('If true, do not throw on non-zero exit'),
|
|
78
|
-
timeout: z
|
|
79
|
-
.number()
|
|
80
|
-
.optional()
|
|
81
|
-
.default(300000)
|
|
82
|
-
.describe('Timeout in milliseconds (default: 300000 = 5 minutes)'),
|
|
83
|
-
})
|
|
84
|
-
.strict(),
|
|
85
|
-
execute(
|
|
86
|
-
{
|
|
87
|
-
cmd,
|
|
88
|
-
cwd,
|
|
89
|
-
allowNonZeroExit,
|
|
90
|
-
timeout = 300000,
|
|
91
|
-
}: {
|
|
92
|
-
cmd: string;
|
|
93
|
-
cwd?: string;
|
|
94
|
-
allowNonZeroExit?: boolean;
|
|
95
|
-
timeout?: number;
|
|
96
|
-
},
|
|
97
|
-
options?: { abortSignal?: AbortSignal },
|
|
98
|
-
): AsyncIterable<BashStreamChunk> | BashResult {
|
|
99
|
-
const abortSignal = options?.abortSignal;
|
|
100
|
-
|
|
101
|
-
if (abortSignal?.aborted) {
|
|
102
|
-
return createToolError('Command aborted before execution', 'abort', {
|
|
103
|
-
cmd,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const absCwd = resolveSafePath(projectRoot, cwd || '.');
|
|
108
|
-
const finalCmd = injectCoAuthorIntoGitCommit(cmd);
|
|
109
|
-
|
|
110
|
-
const proc = spawn(finalCmd, {
|
|
111
|
-
cwd: absCwd,
|
|
112
|
-
shell: true,
|
|
113
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
114
|
-
env: { ...process.env, PATH: getAugmentedPath() },
|
|
115
|
-
detached: true,
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
let stdout = '';
|
|
119
|
-
let stderr = '';
|
|
120
|
-
let didTimeout = false;
|
|
121
|
-
let didAbort = false;
|
|
122
|
-
let settled = false;
|
|
123
|
-
let done = false;
|
|
124
|
-
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
125
|
-
const queue: BashStreamChunk[] = [];
|
|
126
|
-
let notify: (() => void) | null = null;
|
|
127
|
-
|
|
128
|
-
const wake = () => {
|
|
129
|
-
if (!notify) return;
|
|
130
|
-
notify();
|
|
131
|
-
notify = null;
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const pushDelta = (text: string) => {
|
|
135
|
-
if (!text) return;
|
|
136
|
-
queue.push({ channel: 'output', delta: text });
|
|
137
|
-
wake();
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const settle = (result: BashResult) => {
|
|
141
|
-
if (settled) return;
|
|
142
|
-
settled = true;
|
|
143
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
144
|
-
if (abortSignal) {
|
|
145
|
-
abortSignal.removeEventListener('abort', onAbort);
|
|
146
|
-
}
|
|
147
|
-
queue.push({ result });
|
|
148
|
-
done = true;
|
|
149
|
-
wake();
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
const onAbort = () => {
|
|
153
|
-
if (settled) return;
|
|
154
|
-
didAbort = true;
|
|
155
|
-
if (proc.pid) killProcessTree(proc.pid);
|
|
156
|
-
else proc.kill('SIGTERM');
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
if (abortSignal) {
|
|
160
|
-
abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (timeout > 0) {
|
|
164
|
-
timeoutId = setTimeout(() => {
|
|
165
|
-
didTimeout = true;
|
|
166
|
-
if (proc.pid) killProcessTree(proc.pid);
|
|
167
|
-
else proc.kill();
|
|
168
|
-
}, timeout);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
proc.stdout?.on('data', (chunk) => {
|
|
172
|
-
const text = chunk.toString();
|
|
173
|
-
stdout += text;
|
|
174
|
-
pushDelta(text);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
proc.stderr?.on('data', (chunk) => {
|
|
178
|
-
const text = chunk.toString();
|
|
179
|
-
stderr += text;
|
|
180
|
-
pushDelta(text);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
proc.on('close', (exitCode) => {
|
|
184
|
-
if (didAbort) {
|
|
185
|
-
settle(
|
|
186
|
-
createToolError(`Command aborted by user: ${cmd}`, 'abort', {
|
|
187
|
-
cmd,
|
|
188
|
-
stdout,
|
|
189
|
-
stderr,
|
|
190
|
-
}),
|
|
191
|
-
);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (didTimeout) {
|
|
196
|
-
settle(
|
|
197
|
-
createToolError(
|
|
198
|
-
`Command timed out after ${timeout}ms: ${cmd}`,
|
|
199
|
-
'timeout',
|
|
200
|
-
{
|
|
201
|
-
parameter: 'timeout',
|
|
202
|
-
value: timeout,
|
|
203
|
-
stdout,
|
|
204
|
-
stderr,
|
|
205
|
-
suggestion: 'Increase timeout or optimize the command',
|
|
206
|
-
},
|
|
207
|
-
),
|
|
208
|
-
);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (exitCode !== 0 && !allowNonZeroExit) {
|
|
213
|
-
const errorDetail = stderr.trim() || stdout.trim() || '';
|
|
214
|
-
const errorMsg = `Command failed with exit code ${exitCode}${errorDetail ? `\n\n${errorDetail}` : ''}`;
|
|
215
|
-
settle(
|
|
216
|
-
createToolError(errorMsg, 'execution', {
|
|
217
|
-
exitCode,
|
|
218
|
-
stdout,
|
|
219
|
-
stderr,
|
|
220
|
-
cmd,
|
|
221
|
-
suggestion: 'Check command syntax or use allowNonZeroExit: true',
|
|
222
|
-
}),
|
|
223
|
-
);
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
settle({
|
|
228
|
-
ok: true,
|
|
229
|
-
exitCode: exitCode ?? 0,
|
|
230
|
-
stdout,
|
|
231
|
-
stderr,
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
proc.on('error', (err) => {
|
|
236
|
-
settle(
|
|
237
|
-
createToolError(
|
|
238
|
-
`Command execution failed: ${err.message}`,
|
|
239
|
-
'execution',
|
|
240
|
-
{
|
|
241
|
-
cmd,
|
|
242
|
-
originalError: err.message,
|
|
243
|
-
},
|
|
244
|
-
),
|
|
245
|
-
);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
const stream = async function* (): AsyncGenerator<BashStreamChunk> {
|
|
249
|
-
while (!done || queue.length > 0) {
|
|
250
|
-
if (queue.length === 0) {
|
|
251
|
-
await new Promise<void>((resolve) => {
|
|
252
|
-
notify = resolve;
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
while (queue.length > 0) {
|
|
256
|
-
const chunk = queue.shift();
|
|
257
|
-
if (chunk) yield chunk;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
return stream();
|
|
263
|
-
},
|
|
264
|
-
} as any);
|
|
265
|
-
return { name: 'bash', tool: bash };
|
|
266
|
-
}
|
|
1
|
+
export { buildShellTool, buildShellTool as buildBashTool } from './shell.ts';
|
|
@@ -34,7 +34,7 @@ export function applyStringEdit(
|
|
|
34
34
|
): { content: string; occurrences: number } {
|
|
35
35
|
if (oldString.length === 0) {
|
|
36
36
|
throw new Error(
|
|
37
|
-
'oldString must not be empty. Use write to create files or
|
|
37
|
+
'oldString must not be empty. Use write to create files or a structural editing tool for larger insertions.',
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
if (oldString === newString) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Replace an exact text block in an existing file.
|
|
2
2
|
|
|
3
|
-
Use this for targeted edits instead of
|
|
3
|
+
Use this for targeted edits instead of structural patch-style editing whenever possible.
|
|
4
4
|
|
|
5
5
|
Rules:
|
|
6
6
|
- You must read the file first in the current session before editing it.
|
|
7
7
|
- `oldString` must match the file exactly, including whitespace.
|
|
8
8
|
- If `oldString` appears multiple times, provide more context or set `replaceAll: true`.
|
|
9
|
-
- Use `write` for new files and
|
|
9
|
+
- Use `write` for new files and a structural editing tool for multi-file diffs when that capability exists.
|
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
Usage tips:
|
|
7
7
|
- Use for creating NEW files
|
|
8
8
|
- Use when replacing >70% of a file's content (almost complete rewrite)
|
|
9
|
-
- NEVER use for partial/targeted edits - use
|
|
9
|
+
- NEVER use for partial/targeted edits - use the dedicated exact-editing tools first, or a structural editing tool for larger changes when available
|
|
10
10
|
- Using write for partial edits wastes output tokens and risks hallucinating unchanged parts
|
|
11
11
|
- Prefer idempotent writes by providing the full intended content when you do use write
|