gencode-ai 0.1.0 → 0.1.1

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.
Files changed (149) hide show
  1. package/README.md +8 -90
  2. package/dist/agent/agent.d.ts +1 -1
  3. package/dist/agent/agent.d.ts.map +1 -1
  4. package/dist/agent/agent.js +8 -2
  5. package/dist/agent/agent.js.map +1 -1
  6. package/dist/agent/types.d.ts +9 -1
  7. package/dist/agent/types.d.ts.map +1 -1
  8. package/dist/cli/components/AllModelsSelector.d.ts +11 -0
  9. package/dist/cli/components/AllModelsSelector.d.ts.map +1 -0
  10. package/dist/cli/components/AllModelsSelector.js +153 -0
  11. package/dist/cli/components/AllModelsSelector.js.map +1 -0
  12. package/dist/cli/components/App.d.ts.map +1 -1
  13. package/dist/cli/components/App.js +59 -25
  14. package/dist/cli/components/App.js.map +1 -1
  15. package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
  16. package/dist/cli/components/CommandSuggestions.js +1 -0
  17. package/dist/cli/components/CommandSuggestions.js.map +1 -1
  18. package/dist/cli/components/Messages.d.ts +15 -1
  19. package/dist/cli/components/Messages.d.ts.map +1 -1
  20. package/dist/cli/components/Messages.js +41 -15
  21. package/dist/cli/components/Messages.js.map +1 -1
  22. package/dist/cli/components/ModelSelector.d.ts +7 -7
  23. package/dist/cli/components/ModelSelector.d.ts.map +1 -1
  24. package/dist/cli/components/ModelSelector.js +116 -33
  25. package/dist/cli/components/ModelSelector.js.map +1 -1
  26. package/dist/cli/components/ProviderManager.d.ts +8 -0
  27. package/dist/cli/components/ProviderManager.d.ts.map +1 -0
  28. package/dist/cli/components/ProviderManager.js +280 -0
  29. package/dist/cli/components/ProviderManager.js.map +1 -0
  30. package/dist/cli/components/markdown.d.ts +9 -0
  31. package/dist/cli/components/markdown.d.ts.map +1 -0
  32. package/dist/cli/components/markdown.js +129 -0
  33. package/dist/cli/components/markdown.js.map +1 -0
  34. package/dist/cli/components/theme.d.ts +5 -0
  35. package/dist/cli/components/theme.d.ts.map +1 -1
  36. package/dist/cli/components/theme.js +7 -0
  37. package/dist/cli/components/theme.js.map +1 -1
  38. package/dist/cli/index.js +19 -5
  39. package/dist/cli/index.js.map +1 -1
  40. package/dist/config/index.d.ts +3 -2
  41. package/dist/config/index.d.ts.map +1 -1
  42. package/dist/config/index.js +2 -1
  43. package/dist/config/index.js.map +1 -1
  44. package/dist/config/providers-config.d.ts +28 -0
  45. package/dist/config/providers-config.d.ts.map +1 -0
  46. package/dist/config/providers-config.js +79 -0
  47. package/dist/config/providers-config.js.map +1 -0
  48. package/dist/config/types.d.ts +31 -1
  49. package/dist/config/types.d.ts.map +1 -1
  50. package/dist/config/types.js +1 -0
  51. package/dist/config/types.js.map +1 -1
  52. package/dist/providers/gemini.d.ts.map +1 -1
  53. package/dist/providers/gemini.js +14 -3
  54. package/dist/providers/gemini.js.map +1 -1
  55. package/dist/providers/index.d.ts +5 -3
  56. package/dist/providers/index.d.ts.map +1 -1
  57. package/dist/providers/index.js +13 -1
  58. package/dist/providers/index.js.map +1 -1
  59. package/dist/providers/registry.d.ts +66 -0
  60. package/dist/providers/registry.d.ts.map +1 -0
  61. package/dist/providers/registry.js +158 -0
  62. package/dist/providers/registry.js.map +1 -0
  63. package/dist/providers/search/brave.d.ts +14 -0
  64. package/dist/providers/search/brave.d.ts.map +1 -0
  65. package/dist/providers/search/brave.js +87 -0
  66. package/dist/providers/search/brave.js.map +1 -0
  67. package/dist/providers/search/exa.d.ts +12 -0
  68. package/dist/providers/search/exa.d.ts.map +1 -0
  69. package/dist/providers/search/exa.js +158 -0
  70. package/dist/providers/search/exa.js.map +1 -0
  71. package/dist/providers/search/index.d.ts +31 -0
  72. package/dist/providers/search/index.d.ts.map +1 -0
  73. package/dist/providers/search/index.js +75 -0
  74. package/dist/providers/search/index.js.map +1 -0
  75. package/dist/providers/search/serper.d.ts +14 -0
  76. package/dist/providers/search/serper.d.ts.map +1 -0
  77. package/dist/providers/search/serper.js +87 -0
  78. package/dist/providers/search/serper.js.map +1 -0
  79. package/dist/providers/search/types.d.ts +21 -0
  80. package/dist/providers/search/types.d.ts.map +1 -0
  81. package/dist/providers/search/types.js +5 -0
  82. package/dist/providers/search/types.js.map +1 -0
  83. package/dist/providers/store.d.ts +104 -0
  84. package/dist/providers/store.d.ts.map +1 -0
  85. package/dist/providers/store.js +171 -0
  86. package/dist/providers/store.js.map +1 -0
  87. package/dist/providers/types.d.ts +7 -1
  88. package/dist/providers/types.d.ts.map +1 -1
  89. package/dist/providers/vertex-ai.d.ts +33 -0
  90. package/dist/providers/vertex-ai.d.ts.map +1 -0
  91. package/dist/providers/vertex-ai.js +407 -0
  92. package/dist/providers/vertex-ai.js.map +1 -0
  93. package/dist/tools/builtin/webfetch.d.ts +20 -0
  94. package/dist/tools/builtin/webfetch.d.ts.map +1 -0
  95. package/dist/tools/builtin/webfetch.js +231 -0
  96. package/dist/tools/builtin/webfetch.js.map +1 -0
  97. package/dist/tools/builtin/websearch.d.ts +17 -0
  98. package/dist/tools/builtin/websearch.d.ts.map +1 -0
  99. package/dist/tools/builtin/websearch.js +101 -0
  100. package/dist/tools/builtin/websearch.js.map +1 -0
  101. package/dist/tools/index.d.ts +11 -0
  102. package/dist/tools/index.d.ts.map +1 -1
  103. package/dist/tools/index.js +24 -2
  104. package/dist/tools/index.js.map +1 -1
  105. package/dist/tools/types.d.ts +19 -0
  106. package/dist/tools/types.d.ts.map +1 -1
  107. package/dist/tools/types.js +8 -0
  108. package/dist/tools/types.js.map +1 -1
  109. package/dist/tools/utils/ssrf.d.ts +18 -0
  110. package/dist/tools/utils/ssrf.d.ts.map +1 -0
  111. package/dist/tools/utils/ssrf.js +70 -0
  112. package/dist/tools/utils/ssrf.js.map +1 -0
  113. package/docs/README.md +5 -4
  114. package/docs/proposals/0001-web-fetch-tool.md +32 -2
  115. package/docs/proposals/0002-web-search-tool.md +59 -2
  116. package/docs/proposals/0041-configuration-system.md +556 -0
  117. package/docs/proposals/README.md +3 -2
  118. package/docs/providers.md +220 -0
  119. package/package.json +7 -2
  120. package/src/agent/agent.ts +9 -2
  121. package/src/agent/types.ts +9 -1
  122. package/src/cli/components/App.tsx +72 -23
  123. package/src/cli/components/CommandSuggestions.tsx +1 -0
  124. package/src/cli/components/Messages.tsx +117 -29
  125. package/src/cli/components/ModelSelector.tsx +169 -52
  126. package/src/cli/components/ProviderManager.tsx +534 -0
  127. package/src/cli/components/markdown.ts +157 -0
  128. package/src/cli/components/theme.ts +7 -0
  129. package/src/cli/index.tsx +22 -7
  130. package/src/config/index.ts +3 -2
  131. package/src/config/providers-config.ts +85 -0
  132. package/src/config/types.ts +35 -1
  133. package/src/providers/gemini.ts +20 -4
  134. package/src/providers/index.ts +18 -3
  135. package/src/providers/registry.ts +198 -0
  136. package/src/providers/search/brave.ts +132 -0
  137. package/src/providers/search/exa.ts +217 -0
  138. package/src/providers/search/index.ts +79 -0
  139. package/src/providers/search/serper.ts +133 -0
  140. package/src/providers/search/types.ts +24 -0
  141. package/src/providers/store.ts +216 -0
  142. package/src/providers/types.ts +9 -1
  143. package/src/providers/vertex-ai.ts +594 -0
  144. package/src/tools/builtin/webfetch.ts +264 -0
  145. package/src/tools/builtin/websearch.ts +117 -0
  146. package/src/tools/index.ts +24 -2
  147. package/src/tools/types.ts +20 -0
  148. package/src/tools/utils/ssrf.ts +79 -0
  149. package/CLAUDE.md +0 -70
@@ -14,6 +14,7 @@ export const colors = {
14
14
  textMuted: '#64748B', // Slate 500
15
15
  tool: '#C084FC', // Purple 400
16
16
  separator: '#1E293B', // Slate 800
17
+ inputBg: '#111827', // Gray 900 - subtle background for user input
17
18
  };
18
19
 
19
20
  export const icons = {
@@ -29,8 +30,14 @@ export const icons = {
29
30
  info: 'ℹ',
30
31
  // Tools
31
32
  tool: '⚡', // Lightning for tools
33
+ fetch: '●', // Filled circle for fetch (Claude Code style)
32
34
  arrow: '→',
33
35
  // UI
34
36
  thinking: '✱', // Star for thinking state
35
37
  cursor: '▋',
38
+ // Selection
39
+ radio: '●', // Filled radio for selected
40
+ radioEmpty: '○', // Empty radio for unselected
41
+ // Tree connectors
42
+ treeEnd: '└', // Tree end connector for tool results
36
43
  };
package/src/cli/index.tsx CHANGED
@@ -9,7 +9,7 @@ import { render } from 'ink';
9
9
  import React from 'react';
10
10
  import { App } from './components/App.js';
11
11
  import type { AgentConfig } from '../agent/types.js';
12
- import { SettingsManager, type Settings } from '../config/index.js';
12
+ import { SettingsManager, ProvidersConfigManager, type Settings, type ProviderName } from '../config/index.js';
13
13
 
14
14
  // ============================================================================
15
15
  // Proxy Setup
@@ -31,12 +31,17 @@ async function setupProxy(): Promise<void> {
31
31
  // ============================================================================
32
32
  // Configuration
33
33
  // ============================================================================
34
- function detectConfig(settings: Settings): AgentConfig {
35
- let provider: 'openai' | 'anthropic' | 'gemini' = 'gemini';
34
+ function detectConfig(settings: Settings, providersConfig: ProvidersConfigManager): AgentConfig {
35
+ let provider: ProviderName = 'gemini';
36
36
  let model = 'gemini-2.0-flash';
37
37
 
38
+ // Check for explicit Vertex AI enablement first (highest priority for auto-detect)
39
+ if (process.env.GENCODE_USE_VERTEX === '1' || process.env.CLAUDE_CODE_USE_VERTEX === '1') {
40
+ provider = 'vertex-ai';
41
+ model = process.env.VERTEX_AI_MODEL ?? 'claude-sonnet-4-5@20250929';
42
+ }
38
43
  // Auto-detect from API keys
39
- if (process.env.ANTHROPIC_API_KEY) {
44
+ else if (process.env.ANTHROPIC_API_KEY) {
40
45
  provider = 'anthropic';
41
46
  model = 'claude-sonnet-4-20250514';
42
47
  } else if (process.env.OPENAI_API_KEY) {
@@ -49,7 +54,7 @@ function detectConfig(settings: Settings): AgentConfig {
49
54
 
50
55
  // Override from env vars
51
56
  if (process.env.GENCODE_PROVIDER) {
52
- provider = process.env.GENCODE_PROVIDER as 'openai' | 'anthropic' | 'gemini';
57
+ provider = process.env.GENCODE_PROVIDER as ProviderName;
53
58
  }
54
59
  if (process.env.GENCODE_MODEL) {
55
60
  model = process.env.GENCODE_MODEL;
@@ -61,6 +66,13 @@ function detectConfig(settings: Settings): AgentConfig {
61
66
  }
62
67
  if (settings.model) {
63
68
  model = settings.model;
69
+ // Auto-infer provider from model using providers.json (if not explicitly set)
70
+ if (!settings.provider) {
71
+ const inferredProvider = providersConfig.inferProvider(model);
72
+ if (inferredProvider) {
73
+ provider = inferredProvider;
74
+ }
75
+ }
64
76
  }
65
77
 
66
78
  return {
@@ -114,11 +126,14 @@ async function main() {
114
126
 
115
127
  await setupProxy();
116
128
 
117
- // Load saved settings
129
+ // Load saved settings and providers config
118
130
  const settingsManager = new SettingsManager();
119
131
  const settings = await settingsManager.load();
120
132
 
121
- const config = detectConfig(settings);
133
+ const providersConfig = new ProvidersConfigManager();
134
+ await providersConfig.load();
135
+
136
+ const config = detectConfig(settings, providersConfig);
122
137
 
123
138
  // Render the Ink app
124
139
  render(
@@ -3,5 +3,6 @@
3
3
  */
4
4
 
5
5
  export { SettingsManager } from './manager.js';
6
- export type { Settings, SettingsManagerOptions, ProviderName } from './types.js';
7
- export { DEFAULT_SETTINGS_DIR, SETTINGS_FILE_NAME } from './types.js';
6
+ export { ProvidersConfigManager } from './providers-config.js';
7
+ export type { Settings, SettingsManagerOptions, ProviderName, ProvidersConfig } from './types.js';
8
+ export { DEFAULT_SETTINGS_DIR, SETTINGS_FILE_NAME, PROVIDERS_FILE_NAME } from './types.js';
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Providers Config Manager - Reads providers.json for model-to-provider mapping
3
+ */
4
+
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import * as os from 'os';
8
+ import type { ProvidersConfig, ProviderName } from './types.js';
9
+ import { DEFAULT_SETTINGS_DIR, PROVIDERS_FILE_NAME } from './types.js';
10
+
11
+ export class ProvidersConfigManager {
12
+ private settingsDir: string;
13
+ private providersPath: string;
14
+ private config: ProvidersConfig | null = null;
15
+
16
+ constructor(settingsDir?: string) {
17
+ const dir = settingsDir ?? DEFAULT_SETTINGS_DIR;
18
+ this.settingsDir = dir.replace('~', os.homedir());
19
+ this.providersPath = path.join(this.settingsDir, PROVIDERS_FILE_NAME);
20
+ }
21
+
22
+ /**
23
+ * Load providers config from disk
24
+ */
25
+ async load(): Promise<ProvidersConfig | null> {
26
+ try {
27
+ const content = await fs.readFile(this.providersPath, 'utf-8');
28
+ this.config = JSON.parse(content);
29
+ return this.config;
30
+ } catch {
31
+ // File doesn't exist or is invalid
32
+ this.config = null;
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Get cached config (call load() first)
39
+ */
40
+ get(): ProvidersConfig | null {
41
+ return this.config;
42
+ }
43
+
44
+ /**
45
+ * Infer provider from model ID using cached models in providers.json
46
+ * Returns undefined if model not found in any provider's cached list
47
+ */
48
+ inferProvider(modelId: string): ProviderName | undefined {
49
+ if (!this.config?.models) {
50
+ return undefined;
51
+ }
52
+
53
+ for (const [providerKey, providerModels] of Object.entries(this.config.models)) {
54
+ const found = providerModels.list?.some((m) => m.id === modelId);
55
+ if (found) {
56
+ // Map provider key to ProviderName
57
+ // Note: 'anthropic' in providers.json might use vertex connection
58
+ if (providerKey === 'gemini') {
59
+ return 'gemini';
60
+ } else if (providerKey === 'anthropic') {
61
+ // Check connection method to determine if vertex or direct
62
+ const connection = this.config.connections?.[providerKey];
63
+ if (connection?.method === 'vertex') {
64
+ return 'vertex-ai';
65
+ }
66
+ return 'anthropic';
67
+ } else if (providerKey === 'openai') {
68
+ return 'openai';
69
+ }
70
+ }
71
+ }
72
+
73
+ return undefined;
74
+ }
75
+
76
+ /**
77
+ * Get all model IDs for a provider
78
+ */
79
+ getModelIds(provider: string): string[] {
80
+ if (!this.config?.models?.[provider]) {
81
+ return [];
82
+ }
83
+ return this.config.models[provider].list?.map((m) => m.id) ?? [];
84
+ }
85
+ }
@@ -2,7 +2,7 @@
2
2
  * Settings Types - User settings persistence (Claude Code style)
3
3
  */
4
4
 
5
- export type ProviderName = 'openai' | 'anthropic' | 'gemini';
5
+ export type ProviderName = 'openai' | 'anthropic' | 'gemini' | 'vertex-ai';
6
6
 
7
7
  /**
8
8
  * Settings file structure (~/.gencode/settings.json)
@@ -23,3 +23,37 @@ export interface SettingsManagerOptions {
23
23
 
24
24
  export const DEFAULT_SETTINGS_DIR = '~/.gencode';
25
25
  export const SETTINGS_FILE_NAME = 'settings.json';
26
+ export const PROVIDERS_FILE_NAME = 'providers.json';
27
+
28
+ /**
29
+ * Provider connection info
30
+ */
31
+ export interface ProviderConnection {
32
+ method: 'api_key' | 'vertex' | 'oauth';
33
+ connectedAt: string;
34
+ }
35
+
36
+ /**
37
+ * Cached model info
38
+ */
39
+ export interface CachedModel {
40
+ id: string;
41
+ name: string;
42
+ description?: string;
43
+ }
44
+
45
+ /**
46
+ * Cached models for a provider
47
+ */
48
+ export interface ProviderModels {
49
+ cachedAt: string;
50
+ list: CachedModel[];
51
+ }
52
+
53
+ /**
54
+ * Providers config file structure (~/.gencode/providers.json)
55
+ */
56
+ export interface ProvidersConfig {
57
+ connections: Record<string, ProviderConnection>;
58
+ models: Record<string, ProviderModels>;
59
+ }
@@ -65,7 +65,12 @@ export class GeminiProvider implements LLMProvider {
65
65
  const result = await model.generateContentStream({ contents });
66
66
 
67
67
  let textContent = '';
68
- const functionCalls: Array<{ id: string; name: string; args: Record<string, unknown> }> = [];
68
+ const functionCalls: Array<{
69
+ id: string;
70
+ name: string;
71
+ args: Record<string, unknown>;
72
+ thoughtSignature?: string;
73
+ }> = [];
69
74
  let callIndex = 0;
70
75
 
71
76
  for await (const chunk of result.stream) {
@@ -78,10 +83,13 @@ export class GeminiProvider implements LLMProvider {
78
83
  } else if ('functionCall' in part && part.functionCall) {
79
84
  const fc = part.functionCall;
80
85
  const id = `call_${callIndex++}`;
86
+ // Capture thoughtSignature for Gemini 3+ models
87
+ const partAny = part as { thoughtSignature?: string };
81
88
  functionCalls.push({
82
89
  id,
83
90
  name: fc.name,
84
91
  args: (fc.args as Record<string, unknown>) ?? {},
92
+ thoughtSignature: partAny.thoughtSignature,
85
93
  });
86
94
  yield { type: 'tool_start', id, name: fc.name };
87
95
  yield { type: 'tool_input', id, input: JSON.stringify(fc.args) };
@@ -100,6 +108,7 @@ export class GeminiProvider implements LLMProvider {
100
108
  id: fc.id,
101
109
  name: fc.name,
102
110
  input: fc.args,
111
+ thoughtSignature: fc.thoughtSignature,
103
112
  });
104
113
  }
105
114
 
@@ -152,13 +161,17 @@ export class GeminiProvider implements LLMProvider {
152
161
  if (item.type === 'text') {
153
162
  parts.push({ text: item.text });
154
163
  } else if (item.type === 'tool_use' && role === 'model') {
155
- // Function call from model
156
- parts.push({
164
+ // Function call from model - include thoughtSignature for Gemini 3+
165
+ const fcPart: Part & { thoughtSignature?: string } = {
157
166
  functionCall: {
158
167
  name: item.name,
159
168
  args: item.input,
160
169
  },
161
- });
170
+ };
171
+ if (item.thoughtSignature) {
172
+ fcPart.thoughtSignature = item.thoughtSignature;
173
+ }
174
+ parts.push(fcPart as Part);
162
175
  } else if (item.type === 'tool_result' && role === 'user') {
163
176
  // Function response
164
177
  parts.push({
@@ -234,11 +247,14 @@ export class GeminiProvider implements LLMProvider {
234
247
  if ('text' in part && part.text) {
235
248
  content.push({ type: 'text', text: part.text });
236
249
  } else if ('functionCall' in part && part.functionCall) {
250
+ // Capture thoughtSignature for Gemini 3+ models
251
+ const partAny = part as { thoughtSignature?: string };
237
252
  content.push({
238
253
  type: 'tool_use',
239
254
  id: `call_${callIndex++}`,
240
255
  name: part.functionCall.name,
241
256
  input: (part.functionCall.args as Record<string, unknown>) ?? {},
257
+ thoughtSignature: partAny.thoughtSignature,
242
258
  });
243
259
  }
244
260
  }
@@ -1,22 +1,25 @@
1
1
  /**
2
- * LLM Providers - Unified interface for OpenAI, Anthropic, and Gemini
2
+ * LLM Providers - Unified interface for OpenAI, Anthropic, Gemini, and Vertex AI
3
3
  */
4
4
 
5
5
  export * from './types.js';
6
6
  export { OpenAIProvider } from './openai.js';
7
7
  export { AnthropicProvider } from './anthropic.js';
8
8
  export { GeminiProvider } from './gemini.js';
9
+ export { VertexAIProvider } from './vertex-ai.js';
9
10
 
10
- import type { LLMProvider, OpenAIConfig, AnthropicConfig, GeminiConfig } from './types.js';
11
+ import type { LLMProvider, OpenAIConfig, AnthropicConfig, GeminiConfig, VertexAIConfig } from './types.js';
11
12
  import { OpenAIProvider } from './openai.js';
12
13
  import { AnthropicProvider } from './anthropic.js';
13
14
  import { GeminiProvider } from './gemini.js';
15
+ import { VertexAIProvider } from './vertex-ai.js';
14
16
 
15
- export type ProviderName = 'openai' | 'anthropic' | 'gemini';
17
+ export type ProviderName = 'openai' | 'anthropic' | 'gemini' | 'vertex-ai';
16
18
  export type ProviderConfigMap = {
17
19
  openai: OpenAIConfig;
18
20
  anthropic: AnthropicConfig;
19
21
  gemini: GeminiConfig;
22
+ 'vertex-ai': VertexAIConfig;
20
23
  };
21
24
 
22
25
  export interface CreateProviderOptions<T extends ProviderName = ProviderName> {
@@ -35,6 +38,8 @@ export function createProvider(options: CreateProviderOptions): LLMProvider {
35
38
  return new AnthropicProvider(options.config as AnthropicConfig);
36
39
  case 'gemini':
37
40
  return new GeminiProvider(options.config as GeminiConfig);
41
+ case 'vertex-ai':
42
+ return new VertexAIProvider(options.config as VertexAIConfig);
38
43
  default:
39
44
  throw new Error(`Unknown provider: ${options.provider}`);
40
45
  }
@@ -46,6 +51,11 @@ export function createProvider(options: CreateProviderOptions): LLMProvider {
46
51
  export function inferProvider(model: string): ProviderName {
47
52
  const modelLower = model.toLowerCase();
48
53
 
54
+ // Vertex AI models (Claude models with @ version suffix like claude-sonnet-4-5@20250929)
55
+ if (modelLower.includes('claude') && modelLower.includes('@')) {
56
+ return 'vertex-ai';
57
+ }
58
+
49
59
  // OpenAI models
50
60
  if (
51
61
  modelLower.includes('gpt') ||
@@ -94,4 +104,9 @@ export const ModelAliases: Record<string, { provider: ProviderName; model: strin
94
104
  'gemini-2.0-flash': { provider: 'gemini', model: 'gemini-2.0-flash' },
95
105
  'gemini-1.5-pro': { provider: 'gemini', model: 'gemini-1.5-pro' },
96
106
  'gemini-1.5-flash': { provider: 'gemini', model: 'gemini-1.5-flash' },
107
+
108
+ // Vertex AI (Claude on GCP)
109
+ 'vertex-sonnet': { provider: 'vertex-ai', model: 'claude-sonnet-4-5@20250929' },
110
+ 'vertex-haiku': { provider: 'vertex-ai', model: 'claude-haiku-4-5@20251001' },
111
+ 'vertex-opus': { provider: 'vertex-ai', model: 'claude-opus-4-1@20250805' },
97
112
  };
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Provider Registry - Provider definitions with connection options
3
+ */
4
+
5
+ import type { ProviderName } from './index.js';
6
+ import type { SearchProviderName } from './search/types.js';
7
+
8
+ export interface ConnectionOption {
9
+ method: string;
10
+ name: string;
11
+ envVars: string[];
12
+ description?: string;
13
+ providerImpl?: ProviderName; // Override provider implementation (e.g., vertex-ai for Anthropic via GCP)
14
+ }
15
+
16
+ export interface ProviderDefinition {
17
+ id: ProviderName;
18
+ name: string;
19
+ popularity: number; // Lower = more popular, used for sorting
20
+ connections: ConnectionOption[];
21
+ }
22
+
23
+ export interface SearchProviderDefinition {
24
+ id: SearchProviderName;
25
+ name: string;
26
+ popularity: number;
27
+ connections: ConnectionOption[];
28
+ requiresKey: boolean;
29
+ }
30
+
31
+ /**
32
+ * All supported providers with their connection options
33
+ */
34
+ export const PROVIDERS: ProviderDefinition[] = [
35
+ {
36
+ id: 'anthropic',
37
+ name: 'Anthropic',
38
+ popularity: 1,
39
+ connections: [
40
+ {
41
+ method: 'api_key',
42
+ name: 'API Key',
43
+ envVars: ['ANTHROPIC_API_KEY'],
44
+ description: 'Direct API access',
45
+ },
46
+ {
47
+ method: 'vertex',
48
+ name: 'Google Vertex AI',
49
+ envVars: ['ANTHROPIC_VERTEX_PROJECT_ID', 'GOOGLE_CLOUD_PROJECT'],
50
+ description: 'Claude via GCP',
51
+ providerImpl: 'vertex-ai',
52
+ },
53
+ {
54
+ method: 'bedrock',
55
+ name: 'Amazon Bedrock',
56
+ envVars: ['AWS_ACCESS_KEY_ID', 'AWS_PROFILE'],
57
+ description: 'Claude via AWS (coming soon)',
58
+ },
59
+ ],
60
+ },
61
+ {
62
+ id: 'openai',
63
+ name: 'OpenAI',
64
+ popularity: 2,
65
+ connections: [
66
+ {
67
+ method: 'api_key',
68
+ name: 'API Key',
69
+ envVars: ['OPENAI_API_KEY'],
70
+ description: 'Direct API access',
71
+ },
72
+ ],
73
+ },
74
+ {
75
+ id: 'gemini',
76
+ name: 'Google Gemini',
77
+ popularity: 3,
78
+ connections: [
79
+ {
80
+ method: 'api_key',
81
+ name: 'API Key',
82
+ envVars: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'],
83
+ description: 'Direct API access',
84
+ },
85
+ ],
86
+ },
87
+ ];
88
+
89
+ /**
90
+ * All supported search providers
91
+ */
92
+ export const SEARCH_PROVIDERS: SearchProviderDefinition[] = [
93
+ {
94
+ id: 'exa',
95
+ name: 'Exa AI',
96
+ popularity: 1,
97
+ connections: [
98
+ {
99
+ method: 'public',
100
+ name: 'Public API',
101
+ envVars: [],
102
+ description: 'No API key required',
103
+ },
104
+ ],
105
+ requiresKey: false,
106
+ },
107
+ {
108
+ id: 'serper',
109
+ name: 'Serper.dev',
110
+ popularity: 2,
111
+ connections: [
112
+ {
113
+ method: 'api_key',
114
+ name: 'API Key',
115
+ envVars: ['SERPER_API_KEY'],
116
+ description: 'Google Search via Serper',
117
+ },
118
+ ],
119
+ requiresKey: true,
120
+ },
121
+ {
122
+ id: 'brave',
123
+ name: 'Brave Search',
124
+ popularity: 3,
125
+ connections: [
126
+ {
127
+ method: 'api_key',
128
+ name: 'API Key',
129
+ envVars: ['BRAVE_API_KEY'],
130
+ description: 'Privacy-focused search',
131
+ },
132
+ ],
133
+ requiresKey: true,
134
+ },
135
+ ];
136
+
137
+ /**
138
+ * Get provider definition by ID
139
+ */
140
+ export function getProvider(id: ProviderName): ProviderDefinition | undefined {
141
+ return PROVIDERS.find((p) => p.id === id);
142
+ }
143
+
144
+ /**
145
+ * Get search provider definition by ID
146
+ */
147
+ export function getSearchProvider(id: SearchProviderName): SearchProviderDefinition | undefined {
148
+ return SEARCH_PROVIDERS.find((p) => p.id === id);
149
+ }
150
+
151
+ /**
152
+ * Get all search providers sorted by popularity
153
+ */
154
+ export function getSearchProvidersSorted(): SearchProviderDefinition[] {
155
+ return [...SEARCH_PROVIDERS].sort((a, b) => a.popularity - b.popularity);
156
+ }
157
+
158
+ /**
159
+ * Get all providers sorted by popularity
160
+ */
161
+ export function getProvidersSorted(): ProviderDefinition[] {
162
+ return [...PROVIDERS].sort((a, b) => a.popularity - b.popularity);
163
+ }
164
+
165
+ // Helper: check if any env var in the list is set
166
+ const hasAnyEnvVar = (envVars: string[]) => envVars.some((v) => !!process.env[v]);
167
+
168
+ /**
169
+ * Check if any of the provider's env vars are set
170
+ */
171
+ export function hasEnvVars(provider: ProviderDefinition): boolean {
172
+ return provider.connections.some((conn) => hasAnyEnvVar(conn.envVars));
173
+ }
174
+
175
+ /**
176
+ * Get the first available connection method (where env vars are set)
177
+ */
178
+ export function getAvailableConnection(
179
+ provider: ProviderDefinition
180
+ ): ConnectionOption | undefined {
181
+ return provider.connections.find((conn) => hasAnyEnvVar(conn.envVars));
182
+ }
183
+
184
+ /**
185
+ * Check if a specific connection option has its env vars set
186
+ */
187
+ export function isConnectionReady(conn: ConnectionOption): boolean {
188
+ return hasAnyEnvVar(conn.envVars);
189
+ }
190
+
191
+ /**
192
+ * Get all available (ready) connections for a provider
193
+ */
194
+ export function getAvailableConnections(
195
+ provider: ProviderDefinition
196
+ ): ConnectionOption[] {
197
+ return provider.connections.filter((conn) => hasAnyEnvVar(conn.envVars));
198
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Brave Search Provider
3
+ *
4
+ * Uses Brave Search API (same as Claude Code).
5
+ * Requires BRAVE_API_KEY environment variable.
6
+ */
7
+
8
+ import type { SearchProvider, SearchResult, SearchOptions } from './types.js';
9
+
10
+ const API_CONFIG = {
11
+ BASE_URL: 'https://api.search.brave.com',
12
+ ENDPOINT: '/res/v1/web/search',
13
+ DEFAULT_NUM_RESULTS: 10,
14
+ DEFAULT_TIMEOUT: 10000,
15
+ } as const;
16
+
17
+ interface BraveWebResult {
18
+ title: string;
19
+ url: string;
20
+ description: string;
21
+ is_source_local?: boolean;
22
+ is_source_both?: boolean;
23
+ }
24
+
25
+ interface BraveResponse {
26
+ web?: {
27
+ results: BraveWebResult[];
28
+ };
29
+ query?: {
30
+ original: string;
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Filter results by allowed/blocked domains
36
+ */
37
+ function filterByDomain(results: SearchResult[], options?: SearchOptions): SearchResult[] {
38
+ if (!options?.allowedDomains?.length && !options?.blockedDomains?.length) {
39
+ return results;
40
+ }
41
+
42
+ return results.filter((result) => {
43
+ try {
44
+ const domain = new URL(result.url).hostname;
45
+
46
+ if (options.allowedDomains?.length) {
47
+ return options.allowedDomains.some(
48
+ (allowed) => domain === allowed || domain.endsWith('.' + allowed)
49
+ );
50
+ }
51
+
52
+ if (options.blockedDomains?.length) {
53
+ return !options.blockedDomains.some(
54
+ (blocked) => domain === blocked || domain.endsWith('.' + blocked)
55
+ );
56
+ }
57
+
58
+ return true;
59
+ } catch {
60
+ return true;
61
+ }
62
+ });
63
+ }
64
+
65
+ export class BraveProvider implements SearchProvider {
66
+ readonly name = 'brave' as const;
67
+ private apiKey: string;
68
+
69
+ constructor(apiKey?: string) {
70
+ this.apiKey = apiKey ?? process.env.BRAVE_API_KEY ?? '';
71
+ if (!this.apiKey) {
72
+ throw new Error('BRAVE_API_KEY environment variable is required for Brave provider');
73
+ }
74
+ }
75
+
76
+ async search(query: string, options?: SearchOptions): Promise<SearchResult[]> {
77
+ const params = new URLSearchParams({
78
+ q: query,
79
+ count: String(options?.numResults ?? API_CONFIG.DEFAULT_NUM_RESULTS),
80
+ });
81
+
82
+ const controller = new AbortController();
83
+ const timeoutId = setTimeout(
84
+ () => controller.abort(),
85
+ options?.timeout ?? API_CONFIG.DEFAULT_TIMEOUT
86
+ );
87
+
88
+ try {
89
+ const signals = options?.abortSignal
90
+ ? [controller.signal, options.abortSignal]
91
+ : [controller.signal];
92
+
93
+ const response = await fetch(
94
+ `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINT}?${params.toString()}`,
95
+ {
96
+ method: 'GET',
97
+ headers: {
98
+ Accept: 'application/json',
99
+ 'Accept-Encoding': 'gzip',
100
+ 'X-Subscription-Token': this.apiKey,
101
+ },
102
+ signal: AbortSignal.any(signals),
103
+ }
104
+ );
105
+
106
+ clearTimeout(timeoutId);
107
+
108
+ if (!response.ok) {
109
+ const errorText = await response.text();
110
+ throw new Error(`Brave search error (${response.status}): ${errorText}`);
111
+ }
112
+
113
+ const data = (await response.json()) as BraveResponse;
114
+
115
+ const results: SearchResult[] = (data.web?.results || []).map((item) => ({
116
+ title: item.title,
117
+ url: item.url,
118
+ snippet: item.description,
119
+ }));
120
+
121
+ return filterByDomain(results, options);
122
+ } catch (error) {
123
+ clearTimeout(timeoutId);
124
+
125
+ if (error instanceof Error && error.name === 'AbortError') {
126
+ throw new Error('Search request timed out');
127
+ }
128
+
129
+ throw error;
130
+ }
131
+ }
132
+ }