@ottocode/sdk 0.1.173
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/README.md +338 -0
- package/package.json +128 -0
- package/src/agent/types.ts +19 -0
- package/src/auth/src/copilot-oauth.ts +190 -0
- package/src/auth/src/index.ts +100 -0
- package/src/auth/src/oauth.ts +234 -0
- package/src/auth/src/openai-oauth.ts +394 -0
- package/src/auth/src/wallet.ts +51 -0
- package/src/browser.ts +32 -0
- package/src/config/src/index.ts +110 -0
- package/src/config/src/manager.ts +181 -0
- package/src/config/src/paths.ts +98 -0
- package/src/core/src/errors.ts +102 -0
- package/src/core/src/index.ts +108 -0
- package/src/core/src/providers/resolver.ts +244 -0
- package/src/core/src/streaming/artifacts.ts +41 -0
- package/src/core/src/terminals/bun-pty.ts +13 -0
- package/src/core/src/terminals/circular-buffer.ts +30 -0
- package/src/core/src/terminals/ensure-bun-pty.ts +70 -0
- package/src/core/src/terminals/index.ts +8 -0
- package/src/core/src/terminals/manager.ts +158 -0
- package/src/core/src/terminals/rust-libs.ts +30 -0
- package/src/core/src/terminals/terminal.ts +132 -0
- package/src/core/src/tools/bin-manager.ts +250 -0
- package/src/core/src/tools/builtin/bash.ts +155 -0
- package/src/core/src/tools/builtin/bash.txt +7 -0
- package/src/core/src/tools/builtin/file-cache.ts +39 -0
- package/src/core/src/tools/builtin/finish.ts +12 -0
- package/src/core/src/tools/builtin/finish.txt +10 -0
- package/src/core/src/tools/builtin/fs/cd.ts +19 -0
- package/src/core/src/tools/builtin/fs/cd.txt +5 -0
- package/src/core/src/tools/builtin/fs/index.ts +20 -0
- package/src/core/src/tools/builtin/fs/ls.ts +72 -0
- package/src/core/src/tools/builtin/fs/ls.txt +8 -0
- package/src/core/src/tools/builtin/fs/pwd.ts +17 -0
- package/src/core/src/tools/builtin/fs/pwd.txt +5 -0
- package/src/core/src/tools/builtin/fs/read.ts +119 -0
- package/src/core/src/tools/builtin/fs/read.txt +8 -0
- package/src/core/src/tools/builtin/fs/tree.ts +149 -0
- package/src/core/src/tools/builtin/fs/tree.txt +11 -0
- package/src/core/src/tools/builtin/fs/util.ts +95 -0
- package/src/core/src/tools/builtin/fs/write.ts +106 -0
- package/src/core/src/tools/builtin/fs/write.txt +11 -0
- package/src/core/src/tools/builtin/git.commit.txt +6 -0
- package/src/core/src/tools/builtin/git.diff.txt +5 -0
- package/src/core/src/tools/builtin/git.status.txt +5 -0
- package/src/core/src/tools/builtin/git.ts +151 -0
- package/src/core/src/tools/builtin/glob.ts +128 -0
- package/src/core/src/tools/builtin/glob.txt +10 -0
- package/src/core/src/tools/builtin/grep.ts +136 -0
- package/src/core/src/tools/builtin/grep.txt +9 -0
- package/src/core/src/tools/builtin/ignore.ts +45 -0
- package/src/core/src/tools/builtin/patch/apply.ts +546 -0
- package/src/core/src/tools/builtin/patch/constants.ts +5 -0
- package/src/core/src/tools/builtin/patch/normalize.ts +31 -0
- package/src/core/src/tools/builtin/patch/parse-enveloped.ts +209 -0
- package/src/core/src/tools/builtin/patch/parse-unified.ts +231 -0
- package/src/core/src/tools/builtin/patch/parse.ts +28 -0
- package/src/core/src/tools/builtin/patch/text.ts +23 -0
- package/src/core/src/tools/builtin/patch/types.ts +82 -0
- package/src/core/src/tools/builtin/patch.ts +167 -0
- package/src/core/src/tools/builtin/patch.txt +207 -0
- package/src/core/src/tools/builtin/progress.ts +55 -0
- package/src/core/src/tools/builtin/progress.txt +7 -0
- package/src/core/src/tools/builtin/ripgrep.ts +125 -0
- package/src/core/src/tools/builtin/ripgrep.txt +7 -0
- package/src/core/src/tools/builtin/terminal.ts +300 -0
- package/src/core/src/tools/builtin/terminal.txt +93 -0
- package/src/core/src/tools/builtin/todos.ts +66 -0
- package/src/core/src/tools/builtin/todos.txt +7 -0
- package/src/core/src/tools/builtin/websearch.ts +250 -0
- package/src/core/src/tools/builtin/websearch.txt +12 -0
- package/src/core/src/tools/error.ts +67 -0
- package/src/core/src/tools/loader.ts +421 -0
- package/src/core/src/types/index.ts +11 -0
- package/src/core/src/types/types.ts +4 -0
- package/src/core/src/utils/ansi.ts +27 -0
- package/src/core/src/utils/debug.ts +40 -0
- package/src/core/src/utils/logger.ts +150 -0
- package/src/index.ts +313 -0
- package/src/prompts/src/agents/build.txt +89 -0
- package/src/prompts/src/agents/general.txt +15 -0
- package/src/prompts/src/agents/plan.txt +10 -0
- package/src/prompts/src/agents/research.txt +50 -0
- package/src/prompts/src/base.txt +24 -0
- package/src/prompts/src/debug.ts +104 -0
- package/src/prompts/src/index.ts +1 -0
- package/src/prompts/src/modes/oneshot.txt +9 -0
- package/src/prompts/src/providers/anthropic.txt +247 -0
- package/src/prompts/src/providers/anthropicSpoof.txt +1 -0
- package/src/prompts/src/providers/default.txt +466 -0
- package/src/prompts/src/providers/google.txt +230 -0
- package/src/prompts/src/providers/moonshot.txt +24 -0
- package/src/prompts/src/providers/openai.txt +414 -0
- package/src/prompts/src/providers.ts +143 -0
- package/src/providers/src/anthropic-caching.ts +202 -0
- package/src/providers/src/anthropic-oauth-client.ts +157 -0
- package/src/providers/src/authorization.ts +17 -0
- package/src/providers/src/catalog-manual.ts +135 -0
- package/src/providers/src/catalog-merged.ts +9 -0
- package/src/providers/src/catalog.ts +8329 -0
- package/src/providers/src/copilot-client.ts +39 -0
- package/src/providers/src/env.ts +31 -0
- package/src/providers/src/google-client.ts +16 -0
- package/src/providers/src/index.ts +75 -0
- package/src/providers/src/moonshot-client.ts +25 -0
- package/src/providers/src/oauth-models.ts +39 -0
- package/src/providers/src/openai-oauth-client.ts +108 -0
- package/src/providers/src/opencode-client.ts +64 -0
- package/src/providers/src/openrouter-client.ts +31 -0
- package/src/providers/src/pricing.ts +178 -0
- package/src/providers/src/setu-client.ts +643 -0
- package/src/providers/src/utils.ts +210 -0
- package/src/providers/src/validate.ts +39 -0
- package/src/providers/src/zai-client.ts +47 -0
- package/src/skills/index.ts +34 -0
- package/src/skills/loader.ts +152 -0
- package/src/skills/parser.ts +108 -0
- package/src/skills/tool.ts +87 -0
- package/src/skills/types.ts +41 -0
- package/src/skills/validator.ts +110 -0
- package/src/types/src/auth.ts +33 -0
- package/src/types/src/config.ts +36 -0
- package/src/types/src/index.ts +20 -0
- package/src/types/src/provider.ts +71 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { catalog } from './catalog-merged.ts';
|
|
2
|
+
import type { ProviderId, ModelInfo } from '../../types/src/index.ts';
|
|
3
|
+
import { filterModelsForAuthType } from './oauth-models.ts';
|
|
4
|
+
|
|
5
|
+
export const providerIds = Object.keys(catalog) as ProviderId[];
|
|
6
|
+
|
|
7
|
+
export function isProviderId(value: unknown): value is ProviderId {
|
|
8
|
+
return typeof value === 'string' && providerIds.includes(value as ProviderId);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function defaultModelFor(provider: ProviderId): string | undefined {
|
|
12
|
+
return catalog[provider]?.models?.[0]?.id;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function listModels(provider: ProviderId): string[] {
|
|
16
|
+
return (catalog[provider]?.models ?? []).map((m) => m.id);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function hasModel(
|
|
20
|
+
provider: ProviderId,
|
|
21
|
+
model: string | undefined,
|
|
22
|
+
): boolean {
|
|
23
|
+
if (!model) return false;
|
|
24
|
+
return listModels(provider).includes(model);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const PREFERRED_FAST_MODELS: Partial<Record<ProviderId, string[]>> = {
|
|
28
|
+
openai: ['gpt-4.1-mini'],
|
|
29
|
+
anthropic: ['claude-3-5-haiku-latest'],
|
|
30
|
+
google: ['gemini-2.0-flash-lite'],
|
|
31
|
+
openrouter: ['anthropic/claude-3.5-haiku'],
|
|
32
|
+
opencode: ['claude-3-5-haiku'],
|
|
33
|
+
setu: ['claude-3-5-haiku-latest'],
|
|
34
|
+
zai: ['glm-4.5-flash'],
|
|
35
|
+
copilot: ['gpt-4.1-mini'],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function getFastModel(provider: ProviderId): string | undefined {
|
|
39
|
+
const providerModels = catalog[provider]?.models ?? [];
|
|
40
|
+
if (!providerModels.length) return undefined;
|
|
41
|
+
|
|
42
|
+
const preferred = PREFERRED_FAST_MODELS[provider] ?? [];
|
|
43
|
+
for (const modelId of preferred) {
|
|
44
|
+
if (providerModels.some((m) => m.id === modelId)) {
|
|
45
|
+
return modelId;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const sorted = [...providerModels]
|
|
50
|
+
.filter((m) => m.cost?.input !== undefined && m.toolCall !== false)
|
|
51
|
+
.sort((a, b) => (a.cost?.input ?? Infinity) - (b.cost?.input ?? Infinity));
|
|
52
|
+
|
|
53
|
+
return sorted[0]?.id ?? providerModels[0]?.id;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getFastModelForAuth(
|
|
57
|
+
provider: ProviderId,
|
|
58
|
+
authType: 'api' | 'oauth' | 'wallet' | undefined,
|
|
59
|
+
): string | undefined {
|
|
60
|
+
const providerModels = catalog[provider]?.models ?? [];
|
|
61
|
+
if (!providerModels.length) return undefined;
|
|
62
|
+
|
|
63
|
+
const filteredModels = filterModelsForAuthType(
|
|
64
|
+
provider,
|
|
65
|
+
providerModels,
|
|
66
|
+
authType,
|
|
67
|
+
);
|
|
68
|
+
if (!filteredModels.length) return getFastModel(provider);
|
|
69
|
+
|
|
70
|
+
if (authType !== 'oauth') {
|
|
71
|
+
const preferred = PREFERRED_FAST_MODELS[provider] ?? [];
|
|
72
|
+
for (const modelId of preferred) {
|
|
73
|
+
if (filteredModels.some((m) => m.id === modelId)) {
|
|
74
|
+
return modelId;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// For OAuth or when no preferred model found, use cost-based selection
|
|
80
|
+
const sorted = [...filteredModels]
|
|
81
|
+
.filter(
|
|
82
|
+
(m: ModelInfo) => m.cost?.input !== undefined && m.toolCall !== false,
|
|
83
|
+
)
|
|
84
|
+
.sort(
|
|
85
|
+
(a: ModelInfo, b: ModelInfo) =>
|
|
86
|
+
(a.cost?.input ?? Infinity) - (b.cost?.input ?? Infinity),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return sorted[0]?.id ?? filteredModels[0]?.id;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getModelNpmBinding(
|
|
93
|
+
provider: ProviderId,
|
|
94
|
+
model: string,
|
|
95
|
+
): string | undefined {
|
|
96
|
+
// 1) Check provider's own catalog entry
|
|
97
|
+
const entry = catalog[provider];
|
|
98
|
+
const modelInfo = entry?.models?.find((m) => m.id === model);
|
|
99
|
+
if (modelInfo?.provider?.npm) return modelInfo.provider.npm;
|
|
100
|
+
if (entry?.npm) return entry.npm;
|
|
101
|
+
|
|
102
|
+
// 2) Search entire catalog for the model
|
|
103
|
+
for (const key of Object.keys(catalog) as ProviderId[]) {
|
|
104
|
+
const e = catalog[key];
|
|
105
|
+
const m = e?.models?.find((x) => x.id === model);
|
|
106
|
+
if (m?.provider?.npm) return m.provider.npm;
|
|
107
|
+
if (m && e?.npm) return e.npm;
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function isAnthropicBasedModel(
|
|
113
|
+
provider: ProviderId,
|
|
114
|
+
model: string,
|
|
115
|
+
): boolean {
|
|
116
|
+
if (provider === 'anthropic') return true;
|
|
117
|
+
const npm = getModelNpmBinding(provider, model);
|
|
118
|
+
if (npm === '@ai-sdk/anthropic') return true;
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type UnderlyingProviderKey =
|
|
123
|
+
| 'anthropic'
|
|
124
|
+
| 'openai'
|
|
125
|
+
| 'google'
|
|
126
|
+
| 'moonshot'
|
|
127
|
+
| 'openai-compatible'
|
|
128
|
+
| null;
|
|
129
|
+
|
|
130
|
+
export function getUnderlyingProviderKey(
|
|
131
|
+
provider: ProviderId,
|
|
132
|
+
model: string,
|
|
133
|
+
): UnderlyingProviderKey {
|
|
134
|
+
if (provider === 'anthropic') return 'anthropic';
|
|
135
|
+
if (provider === 'openai') return 'openai';
|
|
136
|
+
if (provider === 'google') return 'google';
|
|
137
|
+
if (provider === 'moonshot') return 'moonshot';
|
|
138
|
+
if (provider === 'copilot') return 'openai';
|
|
139
|
+
|
|
140
|
+
const npm = getModelNpmBinding(provider, model);
|
|
141
|
+
if (npm === '@ai-sdk/anthropic') return 'anthropic';
|
|
142
|
+
if (npm === '@ai-sdk/openai') return 'openai';
|
|
143
|
+
if (npm === '@ai-sdk/google') return 'google';
|
|
144
|
+
if (npm === '@ai-sdk/openai-compatible') return 'openai-compatible';
|
|
145
|
+
if (npm === '@openrouter/ai-sdk-provider') return 'openai-compatible';
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function getModelFamily(
|
|
150
|
+
provider: ProviderId,
|
|
151
|
+
model: string,
|
|
152
|
+
): UnderlyingProviderKey {
|
|
153
|
+
// 1) Direct provider mapping
|
|
154
|
+
if (provider === 'anthropic') return 'anthropic';
|
|
155
|
+
if (provider === 'openai') return 'openai';
|
|
156
|
+
if (provider === 'google') return 'google';
|
|
157
|
+
if (provider === 'moonshot') return 'moonshot';
|
|
158
|
+
if (provider === 'copilot') return 'openai';
|
|
159
|
+
|
|
160
|
+
// 2) For aggregate providers, infer from model ID patterns
|
|
161
|
+
if (provider === 'openrouter' || provider === 'opencode') {
|
|
162
|
+
const lowerModel = model.toLowerCase();
|
|
163
|
+
// Anthropic models
|
|
164
|
+
if (lowerModel.includes('claude') || lowerModel.startsWith('anthropic/')) {
|
|
165
|
+
return 'anthropic';
|
|
166
|
+
}
|
|
167
|
+
// OpenAI models
|
|
168
|
+
if (
|
|
169
|
+
lowerModel.includes('gpt') ||
|
|
170
|
+
lowerModel.startsWith('openai/') ||
|
|
171
|
+
lowerModel.includes('codex')
|
|
172
|
+
) {
|
|
173
|
+
return 'openai';
|
|
174
|
+
}
|
|
175
|
+
// Google models
|
|
176
|
+
if (lowerModel.includes('gemini') || lowerModel.startsWith('google/')) {
|
|
177
|
+
return 'google';
|
|
178
|
+
}
|
|
179
|
+
// Moonshot models
|
|
180
|
+
if (lowerModel.includes('kimi') || lowerModel.startsWith('moonshotai/')) {
|
|
181
|
+
return 'moonshot';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 2) Check model's family field in catalog
|
|
186
|
+
const info = getModelInfo(provider, model);
|
|
187
|
+
if (info?.provider?.family) {
|
|
188
|
+
return info.provider.family as UnderlyingProviderKey;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 3) Fall back to npm binding (for zai and other providers)
|
|
192
|
+
return getUnderlyingProviderKey(provider, model);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function getModelInfo(
|
|
196
|
+
provider: ProviderId,
|
|
197
|
+
model: string,
|
|
198
|
+
): ModelInfo | undefined {
|
|
199
|
+
const entry = catalog[provider];
|
|
200
|
+
if (!entry) return undefined;
|
|
201
|
+
return entry.models?.find((m) => m.id === model);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function modelSupportsReasoning(
|
|
205
|
+
provider: ProviderId,
|
|
206
|
+
model: string,
|
|
207
|
+
): boolean {
|
|
208
|
+
const info = getModelInfo(provider, model);
|
|
209
|
+
return info?.reasoningText === true;
|
|
210
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { catalog } from './catalog-merged.ts';
|
|
2
|
+
import type { ProviderId } from '../../types/src/index.ts';
|
|
3
|
+
|
|
4
|
+
export type CapabilityRequest = {
|
|
5
|
+
wantsToolCalls?: boolean;
|
|
6
|
+
wantsVision?: boolean; // input image
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function validateProviderModel(
|
|
10
|
+
provider: string,
|
|
11
|
+
model: string,
|
|
12
|
+
cap?: CapabilityRequest,
|
|
13
|
+
) {
|
|
14
|
+
const p = provider as ProviderId;
|
|
15
|
+
if (!catalog[p]) {
|
|
16
|
+
throw new Error(`Provider not supported: ${provider}`);
|
|
17
|
+
}
|
|
18
|
+
const entry = catalog[p].models.find((m) => m.id === model);
|
|
19
|
+
if (!entry) {
|
|
20
|
+
const list = catalog[p].models
|
|
21
|
+
.slice(0, 10)
|
|
22
|
+
.map((m) => m.id)
|
|
23
|
+
.join(', ');
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Model not found for provider ${provider}: ${model}. Example models: ${list}${catalog[p].models.length > 10 ? ', ...' : ''}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (cap?.wantsToolCalls && !entry.toolCall) {
|
|
29
|
+
throw new Error(`Model ${model} does not support tool calls.`);
|
|
30
|
+
}
|
|
31
|
+
if (cap?.wantsVision) {
|
|
32
|
+
const inputs = entry.modalities?.input as string[] | undefined;
|
|
33
|
+
const outputs = entry.modalities?.output as string[] | undefined;
|
|
34
|
+
const ok =
|
|
35
|
+
(inputs ?? []).includes('image') || (outputs ?? []).includes('image');
|
|
36
|
+
if (!ok)
|
|
37
|
+
throw new Error(`Model ${model} does not support vision input/output.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
2
|
+
import { catalog } from './catalog-merged.ts';
|
|
3
|
+
|
|
4
|
+
export type ZaiProviderConfig = {
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function createZaiModel(model: string, config?: ZaiProviderConfig) {
|
|
9
|
+
const entry = catalog.zai;
|
|
10
|
+
const baseURL = entry?.api || 'https://api.z.ai/api/paas/v4';
|
|
11
|
+
const apiKey =
|
|
12
|
+
config?.apiKey ||
|
|
13
|
+
process.env.ZAI_API_KEY ||
|
|
14
|
+
process.env.ZHIPU_API_KEY ||
|
|
15
|
+
'';
|
|
16
|
+
const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined;
|
|
17
|
+
|
|
18
|
+
const instance = createOpenAICompatible({
|
|
19
|
+
name: entry?.label ?? 'Z.AI',
|
|
20
|
+
baseURL,
|
|
21
|
+
headers,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return instance(model);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createZaiCodingModel(
|
|
28
|
+
model: string,
|
|
29
|
+
config?: ZaiProviderConfig,
|
|
30
|
+
) {
|
|
31
|
+
const entry = catalog['zai-coding'];
|
|
32
|
+
const baseURL = entry?.api || 'https://api.z.ai/api/coding/paas/v4';
|
|
33
|
+
const apiKey =
|
|
34
|
+
config?.apiKey ||
|
|
35
|
+
process.env.ZAI_API_KEY ||
|
|
36
|
+
process.env.ZHIPU_API_KEY ||
|
|
37
|
+
'';
|
|
38
|
+
const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined;
|
|
39
|
+
|
|
40
|
+
const instance = createOpenAICompatible({
|
|
41
|
+
name: entry?.label ?? 'Z.AI Coding',
|
|
42
|
+
baseURL,
|
|
43
|
+
headers,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return instance(model);
|
|
47
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
SkillScope,
|
|
3
|
+
SkillMetadata,
|
|
4
|
+
SkillDefinition,
|
|
5
|
+
DiscoveredSkill,
|
|
6
|
+
SkillLoadResult,
|
|
7
|
+
SkillErrorResult,
|
|
8
|
+
SkillResult,
|
|
9
|
+
} from './types.ts';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
validateMetadata,
|
|
13
|
+
validateSkillName,
|
|
14
|
+
SkillValidationError,
|
|
15
|
+
} from './validator.ts';
|
|
16
|
+
|
|
17
|
+
export { parseSkillFile, extractFrontmatter } from './parser.ts';
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
discoverSkills,
|
|
21
|
+
loadSkill,
|
|
22
|
+
getSkillCache,
|
|
23
|
+
clearSkillCache,
|
|
24
|
+
findGitRoot,
|
|
25
|
+
listSkillsInDir,
|
|
26
|
+
} from './loader.ts';
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
initializeSkills,
|
|
30
|
+
getDiscoveredSkills,
|
|
31
|
+
isSkillsInitialized,
|
|
32
|
+
buildSkillTool,
|
|
33
|
+
rebuildSkillDescription,
|
|
34
|
+
} from './tool.ts';
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { join, dirname } from 'node:path';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { parseSkillFile } from './parser.ts';
|
|
5
|
+
import type { SkillDefinition, DiscoveredSkill, SkillScope } from './types.ts';
|
|
6
|
+
import { getGlobalConfigDir, getHomeDir } from '../config/src/paths.ts';
|
|
7
|
+
|
|
8
|
+
const skillCache = new Map<string, SkillDefinition>();
|
|
9
|
+
|
|
10
|
+
const SKILL_DIRS = [
|
|
11
|
+
'.otto/skills',
|
|
12
|
+
'.claude/skills',
|
|
13
|
+
'.opencode/skills',
|
|
14
|
+
'.codex/skills',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export async function discoverSkills(
|
|
18
|
+
cwd: string,
|
|
19
|
+
repoRoot?: string,
|
|
20
|
+
): Promise<DiscoveredSkill[]> {
|
|
21
|
+
const skills = new Map<string, SkillDefinition>();
|
|
22
|
+
const home = getHomeDir();
|
|
23
|
+
|
|
24
|
+
const globalDirs = [
|
|
25
|
+
join(getGlobalConfigDir(), 'skills'),
|
|
26
|
+
join(home, '.claude/skills'),
|
|
27
|
+
join(home, '.config/opencode/skills'),
|
|
28
|
+
join(home, '.codex/skills'),
|
|
29
|
+
];
|
|
30
|
+
for (const dir of globalDirs) {
|
|
31
|
+
await loadSkillsFromDir(dir, 'user', skills);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (repoRoot && repoRoot !== cwd) {
|
|
35
|
+
for (const skillDir of SKILL_DIRS) {
|
|
36
|
+
await loadSkillsFromDir(join(repoRoot, skillDir), 'repo', skills);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let current = cwd;
|
|
41
|
+
const visited = new Set<string>();
|
|
42
|
+
while (current && !visited.has(current)) {
|
|
43
|
+
visited.add(current);
|
|
44
|
+
const scope: SkillScope =
|
|
45
|
+
current === cwd ? 'cwd' : current === repoRoot ? 'repo' : 'parent';
|
|
46
|
+
for (const skillDir of SKILL_DIRS) {
|
|
47
|
+
await loadSkillsFromDir(join(current, skillDir), scope, skills);
|
|
48
|
+
}
|
|
49
|
+
const parent = dirname(current);
|
|
50
|
+
if (parent === current) break;
|
|
51
|
+
if (repoRoot && !current.startsWith(repoRoot)) break;
|
|
52
|
+
current = parent;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
skillCache.clear();
|
|
56
|
+
for (const [name, def] of skills) {
|
|
57
|
+
skillCache.set(name, def);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return Array.from(skills.values()).map((s) => ({
|
|
61
|
+
name: s.metadata.name,
|
|
62
|
+
description: s.metadata.description,
|
|
63
|
+
path: s.path,
|
|
64
|
+
scope: s.scope,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function loadSkill(name: string): Promise<SkillDefinition | null> {
|
|
69
|
+
return skillCache.get(name) ?? null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getSkillCache(): Map<string, SkillDefinition> {
|
|
73
|
+
return skillCache;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function clearSkillCache(): void {
|
|
77
|
+
skillCache.clear();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function loadSkillsFromDir(
|
|
81
|
+
dir: string,
|
|
82
|
+
scope: SkillScope,
|
|
83
|
+
skills: Map<string, SkillDefinition>,
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
try {
|
|
86
|
+
await fs.access(dir);
|
|
87
|
+
} catch {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const pattern = '*/SKILL.md';
|
|
92
|
+
let files: string[];
|
|
93
|
+
try {
|
|
94
|
+
files = await fg(pattern, { cwd: dir, absolute: true });
|
|
95
|
+
} catch {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const filePath of files) {
|
|
100
|
+
try {
|
|
101
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
102
|
+
const skill = parseSkillFile(content, filePath, scope);
|
|
103
|
+
|
|
104
|
+
const dirName = dirname(filePath).split('/').pop();
|
|
105
|
+
if (dirName !== skill.metadata.name) {
|
|
106
|
+
if (process.env.OTTO_DEBUG === '1') {
|
|
107
|
+
console.warn(
|
|
108
|
+
`Skill name '${skill.metadata.name}' doesn't match directory '${dirName}' in ${filePath}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
skills.set(skill.metadata.name, skill);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (process.env.OTTO_DEBUG === '1') {
|
|
116
|
+
console.error(`Failed to load skill from ${filePath}:`, err);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function findGitRoot(startDir: string): Promise<string | null> {
|
|
123
|
+
let current = startDir;
|
|
124
|
+
const visited = new Set<string>();
|
|
125
|
+
|
|
126
|
+
while (current && !visited.has(current)) {
|
|
127
|
+
visited.add(current);
|
|
128
|
+
try {
|
|
129
|
+
await fs.access(join(current, '.git'));
|
|
130
|
+
return current;
|
|
131
|
+
} catch {
|
|
132
|
+
const parent = dirname(current);
|
|
133
|
+
if (parent === current) break;
|
|
134
|
+
current = parent;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function listSkillsInDir(dir: string): Promise<string[]> {
|
|
142
|
+
try {
|
|
143
|
+
await fs.access(dir);
|
|
144
|
+
} catch {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const pattern = '*/SKILL.md';
|
|
149
|
+
const files = await fg(pattern, { cwd: dir, absolute: false });
|
|
150
|
+
|
|
151
|
+
return files.map((f) => dirname(f));
|
|
152
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { SkillDefinition, SkillMetadata, SkillScope } from './types.ts';
|
|
2
|
+
import { validateMetadata } from './validator.ts';
|
|
3
|
+
|
|
4
|
+
const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
5
|
+
|
|
6
|
+
export function parseSkillFile(
|
|
7
|
+
content: string,
|
|
8
|
+
path: string,
|
|
9
|
+
scope: SkillScope,
|
|
10
|
+
): SkillDefinition {
|
|
11
|
+
const match = content.match(FRONTMATTER_REGEX);
|
|
12
|
+
if (!match) {
|
|
13
|
+
throw new Error(`Invalid SKILL.md format: missing frontmatter in ${path}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const [, yamlStr, body] = match;
|
|
17
|
+
if (!yamlStr) {
|
|
18
|
+
throw new Error(`Empty frontmatter in ${path}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const metadata = parseYamlFrontmatter(yamlStr, path);
|
|
22
|
+
validateMetadata(metadata, path);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
metadata: metadata as SkillMetadata,
|
|
26
|
+
content: body?.trim() ?? '',
|
|
27
|
+
path,
|
|
28
|
+
scope,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseYamlFrontmatter(
|
|
33
|
+
yaml: string,
|
|
34
|
+
_path: string,
|
|
35
|
+
): Record<string, unknown> {
|
|
36
|
+
const result: Record<string, unknown> = {};
|
|
37
|
+
const lines = yaml.split('\n');
|
|
38
|
+
let currentKey: string | null = null;
|
|
39
|
+
let currentIndent = 0;
|
|
40
|
+
let nestedObject: Record<string, string> | null = null;
|
|
41
|
+
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
if (!line.trim()) continue;
|
|
44
|
+
|
|
45
|
+
const indent = line.search(/\S/);
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
|
|
48
|
+
if (indent === 0 || (indent <= currentIndent && nestedObject)) {
|
|
49
|
+
if (nestedObject && currentKey) {
|
|
50
|
+
result[currentKey] = nestedObject;
|
|
51
|
+
nestedObject = null;
|
|
52
|
+
currentKey = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const colonIdx = trimmed.indexOf(':');
|
|
57
|
+
if (colonIdx === -1) continue;
|
|
58
|
+
|
|
59
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
60
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
61
|
+
|
|
62
|
+
if (indent > 0 && nestedObject) {
|
|
63
|
+
nestedObject[key] = parseYamlValue(value);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!value) {
|
|
68
|
+
currentKey = normalizeKey(key);
|
|
69
|
+
currentIndent = indent;
|
|
70
|
+
nestedObject = {};
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
result[normalizeKey(key)] = parseYamlValue(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (nestedObject && currentKey) {
|
|
78
|
+
result[currentKey] = nestedObject;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeKey(key: string): string {
|
|
85
|
+
if (key === 'allowed-tools') return 'allowedTools';
|
|
86
|
+
return key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseYamlValue(value: string): string {
|
|
90
|
+
if (
|
|
91
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
92
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
93
|
+
) {
|
|
94
|
+
return value.slice(1, -1);
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function extractFrontmatter(
|
|
100
|
+
content: string,
|
|
101
|
+
): { frontmatter: string; body: string } | null {
|
|
102
|
+
const match = content.match(FRONTMATTER_REGEX);
|
|
103
|
+
if (!match) return null;
|
|
104
|
+
return {
|
|
105
|
+
frontmatter: match[1] ?? '',
|
|
106
|
+
body: match[2] ?? '',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { tool, type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod/v3';
|
|
3
|
+
import { loadSkill, discoverSkills, findGitRoot } from './loader.ts';
|
|
4
|
+
import type { DiscoveredSkill, SkillResult } from './types.ts';
|
|
5
|
+
|
|
6
|
+
let cachedSkillList: DiscoveredSkill[] = [];
|
|
7
|
+
let initializedForPath: string | null = null;
|
|
8
|
+
|
|
9
|
+
export async function initializeSkills(
|
|
10
|
+
cwd: string,
|
|
11
|
+
repoRoot?: string,
|
|
12
|
+
): Promise<DiscoveredSkill[]> {
|
|
13
|
+
const root = repoRoot ?? (await findGitRoot(cwd)) ?? cwd;
|
|
14
|
+
cachedSkillList = await discoverSkills(cwd, root);
|
|
15
|
+
initializedForPath = cwd;
|
|
16
|
+
return cachedSkillList;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getDiscoveredSkills(): DiscoveredSkill[] {
|
|
20
|
+
return cachedSkillList;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isSkillsInitialized(forPath?: string): boolean {
|
|
24
|
+
if (!initializedForPath) return false;
|
|
25
|
+
if (forPath && forPath !== initializedForPath) return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildSkillTool(): { name: string; tool: Tool } {
|
|
30
|
+
const skillTool = tool({
|
|
31
|
+
description: buildSkillDescription(),
|
|
32
|
+
inputSchema: z.object({
|
|
33
|
+
name: z.string().describe('Name of the skill to load'),
|
|
34
|
+
}),
|
|
35
|
+
async execute({ name }: { name: string }): Promise<SkillResult> {
|
|
36
|
+
const skill = await loadSkill(name);
|
|
37
|
+
if (!skill) {
|
|
38
|
+
return { ok: false, error: `Skill '${name}' not found` };
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
ok: true,
|
|
42
|
+
name: skill.metadata.name,
|
|
43
|
+
description: skill.metadata.description,
|
|
44
|
+
content: skill.content,
|
|
45
|
+
path: skill.path,
|
|
46
|
+
scope: skill.scope,
|
|
47
|
+
allowedTools: skill.metadata.allowedTools,
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return { name: 'skill', tool: skillTool };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildSkillDescription(): string {
|
|
56
|
+
if (cachedSkillList.length === 0) {
|
|
57
|
+
return 'Load a skill by name. No skills are currently available.';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const skillsXml = cachedSkillList
|
|
61
|
+
.map(
|
|
62
|
+
(s) =>
|
|
63
|
+
`<skill><name>${escapeXml(s.name)}</name><description>${escapeXml(s.description)}</description></skill>`,
|
|
64
|
+
)
|
|
65
|
+
.join('\n');
|
|
66
|
+
|
|
67
|
+
return `Load a skill by name to get detailed instructions.
|
|
68
|
+
|
|
69
|
+
<available_skills>
|
|
70
|
+
${skillsXml}
|
|
71
|
+
</available_skills>
|
|
72
|
+
|
|
73
|
+
Call this tool with the skill name when you need the full instructions.`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function escapeXml(str: string): string {
|
|
77
|
+
return str
|
|
78
|
+
.replace(/&/g, '&')
|
|
79
|
+
.replace(/</g, '<')
|
|
80
|
+
.replace(/>/g, '>')
|
|
81
|
+
.replace(/"/g, '"')
|
|
82
|
+
.replace(/'/g, ''');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function rebuildSkillDescription(): string {
|
|
86
|
+
return buildSkillDescription();
|
|
87
|
+
}
|