portos-ai-toolkit 0.4.2 → 0.6.0

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": "portos-ai-toolkit",
3
- "version": "0.4.2",
3
+ "version": "0.6.0",
4
4
  "description": "Shared AI provider, model, and prompt template patterns for PortOS-style applications",
5
5
  "author": "Adam Eivy <adam@eivy.com> (https://atomantic.com)",
6
6
  "license": "MIT",
@@ -541,7 +541,7 @@ function ProviderForm({ provider, onClose, onSave, api, colorPrefix = 'app' }) {
541
541
  type="text"
542
542
  value={formData.defaultModel}
543
543
  onChange={(e) => setFormData(prev => ({ ...prev, defaultModel: e.target.value }))}
544
- placeholder="claude-sonnet-4-20250514"
544
+ placeholder="claude-sonnet-4-6"
545
545
  className={`w-full px-3 py-2 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white focus:border-${colorPrefix}-accent focus:outline-none`}
546
546
  />
547
547
  )}
@@ -7,14 +7,15 @@
7
7
  "type": "cli",
8
8
  "command": "claude",
9
9
  "args": ["--print"],
10
- "models": ["claude-haiku-4-5-20251001", "claude-sonnet-4-20250514", "claude-opus-4-20250514"],
11
- "defaultModel": "claude-sonnet-4-20250514",
12
- "lightModel": "claude-haiku-4-5-20251001",
13
- "mediumModel": "claude-sonnet-4-20250514",
14
- "heavyModel": "claude-opus-4-20250514",
10
+ "models": ["claude-haiku-4-5", "claude-sonnet-4-6", "claude-opus-4-6"],
11
+ "defaultModel": "claude-sonnet-4-6",
12
+ "lightModel": "claude-haiku-4-5",
13
+ "mediumModel": "claude-sonnet-4-6",
14
+ "heavyModel": "claude-opus-4-6",
15
15
  "timeout": 300000,
16
16
  "enabled": true,
17
- "envVars": {}
17
+ "envVars": {},
18
+ "secretEnvVars": []
18
19
  },
19
20
  "claude-code-bedrock": {
20
21
  "id": "claude-code-bedrock",
@@ -22,16 +23,18 @@
22
23
  "type": "cli",
23
24
  "command": "claude",
24
25
  "args": ["--print"],
25
- "models": ["us.anthropic.claude-sonnet-4-5-20250929-v1:0", "global.anthropic.claude-opus-4-5-20251101-v1:0"],
26
- "defaultModel": "global.anthropic.claude-opus-4-5-20251101-v1:0",
26
+ "models": ["us.anthropic.claude-sonnet-4-5-20250929-v1:0", "global.anthropic.claude-opus-4-5-20251101-v1:0", "us.anthropic.claude-opus-4-6-v1:0"],
27
+ "defaultModel": "us.anthropic.claude-opus-4-6-v1:0",
27
28
  "lightModel": "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
28
29
  "mediumModel": "global.anthropic.claude-opus-4-5-20251101-v1:0",
29
- "heavyModel": "global.anthropic.claude-opus-4-5-20251101-v1:0",
30
+ "heavyModel": "us.anthropic.claude-opus-4-6-v1:0",
30
31
  "timeout": 300000,
31
32
  "enabled": false,
32
33
  "envVars": {
33
- "CLAUDE_CODE_USE_BEDROCK": "1"
34
- }
34
+ "CLAUDE_CODE_USE_BEDROCK": "1",
35
+ "AWS_BEARER_TOKEN_BEDROCK": ""
36
+ },
37
+ "secretEnvVars": ["AWS_BEARER_TOKEN_BEDROCK"]
35
38
  },
36
39
  "codex": {
37
40
  "id": "codex",
@@ -46,7 +49,8 @@
46
49
  "heavyModel": "gpt-5.1-codex-max",
47
50
  "timeout": 300000,
48
51
  "enabled": true,
49
- "envVars": {}
52
+ "envVars": {},
53
+ "secretEnvVars": []
50
54
  },
51
55
  "gemini-cli": {
52
56
  "id": "gemini-cli",
@@ -58,7 +62,8 @@
58
62
  "defaultModel": null,
59
63
  "timeout": 300000,
60
64
  "enabled": true,
61
- "envVars": {}
65
+ "envVars": {},
66
+ "secretEnvVars": []
62
67
  },
63
68
  "nvidia-kimi": {
64
69
  "id": "nvidia-kimi",
@@ -74,7 +79,8 @@
74
79
  "fallbackProvider": null,
75
80
  "timeout": 300000,
76
81
  "enabled": false,
77
- "envVars": {}
82
+ "envVars": {},
83
+ "secretEnvVars": []
78
84
  },
79
85
  "lmstudio": {
80
86
  "id": "lmstudio",
@@ -86,7 +92,8 @@
86
92
  "defaultModel": null,
87
93
  "timeout": 300000,
88
94
  "enabled": false,
89
- "envVars": {}
95
+ "envVars": {},
96
+ "secretEnvVars": []
90
97
  },
91
98
  "ollama": {
92
99
  "id": "ollama",
@@ -98,7 +105,8 @@
98
105
  "defaultModel": null,
99
106
  "timeout": 300000,
100
107
  "enabled": false,
101
- "envVars": {}
108
+ "envVars": {},
109
+ "secretEnvVars": []
102
110
  }
103
111
  }
104
112
  }
@@ -12,6 +12,7 @@ export interface ProviderService {
12
12
  deleteProvider(id: string): Promise<boolean>;
13
13
  testProvider(id: string): Promise<{ success: boolean; [key: string]: any }>;
14
14
  refreshProviderModels(id: string): Promise<any | null>;
15
+ getSampleProviders(): Promise<any[]>;
15
16
  }
16
17
 
17
18
  export interface RunnerService {
@@ -1,9 +1,13 @@
1
1
  import { readFile, writeFile, mkdir } from 'fs/promises';
2
2
  import { existsSync } from 'fs';
3
- import { join } from 'path';
3
+ import { join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
4
5
  import { exec } from 'child_process';
5
6
  import { promisify } from 'util';
6
7
 
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const DEFAULT_SAMPLE_PATH = join(__dirname, '../defaults/providers.sample.json');
10
+
7
11
  const execAsync = promisify(exec);
8
12
 
9
13
  /**
@@ -117,7 +121,8 @@ export function createProviderService(config = {}) {
117
121
  fallbackProvider: providerData.fallbackProvider || null,
118
122
  timeout: providerData.timeout || 300000,
119
123
  enabled: providerData.enabled !== false,
120
- envVars: providerData.envVars || {}
124
+ envVars: providerData.envVars || {},
125
+ secretEnvVars: providerData.secretEnvVars || []
121
126
  };
122
127
 
123
128
  data.providers[id] = provider;
@@ -384,6 +389,33 @@ export function createProviderService(config = {}) {
384
389
  return (data.models || [])
385
390
  .filter(m => m.supportedGenerationMethods?.includes('generateContent'))
386
391
  .map(m => m.name.replace('models/', ''));
392
+ },
393
+
394
+ /**
395
+ * Get sample providers not yet in user's configuration.
396
+ * Reads toolkit's built-in defaults and overlays with host app's sample file.
397
+ */
398
+ async getSampleProviders() {
399
+ const data = await loadProviders();
400
+ const existingIds = new Set(Object.keys(data.providers));
401
+
402
+ // Read toolkit's built-in sample
403
+ let sampleProviders = {};
404
+ if (existsSync(DEFAULT_SAMPLE_PATH)) {
405
+ const content = await readFile(DEFAULT_SAMPLE_PATH, 'utf-8');
406
+ const parsed = JSON.parse(content);
407
+ sampleProviders = { ...parsed.providers };
408
+ }
409
+
410
+ // Overlay with host app's sample (takes precedence)
411
+ if (sampleFile && existsSync(sampleFile)) {
412
+ const content = await readFile(sampleFile, 'utf-8');
413
+ const parsed = JSON.parse(content);
414
+ sampleProviders = { ...sampleProviders, ...parsed.providers };
415
+ }
416
+
417
+ // Filter out providers already in user's config
418
+ return Object.values(sampleProviders).filter(p => !existingIds.has(p.id));
387
419
  }
388
420
  };
389
421
  }
@@ -117,4 +117,75 @@ describe('Provider Service', () => {
117
117
  })
118
118
  ).rejects.toThrow('Provider with this ID already exists');
119
119
  });
120
+
121
+ describe('getSampleProviders', () => {
122
+ it('should return sample providers from default sample file', async () => {
123
+ // No providers created yet — all samples should be returned
124
+ const samples = await providerService.getSampleProviders();
125
+ expect(Array.isArray(samples)).toBe(true);
126
+ expect(samples.length).toBeGreaterThan(0);
127
+ // Should include claude-code-bedrock from the default sample
128
+ const bedrock = samples.find(p => p.id === 'claude-code-bedrock');
129
+ expect(bedrock).toBeDefined();
130
+ expect(bedrock.name).toBe('Claude Code CLI: Bedrock');
131
+ });
132
+
133
+ it('should exclude providers already in user config', async () => {
134
+ // Create a provider with an ID that matches a sample
135
+ await providerService.createProvider({
136
+ id: 'claude-code',
137
+ name: 'Claude Code CLI',
138
+ type: 'cli',
139
+ command: 'claude'
140
+ });
141
+
142
+ const samples = await providerService.getSampleProviders();
143
+ const claudeCode = samples.find(p => p.id === 'claude-code');
144
+ expect(claudeCode).toBeUndefined();
145
+ });
146
+
147
+ it('should overlay host app sample over toolkit defaults', async () => {
148
+ // Pre-create providers.json with one existing provider so loadProviders
149
+ // doesn't bootstrap from sampleFile
150
+ const providersPath = join(TEST_DATA_DIR, 'providers-overlay.json');
151
+ await writeFile(providersPath, JSON.stringify({
152
+ activeProvider: 'existing',
153
+ providers: {
154
+ existing: { id: 'existing', name: 'Existing', type: 'cli', command: 'test' }
155
+ }
156
+ }));
157
+
158
+ // Create a host app sample with a unique provider
159
+ const samplePath = join(TEST_DATA_DIR, 'custom-sample.json');
160
+ await writeFile(samplePath, JSON.stringify({
161
+ activeProvider: 'custom-cli',
162
+ providers: {
163
+ 'custom-cli': {
164
+ id: 'custom-cli',
165
+ name: 'Custom CLI',
166
+ type: 'cli',
167
+ command: 'custom',
168
+ args: [],
169
+ models: [],
170
+ timeout: 300000,
171
+ enabled: true
172
+ }
173
+ }
174
+ }));
175
+
176
+ const serviceWithSample = createProviderService({
177
+ dataDir: TEST_DATA_DIR,
178
+ providersFile: 'providers-overlay.json',
179
+ sampleFile: samplePath
180
+ });
181
+
182
+ const samples = await serviceWithSample.getSampleProviders();
183
+ const custom = samples.find(p => p.id === 'custom-cli');
184
+ expect(custom).toBeDefined();
185
+ expect(custom.name).toBe('Custom CLI');
186
+ // 'existing' should NOT appear (already in user's config)
187
+ const existing = samples.find(p => p.id === 'existing');
188
+ expect(existing).toBeUndefined();
189
+ });
190
+ });
120
191
  });
@@ -35,6 +35,12 @@ export function createProvidersRoutes(providerService, options = {}) {
35
35
  res.json(provider);
36
36
  }));
37
37
 
38
+ // GET /providers/samples - Get sample providers not yet in user's config
39
+ router.get('/samples', asyncHandler(async (req, res) => {
40
+ const providers = await providerService.getSampleProviders();
41
+ res.json({ providers });
42
+ }));
43
+
38
44
  // GET /providers/:id - Get provider by ID
39
45
  router.get('/:id', asyncHandler(async (req, res) => {
40
46
  const provider = await providerService.getProviderById(req.params.id);
@@ -14,7 +14,8 @@ export const providerSchema = z.object({
14
14
  defaultModel: z.string().nullable().optional(),
15
15
  timeout: z.number().int().min(1000).max(600000).optional(),
16
16
  enabled: z.boolean().optional(),
17
- envVars: z.record(z.string()).optional()
17
+ envVars: z.record(z.string()).optional(),
18
+ secretEnvVars: z.array(z.string()).optional()
18
19
  });
19
20
 
20
21
  /**