@ottocode/sdk 0.1.244 → 0.1.246
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 +3 -2
- package/src/auth/src/index.ts +2 -2
- package/src/auth/src/wallet.ts +5 -5
- package/src/config/src/index.ts +7 -2
- package/src/config/src/manager.ts +106 -30
- package/src/core/src/index.ts +6 -1
- package/src/core/src/providers/resolver.ts +37 -9
- package/src/core/src/tools/builtin/bash.ts +2 -2
- package/src/core/src/tools/builtin/bash.txt +1 -1
- 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/loader.ts +133 -81
- package/src/core/src/utils/debug.ts +13 -0
- package/src/index.ts +47 -12
- package/src/prompts/src/agents/build.txt +3 -4
- package/src/prompts/src/providers/default.txt +1 -1
- package/src/prompts/src/providers/glm.txt +1 -1
- package/src/prompts/src/providers/google.txt +2 -2
- package/src/prompts/src/providers/moonshot.txt +1 -1
- 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 +41 -23
- package/src/providers/src/catalog-merged.ts +2 -2
- package/src/providers/src/catalog.ts +10284 -10283
- package/src/providers/src/env.ts +11 -6
- package/src/providers/src/index.ts +38 -12
- package/src/providers/src/ollama-discovery.ts +149 -0
- package/src/providers/src/{setu-client.ts → ottorouter-client.ts} +30 -30
- package/src/providers/src/pricing.ts +4 -1
- package/src/providers/src/registry.ts +258 -0
- package/src/providers/src/utils.ts +11 -4
- 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 +34 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.246",
|
|
4
4
|
"description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
|
|
5
5
|
"author": "nitishxyz",
|
|
6
6
|
"license": "MIT",
|
|
@@ -97,9 +97,10 @@
|
|
|
97
97
|
"@modelcontextprotocol/sdk": "^1.12",
|
|
98
98
|
"@openauthjs/openauth": "^0.4.3",
|
|
99
99
|
"@openrouter/ai-sdk-provider": "^1.2.0",
|
|
100
|
-
"@
|
|
100
|
+
"@ottorouter/ai-sdk": "0.2.1",
|
|
101
101
|
"@solana/web3.js": "^1.98.0",
|
|
102
102
|
"ai": "^6.0.0",
|
|
103
|
+
"ai-sdk-ollama": "^3.8.3",
|
|
103
104
|
"bs58": "^6.0.0",
|
|
104
105
|
"bun-pty": "^0.3.2",
|
|
105
106
|
"diff": "^8.0.2",
|
package/src/auth/src/index.ts
CHANGED
package/src/auth/src/wallet.ts
CHANGED
|
@@ -34,25 +34,25 @@ export function isValidPrivateKey(privateKey: string): boolean {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export async function
|
|
37
|
+
export async function getOttoRouterWallet(
|
|
38
38
|
projectRoot?: string,
|
|
39
39
|
): Promise<WalletInfo | null> {
|
|
40
|
-
const auth = await getAuth('
|
|
40
|
+
const auth = await getAuth('ottorouter', projectRoot);
|
|
41
41
|
if (auth?.type === 'wallet' && auth.secret) {
|
|
42
42
|
return importWallet(auth.secret);
|
|
43
43
|
}
|
|
44
44
|
return null;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export async function
|
|
47
|
+
export async function ensureOttoRouterWallet(
|
|
48
48
|
projectRoot?: string,
|
|
49
49
|
): Promise<WalletInfo> {
|
|
50
|
-
const existing = await
|
|
50
|
+
const existing = await getOttoRouterWallet(projectRoot);
|
|
51
51
|
if (existing) return existing;
|
|
52
52
|
|
|
53
53
|
const wallet = generateWallet();
|
|
54
54
|
await setAuth(
|
|
55
|
-
'
|
|
55
|
+
'ottorouter',
|
|
56
56
|
{ type: 'wallet', secret: wallet.privateKey },
|
|
57
57
|
projectRoot,
|
|
58
58
|
'global',
|
package/src/config/src/index.ts
CHANGED
|
@@ -13,10 +13,11 @@ 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 },
|
|
19
|
-
|
|
20
|
+
ottorouter: { enabled: true },
|
|
20
21
|
zai: { enabled: false },
|
|
21
22
|
'zai-coding': { enabled: false },
|
|
22
23
|
moonshot: { enabled: false },
|
|
@@ -29,7 +30,7 @@ const DEFAULTS: {
|
|
|
29
30
|
} = {
|
|
30
31
|
defaults: {
|
|
31
32
|
agent: 'build',
|
|
32
|
-
provider: '
|
|
33
|
+
provider: 'ottorouter',
|
|
33
34
|
model: 'kimi-k2.5',
|
|
34
35
|
toolApproval: 'auto',
|
|
35
36
|
guidedMode: 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,
|
package/src/core/src/index.ts
CHANGED
|
@@ -105,7 +105,12 @@ export {
|
|
|
105
105
|
// Logging & Debug
|
|
106
106
|
// =======================
|
|
107
107
|
export { logger, debug, info, warn, error, time } from './utils/logger.ts';
|
|
108
|
-
export {
|
|
108
|
+
export {
|
|
109
|
+
isDebugEnabled,
|
|
110
|
+
isTraceEnabled,
|
|
111
|
+
setDebugEnabled,
|
|
112
|
+
setTraceEnabled,
|
|
113
|
+
} from './utils/debug.ts';
|
|
109
114
|
|
|
110
115
|
// =======================
|
|
111
116
|
// MCP (Model Context Protocol)
|
|
@@ -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 });
|
|
@@ -163,16 +181,17 @@ export async function resolveModel(
|
|
|
163
181
|
);
|
|
164
182
|
}
|
|
165
183
|
|
|
166
|
-
if (provider === '
|
|
167
|
-
const privateKey =
|
|
184
|
+
if (provider === 'ottorouter') {
|
|
185
|
+
const privateKey =
|
|
186
|
+
config.apiKey || process.env.OTTOROUTER_PRIVATE_KEY || '';
|
|
168
187
|
if (!privateKey) {
|
|
169
188
|
throw new Error(
|
|
170
|
-
'
|
|
189
|
+
'OttoRouter provider requires OTTOROUTER_PRIVATE_KEY (base58 Solana secret).',
|
|
171
190
|
);
|
|
172
191
|
}
|
|
173
|
-
const baseURL = config.baseURL || process.env.
|
|
174
|
-
const rpcURL = process.env.
|
|
175
|
-
return
|
|
192
|
+
const baseURL = config.baseURL || process.env.OTTOROUTER_BASE_URL;
|
|
193
|
+
const rpcURL = process.env.OTTOROUTER_SOLANA_RPC_URL;
|
|
194
|
+
return createOttoRouterModel(
|
|
176
195
|
model,
|
|
177
196
|
{ privateKey },
|
|
178
197
|
{
|
|
@@ -232,6 +251,15 @@ export async function resolveModel(
|
|
|
232
251
|
return instance(model);
|
|
233
252
|
}
|
|
234
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
|
+
|
|
235
263
|
throw new Error(`Unsupported provider: ${provider}`);
|
|
236
264
|
}
|
|
237
265
|
|
|
@@ -60,9 +60,9 @@ export function buildBashTool(projectRoot: string): {
|
|
|
60
60
|
name: string;
|
|
61
61
|
tool: Tool;
|
|
62
62
|
} {
|
|
63
|
-
// biome-ignore lint/suspicious/noExplicitAny: AI SDK tool typings do not model async-iterable execute results yet.
|
|
64
63
|
const bash = tool({
|
|
65
64
|
description: DESCRIPTION,
|
|
65
|
+
|
|
66
66
|
inputSchema: z
|
|
67
67
|
.object({
|
|
68
68
|
cmd: z.string().describe('Shell command to run (bash -c <cmd>)'),
|
|
@@ -261,6 +261,6 @@ export function buildBashTool(projectRoot: string): {
|
|
|
261
261
|
|
|
262
262
|
return stream();
|
|
263
263
|
},
|
|
264
|
-
} as
|
|
264
|
+
}) as unknown as Tool;
|
|
265
265
|
return { name: 'bash', tool: bash };
|
|
266
266
|
}
|
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
- Chain commands with `&&` to fail-fast.
|
|
10
10
|
- For long outputs, redirect to a file and `read` it back.
|
|
11
11
|
- Batch independent checks (e.g. `git status && git diff`) in parallel tool calls rather than sequential bash chains when you need results separately.
|
|
12
|
-
- Never use `bash` with `sed`/`awk` for programmatic file editing — use
|
|
12
|
+
- Never use `bash` with `sed`/`awk` for programmatic file editing — use the dedicated file-editing tools instead.
|
|
@@ -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
|
|
@@ -12,7 +12,11 @@ import { updateTodosTool } from './builtin/todos.ts';
|
|
|
12
12
|
import { buildWebSearchTool } from './builtin/websearch.ts';
|
|
13
13
|
import { buildTerminalTool } from './builtin/terminal.ts';
|
|
14
14
|
import type { TerminalManager } from '../terminals/index.ts';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
initializeSkills,
|
|
17
|
+
buildSkillTool,
|
|
18
|
+
setSkillSettings,
|
|
19
|
+
} from '../../../skills/index.ts';
|
|
16
20
|
import { getMCPManager } from '../mcp/index.ts';
|
|
17
21
|
import {
|
|
18
22
|
getMCPToolBriefs,
|
|
@@ -104,6 +108,7 @@ type FsHelpers = {
|
|
|
104
108
|
const pluginPatterns = ['tools/*/tool.js', 'tools/*/tool.mjs'];
|
|
105
109
|
|
|
106
110
|
let globalTerminalManager: TerminalManager | null = null;
|
|
111
|
+
const staticToolDiscoveryCache = new Map<string, Promise<DiscoveredTool[]>>();
|
|
107
112
|
|
|
108
113
|
export function setTerminalManager(manager: TerminalManager): void {
|
|
109
114
|
globalTerminalManager = manager;
|
|
@@ -113,42 +118,141 @@ export function getTerminalManager(): TerminalManager | null {
|
|
|
113
118
|
return globalTerminalManager;
|
|
114
119
|
}
|
|
115
120
|
|
|
121
|
+
function getStaticToolDiscoveryCacheKey(
|
|
122
|
+
projectRoot: string,
|
|
123
|
+
globalConfigDir?: string,
|
|
124
|
+
): string {
|
|
125
|
+
return `${projectRoot}::${globalConfigDir ?? ''}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function discoverStaticProjectTools(
|
|
129
|
+
projectRoot: string,
|
|
130
|
+
globalConfigDir?: string,
|
|
131
|
+
skillSettings?: {
|
|
132
|
+
enabled?: boolean;
|
|
133
|
+
items?: Record<string, { enabled?: boolean }>;
|
|
134
|
+
},
|
|
135
|
+
): Promise<DiscoveredTool[]> {
|
|
136
|
+
setSkillSettings(skillSettings);
|
|
137
|
+
const cacheKey = getStaticToolDiscoveryCacheKey(projectRoot, globalConfigDir);
|
|
138
|
+
const cached = staticToolDiscoveryCache.get(cacheKey);
|
|
139
|
+
if (cached) return cached;
|
|
140
|
+
|
|
141
|
+
const discoveryPromise = (async () => {
|
|
142
|
+
const tools = new Map<string, Tool>();
|
|
143
|
+
for (const { name, tool } of buildFsTools(projectRoot))
|
|
144
|
+
tools.set(name, tool);
|
|
145
|
+
for (const { name, tool } of buildGitTools(projectRoot))
|
|
146
|
+
tools.set(name, tool);
|
|
147
|
+
// Built-ins
|
|
148
|
+
tools.set('finish', finishTool);
|
|
149
|
+
tools.set('progress_update', progressUpdateTool);
|
|
150
|
+
const bash = buildBashTool(projectRoot);
|
|
151
|
+
tools.set(bash.name, bash.tool);
|
|
152
|
+
// Search
|
|
153
|
+
const rg = buildRipgrepTool(projectRoot);
|
|
154
|
+
tools.set(rg.name, rg.tool);
|
|
155
|
+
const glob = buildGlobTool(projectRoot);
|
|
156
|
+
tools.set(glob.name, glob.tool);
|
|
157
|
+
// Patch/apply
|
|
158
|
+
const ap = buildApplyPatchTool(projectRoot);
|
|
159
|
+
tools.set(ap.name, ap.tool);
|
|
160
|
+
// Todo tracking
|
|
161
|
+
tools.set('update_todos', updateTodosTool);
|
|
162
|
+
// Web search
|
|
163
|
+
const ws = buildWebSearchTool();
|
|
164
|
+
tools.set(ws.name, ws.tool);
|
|
165
|
+
// Skills
|
|
166
|
+
await initializeSkills(projectRoot);
|
|
167
|
+
const skillTool = buildSkillTool();
|
|
168
|
+
tools.set(skillTool.name, skillTool.tool);
|
|
169
|
+
|
|
170
|
+
async function loadFromBase(base: string | null | undefined) {
|
|
171
|
+
if (!base) return;
|
|
172
|
+
try {
|
|
173
|
+
await fs.readdir(base);
|
|
174
|
+
} catch {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
for (const pattern of pluginPatterns) {
|
|
178
|
+
const files = await fg(pattern, { cwd: base, absolute: false });
|
|
179
|
+
for (const rel of files) {
|
|
180
|
+
const match = rel.match(/^tools\/([^/]+)\/tool\.(m?js)$/);
|
|
181
|
+
if (!match || !match[1]) continue;
|
|
182
|
+
const folder = match[1];
|
|
183
|
+
const absPath = join(base, rel).replace(/\\/g, '/');
|
|
184
|
+
try {
|
|
185
|
+
const plugin = await loadPlugin(absPath, folder, projectRoot);
|
|
186
|
+
if (plugin) tools.set(plugin.name, plugin.tool);
|
|
187
|
+
} catch {}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Fallback: manual directory scan
|
|
191
|
+
try {
|
|
192
|
+
const toolsDir = join(base, 'tools');
|
|
193
|
+
const entries = await fs.readdir(toolsDir).catch(() => [] as string[]);
|
|
194
|
+
for (const folder of entries) {
|
|
195
|
+
const js = join(toolsDir, folder, 'tool.js');
|
|
196
|
+
const mjs = join(toolsDir, folder, 'tool.mjs');
|
|
197
|
+
const candidate = await fs
|
|
198
|
+
.stat(js)
|
|
199
|
+
.then(() => js)
|
|
200
|
+
.catch(
|
|
201
|
+
async () =>
|
|
202
|
+
await fs
|
|
203
|
+
.stat(mjs)
|
|
204
|
+
.then(() => mjs)
|
|
205
|
+
.catch(() => null),
|
|
206
|
+
);
|
|
207
|
+
if (!candidate) continue;
|
|
208
|
+
try {
|
|
209
|
+
const plugin = await loadPlugin(
|
|
210
|
+
candidate.replace(/\\/g, '/'),
|
|
211
|
+
folder,
|
|
212
|
+
projectRoot,
|
|
213
|
+
);
|
|
214
|
+
if (plugin) tools.set(plugin.name, plugin.tool);
|
|
215
|
+
} catch {}
|
|
216
|
+
}
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
await loadFromBase(globalConfigDir);
|
|
221
|
+
await loadFromBase(join(projectRoot, '.otto'));
|
|
222
|
+
return Array.from(tools.entries()).map(([name, tool]) => ({ name, tool }));
|
|
223
|
+
})();
|
|
224
|
+
|
|
225
|
+
staticToolDiscoveryCache.set(cacheKey, discoveryPromise);
|
|
226
|
+
try {
|
|
227
|
+
return await discoveryPromise;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
staticToolDiscoveryCache.delete(cacheKey);
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
116
234
|
export async function discoverProjectTools(
|
|
117
235
|
projectRoot: string,
|
|
118
236
|
globalConfigDir?: string,
|
|
237
|
+
skillSettings?: {
|
|
238
|
+
enabled?: boolean;
|
|
239
|
+
items?: Record<string, { enabled?: boolean }>;
|
|
240
|
+
},
|
|
119
241
|
): Promise<DiscoverResult> {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
tools
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const rg = buildRipgrepTool(projectRoot);
|
|
131
|
-
tools.set(rg.name, rg.tool);
|
|
132
|
-
const glob = buildGlobTool(projectRoot);
|
|
133
|
-
tools.set(glob.name, glob.tool);
|
|
134
|
-
// Patch/apply
|
|
135
|
-
const ap = buildApplyPatchTool(projectRoot);
|
|
136
|
-
tools.set(ap.name, ap.tool);
|
|
137
|
-
// Todo tracking
|
|
138
|
-
tools.set('update_todos', updateTodosTool);
|
|
139
|
-
// Web search
|
|
140
|
-
const ws = buildWebSearchTool();
|
|
141
|
-
tools.set(ws.name, ws.tool);
|
|
142
|
-
// Terminal (if manager is available)
|
|
242
|
+
setSkillSettings(skillSettings);
|
|
243
|
+
const staticTools = await discoverStaticProjectTools(
|
|
244
|
+
projectRoot,
|
|
245
|
+
globalConfigDir,
|
|
246
|
+
skillSettings,
|
|
247
|
+
);
|
|
248
|
+
const tools = new Map<string, Tool>(
|
|
249
|
+
staticTools.map(({ name, tool }) => [name, tool]),
|
|
250
|
+
);
|
|
251
|
+
|
|
143
252
|
if (globalTerminalManager) {
|
|
144
253
|
const term = buildTerminalTool(projectRoot, globalTerminalManager);
|
|
145
254
|
tools.set(term.name, term.tool);
|
|
146
255
|
}
|
|
147
|
-
// Skills
|
|
148
|
-
// Always reinitialize to ensure skills are discovered for the current project
|
|
149
|
-
await initializeSkills(projectRoot);
|
|
150
|
-
const skillTool = buildSkillTool();
|
|
151
|
-
tools.set(skillTool.name, skillTool.tool);
|
|
152
256
|
|
|
153
257
|
const mcpManager = getMCPManager();
|
|
154
258
|
let mcpToolsRecord: Record<string, Tool> = {};
|
|
@@ -162,58 +266,6 @@ export async function discoverProjectTools(
|
|
|
162
266
|
}
|
|
163
267
|
}
|
|
164
268
|
|
|
165
|
-
async function loadFromBase(base: string | null | undefined) {
|
|
166
|
-
if (!base) return;
|
|
167
|
-
try {
|
|
168
|
-
await fs.readdir(base);
|
|
169
|
-
} catch {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
for (const pattern of pluginPatterns) {
|
|
173
|
-
const files = await fg(pattern, { cwd: base, absolute: false });
|
|
174
|
-
for (const rel of files) {
|
|
175
|
-
const match = rel.match(/^tools\/([^/]+)\/tool\.(m?js)$/);
|
|
176
|
-
if (!match || !match[1]) continue;
|
|
177
|
-
const folder = match[1];
|
|
178
|
-
const absPath = join(base, rel).replace(/\\/g, '/');
|
|
179
|
-
try {
|
|
180
|
-
const plugin = await loadPlugin(absPath, folder, projectRoot);
|
|
181
|
-
if (plugin) tools.set(plugin.name, plugin.tool);
|
|
182
|
-
} catch {}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
// Fallback: manual directory scan
|
|
186
|
-
try {
|
|
187
|
-
const toolsDir = join(base, 'tools');
|
|
188
|
-
const entries = await fs.readdir(toolsDir).catch(() => [] as string[]);
|
|
189
|
-
for (const folder of entries) {
|
|
190
|
-
const js = join(toolsDir, folder, 'tool.js');
|
|
191
|
-
const mjs = join(toolsDir, folder, 'tool.mjs');
|
|
192
|
-
const candidate = await fs
|
|
193
|
-
.stat(js)
|
|
194
|
-
.then(() => js)
|
|
195
|
-
.catch(
|
|
196
|
-
async () =>
|
|
197
|
-
await fs
|
|
198
|
-
.stat(mjs)
|
|
199
|
-
.then(() => mjs)
|
|
200
|
-
.catch(() => null),
|
|
201
|
-
);
|
|
202
|
-
if (!candidate) continue;
|
|
203
|
-
try {
|
|
204
|
-
const plugin = await loadPlugin(
|
|
205
|
-
candidate.replace(/\\/g, '/'),
|
|
206
|
-
folder,
|
|
207
|
-
projectRoot,
|
|
208
|
-
);
|
|
209
|
-
if (plugin) tools.set(plugin.name, plugin.tool);
|
|
210
|
-
} catch {}
|
|
211
|
-
}
|
|
212
|
-
} catch {}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
await loadFromBase(globalConfigDir);
|
|
216
|
-
await loadFromBase(join(projectRoot, '.otto'));
|
|
217
269
|
return {
|
|
218
270
|
tools: Array.from(tools.entries()).map(([name, tool]) => ({ name, tool })),
|
|
219
271
|
mcpToolsRecord,
|