portos-ai-toolkit 0.4.2 → 0.5.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.5.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",
@@ -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
  /**
@@ -384,6 +388,33 @@ export function createProviderService(config = {}) {
384
388
  return (data.models || [])
385
389
  .filter(m => m.supportedGenerationMethods?.includes('generateContent'))
386
390
  .map(m => m.name.replace('models/', ''));
391
+ },
392
+
393
+ /**
394
+ * Get sample providers not yet in user's configuration.
395
+ * Reads toolkit's built-in defaults and overlays with host app's sample file.
396
+ */
397
+ async getSampleProviders() {
398
+ const data = await loadProviders();
399
+ const existingIds = new Set(Object.keys(data.providers));
400
+
401
+ // Read toolkit's built-in sample
402
+ let sampleProviders = {};
403
+ if (existsSync(DEFAULT_SAMPLE_PATH)) {
404
+ const content = await readFile(DEFAULT_SAMPLE_PATH, 'utf-8');
405
+ const parsed = JSON.parse(content);
406
+ sampleProviders = { ...parsed.providers };
407
+ }
408
+
409
+ // Overlay with host app's sample (takes precedence)
410
+ if (sampleFile && existsSync(sampleFile)) {
411
+ const content = await readFile(sampleFile, 'utf-8');
412
+ const parsed = JSON.parse(content);
413
+ sampleProviders = { ...sampleProviders, ...parsed.providers };
414
+ }
415
+
416
+ // Filter out providers already in user's config
417
+ return Object.values(sampleProviders).filter(p => !existingIds.has(p.id));
387
418
  }
388
419
  };
389
420
  }
@@ -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);