portos-ai-toolkit 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Adam Eivy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # portos-ai-toolkit
2
+
3
+ Shared AI provider, model, and prompt template patterns for PortOS-style applications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install portos-ai-toolkit
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **Provider Management**: Support for CLI-based (Claude Code, Codex, etc.) and API-based (OpenAI-compatible) AI providers
14
+ - **Prompt Templates**: Reusable prompt template system with variable interpolation
15
+ - **Run History**: Track and manage AI run history with streaming support
16
+ - **React Components**: Ready-to-use React components and hooks for AI provider management
17
+ - **Express Routes**: Pre-built Express route handlers for provider, prompt, and run management
18
+
19
+ ## Usage
20
+
21
+ ### Server-side
22
+
23
+ ```javascript
24
+ import { createAIRoutes, ProvidersService, RunnerService } from 'portos-ai-toolkit/server';
25
+ import express from 'express';
26
+
27
+ const app = express();
28
+
29
+ // Initialize services
30
+ const providersService = new ProvidersService('./data');
31
+ const runnerService = new RunnerService(providersService);
32
+
33
+ // Mount AI routes
34
+ app.use('/api', createAIRoutes({ providersService, runnerService }));
35
+ ```
36
+
37
+ ### Client-side
38
+
39
+ ```javascript
40
+ import { AIProviders, useProviders, createApiClient } from 'portos-ai-toolkit/client';
41
+
42
+ // Use the full-featured AIProviders page component
43
+ function ProvidersPage() {
44
+ return <AIProviders onError={console.error} colorPrefix="app" />;
45
+ }
46
+
47
+ // Or use hooks for custom implementations
48
+ function CustomComponent() {
49
+ const { providers, loading, refresh } = useProviders();
50
+ // ...
51
+ }
52
+ ```
53
+
54
+ ### Shared utilities
55
+
56
+ ```javascript
57
+ import { PROVIDER_TYPES, DEFAULT_TIMEOUT } from 'portos-ai-toolkit/shared';
58
+ ```
59
+
60
+ ## Provider Types
61
+
62
+ ### CLI Providers
63
+ Execute AI commands via CLI tools like Claude Code or Codex:
64
+ - Claude Code CLI (`claude`)
65
+ - Codex CLI (`codex`)
66
+ - Custom CLI tools
67
+
68
+ ### API Providers
69
+ Connect to OpenAI-compatible APIs:
70
+ - LM Studio
71
+ - Ollama
72
+ - OpenAI
73
+ - Any OpenAI-compatible endpoint
74
+
75
+ ## API Reference
76
+
77
+ ### Server Exports (`portos-ai-toolkit/server`)
78
+
79
+ - `createAIRoutes(options)` - Create Express router with all AI routes
80
+ - `ProvidersService` - Manage AI provider configurations
81
+ - `RunnerService` - Execute prompts and manage runs
82
+ - `PromptsService` - Manage prompt templates
83
+ - Route handlers: `createProvidersRoutes`, `createRunsRoutes`, `createPromptsRoutes`
84
+
85
+ ### Client Exports (`portos-ai-toolkit/client`)
86
+
87
+ - `AIProviders` - Full-featured provider management page component
88
+ - `ProviderDropdown` - Dropdown for selecting providers
89
+ - `useProviders()` - Hook for provider state management
90
+ - `useRuns()` - Hook for run history
91
+ - `createApiClient(baseUrl)` - Create API client instance
92
+
93
+ ### Shared Exports (`portos-ai-toolkit/shared`)
94
+
95
+ - `PROVIDER_TYPES` - Provider type constants
96
+ - `DEFAULT_TIMEOUT` - Default timeout value
97
+
98
+ ## License
99
+
100
+ MIT
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "portos-ai-toolkit",
3
+ "version": "0.1.0",
4
+ "description": "Shared AI provider, model, and prompt template patterns for PortOS-style applications",
5
+ "author": "Adam Eivy <adam@eivy.com> (https://atomantic.com)",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./src/index.js",
9
+ "exports": {
10
+ ".": "./src/index.js",
11
+ "./server": "./src/server/index.js",
12
+ "./client": "./src/client/index.js",
13
+ "./shared": "./src/shared/index.js"
14
+ },
15
+ "files": [
16
+ "src/**/*"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/atomantic/portos-ai-toolkit.git"
21
+ },
22
+ "homepage": "https://github.com/atomantic/portos-ai-toolkit#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/atomantic/portos-ai-toolkit/issues"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "test": "vitest run",
31
+ "test:watch": "vitest",
32
+ "test:coverage": "vitest run --coverage",
33
+ "prepublishOnly": "npm test"
34
+ },
35
+ "dependencies": {
36
+ "zod": "^3.24.1",
37
+ "uuid": "^11.0.3"
38
+ },
39
+ "peerDependencies": {
40
+ "express": "^4.21.2 || ^5.2.1",
41
+ "socket.io": "^4.8.3",
42
+ "react": "^18.3.1",
43
+ "react-dom": "^18.3.1"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "express": {
47
+ "optional": true
48
+ },
49
+ "socket.io": {
50
+ "optional": true
51
+ },
52
+ "react": {
53
+ "optional": true
54
+ },
55
+ "react-dom": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "devDependencies": {
60
+ "@vitest/coverage-v8": "^4.0.16",
61
+ "supertest": "^7.1.4",
62
+ "vitest": "^4.0.16"
63
+ },
64
+ "keywords": [
65
+ "ai",
66
+ "providers",
67
+ "llm",
68
+ "claude",
69
+ "openai",
70
+ "ollama",
71
+ "lm-studio",
72
+ "prompts",
73
+ "templates",
74
+ "portos"
75
+ ]
76
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * API Client for AI Toolkit
3
+ * Configurable API requests for providers and runs
4
+ */
5
+
6
+ /**
7
+ * Create an API client with configurable base URL and error handler
8
+ */
9
+ export function createApiClient(config = {}) {
10
+ const {
11
+ baseUrl = '/api',
12
+ onError = (error) => console.error(error)
13
+ } = config;
14
+
15
+ async function request(endpoint, options = {}) {
16
+ const url = `${baseUrl}${endpoint}`;
17
+ const requestConfig = {
18
+ headers: {
19
+ 'Content-Type': 'application/json',
20
+ ...options.headers
21
+ },
22
+ ...options
23
+ };
24
+
25
+ const response = await fetch(url, requestConfig);
26
+
27
+ if (!response.ok) {
28
+ const error = await response.json().catch(() => ({ error: 'Request failed' }));
29
+ const errorMessage = error.error || `HTTP ${response.status}`;
30
+ onError(errorMessage);
31
+ throw new Error(errorMessage);
32
+ }
33
+
34
+ // Handle 204 No Content
35
+ if (response.status === 204) {
36
+ return null;
37
+ }
38
+
39
+ // Handle text/plain responses
40
+ const contentType = response.headers.get('content-type');
41
+ if (contentType?.includes('text/plain')) {
42
+ return response.text();
43
+ }
44
+
45
+ return response.json();
46
+ }
47
+
48
+ return {
49
+ // Providers
50
+ providers: {
51
+ getAll: () => request('/providers'),
52
+ getActive: () => request('/providers/active'),
53
+ setActive: (id) => request('/providers/active', {
54
+ method: 'PUT',
55
+ body: JSON.stringify({ id })
56
+ }),
57
+ getById: (id) => request(`/providers/${id}`),
58
+ create: (data) => request('/providers', {
59
+ method: 'POST',
60
+ body: JSON.stringify(data)
61
+ }),
62
+ update: (id, data) => request(`/providers/${id}`, {
63
+ method: 'PUT',
64
+ body: JSON.stringify(data)
65
+ }),
66
+ delete: (id) => request(`/providers/${id}`, {
67
+ method: 'DELETE'
68
+ }),
69
+ test: (id) => request(`/providers/${id}/test`, {
70
+ method: 'POST'
71
+ }),
72
+ refreshModels: (id) => request(`/providers/${id}/refresh-models`, {
73
+ method: 'POST'
74
+ })
75
+ },
76
+
77
+ // Runs
78
+ runs: {
79
+ list: (limit = 50, offset = 0, source = 'all') =>
80
+ request(`/runs?limit=${limit}&offset=${offset}&source=${source}`),
81
+ create: (data) => request('/runs', {
82
+ method: 'POST',
83
+ body: JSON.stringify(data)
84
+ }),
85
+ getById: (id) => request(`/runs/${id}`),
86
+ getOutput: (id) => request(`/runs/${id}/output`),
87
+ getPrompt: (id) => request(`/runs/${id}/prompt`),
88
+ stop: (id) => request(`/runs/${id}/stop`, {
89
+ method: 'POST'
90
+ }),
91
+ delete: (id) => request(`/runs/${id}`, {
92
+ method: 'DELETE'
93
+ }),
94
+ deleteFailedRuns: () => request('/runs?filter=failed', {
95
+ method: 'DELETE'
96
+ })
97
+ }
98
+ };
99
+ }
100
+
101
+ // Default export with default config
102
+ export default createApiClient();
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Provider Dropdown Component
3
+ * Dropdown for selecting an AI provider
4
+ */
5
+
6
+ export function ProviderDropdown({
7
+ providers = [],
8
+ value,
9
+ onChange,
10
+ filter = null,
11
+ showType = false,
12
+ placeholder = 'Select provider...',
13
+ className = '',
14
+ theme = {}
15
+ }) {
16
+ const {
17
+ bg = 'bg-port-bg',
18
+ border = 'border-port-border',
19
+ text = 'text-white'
20
+ } = theme;
21
+
22
+ const filteredProviders = filter ? providers.filter(filter) : providers;
23
+
24
+ return (
25
+ <select
26
+ value={value || ''}
27
+ onChange={(e) => onChange(e.target.value)}
28
+ className={`px-3 py-2 ${bg} border ${border} rounded-lg ${text} ${className}`}
29
+ >
30
+ <option value="">{placeholder}</option>
31
+ {filteredProviders.map(provider => (
32
+ <option key={provider.id} value={provider.id}>
33
+ {provider.name}
34
+ {showType && ` (${provider.type})`}
35
+ </option>
36
+ ))}
37
+ </select>
38
+ );
39
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * AI Toolkit React Components
3
+ */
4
+
5
+ export { ProviderDropdown } from './ProviderDropdown.jsx';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * AI Toolkit React Hooks
3
+ */
4
+
5
+ export { useProviders } from './useProviders.js';
6
+ export { useRuns } from './useRuns.js';
@@ -0,0 +1,96 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+
3
+ /**
4
+ * Hook for managing AI providers
5
+ */
6
+ export function useProviders(apiClient, options = {}) {
7
+ const { autoLoad = true } = options;
8
+
9
+ const [providers, setProviders] = useState([]);
10
+ const [activeProvider, setActiveProvider] = useState(null);
11
+ const [isLoading, setIsLoading] = useState(false);
12
+ const [error, setError] = useState(null);
13
+
14
+ const loadProviders = useCallback(async () => {
15
+ if (!apiClient) {
16
+ setError('API client not configured');
17
+ return;
18
+ }
19
+
20
+ setIsLoading(true);
21
+ setError(null);
22
+
23
+ const data = await apiClient.providers.getAll().catch(err => {
24
+ setError(err.message);
25
+ return { providers: [], activeProvider: null };
26
+ });
27
+
28
+ setProviders(data.providers || []);
29
+ setActiveProvider(data.activeProvider);
30
+ setIsLoading(false);
31
+ }, [apiClient]);
32
+
33
+ const setActive = useCallback(async (id) => {
34
+ if (!apiClient) return;
35
+
36
+ await apiClient.providers.setActive(id);
37
+ setActiveProvider(id);
38
+ }, [apiClient]);
39
+
40
+ const createProvider = useCallback(async (data) => {
41
+ if (!apiClient) return null;
42
+
43
+ const provider = await apiClient.providers.create(data);
44
+ await loadProviders();
45
+ return provider;
46
+ }, [apiClient, loadProviders]);
47
+
48
+ const updateProvider = useCallback(async (id, data) => {
49
+ if (!apiClient) return null;
50
+
51
+ const provider = await apiClient.providers.update(id, data);
52
+ await loadProviders();
53
+ return provider;
54
+ }, [apiClient, loadProviders]);
55
+
56
+ const deleteProvider = useCallback(async (id) => {
57
+ if (!apiClient) return;
58
+
59
+ await apiClient.providers.delete(id);
60
+ await loadProviders();
61
+ }, [apiClient, loadProviders]);
62
+
63
+ const testProvider = useCallback(async (id) => {
64
+ if (!apiClient) return null;
65
+
66
+ return apiClient.providers.test(id);
67
+ }, [apiClient]);
68
+
69
+ const refreshModels = useCallback(async (id) => {
70
+ if (!apiClient) return null;
71
+
72
+ const provider = await apiClient.providers.refreshModels(id);
73
+ await loadProviders();
74
+ return provider;
75
+ }, [apiClient, loadProviders]);
76
+
77
+ useEffect(() => {
78
+ if (autoLoad) {
79
+ loadProviders();
80
+ }
81
+ }, [autoLoad, loadProviders]);
82
+
83
+ return {
84
+ providers,
85
+ activeProvider,
86
+ isLoading,
87
+ error,
88
+ refetch: loadProviders,
89
+ setActive,
90
+ createProvider,
91
+ updateProvider,
92
+ deleteProvider,
93
+ testProvider,
94
+ refreshModels
95
+ };
96
+ }
@@ -0,0 +1,94 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+
3
+ /**
4
+ * Hook for managing AI runs
5
+ */
6
+ export function useRuns(apiClient, options = {}) {
7
+ const { autoLoad = true, limit = 50, offset = 0, source = 'all' } = options;
8
+
9
+ const [runs, setRuns] = useState([]);
10
+ const [total, setTotal] = useState(0);
11
+ const [isLoading, setIsLoading] = useState(false);
12
+ const [error, setError] = useState(null);
13
+
14
+ const loadRuns = useCallback(async () => {
15
+ if (!apiClient) {
16
+ setError('API client not configured');
17
+ return;
18
+ }
19
+
20
+ setIsLoading(true);
21
+ setError(null);
22
+
23
+ const data = await apiClient.runs.list(limit, offset, source).catch(err => {
24
+ setError(err.message);
25
+ return { runs: [], total: 0 };
26
+ });
27
+
28
+ setRuns(data.runs || []);
29
+ setTotal(data.total || 0);
30
+ setIsLoading(false);
31
+ }, [apiClient, limit, offset, source]);
32
+
33
+ const createRun = useCallback(async (data) => {
34
+ if (!apiClient) return null;
35
+
36
+ const result = await apiClient.runs.create(data);
37
+ await loadRuns();
38
+ return result;
39
+ }, [apiClient, loadRuns]);
40
+
41
+ const stopRun = useCallback(async (id) => {
42
+ if (!apiClient) return;
43
+
44
+ await apiClient.runs.stop(id);
45
+ await loadRuns();
46
+ }, [apiClient, loadRuns]);
47
+
48
+ const deleteRun = useCallback(async (id) => {
49
+ if (!apiClient) return;
50
+
51
+ await apiClient.runs.delete(id);
52
+ await loadRuns();
53
+ }, [apiClient, loadRuns]);
54
+
55
+ const deleteFailedRuns = useCallback(async () => {
56
+ if (!apiClient) return;
57
+
58
+ const result = await apiClient.runs.deleteFailedRuns();
59
+ await loadRuns();
60
+ return result;
61
+ }, [apiClient, loadRuns]);
62
+
63
+ const getRunOutput = useCallback(async (id) => {
64
+ if (!apiClient) return null;
65
+
66
+ return apiClient.runs.getOutput(id);
67
+ }, [apiClient]);
68
+
69
+ const getRunPrompt = useCallback(async (id) => {
70
+ if (!apiClient) return null;
71
+
72
+ return apiClient.runs.getPrompt(id);
73
+ }, [apiClient]);
74
+
75
+ useEffect(() => {
76
+ if (autoLoad) {
77
+ loadRuns();
78
+ }
79
+ }, [autoLoad, loadRuns]);
80
+
81
+ return {
82
+ runs,
83
+ total,
84
+ isLoading,
85
+ error,
86
+ refetch: loadRuns,
87
+ createRun,
88
+ stopRun,
89
+ deleteRun,
90
+ deleteFailedRuns,
91
+ getRunOutput,
92
+ getRunPrompt
93
+ };
94
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * AI Toolkit Client
3
+ * React components, hooks, and API client for AI providers and runs
4
+ */
5
+
6
+ export { createApiClient } from './api.js';
7
+ export * from './hooks/index.js';
8
+ export * from './components/index.js';
9
+
10
+ // Pages
11
+ export { default as AIProviders } from './pages/AIProviders.jsx';