@ottocode/sdk 0.1.252 → 0.1.254

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.252",
3
+ "version": "0.1.254",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -57,7 +57,11 @@ export async function loadConfig(
57
57
  const projectCfg = await readJsonOptional(projectConfigPath);
58
58
  const globalCfg = await readJsonOptional(globalConfigPath);
59
59
 
60
- const merged = deepMerge(DEFAULTS, globalCfg, projectCfg);
60
+ const merged = deepMerge(
61
+ DEFAULTS,
62
+ globalCfg,
63
+ omitGlobalOnlySettings(projectCfg),
64
+ );
61
65
 
62
66
  await ensureDir(dataDir);
63
67
 
@@ -81,6 +85,14 @@ export async function loadConfig(
81
85
 
82
86
  type JsonObject = Record<string, unknown>;
83
87
 
88
+ function omitGlobalOnlySettings(
89
+ config: JsonObject | undefined,
90
+ ): JsonObject | undefined {
91
+ if (!config) return undefined;
92
+ const { providers: _providers, skills: _skills, ...rest } = config;
93
+ return rest;
94
+ }
95
+
84
96
  async function readJsonOptional(file: string): Promise<JsonObject | undefined> {
85
97
  const f = Bun.file(file);
86
98
  if (!(await f.exists())) return undefined;
@@ -104,12 +104,12 @@ export async function writeDefaults(
104
104
  * Persist provider settings for a built-in or custom provider entry.
105
105
  */
106
106
  export async function writeProviderSettings(
107
- scope: Scope,
107
+ _scope: Scope,
108
108
  provider: string,
109
109
  updates: ProviderSettingsEntry,
110
- projectRoot?: string,
110
+ _projectRoot?: string,
111
111
  ) {
112
- const filePath = getConfigFilePath(scope, projectRoot);
112
+ const filePath = getConfigFilePath('global');
113
113
  const existing = await readJsonFile(filePath);
114
114
  const prevProviders =
115
115
  existing && typeof existing.providers === 'object'
@@ -133,11 +133,11 @@ export async function writeProviderSettings(
133
133
  * Remove a provider override or custom provider entry from config.
134
134
  */
135
135
  export async function removeProviderSettings(
136
- scope: Scope,
136
+ _scope: Scope,
137
137
  provider: string,
138
- projectRoot?: string,
138
+ _projectRoot?: string,
139
139
  ) {
140
- const filePath = getConfigFilePath(scope, projectRoot);
140
+ const filePath = getConfigFilePath('global');
141
141
  const existing = await readJsonFile(filePath);
142
142
  if (!existing || typeof existing.providers !== 'object') return;
143
143
  const providers = { ...(existing.providers as Record<string, unknown>) };
@@ -147,11 +147,11 @@ export async function removeProviderSettings(
147
147
  }
148
148
 
149
149
  export async function writeSkillSettings(
150
- scope: Scope,
150
+ _scope: Scope,
151
151
  updates: SkillSettings,
152
- projectRoot?: string,
152
+ _projectRoot?: string,
153
153
  ) {
154
- const filePath = getConfigFilePath(scope, projectRoot);
154
+ const filePath = getConfigFilePath('global');
155
155
  const existing = await readJsonFile(filePath);
156
156
  const prevSkills =
157
157
  existing && typeof existing.skills === 'object'
@@ -0,0 +1,270 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { tool, type Tool } from 'ai';
3
+ import { z } from 'zod/v3';
4
+ import DESCRIPTION from './copy-into.txt' with { type: 'text' };
5
+ import { buildWriteArtifact, isAbsoluteLike, resolveSafePath } from './util.ts';
6
+ import {
7
+ convertToLineEnding,
8
+ detectLineEnding,
9
+ normalizeLineEndings,
10
+ } from './edit-shared.ts';
11
+ import { assertFreshRead, rememberFileWrite } from './read-tracker.ts';
12
+ import { createToolError, type ToolResponse } from '../../error.ts';
13
+
14
+ const copyIntoSchema = z.object({
15
+ sourcePath: z
16
+ .string()
17
+ .describe('Relative source file path within the project.'),
18
+ startLine: z
19
+ .number()
20
+ .int()
21
+ .min(1)
22
+ .describe('First source line to copy, 1-indexed and inclusive.'),
23
+ endLine: z
24
+ .number()
25
+ .int()
26
+ .min(1)
27
+ .describe('Last source line to copy, 1-indexed and inclusive.'),
28
+ targetPath: z
29
+ .string()
30
+ .describe('Relative target file path within the project.'),
31
+ insertAtLine: z
32
+ .number()
33
+ .int()
34
+ .min(1)
35
+ .optional()
36
+ .describe(
37
+ 'Line to insert before, 1-indexed. Use totalLines + 1 to append.',
38
+ ),
39
+ mode: z
40
+ .enum(['insert_before', 'insert_after', 'replace_range'])
41
+ .optional()
42
+ .default('insert_before')
43
+ .describe('How to apply copied content to the target file.'),
44
+ targetStartLine: z
45
+ .number()
46
+ .int()
47
+ .min(1)
48
+ .optional()
49
+ .describe('First target line to replace when mode is replace_range.'),
50
+ targetEndLine: z
51
+ .number()
52
+ .int()
53
+ .min(1)
54
+ .optional()
55
+ .describe('Last target line to replace when mode is replace_range.'),
56
+ });
57
+
58
+ type CopyIntoInput = z.infer<typeof copyIntoSchema>;
59
+
60
+ function splitLinesForEdit(content: string): {
61
+ lines: string[];
62
+ trailingNewline: boolean;
63
+ } {
64
+ const normalized = normalizeLineEndings(content);
65
+ const trailingNewline = normalized.endsWith('\n');
66
+ const lines = normalized.split('\n');
67
+ if (trailingNewline) lines.pop();
68
+ return { lines, trailingNewline };
69
+ }
70
+
71
+ function joinLinesForEdit(lines: string[], trailingNewline: boolean): string {
72
+ const joined = lines.join('\n');
73
+ return trailingNewline ? `${joined}\n` : joined;
74
+ }
75
+
76
+ function validateRelativePath(path: string, label: string): string | undefined {
77
+ if (!path || path.trim().length === 0) {
78
+ return `Missing required parameter: ${label}`;
79
+ }
80
+ if (isAbsoluteLike(path)) {
81
+ return `Refusing to access outside project root: ${path}. Use a relative path within the project.`;
82
+ }
83
+ return undefined;
84
+ }
85
+
86
+ function getLineRange(
87
+ lines: string[],
88
+ startLine: number,
89
+ endLine: number,
90
+ ): string[] {
91
+ if (startLine > endLine) {
92
+ throw new Error('startLine must be less than or equal to endLine.');
93
+ }
94
+ if (endLine > lines.length) {
95
+ throw new Error(
96
+ `Source range ${startLine}-${endLine} exceeds source file length (${lines.length} lines).`,
97
+ );
98
+ }
99
+ return lines.slice(startLine - 1, endLine);
100
+ }
101
+
102
+ function applyCopiedLines(
103
+ input: CopyIntoInput,
104
+ targetLines: string[],
105
+ copied: string[],
106
+ ): string[] {
107
+ const mode = input.mode ?? 'insert_before';
108
+ if (mode === 'replace_range') {
109
+ if (
110
+ input.targetStartLine === undefined ||
111
+ input.targetEndLine === undefined
112
+ ) {
113
+ throw new Error(
114
+ 'targetStartLine and targetEndLine are required when mode is replace_range.',
115
+ );
116
+ }
117
+ if (input.targetStartLine > input.targetEndLine) {
118
+ throw new Error(
119
+ 'targetStartLine must be less than or equal to targetEndLine.',
120
+ );
121
+ }
122
+ if (input.targetEndLine > targetLines.length) {
123
+ throw new Error(
124
+ `Target range ${input.targetStartLine}-${input.targetEndLine} exceeds target file length (${targetLines.length} lines).`,
125
+ );
126
+ }
127
+ return [
128
+ ...targetLines.slice(0, input.targetStartLine - 1),
129
+ ...copied,
130
+ ...targetLines.slice(input.targetEndLine),
131
+ ];
132
+ }
133
+
134
+ if (input.insertAtLine === undefined) {
135
+ throw new Error(
136
+ 'insertAtLine is required for insert_before and insert_after modes.',
137
+ );
138
+ }
139
+ if (input.insertAtLine > targetLines.length + 1) {
140
+ throw new Error(
141
+ `insertAtLine ${input.insertAtLine} exceeds append position (${targetLines.length + 1}).`,
142
+ );
143
+ }
144
+
145
+ const insertIndex =
146
+ mode === 'insert_after' ? input.insertAtLine : input.insertAtLine - 1;
147
+ if (insertIndex > targetLines.length) {
148
+ throw new Error(
149
+ `insertAtLine ${input.insertAtLine} with insert_after exceeds target file length (${targetLines.length} lines).`,
150
+ );
151
+ }
152
+
153
+ return [
154
+ ...targetLines.slice(0, insertIndex),
155
+ ...copied,
156
+ ...targetLines.slice(insertIndex),
157
+ ];
158
+ }
159
+
160
+ export function buildCopyIntoTool(projectRoot: string): {
161
+ name: string;
162
+ tool: Tool;
163
+ } {
164
+ const copyInto = tool({
165
+ description: DESCRIPTION,
166
+ inputSchema: copyIntoSchema,
167
+ async execute(input: CopyIntoInput): Promise<
168
+ ToolResponse<{
169
+ sourcePath: string;
170
+ targetPath: string;
171
+ linesCopied: number;
172
+ bytes: number;
173
+ artifact: unknown;
174
+ }>
175
+ > {
176
+ const sourcePathError = validateRelativePath(
177
+ input.sourcePath,
178
+ 'sourcePath',
179
+ );
180
+ if (sourcePathError) {
181
+ return createToolError(sourcePathError, 'validation', {
182
+ parameter: 'sourcePath',
183
+ value: input.sourcePath,
184
+ suggestion: 'Use a relative path within the project',
185
+ });
186
+ }
187
+ const targetPathError = validateRelativePath(
188
+ input.targetPath,
189
+ 'targetPath',
190
+ );
191
+ if (targetPathError) {
192
+ return createToolError(targetPathError, 'validation', {
193
+ parameter: 'targetPath',
194
+ value: input.targetPath,
195
+ suggestion: 'Use a relative path within the project',
196
+ });
197
+ }
198
+
199
+ const sourceAbs = resolveSafePath(projectRoot, input.sourcePath);
200
+ const targetAbs = resolveSafePath(projectRoot, input.targetPath);
201
+
202
+ try {
203
+ await assertFreshRead(projectRoot, targetAbs, input.targetPath);
204
+ const [sourceContent, targetContent] = await Promise.all([
205
+ readFile(sourceAbs, 'utf-8'),
206
+ readFile(targetAbs, 'utf-8'),
207
+ ]);
208
+ const source = splitLinesForEdit(sourceContent);
209
+ const copiedLines = getLineRange(
210
+ source.lines,
211
+ input.startLine,
212
+ input.endLine,
213
+ );
214
+ const target = splitLinesForEdit(targetContent);
215
+ const nextLines = applyCopiedLines(input, target.lines, copiedLines);
216
+ const nextNormalized = joinLinesForEdit(
217
+ nextLines,
218
+ target.trailingNewline,
219
+ );
220
+ const nextContent = convertToLineEnding(
221
+ nextNormalized,
222
+ detectLineEnding(targetContent),
223
+ );
224
+
225
+ if (nextContent === targetContent) {
226
+ return createToolError('No changes applied.', 'validation', {
227
+ suggestion:
228
+ 'Choose a source range or target location that changes the file',
229
+ });
230
+ }
231
+
232
+ await writeFile(targetAbs, nextContent, 'utf-8');
233
+ await rememberFileWrite(projectRoot, targetAbs);
234
+ const artifact = await buildWriteArtifact(
235
+ input.targetPath,
236
+ true,
237
+ targetContent,
238
+ nextContent,
239
+ );
240
+ return {
241
+ ok: true,
242
+ sourcePath: input.sourcePath,
243
+ targetPath: input.targetPath,
244
+ linesCopied: copiedLines.length,
245
+ bytes: nextContent.length,
246
+ artifact,
247
+ };
248
+ } catch (error: unknown) {
249
+ const isEnoent =
250
+ error &&
251
+ typeof error === 'object' &&
252
+ 'code' in error &&
253
+ error.code === 'ENOENT';
254
+ return createToolError(
255
+ isEnoent
256
+ ? 'Source or target file not found.'
257
+ : `Failed to copy into file: ${error instanceof Error ? error.message : String(error)}`,
258
+ isEnoent ? 'not_found' : 'execution',
259
+ {
260
+ suggestion: isEnoent
261
+ ? 'Use read, ls, or tree to confirm both file paths first'
262
+ : undefined,
263
+ },
264
+ );
265
+ }
266
+ },
267
+ });
268
+
269
+ return { name: 'copy_into', tool: copyInto };
270
+ }
@@ -0,0 +1,11 @@
1
+ Copy a line range from one project file into another project file without reprinting the copied content.
2
+
3
+ Use this when duplicating large existing content such as SVGs, licenses, generated blocks, examples, or repeated config snippets.
4
+
5
+ Rules:
6
+ - Source and target paths must be relative paths within the project.
7
+ - You must read the target file first in the current session before modifying it.
8
+ - Line numbers are 1-indexed and inclusive.
9
+ - `insertAtLine` inserts before that line. Use `insertAtLine: totalLines + 1` to append.
10
+ - Use `mode: "replace_range"` with `targetStartLine` and `targetEndLine` to replace target lines.
11
+ - This tool does not use the system clipboard.
@@ -3,6 +3,7 @@ import { buildEditTool } from './edit.ts';
3
3
  import { buildReadTool } from './read.ts';
4
4
  import { buildMultiEditTool } from './multiedit.ts';
5
5
  import { buildWriteTool } from './write.ts';
6
+ import { buildCopyIntoTool } from './copy-into.ts';
6
7
  import { buildLsTool } from './ls.ts';
7
8
  import { buildTreeTool } from './tree.ts';
8
9
  import { buildPwdTool } from './pwd.ts';
@@ -16,6 +17,7 @@ export function buildFsTools(
16
17
  out.push(buildEditTool(projectRoot));
17
18
  out.push(buildMultiEditTool(projectRoot));
18
19
  out.push(buildWriteTool(projectRoot));
20
+ out.push(buildCopyIntoTool(projectRoot));
19
21
  out.push(buildLsTool(projectRoot));
20
22
  out.push(buildTreeTool(projectRoot));
21
23
  out.push(buildPwdTool());
@@ -16,8 +16,10 @@ export type CachedModelCatalog = {
16
16
  export const DEFAULT_REMOTE_MODEL_CATALOG_URL =
17
17
  'https://ottocode.io/catalog/models.json';
18
18
 
19
+ const MODEL_CATALOG_CACHE_FILENAME = 'catalog-models.json';
20
+
19
21
  export function getModelCatalogCachePath(): string {
20
- return joinPath(getGlobalConfigDir(), 'models-catalog.json');
22
+ return joinPath(getGlobalConfigDir(), MODEL_CATALOG_CACHE_FILENAME);
21
23
  }
22
24
 
23
25
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -30,10 +32,10 @@ async function loadFsPromises(): Promise<typeof import('node:fs/promises')> {
30
32
 
31
33
  function readFileSyncCompat(path: string): string | null {
32
34
  try {
33
- const req = Function(
34
- 'return typeof require === "function" ? require : null',
35
- )() as ((specifier: string) => typeof import('node:fs')) | null;
36
- return req?.('node:fs').readFileSync(path, 'utf8') ?? null;
35
+ const fs = globalThis.process?.getBuiltinModule?.('node:fs') as
36
+ | { readFileSync: (filePath: string, encoding: 'utf8') => string }
37
+ | undefined;
38
+ return fs?.readFileSync(path, 'utf8') ?? null;
37
39
  } catch {
38
40
  return null;
39
41
  }
@@ -70,21 +72,27 @@ export function normalizeModelCatalogPayload(
70
72
  return providers;
71
73
  }
72
74
 
75
+ function normalizeCachedModelCatalogPayload(
76
+ payload: unknown,
77
+ ): CachedModelCatalog {
78
+ const updatedAt =
79
+ isRecord(payload) && typeof payload.updatedAt === 'string'
80
+ ? payload.updatedAt
81
+ : new Date(0).toISOString();
82
+ return {
83
+ version: 1,
84
+ updatedAt,
85
+ providers: normalizeModelCatalogPayload(payload),
86
+ };
87
+ }
88
+
73
89
  export async function readCachedModelCatalog(): Promise<CachedModelCatalog | null> {
74
90
  try {
75
91
  const { readFile } = await loadFsPromises();
76
92
  const payload = JSON.parse(
77
93
  await readFile(getModelCatalogCachePath(), 'utf8'),
78
94
  );
79
- const providers = normalizeModelCatalogPayload(payload);
80
- return {
81
- version: 1,
82
- updatedAt:
83
- isRecord(payload) && typeof payload.updatedAt === 'string'
84
- ? payload.updatedAt
85
- : new Date(0).toISOString(),
86
- providers,
87
- };
95
+ return normalizeCachedModelCatalogPayload(payload);
88
96
  } catch {
89
97
  return null;
90
98
  }
@@ -95,15 +103,7 @@ export function readCachedModelCatalogSync(): CachedModelCatalog | null {
95
103
  const text = readFileSyncCompat(getModelCatalogCachePath());
96
104
  if (!text) return null;
97
105
  const payload = JSON.parse(text);
98
- const providers = normalizeModelCatalogPayload(payload);
99
- return {
100
- version: 1,
101
- updatedAt:
102
- isRecord(payload) && typeof payload.updatedAt === 'string'
103
- ? payload.updatedAt
104
- : new Date(0).toISOString(),
105
- providers,
106
- };
106
+ return normalizeCachedModelCatalogPayload(payload);
107
107
  } catch {
108
108
  return null;
109
109
  }
@@ -56,25 +56,7 @@ const BUILTIN_FAMILY: Record<BuiltInProviderId, ProviderPromptFamily> = {
56
56
  minimax: 'minimax',
57
57
  };
58
58
 
59
- function normalizeCustomModels(
60
- models: Array<string | ModelInfo> | undefined,
61
- ): ModelInfo[] {
62
- return (models ?? [])
63
- .map((model) => {
64
- if (typeof model === 'string') {
65
- const id = String(model).trim();
66
- return id ? ({ id, label: id } satisfies ModelInfo) : null;
67
- }
68
- const id = String(model.id ?? '').trim();
69
- if (!id) return null;
70
- return {
71
- ...model,
72
- id,
73
- label: model.label?.trim() || id,
74
- } satisfies ModelInfo;
75
- })
76
- .filter((model): model is ModelInfo => Boolean(model));
77
- }
59
+ const USE_BUILTIN_MODEL_CATALOG = process.env.CI === 'true';
78
60
 
79
61
  function normalizeOptionalText(value: string | undefined): string | undefined {
80
62
  if (!value) return undefined;
@@ -97,13 +79,6 @@ function resolveCustomFamily(
97
79
  return settings.family ?? 'default';
98
80
  }
99
81
 
100
- function resolveCustomProviderLabel(
101
- id: string,
102
- settings: ProviderSettingsEntry,
103
- ): string {
104
- return settings.label ?? id;
105
- }
106
-
107
82
  export function isBuiltInProviderId(
108
83
  value: unknown,
109
84
  ): value is BuiltInProviderId {
@@ -129,9 +104,8 @@ export function getProviderDefinition(
129
104
  const entry = catalog[provider];
130
105
  if (!entry) return undefined;
131
106
  const cachedEntry = getCachedProviderCatalogEntry(provider);
132
- const models = cachedEntry?.models.length
133
- ? cachedEntry.models
134
- : entry.models;
107
+ const models =
108
+ cachedEntry?.models ?? (USE_BUILTIN_MODEL_CATALOG ? entry.models : []);
135
109
  return {
136
110
  id: provider,
137
111
  label: settings?.label ?? cachedEntry?.label ?? entry.label ?? provider,
@@ -148,10 +122,11 @@ export function getProviderDefinition(
148
122
  }
149
123
 
150
124
  if (!settings?.custom) return undefined;
151
- const models = normalizeCustomModels(settings.models);
125
+ const cachedEntry = getCachedProviderCatalogEntry(provider);
126
+ const models = cachedEntry?.models ?? [];
152
127
  return {
153
128
  id: provider,
154
- label: resolveCustomProviderLabel(provider, settings),
129
+ label: settings.label ?? cachedEntry?.label ?? provider,
155
130
  source: 'custom',
156
131
  compatibility: resolveCustomCompatibility(settings),
157
132
  family: resolveCustomFamily(settings),
@@ -3,7 +3,6 @@ import { getCachedProviderCatalogEntry } from './model-catalog-cache.ts';
3
3
  import type { OttoConfig, ProviderId } from '../../types/src/index.ts';
4
4
  import {
5
5
  getProviderDefinition,
6
- getConfiguredProviderModels,
7
6
  hasConfiguredModel,
8
7
  providerAllowsAnyModel,
9
8
  } from './registry.ts';
@@ -19,51 +18,76 @@ export function validateProviderModel(
19
18
  cfgOrCap?: OttoConfig | CapabilityRequest,
20
19
  cap?: CapabilityRequest,
21
20
  ) {
21
+ const providerId = provider.trim() as ProviderId;
22
+ const modelId = model.trim();
22
23
  const cfg = isOttoConfigLike(cfgOrCap) ? cfgOrCap : undefined;
23
24
  const effectiveCap = isOttoConfigLike(cfgOrCap) ? cap : cfgOrCap;
24
25
 
25
26
  if (cfg) {
26
- const definition = getProviderDefinition(cfg, provider);
27
+ const definition = getProviderDefinition(cfg, providerId);
28
+ const cachedModels =
29
+ getCachedProviderCatalogEntry(providerId)?.models ?? [];
27
30
  if (!definition) {
28
- throw new Error(`Provider not supported: ${provider}`);
31
+ if (!cachedModels.length) {
32
+ throw new Error(`Provider not supported: ${providerId}`);
33
+ }
34
+ const entry = cachedModels.find((m) => m.id === modelId);
35
+ if (!entry) {
36
+ throwModelNotFound(providerId, modelId, cachedModels);
37
+ }
38
+ applyCapabilityValidation(modelId, entry, effectiveCap, {
39
+ strict: false,
40
+ });
41
+ return;
29
42
  }
30
- if (!providerAllowsAnyModel(cfg, provider)) {
31
- if (!hasConfiguredModel(cfg, provider, model)) {
32
- const list = getConfiguredProviderModels(cfg, provider)
33
- .slice(0, 10)
34
- .map((m) => m.id)
35
- .join(', ');
36
- throw new Error(
37
- `Model not found for provider ${provider}: ${model}. Example models: ${list}${getConfiguredProviderModels(cfg, provider).length > 10 ? ', ...' : ''}`,
38
- );
43
+ if (!providerAllowsAnyModel(cfg, providerId)) {
44
+ const knownModels = definition.models.length
45
+ ? definition.models
46
+ : cachedModels;
47
+ const hasModel =
48
+ hasConfiguredModel(cfg, providerId, modelId) ||
49
+ cachedModels.some((m) => m.id === modelId);
50
+ if (!hasModel) {
51
+ throwModelNotFound(providerId, modelId, knownModels);
39
52
  }
40
53
  }
41
54
 
42
- const entry = definition.models.find((m) => m.id === model);
55
+ const entry =
56
+ definition.models.find((m) => m.id === modelId) ??
57
+ cachedModels.find((m) => m.id === modelId);
43
58
  if (entry) {
44
- applyCapabilityValidation(model, entry, effectiveCap, {
59
+ applyCapabilityValidation(modelId, entry, effectiveCap, {
45
60
  strict: definition.source !== 'custom',
46
61
  });
47
62
  }
48
63
  return;
49
64
  }
50
65
 
51
- const p = provider as ProviderId;
52
- if (!catalog[p]) {
53
- throw new Error(`Provider not supported: ${provider}`);
66
+ const p = providerId;
67
+ if (!catalog[p] && !getCachedProviderCatalogEntry(p)) {
68
+ throw new Error(`Provider not supported: ${providerId}`);
54
69
  }
55
- const models = getCachedProviderCatalogEntry(p)?.models ?? catalog[p].models;
56
- const entry = models.find((m) => m.id === model);
70
+ const models =
71
+ getCachedProviderCatalogEntry(p)?.models ?? catalog[p]?.models ?? [];
72
+ const entry = models.find((m) => m.id === modelId);
57
73
  if (!entry) {
58
- const list = models
59
- .slice(0, 10)
60
- .map((m) => m.id)
61
- .join(', ');
62
- throw new Error(
63
- `Model not found for provider ${provider}: ${model}. Example models: ${list}${models.length > 10 ? ', ...' : ''}`,
64
- );
74
+ throwModelNotFound(providerId, modelId, models);
65
75
  }
66
- applyCapabilityValidation(model, entry, effectiveCap, { strict: true });
76
+ applyCapabilityValidation(modelId, entry, effectiveCap, { strict: true });
77
+ }
78
+
79
+ function throwModelNotFound(
80
+ provider: ProviderId,
81
+ model: string,
82
+ models: Array<{ id: string }>,
83
+ ): never {
84
+ const list = models
85
+ .slice(0, 10)
86
+ .map((m) => m.id)
87
+ .join(', ');
88
+ throw new Error(
89
+ `Model not found for provider ${provider}: ${model}. Example models: ${list}${models.length > 10 ? ', ...' : ''}`,
90
+ );
67
91
  }
68
92
 
69
93
  function applyCapabilityValidation(
@@ -12,15 +12,6 @@ import { getGlobalConfigDir, getHomeDir } from '../config/src/paths.ts';
12
12
 
13
13
  const skillCache = new Map<string, SkillDefinition>();
14
14
 
15
- const SKILL_DIRS = [
16
- '.otto/skills',
17
- '.agents/skills',
18
- '.agenst/skills',
19
- '.claude/skills',
20
- '.opencode/skills',
21
- '.codex/skills',
22
- ];
23
-
24
15
  const ALLOWED_EXTENSIONS = new Set([
25
16
  '.md',
26
17
  '.txt',
@@ -47,8 +38,8 @@ const ALLOWED_EXTENSIONS = new Set([
47
38
  const MAX_FILE_SIZE = 256 * 1024;
48
39
 
49
40
  export async function discoverSkills(
50
- cwd: string,
51
- repoRoot?: string,
41
+ _cwd: string,
42
+ _repoRoot?: string,
52
43
  ): Promise<DiscoveredSkill[]> {
53
44
  const skills = new Map<string, SkillDefinition>();
54
45
  const home = getHomeDir();
@@ -65,27 +56,6 @@ export async function discoverSkills(
65
56
  await loadSkillsFromDir(dir, 'user', skills);
66
57
  }
67
58
 
68
- if (repoRoot && repoRoot !== cwd) {
69
- for (const skillDir of SKILL_DIRS) {
70
- await loadSkillsFromDir(join(repoRoot, skillDir), 'repo', skills);
71
- }
72
- }
73
-
74
- let current = cwd;
75
- const visited = new Set<string>();
76
- while (current && !visited.has(current)) {
77
- if (repoRoot && !current.startsWith(repoRoot)) break;
78
- visited.add(current);
79
- const scope: SkillScope =
80
- current === cwd ? 'cwd' : current === repoRoot ? 'repo' : 'parent';
81
- for (const skillDir of SKILL_DIRS) {
82
- await loadSkillsFromDir(join(current, skillDir), scope, skills);
83
- }
84
- const parent = dirname(current);
85
- if (parent === current) break;
86
- current = parent;
87
- }
88
-
89
59
  skillCache.clear();
90
60
  for (const [name, def] of skills) {
91
61
  skillCache.set(name, def);