cc-x10ded 3.0.17 → 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.
@@ -2,10 +2,14 @@ import { spawn } from "bun";
2
2
  import { ConfigManager } from "../core/config";
3
3
  import { startProxyServer } from "../proxy/server";
4
4
  import { ShellIntegrator } from "../core/shell";
5
+ import { pluginManager } from "../core/plugins";
5
6
  import { parseProviderModel } from "../proxy/map";
7
+ import { telemetry } from "../core/telemetry";
6
8
  import * as pc from "picocolors";
7
9
 
8
10
  export async function runCommand(args: string[], options: { model?: string; port?: number }) {
11
+ await pluginManager.discoverAndLoad();
12
+
9
13
  const configManager = new ConfigManager();
10
14
  const config = await configManager.read();
11
15
 
@@ -13,53 +17,51 @@ export async function runCommand(args: string[], options: { model?: string; port
13
17
  console.log(pc.yellow("Configuration missing. Running setup..."));
14
18
  const { setupCommand } = await import("./setup");
15
19
  await setupCommand();
16
- // Re-read config
17
20
  Object.assign(config, await configManager.read());
18
21
  }
19
22
 
20
23
  const model = options.model || config.defaults.model || "glm-4.7";
21
24
  const { provider } = parseProviderModel(model);
22
-
23
- // Check if we should use the proxy or fallback to native Claude (for Anthropic without API Key)
25
+
24
26
  let useProxy = true;
25
- if (provider === "anthropic" && !config.providers.anthropic?.apiKey) {
26
- useProxy = false;
27
+
28
+ if (provider === "anthropic") {
29
+ useProxy = false;
27
30
  }
28
31
 
29
- // Port hunting logic
30
32
  let port = options.port || 17870;
31
- let server;
32
-
33
+ let server: ReturnType<typeof startProxyServer> | undefined = undefined;
34
+
33
35
  if (useProxy) {
34
36
  let retries = 0;
35
37
  while (retries < 10) {
36
- try {
37
- server = startProxyServer(config, port);
38
- break;
39
- } catch (e: any) {
40
- if (e.code === "EADDRINUSE" || e.message.includes("EADDRINUSE")) {
41
- port++;
42
- retries++;
43
- } else {
44
- throw e;
45
- }
38
+ try {
39
+ server = startProxyServer(config, port);
40
+ break;
41
+ } catch (e: unknown) {
42
+ const error = e as { code?: string; message?: string };
43
+ if (error.code === "EADDRINUSE" || (error.message && error.message.includes("EADDRINUSE"))) {
44
+ port++;
45
+ retries++;
46
+ } else {
47
+ throw e;
46
48
  }
49
+ }
47
50
  }
48
-
51
+
49
52
  if (!server) {
50
- console.error(pc.red(`Failed to start proxy server after 10 attempts (ports ${options.port || 17870}-${port}).`));
51
- process.exit(1);
53
+ console.error(pc.red(`Failed to start proxy server after 10 attempts (ports ${options.port || 17870}-${port}).`));
54
+ process.exit(1);
52
55
  }
53
56
  }
54
57
 
55
- // Robust binary finding
56
58
  const shellInt = new ShellIntegrator();
57
59
  const claudePath = await shellInt.findClaudeBinary();
58
60
 
59
61
  if (!claudePath) {
60
- console.error(pc.red("Error: 'claude' command not found."));
61
- console.error(pc.yellow("Self-Healing Tip: Run 'ccx setup' or 'npm install -g @anthropic-ai/claude-code'"));
62
- process.exit(1);
62
+ console.error(pc.red("Error: 'claude' command not found."));
63
+ console.error(pc.yellow("Self-Healing Tip: Run 'ccx setup' or 'npm install -g @anthropic-ai/claude-code'"));
64
+ process.exit(1);
63
65
  }
64
66
 
65
67
  const env: Record<string, string | undefined> = {
@@ -67,9 +69,9 @@ export async function runCommand(args: string[], options: { model?: string; port
67
69
  };
68
70
 
69
71
  if (useProxy) {
70
- env.ANTHROPIC_BASE_URL = `http://127.0.0.1:${port}`;
71
- env.ANTHROPIC_AUTH_TOKEN = "ccx-proxy-token"; // Dummy token for the client
72
- env.ANTHROPIC_MODEL = model;
72
+ env.ANTHROPIC_BASE_URL = `http://127.0.0.1:${port}`;
73
+ env.ANTHROPIC_AUTH_TOKEN = `ccx-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
74
+ env.ANTHROPIC_MODEL = model;
73
75
  }
74
76
 
75
77
  try {
@@ -85,5 +87,6 @@ export async function runCommand(args: string[], options: { model?: string; port
85
87
  process.exit(1);
86
88
  } finally {
87
89
  if (server) server.stop();
90
+ telemetry.trackEvent({ type: "session_end" });
88
91
  }
89
92
  }
@@ -0,0 +1,167 @@
1
+ import type { CircuitState } from "../types";
2
+ import { providerRegistry } from "./registry";
3
+ import { telemetry } from "./telemetry";
4
+ import { createLogger } from "./logger";
5
+
6
+ const logger = createLogger();
7
+
8
+ const CIRCUIT_THRESHOLD = 5;
9
+ const CIRCUIT_TIMEOUT = 30000;
10
+
11
+ export class CircuitBreaker {
12
+ private states: Map<string, CircuitState> = new Map();
13
+ private static instance: CircuitBreaker | null = null;
14
+
15
+ private constructor() {}
16
+
17
+ static getInstance(): CircuitBreaker {
18
+ if (!CircuitBreaker.instance) {
19
+ CircuitBreaker.instance = new CircuitBreaker();
20
+ }
21
+ return CircuitBreaker.instance;
22
+ }
23
+
24
+ private getState(provider: string): CircuitState {
25
+ let state = this.states.get(provider);
26
+ if (!state) {
27
+ state = {
28
+ provider,
29
+ state: "closed",
30
+ failures: 0,
31
+ lastFailure: null
32
+ };
33
+ this.states.set(provider, state);
34
+ }
35
+ return state;
36
+ }
37
+
38
+ isOpen(provider: string): boolean {
39
+ const state = this.getState(provider);
40
+ if (state.state === "open") {
41
+ const timeSinceFailure = Date.now() - (state.lastFailure || 0);
42
+ if (timeSinceFailure > CIRCUIT_TIMEOUT) {
43
+ state.state = "half-open";
44
+ logger.debug("Circuit breaker transitioning to half-open", { provider });
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+ return false;
50
+ }
51
+
52
+ isClosed(provider: string): boolean {
53
+ return this.getState(provider).state === "closed";
54
+ }
55
+
56
+ async execute<T>(
57
+ provider: string,
58
+ fn: () => Promise<T>
59
+ ): Promise<T> {
60
+ const state = this.getState(provider);
61
+
62
+ if (state.state === "open") {
63
+ const timeSinceFailure = Date.now() - (state.lastFailure || 0);
64
+ if (timeSinceFailure > CIRCUIT_TIMEOUT) {
65
+ state.state = "half-open";
66
+ logger.debug("Circuit breaker transitioning to half-open", { provider });
67
+ } else {
68
+ const fallbackProvider = this.getFallbackProvider(provider);
69
+ if (fallbackProvider) {
70
+ logger.warn("Circuit open, falling back", { from: provider, to: fallbackProvider });
71
+ telemetry.trackEvent({
72
+ type: "fallback",
73
+ fromProvider: provider,
74
+ toProvider: fallbackProvider,
75
+ reason: "circuit_open"
76
+ });
77
+ return this.execute(fallbackProvider, fn);
78
+ }
79
+ throw new CircuitOpenError(provider, CIRCUIT_TIMEOUT - timeSinceFailure);
80
+ }
81
+ }
82
+
83
+ try {
84
+ const result = await fn();
85
+ this.recordSuccess(provider);
86
+ return result;
87
+ } catch (error) {
88
+ this.recordFailure(provider, error as Error);
89
+ throw error;
90
+ }
91
+ }
92
+
93
+ private getFallbackProvider(currentProvider: string): string | null {
94
+ const order = providerRegistry.getProviderOrder();
95
+ const currentIndex = order.indexOf(currentProvider);
96
+
97
+ for (let i = currentIndex + 1; i < order.length; i++) {
98
+ const candidate = order[i];
99
+ if (candidate && !this.isOpen(candidate) && !providerRegistry.isNative(candidate)) {
100
+ return candidate;
101
+ }
102
+ }
103
+
104
+ for (const provider of order) {
105
+ if (provider !== currentProvider && !this.isOpen(provider) && !providerRegistry.isNative(provider)) {
106
+ return provider;
107
+ }
108
+ }
109
+
110
+ return null;
111
+ }
112
+
113
+ private recordSuccess(provider: string): void {
114
+ const state = this.getState(provider);
115
+
116
+ if (state.state === "half-open") {
117
+ state.state = "closed";
118
+ state.failures = 0;
119
+ state.lastFailure = null;
120
+ logger.debug("Circuit breaker closed", { provider });
121
+ } else if (state.state === "closed" && state.failures > 0) {
122
+ state.failures = Math.max(0, state.failures - 1);
123
+ }
124
+ }
125
+
126
+ private recordFailure(provider: string, error: Error): void {
127
+ const state = this.getState(provider);
128
+ state.failures++;
129
+ state.lastFailure = Date.now();
130
+
131
+ if (state.failures >= CIRCUIT_THRESHOLD && state.state !== "open") {
132
+ state.state = "open";
133
+ logger.warn("Circuit breaker opened", { provider, failures: state.failures });
134
+ }
135
+ }
136
+
137
+ reset(provider: string): void {
138
+ const state = this.getState(provider);
139
+ state.state = "closed";
140
+ state.failures = 0;
141
+ state.lastFailure = null;
142
+ }
143
+
144
+ resetAll(): void {
145
+ this.states.clear();
146
+ }
147
+
148
+ getStates(): CircuitState[] {
149
+ return Array.from(this.states.values());
150
+ }
151
+
152
+ getStateInfo(provider: string): CircuitState {
153
+ return this.getState(provider);
154
+ }
155
+ }
156
+
157
+ export class CircuitOpenError extends Error {
158
+ constructor(
159
+ public readonly provider: string,
160
+ public readonly retryAfterMs: number
161
+ ) {
162
+ super(`Circuit open for ${provider}. Retry after ${Math.ceil(retryAfterMs / 1000)}s`);
163
+ this.name = "CircuitOpenError";
164
+ }
165
+ }
166
+
167
+ export const circuitBreaker = CircuitBreaker.getInstance();
@@ -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();