chutes-plugin 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/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "chutes-plugin",
3
+ "version": "0.1.0",
4
+ "description": "Chutes Models plugin for OpenCode - Access 48+ state-of-the-art AI models through the Chutes API",
5
+ "author": {
6
+ "name": "Gianmarco Martinelli",
7
+ "email": "mark182@gmail.com"
8
+ },
9
+ "type": "module",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "bin": {
17
+ "chutes-plugin": "dist/cli/index.js"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/zenobi-us/chutes-plugin"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "src/version.ts",
29
+ "src/cli"
30
+ ],
31
+ "dependencies": {
32
+ "@opencode-ai/plugin": "1.0.85",
33
+ "cac": "^6.7.14"
34
+ },
35
+ "scripts": {
36
+ "build": "bun build ./src/index.ts --outdir dist --target bun && bun build ./src/cli/index.ts --outdir dist/cli --target bun --external cac",
37
+ "build:plugin": "bun build ./src/index.ts --outdir dist --target bun",
38
+ "build:cli": "bun build ./src/cli/index.ts --outdir dist/cli --target bun --external cac"
39
+ },
40
+ "devDependencies": {
41
+ "@eslint/js": "^9.39.1",
42
+ "@types/node": "^20.11.5",
43
+ "@typescript-eslint/eslint-plugin": "8.47.0",
44
+ "@typescript-eslint/parser": "8.47.0",
45
+ "bun-types": "latest",
46
+ "eslint": "^9.39.1",
47
+ "eslint-config-prettier": "10.1.8",
48
+ "eslint-plugin-prettier": "^5.1.3",
49
+ "prettier": "^3.2.4",
50
+ "typescript-eslint": "^8.47.0",
51
+ "vitest": "^3.2.4"
52
+ }
53
+ }
@@ -0,0 +1,119 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ export async function doctor(): Promise<void> {
5
+ console.log('🐍 chutes-plugin doctor\n');
6
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
7
+
8
+ let checks = 0;
9
+ let passed = 0;
10
+
11
+ function check(name: string, result: boolean, message: string): void {
12
+ checks++;
13
+ if (result) {
14
+ passed++;
15
+ console.log(`✅ ${name}`);
16
+ } else {
17
+ console.log(`❌ ${name}`);
18
+ console.log(` ${message}`);
19
+ }
20
+ }
21
+
22
+ const projectDir = process.cwd();
23
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
24
+
25
+ console.log('Installation Checks:\n');
26
+
27
+ let hasProjectConfig = false;
28
+ let hasGlobalConfig = false;
29
+ let installType = 'none';
30
+
31
+ const projectConfigPath = path.join(projectDir, 'opencode.json');
32
+ if (fs.existsSync(projectConfigPath)) {
33
+ const content = fs.readFileSync(projectConfigPath, 'utf-8');
34
+ const config = JSON.parse(content);
35
+ const plugins = (config.plugins as string[]) || [];
36
+
37
+ for (const p of plugins) {
38
+ if (p.includes('chutes-plugin')) {
39
+ hasProjectConfig = true;
40
+ installType = 'npm';
41
+ break;
42
+ }
43
+ }
44
+
45
+ const opencodeDir = path.join(projectDir, '.opencode', 'plugin', 'chutes-plugin');
46
+ if (fs.existsSync(opencodeDir)) {
47
+ hasProjectConfig = true;
48
+ installType = 'project';
49
+ }
50
+ }
51
+
52
+ if (!hasProjectConfig && homeDir) {
53
+ const globalConfigPath = path.join(homeDir, '.config', 'opencode', 'opencode.json');
54
+ if (fs.existsSync(globalConfigPath)) {
55
+ const content = fs.readFileSync(globalConfigPath, 'utf-8');
56
+ const config = JSON.parse(content);
57
+ const plugins = (config.plugins as string[]) || [];
58
+
59
+ for (const p of plugins) {
60
+ if (p.includes('chutes-plugin')) {
61
+ hasGlobalConfig = true;
62
+ installType = 'global';
63
+ break;
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ check(
70
+ 'Plugin installed',
71
+ hasProjectConfig || hasGlobalConfig,
72
+ installType === 'none' ? 'Run: bunx chutes-plugin install' : `Installed as ${installType}`
73
+ );
74
+
75
+ console.log('\nAPI Checks:\n');
76
+
77
+ let hasAuth = false;
78
+ if (homeDir) {
79
+ const authPath = path.join(homeDir, '.local', 'share', 'opencode', 'auth.json');
80
+ if (fs.existsSync(authPath)) {
81
+ const content = fs.readFileSync(authPath, 'utf-8');
82
+ const auth = JSON.parse(content);
83
+ hasAuth = !!(auth.chutes || auth.CHUTES_API_TOKEN || auth.chutes_api_token);
84
+ }
85
+ }
86
+
87
+ check('API token connected', hasAuth, 'Run: opencode, then type: /connect chutes');
88
+
89
+ console.log('\nNetwork Checks:\n');
90
+
91
+ try {
92
+ const response = await fetch('https://llm.chutes.ai/v1/models', {
93
+ method: 'GET',
94
+ headers: { 'Content-Type': 'application/json' },
95
+ });
96
+
97
+ check('Can reach Chutes API', response.ok, `HTTP ${response.status}: ${response.statusText}`);
98
+
99
+ if (response.ok) {
100
+ const data = (await response.json()) as { data: Array<{ id: string }> };
101
+ console.log(` Found ${data.data?.length || 0} models available\n`);
102
+ }
103
+ } catch (error) {
104
+ check(
105
+ 'Can reach Chutes API',
106
+ false,
107
+ `Connection failed: ${error instanceof Error ? error.message : String(error)}`
108
+ );
109
+ }
110
+
111
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
112
+ console.log(`Results: ${passed}/${checks} checks passed\n`);
113
+
114
+ if (passed === checks) {
115
+ console.log("🎉 Everything looks good! You're ready to use chutes-plugin.\n");
116
+ } else {
117
+ console.log('⚠️ Some checks failed. Please address the issues above.\n');
118
+ }
119
+ }
@@ -0,0 +1,61 @@
1
+ import cac from 'cac';
2
+
3
+ const cli = cac('chutes-plugin');
4
+
5
+ cli.version('0.1.0');
6
+ cli.usage(`
7
+ 🐍 chutes-plugin
8
+
9
+ A plugin for OpenCode that provides access to 48+ state-of-the-art AI models through the Chutes API.
10
+
11
+ Usage:
12
+ $ chutes-plugin <command>
13
+
14
+ Commands:
15
+ install Install the plugin (project, global, or npm)
16
+ status Check plugin status and configuration
17
+ list List available models from the Chutes API
18
+ refresh Force refresh the model cache
19
+ doctor Verify that everything is configured correctly
20
+
21
+ Examples:
22
+ $ chutes-plugin install
23
+ $ chutes-plugin status
24
+ $ chutes-plugin list
25
+ $ chutes-plugin refresh
26
+ $ chutes-plugin doctor
27
+ `);
28
+
29
+ cli
30
+ .command('install', 'Install the plugin to OpenCode')
31
+ .option('--type <type>', 'Installation type: project, global, or npm')
32
+ .action(async (options) => {
33
+ const { install } = await import('./install.js');
34
+ await install(options);
35
+ });
36
+
37
+ cli.command('status', 'Check plugin status and configuration').action(async () => {
38
+ const { status } = await import('./status.js');
39
+ await status();
40
+ });
41
+
42
+ cli.command('list', 'List available models from the Chutes API').action(async () => {
43
+ const { list } = await import('./list.js');
44
+ await list();
45
+ });
46
+
47
+ cli.command('refresh', 'Force refresh the model cache').action(async () => {
48
+ const { refresh } = await import('./refresh.js');
49
+ await refresh();
50
+ });
51
+
52
+ cli.command('doctor', 'Verify that everything is configured correctly').action(async () => {
53
+ const { doctor } = await import('./doctor.js');
54
+ await doctor();
55
+ });
56
+
57
+ cli.parse();
58
+
59
+ if (process.argv.length === 2) {
60
+ cli.outputHelp();
61
+ }
@@ -0,0 +1,186 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ interface InstallOptions {
6
+ type?: string;
7
+ }
8
+
9
+ export async function install(options: InstallOptions): Promise<void> {
10
+ console.log('🐍 chutes-plugin installer\n');
11
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
12
+
13
+ let installType = options.type;
14
+
15
+ if (!installType) {
16
+ console.log('Choose installation type:');
17
+ console.log('1. Project (install to .opencode/plugin/)');
18
+ console.log('2. Global (install to ~/.config/opencode/plugin/)');
19
+ console.log('3. NPM (add to opencode.json for auto-install)\n');
20
+
21
+ const readline = await import('node:readline');
22
+ const rl = readline.createInterface({
23
+ input: process.stdin,
24
+ output: process.stdout,
25
+ });
26
+
27
+ installType = await new Promise<string>((resolve) => {
28
+ rl.question('> ', (answer) => {
29
+ rl.close();
30
+ resolve(answer.trim());
31
+ });
32
+ });
33
+ }
34
+
35
+ switch (installType) {
36
+ case '1':
37
+ case 'project':
38
+ await installProject();
39
+ break;
40
+ case '2':
41
+ case 'global':
42
+ await installGlobal();
43
+ break;
44
+ case '3':
45
+ case 'npm':
46
+ await installNpm();
47
+ break;
48
+ default:
49
+ console.log('Invalid installation type. Please choose 1, 2, or 3.');
50
+ process.exit(1);
51
+ }
52
+ }
53
+
54
+ async function installProject(): Promise<void> {
55
+ const projectDir = process.cwd();
56
+ const opencodeDir = path.join(projectDir, '.opencode');
57
+ const pluginDir = path.join(opencodeDir, 'plugin', 'chutes-plugin');
58
+
59
+ console.log('Installing to project...\n');
60
+
61
+ if (!fs.existsSync(opencodeDir)) {
62
+ fs.mkdirSync(opencodeDir, { recursive: true });
63
+ }
64
+
65
+ if (!fs.existsSync(path.join(opencodeDir, 'plugin'))) {
66
+ fs.mkdirSync(path.join(opencodeDir, 'plugin'), { recursive: true });
67
+ }
68
+
69
+ fs.mkdirSync(pluginDir, { recursive: true });
70
+
71
+ const distDir = path.join(__dirname, '..', '..', 'dist');
72
+ if (fs.existsSync(distDir)) {
73
+ fs.cpSync(distDir, path.join(pluginDir, 'dist'), { recursive: true });
74
+ }
75
+
76
+ const srcCliDir = path.join(__dirname, '..');
77
+ if (fs.existsSync(srcCliDir)) {
78
+ fs.cpSync(srcCliDir, path.join(pluginDir, 'src'), { recursive: true });
79
+ }
80
+
81
+ const packageJsonPath = path.join(opencodeDir, 'opencode.json');
82
+ let config: Record<string, unknown> = {};
83
+ if (fs.existsSync(packageJsonPath)) {
84
+ const content = fs.readFileSync(packageJsonPath, 'utf-8');
85
+ config = JSON.parse(content);
86
+ }
87
+
88
+ config.plugins = config.plugins || [];
89
+ if (!Array.isArray(config.plugins)) {
90
+ config.plugins = [];
91
+ }
92
+
93
+ const pluginPath = './.opencode/plugin/chutes-plugin/dist/index.js';
94
+ if (!config.plugins.includes(pluginPath)) {
95
+ config.plugins.push(pluginPath);
96
+ }
97
+
98
+ fs.writeFileSync(packageJsonPath, JSON.stringify(config, null, 2));
99
+
100
+ console.log('✅ Successfully installed to project!');
101
+ console.log('\nNext steps:');
102
+ console.log('1. Start OpenCode: opencode');
103
+ console.log('2. Connect your Chutes token: /connect chutes');
104
+ console.log('3. Select a Chutes model from the dropdown\n');
105
+ }
106
+
107
+ async function installGlobal(): Promise<void> {
108
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
109
+ if (!homeDir) {
110
+ console.log('❌ Error: Could not determine home directory');
111
+ process.exit(1);
112
+ }
113
+
114
+ const configDir = path.join(homeDir, '.config', 'opencode');
115
+ const pluginDir = path.join(configDir, 'plugin', 'chutes-plugin');
116
+
117
+ console.log('Installing globally...\n');
118
+
119
+ fs.mkdirSync(pluginDir, { recursive: true });
120
+
121
+ const distDir = path.join(__dirname, '..', '..', 'dist');
122
+ if (fs.existsSync(distDir)) {
123
+ fs.cpSync(distDir, path.join(pluginDir, 'dist'), { recursive: true });
124
+ }
125
+
126
+ const srcCliDir = path.join(__dirname, '..');
127
+ if (fs.existsSync(srcCliDir)) {
128
+ fs.cpSync(srcCliDir, path.join(pluginDir, 'src'), { recursive: true });
129
+ }
130
+
131
+ const configPath = path.join(configDir, 'opencode.json');
132
+ let config: Record<string, unknown> = {};
133
+ if (fs.existsSync(configPath)) {
134
+ const content = fs.readFileSync(configPath, 'utf-8');
135
+ config = JSON.parse(content);
136
+ }
137
+
138
+ config.plugins = config.plugins || [];
139
+ if (!Array.isArray(config.plugins)) {
140
+ config.plugins = [];
141
+ }
142
+
143
+ const pluginPath = `${configDir}/plugin/chutes-plugin/dist/index.js`;
144
+ if (!config.plugins.includes(pluginPath)) {
145
+ config.plugins.push(pluginPath);
146
+ }
147
+
148
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
149
+
150
+ console.log('✅ Successfully installed globally!');
151
+ console.log('\nNext steps:');
152
+ console.log('1. Start OpenCode: opencode');
153
+ console.log('2. Connect your Chutes token: /connect chutes');
154
+ console.log('3. Select a Chutes model from the dropdown\n');
155
+ }
156
+
157
+ async function installNpm(): Promise<void> {
158
+ const projectDir = process.cwd();
159
+ const configPath = path.join(projectDir, 'opencode.json');
160
+
161
+ console.log('Installing via npm...\n');
162
+
163
+ let config: Record<string, unknown> = {};
164
+ if (fs.existsSync(configPath)) {
165
+ const content = fs.readFileSync(configPath, 'utf-8');
166
+ config = JSON.parse(content);
167
+ }
168
+
169
+ config.plugins = config.plugins || [];
170
+ if (!Array.isArray(config.plugins)) {
171
+ config.plugins = [];
172
+ }
173
+
174
+ if (!config.plugins.includes('chutes-plugin')) {
175
+ config.plugins.push('chutes-plugin');
176
+ }
177
+
178
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
179
+
180
+ console.log('✅ Added "chutes-plugin" to opencode.json!');
181
+ console.log('\nNext steps:');
182
+ console.log('1. Start OpenCode: opencode');
183
+ console.log('2. The plugin will be auto-installed on first run');
184
+ console.log('3. Connect your Chutes token: /connect chutes');
185
+ console.log('4. Select a Chutes model from the dropdown\n');
186
+ }
@@ -0,0 +1,50 @@
1
+ export async function list(): Promise<void> {
2
+ console.log('🐍 chutes-plugin models\n');
3
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
4
+ console.log('Fetching models from Chutes API...\n');
5
+
6
+ try {
7
+ const response = await fetch('https://llm.chutes.ai/v1/models', {
8
+ method: 'GET',
9
+ headers: {
10
+ 'Content-Type': 'application/json',
11
+ },
12
+ });
13
+
14
+ if (!response.ok) {
15
+ console.log(`❌ Error fetching models: ${response.status} ${response.statusText}`);
16
+ process.exit(1);
17
+ }
18
+
19
+ const data = (await response.json()) as {
20
+ data: Array<{
21
+ id: string;
22
+ owned_by: string;
23
+ pricing: { prompt: number; completion: number };
24
+ }>;
25
+ };
26
+ const models = data.data || [];
27
+
28
+ console.log(`Found ${models.length} models:\n`);
29
+
30
+ for (const model of models) {
31
+ console.log(`• ${model.id}`);
32
+ console.log(` Owner: ${model.owned_by}`);
33
+ console.log(
34
+ ` Pricing: $${model.pricing.prompt.toFixed(2)}/M input, $${model.pricing.completion.toFixed(2)}/M output\n`
35
+ );
36
+ }
37
+
38
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
39
+ console.log('Tip: To use these models:');
40
+ console.log('1. Install the plugin: bunx chutes-plugin install');
41
+ console.log('2. Start OpenCode: opencode');
42
+ console.log('3. Connect token: /connect chutes');
43
+ console.log('4. Select a Chutes model from the dropdown\n');
44
+ } catch (error) {
45
+ console.log(
46
+ `❌ Error fetching models: ${error instanceof Error ? error.message : String(error)}`
47
+ );
48
+ process.exit(1);
49
+ }
50
+ }
@@ -0,0 +1,31 @@
1
+ export async function refresh(): Promise<void> {
2
+ console.log('🐍 chutes-plugin refresh\n');
3
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
4
+ console.log('Refreshing model cache...\n');
5
+
6
+ try {
7
+ const response = await fetch('https://llm.chutes.ai/v1/models', {
8
+ method: 'GET',
9
+ headers: {
10
+ 'Content-Type': 'application/json',
11
+ },
12
+ });
13
+
14
+ if (!response.ok) {
15
+ console.log(`❌ Error refreshing models: ${response.status} ${response.statusText}`);
16
+ process.exit(1);
17
+ }
18
+
19
+ const data = (await response.json()) as { data: Array<{ id: string }> };
20
+ const models = data.data || [];
21
+
22
+ console.log(`✅ Successfully refreshed ${models.length} models\n`);
23
+ console.log('Note: The cache will be automatically refreshed when OpenCode restarts.');
24
+ console.log('Models are cached in-memory for 1 hour during each OpenCode session.\n');
25
+ } catch (error) {
26
+ console.log(
27
+ `❌ Error refreshing models: ${error instanceof Error ? error.message : String(error)}`
28
+ );
29
+ process.exit(1);
30
+ }
31
+ }
@@ -0,0 +1,80 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ export async function status(): Promise<void> {
5
+ console.log('🐍 chutes-plugin status\n');
6
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
7
+
8
+ let hasConfig = false;
9
+ let pluginPath = '';
10
+ let installType = '';
11
+
12
+ const projectDir = process.cwd();
13
+ const projectConfigPath = path.join(projectDir, 'opencode.json');
14
+
15
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
16
+ const globalConfigPath = homeDir
17
+ ? path.join(homeDir, '.config', 'opencode', 'opencode.json')
18
+ : null;
19
+
20
+ if (fs.existsSync(projectConfigPath)) {
21
+ const content = fs.readFileSync(projectConfigPath, 'utf-8');
22
+ const config = JSON.parse(content);
23
+ const plugins = (config.plugins as string[]) || [];
24
+
25
+ for (const p of plugins) {
26
+ if (p.includes('chutes-plugin')) {
27
+ hasConfig = true;
28
+ installType = 'npm';
29
+ pluginPath = p;
30
+ break;
31
+ }
32
+ }
33
+
34
+ const opencodeDir = path.join(projectDir, '.opencode', 'plugin', 'chutes-plugin');
35
+ if (fs.existsSync(opencodeDir)) {
36
+ hasConfig = true;
37
+ installType = 'project';
38
+ pluginPath = opencodeDir;
39
+ }
40
+ }
41
+
42
+ if (!hasConfig && globalConfigPath && fs.existsSync(globalConfigPath)) {
43
+ const content = fs.readFileSync(globalConfigPath, 'utf-8');
44
+ const config = JSON.parse(content);
45
+ const plugins = (config.plugins as string[]) || [];
46
+
47
+ for (const p of plugins) {
48
+ if (p.includes('chutes-plugin')) {
49
+ hasConfig = true;
50
+ installType = 'global';
51
+ pluginPath = p;
52
+ break;
53
+ }
54
+ }
55
+ }
56
+
57
+ console.log(`Installation: ${hasConfig ? '✅ Configured' : '❌ Not configured'}`);
58
+ if (hasConfig) {
59
+ console.log(`Type: ${installType}`);
60
+ console.log(`Path: ${pluginPath}\n`);
61
+ }
62
+
63
+ const authPath = homeDir ? path.join(homeDir, '.local', 'share', 'opencode', 'auth.json') : null;
64
+ let hasAuth = false;
65
+ if (authPath && fs.existsSync(authPath)) {
66
+ const content = fs.readFileSync(authPath, 'utf-8');
67
+ const auth = JSON.parse(content);
68
+ hasAuth = !!(auth.chutes || auth.CHUTES_API_TOKEN || auth.chutes_api_token);
69
+ }
70
+
71
+ console.log(`API Token: ${hasAuth ? '✅ Connected' : '⚠️ Not connected'}`);
72
+ if (!hasAuth) {
73
+ console.log('\nTo connect your token:');
74
+ console.log('1. Run: opencode');
75
+ console.log('2. Type: /connect chutes');
76
+ console.log('3. Follow the prompts\n');
77
+ }
78
+
79
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
80
+ }