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.
@@ -0,0 +1,173 @@
1
+ import type { LogLevel } from "../types";
2
+
3
+ const LOG_LEVELS: LogLevel[] = ["debug", "info", "warn", "error"];
4
+
5
+ export interface LogContext {
6
+ requestId?: string;
7
+ provider?: string;
8
+ model?: string;
9
+ port?: number;
10
+ command?: string;
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ export interface LogEntry {
15
+ level: LogLevel;
16
+ time: string;
17
+ msg: string;
18
+ requestId?: string;
19
+ provider?: string;
20
+ model?: string;
21
+ port?: number;
22
+ command?: string;
23
+ error?: string;
24
+ stack?: string;
25
+ [key: string]: unknown;
26
+ }
27
+
28
+ export class Logger {
29
+ private level: LogLevel = "info";
30
+ private jsonMode: boolean = false;
31
+ private requestId: string | null = null;
32
+
33
+ constructor(options?: { level?: LogLevel; jsonMode?: boolean }) {
34
+ if (options?.level) this.level = options.level;
35
+ if (options?.jsonMode) this.jsonMode = options.jsonMode;
36
+ if (process.env.CCX_LOG_LEVEL) {
37
+ const envLevel = process.env.CCX_LOG_LEVEL as LogLevel;
38
+ if (LOG_LEVELS.includes(envLevel)) {
39
+ this.level = envLevel;
40
+ }
41
+ }
42
+ if (process.env.CCX_JSON_LOG === "1" || process.env.CCX_JSON_LOG === "true") {
43
+ this.jsonMode = true;
44
+ }
45
+ }
46
+
47
+ setJsonMode(enabled: boolean): void {
48
+ this.jsonMode = enabled;
49
+ }
50
+
51
+ setLevel(level: LogLevel): void {
52
+ this.level = level;
53
+ }
54
+
55
+ setRequestId(id: string): void {
56
+ this.requestId = id;
57
+ }
58
+
59
+ clearRequestId(): void {
60
+ this.requestId = null;
61
+ }
62
+
63
+ private shouldLog(level: LogLevel): boolean {
64
+ return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(this.level);
65
+ }
66
+
67
+ private formatLog(entry: LogEntry): string {
68
+ if (this.jsonMode) {
69
+ return JSON.stringify(entry);
70
+ }
71
+
72
+ const timeParts = entry.time.split("T");
73
+ const timestamp = timeParts[1]?.split(".")[0] || "00:00:00";
74
+ const parts = [timestamp];
75
+
76
+ const levelColors: Record<LogLevel, string> = {
77
+ debug: "\x1b[90m",
78
+ info: "\x1b[36m",
79
+ warn: "\x1b[33m",
80
+ error: "\x1b[31m",
81
+ };
82
+ const reset = "\x1b[0m";
83
+
84
+ parts.push(`${levelColors[entry.level]}[${entry.level.toUpperCase()}]${reset}`);
85
+
86
+ if (entry.provider) {
87
+ parts.push(`[${entry.provider}]`);
88
+ }
89
+
90
+ parts.push(entry.msg);
91
+
92
+ const contextParts: string[] = [];
93
+ if (entry.requestId) contextParts.push(`req=${entry.requestId}`);
94
+ if (entry.model) contextParts.push(`model=${entry.model}`);
95
+ if (entry.port) contextParts.push(`port=${entry.port}`);
96
+
97
+ if (contextParts.length > 0) {
98
+ parts.push(`\x1b[90m${contextParts.join(" ")}${reset}`);
99
+ }
100
+
101
+ if (entry.error) {
102
+ parts.push(`\x1b[31m${entry.error}${reset}`);
103
+ }
104
+
105
+ return parts.join(" ");
106
+ }
107
+
108
+ private createEntry(
109
+ level: LogLevel,
110
+ msg: string,
111
+ context?: LogContext,
112
+ error?: Error
113
+ ): LogEntry {
114
+ const entry: LogEntry = {
115
+ level,
116
+ time: new Date().toISOString(),
117
+ msg,
118
+ };
119
+
120
+ if (this.requestId) {
121
+ entry.requestId = this.requestId;
122
+ }
123
+
124
+ if (context) {
125
+ if (context.requestId && !entry.requestId) {
126
+ entry.requestId = context.requestId;
127
+ }
128
+ if (context.provider) entry.provider = context.provider;
129
+ if (context.model) entry.model = context.model;
130
+ if (context.port) entry.port = context.port;
131
+ if (context.command) entry.command = context.command;
132
+
133
+ for (const [key, value] of Object.entries(context)) {
134
+ if (!["requestId", "provider", "model", "port", "command"].includes(key)) {
135
+ entry[key] = value;
136
+ }
137
+ }
138
+ }
139
+
140
+ if (error) {
141
+ entry.error = error.message;
142
+ entry.stack = error.stack;
143
+ }
144
+
145
+ return entry;
146
+ }
147
+
148
+ debug(msg: string, context?: LogContext, error?: Error): void {
149
+ if (!this.shouldLog("debug")) return;
150
+ console.log(this.formatLog(this.createEntry("debug", msg, context, error)));
151
+ }
152
+
153
+ info(msg: string, context?: LogContext, error?: Error): void {
154
+ if (!this.shouldLog("info")) return;
155
+ console.log(this.formatLog(this.createEntry("info", msg, context, error)));
156
+ }
157
+
158
+ warn(msg: string, context?: LogContext, error?: Error): void {
159
+ if (!this.shouldLog("warn")) return;
160
+ console.warn(this.formatLog(this.createEntry("warn", msg, context, error)));
161
+ }
162
+
163
+ error(msg: string, context?: LogContext, error?: Error): void {
164
+ if (!this.shouldLog("error")) return;
165
+ console.error(this.formatLog(this.createEntry("error", msg, context, error)));
166
+ }
167
+ }
168
+
169
+ export function createLogger(options?: { level?: LogLevel; jsonMode?: boolean }): Logger {
170
+ return new Logger(options);
171
+ }
172
+
173
+ export const defaultLogger = createLogger();
@@ -0,0 +1,138 @@
1
+ import { join, dirname } from "path";
2
+ import { existsSync, readdirSync, readFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import type { ProviderPlugin, ProviderInfo } from "../types";
5
+ import { providerRegistry } from "./registry";
6
+ import { createLogger } from "./logger";
7
+
8
+ const logger = createLogger();
9
+
10
+ interface PluginManifest {
11
+ id: string;
12
+ name: string;
13
+ version: string;
14
+ description?: string;
15
+ entry: string;
16
+ }
17
+
18
+ export class PluginManager {
19
+ private plugins: Map<string, ProviderPlugin> = new Map();
20
+ private pluginDir: string;
21
+ private static instance: PluginManager | null = null;
22
+
23
+ private constructor() {
24
+ this.pluginDir = join(homedir(), ".config", "claude-glm", "plugins");
25
+ }
26
+
27
+ static getInstance(): PluginManager {
28
+ if (!PluginManager.instance) {
29
+ PluginManager.instance = new PluginManager();
30
+ }
31
+ return PluginManager.instance;
32
+ }
33
+
34
+ async discoverAndLoad(): Promise<void> {
35
+ if (!existsSync(this.pluginDir)) {
36
+ logger.debug("Plugin directory does not exist", { path: this.pluginDir });
37
+ return;
38
+ }
39
+
40
+ const entries = readdirSync(this.pluginDir, { withFileTypes: true });
41
+
42
+ for (const entry of entries) {
43
+ if (!entry.isDirectory()) continue;
44
+
45
+ const pluginPath = join(this.pluginDir, entry.name);
46
+ await this.loadPlugin(pluginPath);
47
+ }
48
+
49
+ logger.info("Plugin discovery complete", { count: this.plugins.size });
50
+ }
51
+
52
+ private async loadPlugin(pluginPath: string): Promise<void> {
53
+ const manifestPath = join(pluginPath, "plugin.json");
54
+
55
+ if (!existsSync(manifestPath)) {
56
+ logger.warn("Plugin missing manifest", { path: pluginPath });
57
+ return;
58
+ }
59
+
60
+ let manifest: PluginManifest;
61
+ try {
62
+ const content = readFileSync(manifestPath, "utf-8");
63
+ manifest = JSON.parse(content);
64
+ } catch (error) {
65
+ logger.warn("Failed to parse plugin manifest", { error: (error as Error).message });
66
+ return;
67
+ }
68
+
69
+ if (!manifest.id || !manifest.entry) {
70
+ logger.warn("Plugin manifest incomplete", { manifest });
71
+ return;
72
+ }
73
+
74
+ const entryPath = join(pluginPath, manifest.entry);
75
+ if (!existsSync(entryPath)) {
76
+ logger.warn("Plugin entry file not found", { entry: manifest.entry });
77
+ return;
78
+ }
79
+
80
+ try {
81
+ const pluginModule = await import(entryPath);
82
+ const plugin = pluginModule.default as ProviderPlugin;
83
+
84
+ if (!plugin.id || !plugin.name || !plugin.models || !plugin.createClient) {
85
+ logger.warn("Plugin missing required fields", { id: plugin.id });
86
+ return;
87
+ }
88
+
89
+ this.plugins.set(plugin.id, plugin);
90
+ providerRegistry.registerPlugin({
91
+ id: plugin.id,
92
+ name: plugin.name,
93
+ models: plugin.models,
94
+ isNative: false,
95
+ requiresKey: `plugins.${plugin.id}.apiKey`
96
+ });
97
+
98
+ logger.info("Plugin loaded", { id: plugin.id, name: plugin.name, version: plugin.version });
99
+ } catch (error) {
100
+ logger.warn("Failed to load plugin", { id: manifest.id, error: (error as Error).message });
101
+ }
102
+ }
103
+
104
+ getPlugins(): ProviderPlugin[] {
105
+ return Array.from(this.plugins.values());
106
+ }
107
+
108
+ getPlugin(id: string): ProviderPlugin | undefined {
109
+ return this.plugins.get(id);
110
+ }
111
+
112
+ isPluginLoaded(id: string): boolean {
113
+ return this.plugins.has(id);
114
+ }
115
+
116
+ async unloadPlugin(id: string): Promise<void> {
117
+ const plugin = this.plugins.get(id);
118
+ if (!plugin) return;
119
+
120
+ if (typeof (plugin as unknown as { onUnload?: () => Promise<void> }).onUnload === "function") {
121
+ try {
122
+ await (plugin as unknown as { onUnload: () => Promise<void> }).onUnload();
123
+ } catch (error) {
124
+ logger.warn("Plugin onUnload failed", { id, error: (error as Error).message });
125
+ }
126
+ }
127
+
128
+ this.plugins.delete(id);
129
+ providerRegistry.unregisterPlugin(id);
130
+ logger.info("Plugin unloaded", { id });
131
+ }
132
+
133
+ getPluginCount(): number {
134
+ return this.plugins.size;
135
+ }
136
+ }
137
+
138
+ export const pluginManager = PluginManager.getInstance();
@@ -0,0 +1,172 @@
1
+ import type { ProviderInfo, ModelInfo } from "../types";
2
+
3
+ const BUILTIN_PROVIDERS: ProviderInfo[] = [
4
+ {
5
+ id: "glm",
6
+ name: "GLM (Z.AI)",
7
+ models: [
8
+ { id: "glm-4.7", name: "GLM-4.7", default: true },
9
+ { id: "glm-4.6", name: "GLM-4.6" },
10
+ { id: "glm-4.5", name: "GLM-4.5" },
11
+ { id: "glm-4.5-air", name: "GLM-4.5-Air" }
12
+ ],
13
+ isNative: false,
14
+ requiresKey: "zaiApiKey"
15
+ },
16
+ {
17
+ id: "minimax",
18
+ name: "Minimax",
19
+ models: [
20
+ { id: "MiniMax-M2.1", name: "MiniMax-M2.1", default: true },
21
+ { id: "MiniMax-M2.1-32k", name: "MiniMax-M2.1-32k" }
22
+ ],
23
+ isNative: false,
24
+ requiresKey: "minimaxApiKey"
25
+ },
26
+ {
27
+ id: "openai",
28
+ name: "OpenAI",
29
+ models: [
30
+ { id: "gpt-4o", name: "GPT-4o", default: true },
31
+ { id: "gpt-4o-mini", name: "GPT-4o Mini" },
32
+ { id: "gpt-4-turbo", name: "GPT-4 Turbo" }
33
+ ],
34
+ isNative: false,
35
+ requiresKey: "providers.openai.apiKey"
36
+ },
37
+ {
38
+ id: "anthropic",
39
+ name: "Anthropic (Claude Code)",
40
+ models: [
41
+ { id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4", default: true },
42
+ { id: "claude-haiku-4-20250514", name: "Claude Haiku 4" },
43
+ { id: "claude-opus-4-20250514", name: "Claude Opus 4" }
44
+ ],
45
+ isNative: true,
46
+ requiresKey: "providers.anthropic.apiKey"
47
+ },
48
+ {
49
+ id: "gemini",
50
+ name: "Google Gemini",
51
+ models: [
52
+ { id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", default: true },
53
+ { id: "gemini-1.5-pro", name: "Gemini 1.5 Pro" }
54
+ ],
55
+ isNative: false,
56
+ requiresKey: "providers.gemini.apiKey"
57
+ },
58
+ {
59
+ id: "openrouter",
60
+ name: "OpenRouter",
61
+ models: [
62
+ { id: "openrouter.auto", name: "Auto-Select", default: true },
63
+ { id: "anthropic/claude-sonnet-4", name: "Anthropic Sonnet" }
64
+ ],
65
+ isNative: false,
66
+ requiresKey: "providers.openrouter.apiKey"
67
+ }
68
+ ];
69
+
70
+ const DEFAULT_PROVIDER_ORDER = ["glm", "minimax", "openai", "gemini", "openrouter", "anthropic"];
71
+
72
+ export class ProviderRegistry {
73
+ private providers: Map<string, ProviderInfo> = new Map();
74
+ private plugins: Map<string, ProviderInfo> = new Map();
75
+
76
+ constructor() {
77
+ for (const provider of BUILTIN_PROVIDERS) {
78
+ this.providers.set(provider.id, provider);
79
+ }
80
+ }
81
+
82
+ registerPlugin(plugin: ProviderInfo): void {
83
+ this.plugins.set(plugin.id, plugin);
84
+ }
85
+
86
+ unregisterPlugin(pluginId: string): void {
87
+ this.plugins.delete(pluginId);
88
+ }
89
+
90
+ listProviders(): ProviderInfo[] {
91
+ const all: ProviderInfo[] = [];
92
+ for (const id of DEFAULT_PROVIDER_ORDER) {
93
+ const builtin = this.providers.get(id);
94
+ const plugin = this.plugins.get(id);
95
+ if (builtin) all.push(builtin);
96
+ if (plugin) all.push(plugin);
97
+ }
98
+ for (const [id, plugin] of this.plugins) {
99
+ if (!DEFAULT_PROVIDER_ORDER.includes(id)) {
100
+ all.push(plugin);
101
+ }
102
+ }
103
+ return all;
104
+ }
105
+
106
+ getProvider(id: string): ProviderInfo | null {
107
+ return this.providers.get(id) || this.plugins.get(id) || null;
108
+ }
109
+
110
+ getProviderOrder(): string[] {
111
+ const order: string[] = [];
112
+ for (const id of DEFAULT_PROVIDER_ORDER) {
113
+ if (this.providers.has(id) || this.plugins.has(id)) {
114
+ order.push(id);
115
+ }
116
+ }
117
+ for (const id of this.plugins.keys()) {
118
+ if (!DEFAULT_PROVIDER_ORDER.includes(id) && !order.includes(id)) {
119
+ order.push(id);
120
+ }
121
+ }
122
+ return order;
123
+ }
124
+
125
+ getDefaultProvider(): ProviderInfo | null {
126
+ const order = this.getProviderOrder();
127
+ for (const id of order) {
128
+ const provider = this.getProvider(id);
129
+ if (provider) {
130
+ const defaultModel = provider.models.find(m => m.default);
131
+ if (defaultModel) {
132
+ return provider;
133
+ }
134
+ }
135
+ }
136
+ return null;
137
+ }
138
+
139
+ getDefaultModelForProvider(providerId: string): ModelInfo | null {
140
+ const provider = this.getProvider(providerId);
141
+ if (!provider) return null;
142
+ return provider.models.find(m => m.default) || provider.models[0] || null;
143
+ }
144
+
145
+ getModel(providerId: string, modelId: string): ModelInfo | null {
146
+ const provider = this.getProvider(providerId);
147
+ if (!provider) return null;
148
+ return provider.models.find(m => m.id === modelId) || null;
149
+ }
150
+
151
+ getAllModels(): Array<{ provider: ProviderInfo; model: ModelInfo }> {
152
+ const all: Array<{ provider: ProviderInfo; model: ModelInfo }> = [];
153
+ for (const provider of this.listProviders()) {
154
+ for (const model of provider.models) {
155
+ all.push({ provider, model });
156
+ }
157
+ }
158
+ return all;
159
+ }
160
+
161
+ isNative(providerId: string): boolean {
162
+ const provider = this.getProvider(providerId);
163
+ return provider?.isNative ?? false;
164
+ }
165
+
166
+ requiresKey(providerId: string): string | null {
167
+ const provider = this.getProvider(providerId);
168
+ return provider?.requiresKey ?? null;
169
+ }
170
+ }
171
+
172
+ export const providerRegistry = new ProviderRegistry();
package/src/core/shell.ts CHANGED
@@ -180,6 +180,53 @@ Function ccm { ccx --model=MiniMax-M2.1 @args }`;
180
180
  return await this.installAliases(shell);
181
181
  }
182
182
 
183
+ /**
184
+ * Remove old ccx binaries from common locations that might shadow the bun global
185
+ */
186
+ async cleanupOldBinaries(): Promise<string[]> {
187
+ const removed: string[] = [];
188
+ const oldLocations = [
189
+ join(this.home, ".local", "bin", "ccx"),
190
+ join(this.home, ".npm-global", "bin", "ccx"),
191
+ join(this.home, "bin", "ccx"),
192
+ "/usr/local/bin/ccx",
193
+ ];
194
+
195
+ for (const loc of oldLocations) {
196
+ if (existsSync(loc)) {
197
+ try {
198
+ const { unlinkSync } = await import("fs");
199
+ unlinkSync(loc);
200
+ removed.push(loc);
201
+ } catch {
202
+ // Ignore permission errors
203
+ }
204
+ }
205
+ }
206
+
207
+ return removed;
208
+ }
209
+
210
+ /**
211
+ * Check if bun bin is properly prioritized in PATH
212
+ */
213
+ isBunBinFirst(): boolean {
214
+ const bunBin = join(this.home, ".bun", "bin");
215
+ const path = process.env.PATH || "";
216
+ const paths = path.split(":");
217
+
218
+ // Check if bun bin comes before other common locations
219
+ const bunIndex = paths.findIndex(p => p.includes(".bun/bin"));
220
+ const localIndex = paths.findIndex(p => p.includes(".local/bin"));
221
+ const npmGlobalIndex = paths.findIndex(p => p.includes(".npm-global/bin"));
222
+
223
+ if (bunIndex === -1) return false;
224
+ if (localIndex !== -1 && localIndex < bunIndex) return false;
225
+ if (npmGlobalIndex !== -1 && npmGlobalIndex < bunIndex) return false;
226
+
227
+ return true;
228
+ }
229
+
183
230
  /**
184
231
  * Hunt for the 'claude' binary in common locations
185
232
  */