@ottocode/server 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/server",
3
- "version": "0.1.252",
3
+ "version": "0.1.254",
4
4
  "description": "HTTP API server for ottocode",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -49,8 +49,8 @@
49
49
  "typecheck": "tsc --noEmit"
50
50
  },
51
51
  "dependencies": {
52
- "@ottocode/database": "0.1.252",
53
- "@ottocode/sdk": "0.1.252",
52
+ "@ottocode/database": "0.1.254",
53
+ "@ottocode/sdk": "0.1.254",
54
54
  "ai-sdk-ollama": "^3.8.3",
55
55
  "drizzle-orm": "^0.44.5",
56
56
  "hono": "^4.9.9",
@@ -139,10 +139,6 @@ export const configPaths = {
139
139
  items: { type: 'string' },
140
140
  },
141
141
  allowAnyModel: { type: 'boolean' },
142
- scope: {
143
- type: 'string',
144
- enum: ['global', 'local'],
145
- },
146
142
  },
147
143
  },
148
144
  },
@@ -176,15 +172,6 @@ export const configPaths = {
176
172
  summary: 'Delete provider override or custom provider entry',
177
173
  parameters: [
178
174
  projectQueryParam(),
179
- {
180
- in: 'query',
181
- name: 'scope',
182
- required: false,
183
- schema: {
184
- type: 'string',
185
- enum: ['global', 'local'],
186
- },
187
- },
188
175
  {
189
176
  in: 'path',
190
177
  name: 'provider',
@@ -60,7 +60,6 @@ export const skillsPaths = {
60
60
  type: 'object',
61
61
  properties: {
62
62
  enabled: { type: 'boolean' },
63
- scope: { type: 'string', enum: ['global', 'local'] },
64
63
  items: {
65
64
  type: 'object',
66
65
  additionalProperties: {
package/src/presets.ts CHANGED
@@ -64,6 +64,7 @@ export const BUILTIN_TOOLS = [
64
64
  'edit',
65
65
  'multiedit',
66
66
  'write',
67
+ 'copy_into',
67
68
  'ls',
68
69
  'tree',
69
70
  'pwd',
@@ -1,10 +1,9 @@
1
1
  import type { Hono } from 'hono';
2
2
  import {
3
3
  DEFAULT_REMOTE_MODEL_CATALOG_URL,
4
+ catalog,
4
5
  discoverOllamaModels,
5
6
  loadConfig,
6
- catalog,
7
- getConfiguredProviderModels,
8
7
  getProviderDefinition,
9
8
  providerAllowsAnyModel,
10
9
  getAuth,
@@ -29,6 +28,7 @@ import {
29
28
  const COPILOT_MODELS_URL = 'https://api.githubcopilot.com/models';
30
29
  const REMOTE_CATALOG_REFRESH_TTL_MS = 5 * 60 * 1000;
31
30
  const PROVIDER_MODEL_REFRESH_TTL_MS = 60 * 1000;
31
+ const USE_BUILTIN_MODEL_CATALOG = process.env.CI === 'true';
32
32
 
33
33
  type UiModel = {
34
34
  id: string;
@@ -76,6 +76,12 @@ function getRemoteCatalogUrl(): string {
76
76
  );
77
77
  }
78
78
 
79
+ function getModelCatalogProviders(
80
+ cachedCatalog: Awaited<ReturnType<typeof readCachedModelCatalog>>,
81
+ ) {
82
+ return cachedCatalog?.providers ?? (USE_BUILTIN_MODEL_CATALOG ? catalog : {});
83
+ }
84
+
79
85
  async function refreshRemoteCatalogInBackground(): Promise<void> {
80
86
  const url = getRemoteCatalogUrl();
81
87
  const now = Date.now();
@@ -132,19 +138,11 @@ async function refreshProviderModelsInBackground(args: {
132
138
  projectRoot,
133
139
  });
134
140
  if (!discoveredModels) return;
135
- const configuredModels = getConfiguredProviderModels(
136
- await loadConfig(projectRoot),
137
- provider,
138
- );
139
- const models = mergeConfiguredAndCachedModels(
140
- configuredModels,
141
- discoveredModels,
142
- );
143
141
  await mergeCachedModelCatalog({
144
142
  [provider]: {
145
143
  id: provider,
146
144
  label: providerDefinition.label,
147
- models,
145
+ models: discoveredModels,
148
146
  },
149
147
  });
150
148
  } catch (error) {
@@ -261,40 +259,13 @@ function shouldLazyLoadProviderModels(
261
259
  );
262
260
  }
263
261
 
264
- function mergeConfiguredAndCachedModels(
265
- configuredModels: ModelInfo[],
266
- cachedModels: ModelInfo[],
267
- ): ModelInfo[] {
268
- const modelsById = new Map<string, ModelInfo>();
269
- for (const model of configuredModels) {
270
- modelsById.set(model.id, model);
271
- }
272
- for (const model of cachedModels) {
273
- const configuredModel = modelsById.get(model.id);
274
- modelsById.set(
275
- model.id,
276
- configuredModel ? { ...model, ...configuredModel } : model,
277
- );
278
- }
279
- return Array.from(modelsById.values());
280
- }
281
-
282
262
  function getProviderModelsForUi(args: {
283
- providerDefinition: NonNullable<ReturnType<typeof getProviderDefinition>>;
284
263
  catalogModels: ModelInfo[] | undefined;
285
- cfg: Awaited<ReturnType<typeof loadConfig>>;
286
264
  provider: ProviderId;
287
265
  authType: 'api' | 'oauth' | 'wallet' | undefined;
288
266
  }): ModelInfo[] {
289
- const configuredModels = getConfiguredProviderModels(args.cfg, args.provider);
290
267
  const catalogModels = args.catalogModels ?? [];
291
- if (args.providerDefinition.source === 'custom') {
292
- return mergeConfiguredAndCachedModels(configuredModels, catalogModels);
293
- }
294
- if (catalogModels.length > 0) {
295
- return filterModelsForAuthType(args.provider, catalogModels, args.authType);
296
- }
297
- return configuredModels;
268
+ return filterModelsForAuthType(args.provider, catalogModels, args.authType);
298
269
  }
299
270
 
300
271
  function getUiProviderLabel(
@@ -318,6 +289,9 @@ export function registerModelsRoutes(app: Hono) {
318
289
 
319
290
  const projectRoot = c.req.query('project') || process.cwd();
320
291
  const cfg = await loadConfig(projectRoot);
292
+ const cachedCatalog = await readCachedModelCatalog();
293
+ const modelCatalogProviders = getModelCatalogProviders(cachedCatalog);
294
+ const providerCatalog = modelCatalogProviders[provider];
321
295
 
322
296
  const authorized = await isProviderAuthorizedHybrid(
323
297
  embeddedConfig,
@@ -325,17 +299,13 @@ export function registerModelsRoutes(app: Hono) {
325
299
  provider,
326
300
  );
327
301
 
328
- if (!authorized) {
302
+ if (!authorized && !providerCatalog) {
329
303
  logger.warn('Provider not authorized', { provider });
330
304
  return c.json({ error: 'Provider not authorized' }, 403);
331
305
  }
332
306
 
333
- const cachedCatalog = await readCachedModelCatalog();
334
- const providerCatalog =
335
- cachedCatalog?.providers[provider] ??
336
- catalog[provider as keyof typeof catalog];
337
307
  const providerDefinition = getProviderDefinition(cfg, provider);
338
- if (!providerDefinition) {
308
+ if (!providerDefinition && !providerCatalog) {
339
309
  logger.warn('Provider not found in catalog', { provider });
340
310
  return c.json({ error: 'Provider not found' }, 404);
341
311
  }
@@ -346,7 +316,10 @@ export function registerModelsRoutes(app: Hono) {
346
316
  provider,
347
317
  projectRoot,
348
318
  );
349
- if (shouldLazyLoadProviderModels(providerDefinition)) {
319
+ if (
320
+ providerDefinition &&
321
+ shouldLazyLoadProviderModels(providerDefinition)
322
+ ) {
350
323
  void refreshProviderModelsInBackground({
351
324
  provider,
352
325
  providerDefinition,
@@ -354,9 +327,7 @@ export function registerModelsRoutes(app: Hono) {
354
327
  });
355
328
  }
356
329
  const filteredModels = getProviderModelsForUi({
357
- providerDefinition,
358
330
  catalogModels: providerCatalog?.models,
359
- cfg,
360
331
  provider,
361
332
  authType,
362
333
  });
@@ -378,8 +349,12 @@ export function registerModelsRoutes(app: Hono) {
378
349
  embeddedConfig?.defaults?.model,
379
350
  cfg.defaults.model,
380
351
  ),
381
- allowAnyModel: providerAllowsAnyModel(cfg, provider),
382
- label: getUiProviderLabel(providerDefinition),
352
+ allowAnyModel: providerDefinition
353
+ ? providerAllowsAnyModel(cfg, provider)
354
+ : undefined,
355
+ label: providerDefinition
356
+ ? getUiProviderLabel(providerDefinition)
357
+ : (providerCatalog?.label ?? provider),
383
358
  });
384
359
  } catch (error) {
385
360
  logger.error('Failed to get provider models', error);
@@ -405,24 +380,31 @@ export function registerModelsRoutes(app: Hono) {
405
380
  );
406
381
 
407
382
  const cachedCatalog = await readCachedModelCatalog();
383
+ const modelCatalogProviders = getModelCatalogProviders(cachedCatalog);
408
384
  void refreshRemoteCatalogInBackground();
409
385
 
410
386
  const modelsMap: Record<string, UiProviderModels> = {};
411
387
 
412
- for (const provider of authorizedProviders) {
413
- const providerCatalog =
414
- cachedCatalog?.providers[provider] ??
415
- catalog[provider as keyof typeof catalog];
388
+ const cachedProviderIds = Object.keys(
389
+ modelCatalogProviders,
390
+ ) as ProviderId[];
391
+ const modelProviders = Array.from(
392
+ new Set<ProviderId>([...authorizedProviders, ...cachedProviderIds]),
393
+ );
394
+
395
+ for (const provider of modelProviders) {
396
+ const providerCatalog = modelCatalogProviders[provider];
416
397
  const providerDefinition = getProviderDefinition(cfg, provider);
417
- if (providerDefinition) {
398
+ if (providerCatalog) {
418
399
  const dynamicModels =
400
+ providerDefinition &&
419
401
  shouldLazyLoadProviderModels(providerDefinition);
420
402
  const authType = await getAuthTypeForProvider(
421
403
  embeddedConfig,
422
404
  provider,
423
405
  projectRoot,
424
406
  );
425
- if (dynamicModels) {
407
+ if (dynamicModels && providerDefinition) {
426
408
  void refreshProviderModelsInBackground({
427
409
  provider,
428
410
  providerDefinition,
@@ -430,16 +412,16 @@ export function registerModelsRoutes(app: Hono) {
430
412
  });
431
413
  }
432
414
  const filteredModels = getProviderModelsForUi({
433
- providerDefinition,
434
415
  catalogModels: providerCatalog?.models,
435
- cfg,
436
416
  provider,
437
417
  authType,
438
418
  });
439
419
  modelsMap[provider] = {
440
- label: getUiProviderLabel(providerDefinition),
420
+ label: providerDefinition
421
+ ? getUiProviderLabel(providerDefinition)
422
+ : (providerCatalog.label ?? provider),
441
423
  authType,
442
- allowAnyModel: providerDefinition.allowAnyModel,
424
+ allowAnyModel: providerDefinition?.allowAnyModel,
443
425
  dynamicModels,
444
426
  models: filteredModels.map(toUiModel),
445
427
  };
@@ -31,7 +31,6 @@ type ProviderMutationBody = {
31
31
  apiKeyEnv?: string | null;
32
32
  models?: string[];
33
33
  allowAnyModel?: boolean;
34
- scope?: 'global' | 'local';
35
34
  };
36
35
 
37
36
  type ProviderDiscoveryBody = {
@@ -165,7 +164,6 @@ export function registerProvidersRoute(app: Hono) {
165
164
  const projectRoot = c.req.query('project') || process.cwd();
166
165
  const provider = c.req.param('provider').trim();
167
166
  const body = await c.req.json<ProviderMutationBody>();
168
- const scope = body.scope || 'local';
169
167
  if (!provider) return c.json({ error: 'Provider is required' }, 400);
170
168
 
171
169
  const updates: ProviderSettingsEntry = {
@@ -202,7 +200,7 @@ export function registerProvidersRoute(app: Hono) {
202
200
  return c.json({ error: 'Custom providers require compatibility' }, 400);
203
201
  }
204
202
 
205
- await writeProviderSettings(scope, provider, updates, projectRoot);
203
+ await writeProviderSettings('global', provider, updates, projectRoot);
206
204
  const cfg = await loadConfig(projectRoot);
207
205
  const details = await getProviderDetails(undefined, cfg);
208
206
  return c.json({
@@ -230,11 +228,9 @@ export function registerProvidersRoute(app: Hono) {
230
228
 
231
229
  const projectRoot = c.req.query('project') || process.cwd();
232
230
  const provider = c.req.param('provider').trim();
233
- const scope =
234
- (c.req.query('scope') as 'global' | 'local' | undefined) || 'local';
235
231
  if (!provider) return c.json({ error: 'Provider is required' }, 400);
236
232
 
237
- await removeProviderSettings(scope, provider, projectRoot);
233
+ await removeProviderSettings('global', provider, projectRoot);
238
234
  const cfg = await loadConfig(projectRoot);
239
235
  const details = await getProviderDetails(undefined, cfg);
240
236
  return c.json({ success: true, provider, details });
@@ -10,10 +10,12 @@ const FILE_EDIT_TOOLS = [
10
10
  'Write',
11
11
  'Edit',
12
12
  'MultiEdit',
13
+ 'CopyInto',
13
14
  'ApplyPatch',
14
15
  'write',
15
16
  'edit',
16
17
  'multiedit',
18
+ 'copy_into',
17
19
  'apply_patch',
18
20
  ];
19
21
 
@@ -42,6 +44,7 @@ interface SessionFile {
42
44
 
43
45
  interface ToolResultData {
44
46
  path?: string;
47
+ targetPath?: string;
45
48
  args?: Record<string, unknown>;
46
49
  files?: Array<string | { path: string }>;
47
50
  result?: {
@@ -75,6 +78,10 @@ function extractFilePathFromToolCall(
75
78
  if (args && typeof args.path === 'string') return args.path;
76
79
  if (typeof c.path === 'string') return c.path;
77
80
  }
81
+ if (name === 'copyinto' || name === 'copy_into') {
82
+ if (args && typeof args.targetPath === 'string') return args.targetPath;
83
+ if (typeof c.targetPath === 'string') return c.targetPath;
84
+ }
78
85
 
79
86
  if (name === 'applypatch' || name === 'apply_patch') {
80
87
  const patch = args?.patch ?? c.patch;
@@ -146,6 +153,16 @@ function extractFilesFromToolResult(
146
153
  if (args && typeof args.path === 'string' && !files.includes(args.path)) {
147
154
  files.push(args.path);
148
155
  }
156
+ if (
157
+ args &&
158
+ typeof args.targetPath === 'string' &&
159
+ !files.includes(args.targetPath)
160
+ ) {
161
+ files.push(args.targetPath);
162
+ }
163
+ if (typeof c.targetPath === 'string' && !files.includes(c.targetPath)) {
164
+ files.push(c.targetPath);
165
+ }
149
166
 
150
167
  if (Array.isArray(c.files)) {
151
168
  for (const f of c.files) {
@@ -199,7 +216,7 @@ function extractDataFromToolResult(
199
216
  }
200
217
 
201
218
  if (
202
- (name === 'edit' || name === 'multiedit') &&
219
+ (name === 'edit' || name === 'multiedit' || name === 'copy_into') &&
203
220
  typeof c.result?.artifact?.patch === 'string'
204
221
  ) {
205
222
  patch = c.result.artifact.patch;
@@ -229,7 +246,8 @@ function extractDataFromToolResult(
229
246
  function getOperationType(toolName: string): 'write' | 'patch' | 'create' {
230
247
  const name = toolName.toLowerCase();
231
248
  if (name === 'write') return 'write';
232
- if (name === 'edit' || name === 'multiedit') return 'patch';
249
+ if (name === 'edit' || name === 'multiedit' || name === 'copy_into')
250
+ return 'patch';
233
251
  if (name === 'applypatch' || name === 'apply_patch') return 'patch';
234
252
  return 'write';
235
253
  }
@@ -97,10 +97,9 @@ export function registerSkillsRoutes(app: Hono) {
97
97
  const body = await c.req.json<{
98
98
  enabled?: boolean;
99
99
  items?: Record<string, { enabled?: boolean }>;
100
- scope?: 'global' | 'local';
101
100
  }>();
102
101
  await writeSkillSettings(
103
- body.scope || 'local',
102
+ 'global',
104
103
  {
105
104
  ...(body.enabled !== undefined ? { enabled: body.enabled } : {}),
106
105
  ...(body.items ? { items: body.items } : {}),
@@ -124,6 +124,7 @@ const defaultToolExtras: Record<string, string[]> = {
124
124
  'edit',
125
125
  'multiedit',
126
126
  'write',
127
+ 'copy_into',
127
128
  'ls',
128
129
  'tree',
129
130
  'shell',
@@ -142,6 +143,7 @@ const defaultToolExtras: Record<string, string[]> = {
142
143
  'edit',
143
144
  'multiedit',
144
145
  'write',
146
+ 'copy_into',
145
147
  'ls',
146
148
  'tree',
147
149
  'shell',
@@ -156,6 +158,7 @@ const defaultToolExtras: Record<string, string[]> = {
156
158
  'edit',
157
159
  'multiedit',
158
160
  'write',
161
+ 'copy_into',
159
162
  'ls',
160
163
  'tree',
161
164
  'shell',
@@ -115,7 +115,13 @@ export function mergeProviderOptions(
115
115
  return base;
116
116
  }
117
117
 
118
- const EDITING_TOOL_NAMES = ['edit', 'multiedit', 'write', 'apply_patch'];
118
+ const EDITING_TOOL_NAMES = [
119
+ 'edit',
120
+ 'multiedit',
121
+ 'write',
122
+ 'copy_into',
123
+ 'apply_patch',
124
+ ];
119
125
  const MODEL_FAMILY_EDIT_TOOL_POLICY_AGENTS = new Set([
120
126
  'build',
121
127
  'general',
@@ -148,8 +154,8 @@ export function applyModelFamilyEditToolPolicy(
148
154
  );
149
155
  const preferredEditingTools =
150
156
  family === 'anthropic' || family === 'openai'
151
- ? ['write', 'apply_patch']
152
- : ['write', 'edit', 'multiedit'];
157
+ ? ['write', 'copy_into', 'apply_patch']
158
+ : ['write', 'edit', 'multiedit', 'copy_into'];
153
159
 
154
160
  return Array.from(new Set([...next, ...preferredEditingTools]));
155
161
  }
@@ -61,6 +61,7 @@ const DEFAULT_TRACED_TOOL_INPUTS = new Set([
61
61
  'write',
62
62
  'edit',
63
63
  'multiedit',
64
+ 'copy_into',
64
65
  'apply_patch',
65
66
  ]);
66
67
 
@@ -8,6 +8,7 @@ export const DANGEROUS_TOOLS = new Set([
8
8
  'edit',
9
9
  'multiedit',
10
10
  'write',
11
+ 'copy_into',
11
12
  'apply_patch',
12
13
  'terminal',
13
14
  'git_commit',
@@ -25,6 +25,7 @@ export function guardToolCall(
25
25
  case 'read':
26
26
  return guardReadPath(String(a.path ?? ''), context.projectRoot);
27
27
  case 'write':
28
+ case 'copy_into':
28
29
  return guardWritePath(toolName, a);
29
30
  default:
30
31
  return { type: 'allow' };
@@ -193,9 +194,11 @@ function guardWritePath(
193
194
  const path =
194
195
  typeof args.path === 'string'
195
196
  ? args.path
196
- : typeof args.filePath === 'string'
197
- ? args.filePath
198
- : '';
197
+ : typeof args.targetPath === 'string'
198
+ ? args.targetPath
199
+ : typeof args.filePath === 'string'
200
+ ? args.filePath
201
+ : '';
199
202
  if (!path) return { type: 'allow' };
200
203
  const p = path.trim();
201
204
 
@@ -20,6 +20,7 @@ export const CANONICAL_TO_PASCAL: Record<string, string> = {
20
20
  edit: 'Edit',
21
21
  multiedit: 'MultiEdit',
22
22
  write: 'Write',
23
+ copy_into: 'CopyInto',
23
24
  ls: 'Ls',
24
25
  tree: 'Tree',
25
26
  cd: 'Cd',
@@ -61,6 +62,7 @@ export const PASCAL_TO_CANONICAL: Record<string, string> = {
61
62
  Edit: 'edit',
62
63
  MultiEdit: 'multiedit',
63
64
  Write: 'write',
65
+ CopyInto: 'copy_into',
64
66
  Ls: 'ls',
65
67
  Tree: 'tree',
66
68
  Cd: 'cd',
@@ -64,6 +64,7 @@ const DEFAULT_TRACED_TOOL_INPUTS = new Set([
64
64
  'write',
65
65
  'edit',
66
66
  'multiedit',
67
+ 'copy_into',
67
68
  'apply_patch',
68
69
  ]);
69
70