cc-x10ded 3.0.16 → 3.0.18

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/PLUGINS.md ADDED
@@ -0,0 +1,173 @@
1
+ # ccx Plugin System
2
+
3
+ Extend ccx with custom model providers.
4
+
5
+ ## Overview
6
+
7
+ The plugin system allows you to add any OpenAI-compatible API as a provider in ccx. Plugins are discovered from `~/.config/claude-glm/plugins/` and automatically loaded at runtime.
8
+
9
+ ## Quick Start
10
+
11
+ ### 1. Create Plugin Directory
12
+
13
+ ```bash
14
+ mkdir -p ~/.config/claude-glm/plugins/my-provider
15
+ ```
16
+
17
+ ### 2. Create Plugin Manifest
18
+
19
+ Create `plugin.json` in your plugin directory:
20
+
21
+ ```json
22
+ {
23
+ "id": "my-provider",
24
+ "name": "My Custom Provider",
25
+ "version": "1.0.0",
26
+ "description": "A custom model provider",
27
+ "entry": "./dist/index.js"
28
+ }
29
+ ```
30
+
31
+ ### 3. Write Plugin Implementation
32
+
33
+ Create `index.ts` with your plugin code (see Example below).
34
+
35
+ ### 4. Build Plugin
36
+
37
+ ```bash
38
+ bun build ./index.ts --outdir ./dist --target bun
39
+ ```
40
+
41
+ ### 5. Verify Installation
42
+
43
+ ```bash
44
+ ccx models
45
+ ```
46
+
47
+ Your provider should now appear in the list.
48
+
49
+ ## Plugin API
50
+
51
+ ### ProviderPlugin Interface
52
+
53
+ ```typescript
54
+ import type { ProviderPlugin } from "@ccx/plugin-types";
55
+
56
+ export default {
57
+ id: "my-provider",
58
+ name: "My Provider",
59
+ version: "1.0.0",
60
+ description: "Description of your provider",
61
+ models: [
62
+ { id: "model-1", name: "Model 1", contextWindow: 128000 },
63
+ { id: "model-2", name: "Model 2", contextWindow: 64000, default: true }
64
+ ],
65
+ createClient(config) {
66
+ return new MyProviderClient(config);
67
+ }
68
+ } satisfies ProviderPlugin;
69
+ ```
70
+
71
+ ### ProviderClient Interface
72
+
73
+ Your client must implement:
74
+
75
+ ```typescript
76
+ interface ProviderClient {
77
+ readonly provider: string;
78
+
79
+ // Stream completions (required)
80
+ streamComplete(request: AnthropicRequest): AsyncGenerator<SSEMessage>;
81
+
82
+ // Get model info (optional)
83
+ getModelInfo(): ModelInfo | undefined;
84
+
85
+ // Health check (optional)
86
+ healthCheck(): Promise<HealthStatus>;
87
+ }
88
+ ```
89
+
90
+ ### SSE Message Format
91
+
92
+ Messages must be in Anthropic's SSE format:
93
+
94
+ ```typescript
95
+ interface SSEMessage {
96
+ type: string; // "message_start", "content_block_delta", "message_delta", "message_stop"
97
+ data: {
98
+ type?: string;
99
+ index?: number;
100
+ delta?: { type: "text_delta"; text: string };
101
+ stop_reason?: string;
102
+ usage?: { input_tokens: number; output_tokens: number };
103
+ };
104
+ }
105
+ ```
106
+
107
+ ## Example: Ollama Plugin
108
+
109
+ See `examples/plugins/ollama/` for a complete working example.
110
+
111
+ ## Type Definitions
112
+
113
+ Install the official type definitions:
114
+
115
+ ```bash
116
+ bun add -D @ccx/plugin-types
117
+ ```
118
+
119
+ ## Configuration
120
+
121
+ Plugins can read configuration from the main config file:
122
+
123
+ ```json
124
+ {
125
+ "plugins": {
126
+ "my-provider": {
127
+ "apiKey": "your-api-key",
128
+ "baseUrl": "https://api.example.com/v1",
129
+ "extra": {
130
+ "customOption": true
131
+ }
132
+ }
133
+ }
134
+ }
135
+ ```
136
+
137
+ ## Best Practices
138
+
139
+ 1. **Handle errors gracefully** - Return meaningful error messages
140
+ 2. **Implement health checks** - Helps with `ccx doctor` diagnostics
141
+ 3. **Support streaming** - Users expect real-time responses
142
+ 4. **Respect rate limits** - Implement backoff on 429 errors
143
+ 5. **Use environment variables** - Allow API key via env vars
144
+
145
+ ## Publishing
146
+
147
+ Want to share your plugin?
148
+
149
+ 1. Create a GitHub repository with your plugin code
150
+ 2. Add installation instructions to your README
151
+ 3. Submit a PR to add your plugin to the examples directory
152
+
153
+ ## Troubleshooting
154
+
155
+ ### Plugin Not Loading
156
+
157
+ Check `ccx doctor` for plugin errors. Common issues:
158
+ - Missing `plugin.json`
159
+ - Entry file not found
160
+ - TypeScript compilation errors
161
+
162
+ ### Model Not Found
163
+
164
+ Ensure your model's `id` matches what users will type:
165
+ - `ccx --model=my-provider:model-1`
166
+
167
+ ### API Errors
168
+
169
+ Implement proper error handling in your plugin to surface meaningful errors to users.
170
+
171
+ ## API Reference
172
+
173
+ See [@ccx/plugin-types](https://www.npmjs.com/package/@ccx/plugin-types) for complete type definitions.
@@ -0,0 +1,67 @@
1
+ # Ollama Plugin for ccx
2
+
3
+ This example plugin demonstrates how to integrate local Ollama models with ccx.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # 1. Ensure Ollama is running (default: http://localhost:11434)
9
+ ollama serve
10
+
11
+ # 2. Pull a model
12
+ ollama pull llama3.1
13
+
14
+ # 3. Create plugin directory
15
+ mkdir -p ~/.config/claude-glm/plugins/ollama
16
+
17
+ # 4. Copy plugin files
18
+ cp plugin.json ~/.config/claude-glm/plugins/ollama/
19
+ cp dist/index.js ~/.config/claude-glm/plugins/ollama/
20
+
21
+ # 5. Verify installation
22
+ ccx models
23
+ ```
24
+
25
+ ## Building
26
+
27
+ ```bash
28
+ # From the examples/plugins/ollama directory:
29
+ bun build ./index.ts --outdir ./dist --target bun
30
+ ```
31
+
32
+ ## Configuration
33
+
34
+ No API key required for local Ollama. The plugin connects to `http://localhost:11434` by default.
35
+
36
+ You can override the base URL by adding to your config:
37
+
38
+ ```json
39
+ {
40
+ "plugins": {
41
+ "ollama": {
42
+ "baseUrl": "http://ollama:11434"
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ ## Supported Models
49
+
50
+ - Llama 3.1 8B (131K context)
51
+ - Qwen 2.5 72B (131K context)
52
+ - Mistral 7B (32K context)
53
+ - CodeLlama 7B (16K context)
54
+ - DeepSeek Coder 6.7B (16K context)
55
+
56
+ ## Adding Custom Models
57
+
58
+ Edit `index.ts` to add more models:
59
+
60
+ ```typescript
61
+ const MODELS: ModelInfo[] = [
62
+ { id: "your-model", name: "Your Model Name", contextWindow: 32768 },
63
+ // Add more models...
64
+ ];
65
+ ```
66
+
67
+ Then rebuild and reinstall.
@@ -0,0 +1,138 @@
1
+ import type {
2
+ ProviderPlugin,
3
+ ProviderClient,
4
+ PluginConfig,
5
+ ModelInfo,
6
+ HealthStatus,
7
+ SSEMessage,
8
+ AnthropicRequest
9
+ } from "../../../packages/plugin-types/index";
10
+
11
+ const MODELS: ModelInfo[] = [
12
+ { id: "llama3.1", name: "Llama 3.1 8B", contextWindow: 131072 },
13
+ { id: "qwen2.5", name: "Qwen 2.5 72B", contextWindow: 131072 },
14
+ { id: "mistral", name: "Mistral 7B", contextWindow: 32768 },
15
+ { id: "codellama", name: "CodeLlama 7B", contextWindow: 16384 },
16
+ { id: "deepseek-coder", name: "DeepSeek Coder 6.7B", contextWindow: 16384 }
17
+ ];
18
+
19
+ export default {
20
+ id: "ollama",
21
+ name: "Ollama (Local)",
22
+ version: "1.0.0",
23
+ description: "Run local Ollama models with Claude Code",
24
+ models: MODELS,
25
+
26
+ createClient(config: PluginConfig): ProviderClient {
27
+ return new OllamaClient(config);
28
+ }
29
+ } satisfies ProviderPlugin;
30
+
31
+ class OllamaClient implements ProviderClient {
32
+ readonly provider = "ollama";
33
+ private baseUrl: string;
34
+
35
+ constructor(config: PluginConfig) {
36
+ this.baseUrl = config.baseUrl || "http://localhost:11434";
37
+ }
38
+
39
+ async *streamComplete(request: AnthropicRequest): AsyncGenerator<SSEMessage> {
40
+ const prompt = this.buildPrompt(request);
41
+
42
+ const response = await fetch(`${this.baseUrl}/api/generate`, {
43
+ method: "POST",
44
+ headers: { "Content-Type": "application/json" },
45
+ body: JSON.stringify({
46
+ model: request.model,
47
+ prompt,
48
+ stream: true,
49
+ options: {
50
+ temperature: request.temperature ?? 0.7,
51
+ num_predict: request.max_tokens
52
+ }
53
+ })
54
+ });
55
+
56
+ if (!response.ok) {
57
+ const errorText = await response.text();
58
+ throw new Error(`Ollama error: ${response.status} ${errorText}`);
59
+ }
60
+
61
+ const reader = response.body?.getReader();
62
+ if (!reader) throw new Error("No response body from Ollama");
63
+
64
+ const decoder = new TextDecoder();
65
+
66
+ while (true) {
67
+ const { done, value } = await reader.read();
68
+ if (done) break;
69
+
70
+ const chunk = decoder.decode(value);
71
+ const lines = chunk.split("\n");
72
+
73
+ for (const line of lines) {
74
+ if (!line.trim()) continue;
75
+
76
+ try {
77
+ const data = JSON.parse(line);
78
+
79
+ if (data.response) {
80
+ yield {
81
+ type: "content_block_delta",
82
+ data: { text: data.response }
83
+ };
84
+ }
85
+
86
+ if (data.done) {
87
+ yield {
88
+ type: "message_delta",
89
+ data: { stop_reason: "end_turn" }
90
+ };
91
+ yield {
92
+ type: "message_stop",
93
+ data: {}
94
+ };
95
+ break;
96
+ }
97
+ } catch {
98
+ continue;
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ getModelInfo(): ModelInfo | undefined {
105
+ return MODELS.find(m => m.id === "llama3.1");
106
+ }
107
+
108
+ async healthCheck(): Promise<HealthStatus> {
109
+ try {
110
+ const start = Date.now();
111
+ const response = await fetch(`${this.baseUrl}/api/tags`);
112
+ if (!response.ok) {
113
+ return { healthy: false, error: `HTTP ${response.status}` };
114
+ }
115
+ return { healthy: true, latencyMs: Date.now() - start };
116
+ } catch (error) {
117
+ return { healthy: false, error: (error as Error).message };
118
+ }
119
+ }
120
+
121
+ private buildPrompt(request: AnthropicRequest): string {
122
+ const parts: string[] = [];
123
+
124
+ if (request.system) {
125
+ parts.push(`<system>${request.system}</system>`);
126
+ }
127
+
128
+ for (const message of request.messages) {
129
+ const role = message.role === "assistant" ? "assistant" : "user";
130
+ const content = typeof message.content === "string"
131
+ ? message.content
132
+ : message.content.map(c => c.type === "text" ? c.text : "").join("");
133
+ parts.push(`<${role}>${content}</${role}>`);
134
+ }
135
+
136
+ return parts.join("\n");
137
+ }
138
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "ollama",
3
+ "name": "Ollama (Local)",
4
+ "version": "1.0.0",
5
+ "description": "Use local Ollama models with Claude Code",
6
+ "entry": "./dist/index.js"
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-x10ded",
3
- "version": "3.0.16",
3
+ "version": "3.0.18",
4
4
  "description": "Extend Claude Code with custom OpenAI-compatible model providers",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,60 @@
1
+ export interface ProviderPlugin {
2
+ readonly id: string;
3
+ readonly name: string;
4
+ readonly version: string;
5
+ readonly description?: string;
6
+ readonly models: ModelInfo[];
7
+
8
+ createClient(config: PluginConfig): ProviderClient;
9
+ }
10
+
11
+ export interface ModelInfo {
12
+ readonly id: string;
13
+ readonly name: string;
14
+ readonly contextWindow?: number;
15
+ readonly maxOutputTokens?: number;
16
+ readonly capabilities?: readonly ("text" | "vision" | "tools")[];
17
+ readonly default?: boolean;
18
+ }
19
+
20
+ export interface ProviderClient {
21
+ readonly provider: string;
22
+
23
+ streamComplete(request: AnthropicRequest): AsyncGenerator<SSEMessage>;
24
+
25
+ getModelInfo(): ModelInfo | undefined;
26
+
27
+ healthCheck(): Promise<HealthStatus>;
28
+ }
29
+
30
+ export interface PluginConfig {
31
+ readonly apiKey?: string;
32
+ readonly baseUrl: string;
33
+ readonly extra?: Record<string, unknown>;
34
+ }
35
+
36
+ export interface HealthStatus {
37
+ readonly healthy: boolean;
38
+ readonly latencyMs?: number;
39
+ readonly error?: string;
40
+ }
41
+
42
+ export interface SSEMessage {
43
+ type: string;
44
+ data: unknown;
45
+ }
46
+
47
+ export interface AnthropicRequest {
48
+ model: string;
49
+ messages: AnthropicMessage[];
50
+ max_tokens: number;
51
+ temperature?: number;
52
+ system?: string;
53
+ tools?: unknown[];
54
+ stream?: boolean;
55
+ }
56
+
57
+ export interface AnthropicMessage {
58
+ role: "user" | "assistant" | "system";
59
+ content: string | Array<{ type: "text"; text: string } | { type: "tool_result"; content: string | unknown }>;
60
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@ccx/plugin-types",
3
+ "version": "1.0.0",
4
+ "description": "TypeScript types for ccx plugins",
5
+ "main": "index.ts",
6
+ "types": "index.ts",
7
+ "exports": {
8
+ ".": "./index.ts"
9
+ },
10
+ "keywords": ["ccx", "claude", "plugin", "types"],
11
+ "author": "",
12
+ "license": "MIT"
13
+ }