@kaitranntt/ccs 3.2.0 → 3.4.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/README.md +152 -124
- package/VERSION +1 -1
- package/bin/ccs.js +120 -4
- package/bin/delta-accumulator.js +155 -0
- package/bin/glmt-proxy.js +467 -0
- package/bin/glmt-transformer.js +684 -0
- package/bin/sse-parser.js +96 -0
- package/config/base-glmt.settings.json +17 -0
- package/lib/ccs +2 -1
- package/lib/ccs.ps1 +2 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +105 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Stop hitting rate limits. Keep working continuously.
|
|
|
30
30
|
claude /login
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
###
|
|
33
|
+
### Installation
|
|
34
34
|
|
|
35
35
|
#### Option 1: npm Package (Recommended)
|
|
36
36
|
|
|
@@ -75,6 +75,7 @@ irm ccs.kaitran.ca/install | iex
|
|
|
75
75
|
{
|
|
76
76
|
"profiles": {
|
|
77
77
|
"glm": "~/.ccs/glm.settings.json",
|
|
78
|
+
"glmt": "~/.ccs/glmt.settings.json",
|
|
78
79
|
"kimi": "~/.ccs/kimi.settings.json",
|
|
79
80
|
"default": "~/.claude/settings.json"
|
|
80
81
|
}
|
|
@@ -106,19 +107,23 @@ $env:CCS_CLAUDE_PATH = "D:\Tools\Claude\claude.exe" # Windows
|
|
|
106
107
|
|
|
107
108
|
### Your First Switch
|
|
108
109
|
|
|
109
|
-
> **⚠️ Important**: Before using GLM or Kimi profiles,
|
|
110
|
+
> **⚠️ Important**: Before using GLM/GLMT or Kimi profiles, update API keys in settings files:
|
|
110
111
|
> - **GLM**: Edit `~/.ccs/glm.settings.json` and add your GLM API key
|
|
112
|
+
> - **GLMT**: Edit `~/.ccs/glmt.settings.json` and add your Z.AI API key (requires coding plan)
|
|
111
113
|
> - **Kimi**: Edit `~/.ccs/kimi.settings.json` and add your Kimi API key
|
|
112
114
|
|
|
113
115
|
```bash
|
|
114
|
-
#
|
|
115
|
-
ccs "Plan
|
|
116
|
+
# Default Claude subscription
|
|
117
|
+
ccs "Plan microservices architecture"
|
|
116
118
|
|
|
117
|
-
# Switch to GLM
|
|
118
|
-
ccs glm "Create
|
|
119
|
+
# Switch to GLM (cost-optimized)
|
|
120
|
+
ccs glm "Create REST API"
|
|
119
121
|
|
|
120
|
-
#
|
|
121
|
-
ccs
|
|
122
|
+
# GLM with thinking mode
|
|
123
|
+
ccs glmt "Solve algorithmic problem"
|
|
124
|
+
|
|
125
|
+
# Kimi for coding
|
|
126
|
+
ccs kimi "Write integration tests"
|
|
122
127
|
```
|
|
123
128
|
|
|
124
129
|
---
|
|
@@ -149,186 +154,208 @@ Manual context switching breaks your workflow. **CCS manages it seamlessly**.
|
|
|
149
154
|
|
|
150
155
|
</div>
|
|
151
156
|
|
|
152
|
-
**The Solution**:
|
|
153
|
-
```bash
|
|
154
|
-
ccs work # Use company Claude account
|
|
155
|
-
ccs personal # Switch to personal Claude account
|
|
156
|
-
ccs glm # Switch to GLM for cost-effective tasks
|
|
157
|
-
ccs kimi # Switch to Kimi for alternative option
|
|
158
|
-
# Hit rate limit? Switch instantly:
|
|
159
|
-
ccs glm # Continue working with GLM
|
|
160
|
-
# Need different company account?
|
|
161
|
-
ccs work-2 # Switch to second company account
|
|
162
|
-
```
|
|
163
|
-
|
|
164
157
|
---
|
|
165
158
|
|
|
166
|
-
##
|
|
159
|
+
## Architecture
|
|
160
|
+
|
|
161
|
+
### Profile Types
|
|
162
|
+
|
|
163
|
+
**Settings-based**: GLM, GLMT, Kimi, default
|
|
164
|
+
- Uses `--settings` flag pointing to config files
|
|
165
|
+
- GLMT: Embedded proxy for thinking mode support
|
|
166
|
+
|
|
167
|
+
**Account-based**: work, personal, team
|
|
168
|
+
- Uses `CLAUDE_CONFIG_DIR` for isolated instances
|
|
169
|
+
- Create with `ccs auth create <profile>`
|
|
170
|
+
|
|
171
|
+
### Shared Data (v3.1)
|
|
167
172
|
|
|
168
|
-
|
|
173
|
+
Commands and skills symlinked from `~/.ccs/shared/` - no duplication across profiles.
|
|
169
174
|
|
|
170
|
-
**Directory Structure**:
|
|
171
175
|
```
|
|
172
176
|
~/.ccs/
|
|
173
177
|
├── shared/ # Shared across all profiles
|
|
174
|
-
│ ├──
|
|
175
|
-
│
|
|
178
|
+
│ ├── agents/
|
|
179
|
+
│ ├── commands/
|
|
180
|
+
│ └── skills/
|
|
176
181
|
├── instances/ # Profile-specific data
|
|
177
|
-
│
|
|
178
|
-
│
|
|
179
|
-
│
|
|
180
|
-
│
|
|
181
|
-
│
|
|
182
|
-
│
|
|
182
|
+
│ └── work/
|
|
183
|
+
│ ├── agents@ → shared/agents/
|
|
184
|
+
│ ├── commands@ → shared/commands/
|
|
185
|
+
│ ├── skills@ → shared/skills/
|
|
186
|
+
│ ├── settings.json # API keys, credentials
|
|
187
|
+
│ └── sessions/ # Conversation history
|
|
183
188
|
│ └── ...
|
|
184
189
|
```
|
|
185
190
|
|
|
186
|
-
**
|
|
187
|
-
-
|
|
188
|
-
- Single source of truth for shared resources
|
|
189
|
-
- Automatic migration from v3.0 (runs on first use)
|
|
190
|
-
- Windows fallback: copies if symlinks unavailable (enable Developer Mode for true symlinks)
|
|
191
|
+
**Shared**: commands/, skills/, agents/
|
|
192
|
+
**Profile-specific**: settings.json, sessions/, todolists/, logs/
|
|
191
193
|
|
|
192
|
-
**
|
|
193
|
-
- `.claude/commands/` - Custom slash commands
|
|
194
|
-
- `.claude/skills/` - Claude Code skills
|
|
195
|
-
|
|
196
|
-
**What's Profile-Specific**:
|
|
197
|
-
- `settings.json` - API keys, credentials
|
|
198
|
-
- `sessions/` - Conversation history
|
|
199
|
-
- `todolists/` - Task tracking
|
|
200
|
-
- `logs/` - Profile-specific logs
|
|
194
|
+
**[i] Windows**: Copies dirs if symlinks unavailable (enable Developer Mode for true symlinks)
|
|
201
195
|
|
|
202
196
|
---
|
|
203
197
|
|
|
204
|
-
##
|
|
198
|
+
## GLM with Thinking (GLMT)
|
|
199
|
+
|
|
200
|
+
> **[!] Important**: GLMT requires npm installation (`npm install -g @kaitranntt/ccs`). Not available in native shell versions (requires Node.js HTTP server).
|
|
205
201
|
|
|
206
|
-
|
|
202
|
+
### GLM vs GLMT
|
|
207
203
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
204
|
+
| Feature | GLM (`ccs glm`) | GLMT (`ccs glmt`) |
|
|
205
|
+
|---------|-----------------|-------------------|
|
|
206
|
+
| **Endpoint** | Anthropic-compatible | OpenAI-compatible |
|
|
207
|
+
| **Thinking** | No | Yes (reasoning_content) |
|
|
208
|
+
| **Streaming** | Yes | **Yes (v3.4+)** |
|
|
209
|
+
| **TTFB** | <500ms | <500ms (streaming), 2-10s (buffered) |
|
|
210
|
+
| **Use Case** | Fast responses | Complex reasoning |
|
|
213
211
|
|
|
214
|
-
|
|
215
|
-
DETECT[ProfileDetector]
|
|
216
|
-
PROFILE_CHECK{Profile exists?}
|
|
212
|
+
### Streaming Support (v3.4)
|
|
217
213
|
|
|
218
|
-
|
|
219
|
-
SETTINGS["Settings-based<br/>glm, kimi, default"]
|
|
220
|
-
ACCOUNT["Account-based<br/>work, personal, team"]
|
|
221
|
-
end
|
|
222
|
-
end
|
|
214
|
+
**GLMT now supports real-time streaming** with incremental reasoning content delivery.
|
|
223
215
|
|
|
224
|
-
|
|
225
|
-
|
|
216
|
+
- **Default**: Streaming enabled (TTFB <500ms)
|
|
217
|
+
- **Disable**: Set `CCS_GLMT_STREAMING=disabled` for buffered mode
|
|
218
|
+
- **Force**: Set `CCS_GLMT_STREAMING=force` to override client preferences
|
|
226
219
|
|
|
227
|
-
|
|
228
|
-
SETTINGS_MGR["SettingsManager<br/>→ --settings flag"]
|
|
229
|
-
INSTANCE_MGR["InstanceManager<br/>→ CLAUDE_CONFIG_DIR"]
|
|
230
|
-
end
|
|
231
|
-
end
|
|
220
|
+
**Confirmed working**: Z.AI (1498 reasoning chunks tested)
|
|
232
221
|
|
|
233
|
-
|
|
234
|
-
CLAUDE_DETECT["Claude CLI Detection<br/>CCS_CLAUDE_PATH support"]
|
|
222
|
+
### How It Works
|
|
235
223
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
224
|
+
1. CCS spawns embedded HTTP proxy on localhost
|
|
225
|
+
2. Proxy converts Anthropic format → OpenAI format (streaming or buffered)
|
|
226
|
+
3. Forwards to Z.AI with reasoning parameters
|
|
227
|
+
4. Converts `reasoning_content` → thinking blocks (incremental or complete)
|
|
228
|
+
5. Thinking appears in Claude Code UI in real-time
|
|
241
229
|
|
|
242
|
-
|
|
243
|
-
API["API Response<br/>Claude Sonnet 4.5<br/>GLM 4.6<br/>Kimi K2 Thinking"]
|
|
244
|
-
end
|
|
230
|
+
### Control Tags
|
|
245
231
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
DETECT --> PROFILE_CHECK
|
|
249
|
-
PROFILE_CHECK -->|Yes| SETTINGS
|
|
250
|
-
PROFILE_CHECK -->|Yes| ACCOUNT
|
|
232
|
+
- `<Thinking:On|Off>` - Enable/disable reasoning blocks (default: On)
|
|
233
|
+
- `<Effort:Low|Medium|High>` - Control reasoning depth (default: Medium)
|
|
251
234
|
|
|
252
|
-
|
|
253
|
-
ACCOUNT --> CONFIG
|
|
235
|
+
### API Key Setup
|
|
254
236
|
|
|
255
|
-
|
|
256
|
-
|
|
237
|
+
```bash
|
|
238
|
+
# Edit GLMT settings
|
|
239
|
+
nano ~/.ccs/glmt.settings.json
|
|
257
240
|
|
|
258
|
-
|
|
259
|
-
|
|
241
|
+
# Set Z.AI API key (requires coding plan)
|
|
242
|
+
{
|
|
243
|
+
"env": {
|
|
244
|
+
"ANTHROPIC_AUTH_TOKEN": "your-z-ai-api-key"
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
260
248
|
|
|
261
|
-
|
|
262
|
-
INSTANCE_EXEC --> CLAUDE_DETECT
|
|
249
|
+
### Security Limits
|
|
263
250
|
|
|
264
|
-
|
|
251
|
+
**DoS protection** (v3.4):
|
|
252
|
+
- SSE buffer: 1MB max per event
|
|
253
|
+
- Content buffer: 10MB max per block (thinking/text)
|
|
254
|
+
- Content blocks: 100 max per message
|
|
255
|
+
- Request timeout: 120s (both streaming and buffered)
|
|
256
|
+
|
|
257
|
+
### Debugging
|
|
258
|
+
|
|
259
|
+
**Enable verbose logging**:
|
|
260
|
+
```bash
|
|
261
|
+
ccs glmt --verbose "your prompt"
|
|
265
262
|
```
|
|
266
263
|
|
|
267
|
-
|
|
264
|
+
**Enable debug file logging**:
|
|
265
|
+
```bash
|
|
266
|
+
export CCS_DEBUG_LOG=1
|
|
267
|
+
ccs glmt --verbose "your prompt"
|
|
268
|
+
# Logs: ~/.ccs/logs/
|
|
269
|
+
```
|
|
268
270
|
|
|
269
|
-
|
|
271
|
+
**Check streaming mode**:
|
|
272
|
+
```bash
|
|
273
|
+
# Disable streaming for debugging
|
|
274
|
+
CCS_GLMT_STREAMING=disabled ccs glmt "test"
|
|
275
|
+
```
|
|
270
276
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
- **Zero Downtime** - Switch instantly, no workflow interruption
|
|
277
|
+
**Check reasoning content**:
|
|
278
|
+
```bash
|
|
279
|
+
cat ~/.ccs/logs/*response-openai.json | jq '.choices[0].message.reasoning_content'
|
|
280
|
+
```
|
|
276
281
|
|
|
282
|
+
**If absent**: Z.AI API issue (verify key, account status)
|
|
283
|
+
**If present**: Transformation issue (check response-anthropic.json)
|
|
277
284
|
|
|
278
285
|
---
|
|
279
286
|
|
|
280
|
-
##
|
|
287
|
+
## Usage Examples
|
|
281
288
|
|
|
282
|
-
### Basic
|
|
289
|
+
### Basic Switching
|
|
283
290
|
```bash
|
|
284
|
-
ccs #
|
|
285
|
-
ccs glm #
|
|
286
|
-
ccs
|
|
287
|
-
ccs
|
|
291
|
+
ccs # Claude subscription (default)
|
|
292
|
+
ccs glm # GLM (no thinking)
|
|
293
|
+
ccs glmt # GLM with thinking
|
|
294
|
+
ccs kimi # Kimi for Coding
|
|
295
|
+
ccs --version # Show version
|
|
288
296
|
```
|
|
289
297
|
|
|
290
|
-
###
|
|
298
|
+
### Multi-Account Setup
|
|
291
299
|
```bash
|
|
292
|
-
# Create
|
|
293
|
-
ccs auth create work
|
|
294
|
-
ccs auth create personal
|
|
295
|
-
ccs auth create team # Team account
|
|
300
|
+
# Create accounts
|
|
301
|
+
ccs auth create work
|
|
302
|
+
ccs auth create personal
|
|
296
303
|
|
|
297
|
-
# Terminal 1
|
|
304
|
+
# Terminal 1
|
|
298
305
|
ccs work "implement feature"
|
|
299
306
|
|
|
300
|
-
# Terminal 2
|
|
307
|
+
# Terminal 2 (concurrent)
|
|
301
308
|
ccs personal "review code"
|
|
302
309
|
```
|
|
303
310
|
|
|
311
|
+
### Custom Claude CLI Path
|
|
312
|
+
|
|
313
|
+
Non-standard installation location:
|
|
314
|
+
```bash
|
|
315
|
+
export CCS_CLAUDE_PATH="/path/to/claude" # Unix
|
|
316
|
+
$env:CCS_CLAUDE_PATH = "D:\Tools\Claude\claude.exe" # Windows
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
See [Troubleshooting Guide](./docs/en/troubleshooting.md#claude-cli-in-non-standard-location)
|
|
320
|
+
|
|
304
321
|
---
|
|
305
322
|
|
|
306
|
-
|
|
323
|
+
## Configuration
|
|
324
|
+
|
|
325
|
+
Auto-created during installation via npm postinstall script.
|
|
326
|
+
|
|
327
|
+
**~/.ccs/config.json**:
|
|
328
|
+
```json
|
|
329
|
+
{
|
|
330
|
+
"profiles": {
|
|
331
|
+
"glm": "~/.ccs/glm.settings.json",
|
|
332
|
+
"glmt": "~/.ccs/glmt.settings.json",
|
|
333
|
+
"kimi": "~/.ccs/kimi.settings.json",
|
|
334
|
+
"default": "~/.claude/settings.json"
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Complete guide: [docs/en/configuration.md](./docs/en/configuration.md)
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Uninstall
|
|
307
344
|
|
|
308
345
|
**Package Managers**
|
|
309
346
|
```bash
|
|
310
|
-
# npm
|
|
311
347
|
npm uninstall -g @kaitranntt/ccs
|
|
312
|
-
|
|
313
|
-
# yarn
|
|
314
348
|
yarn global remove @kaitranntt/ccs
|
|
315
|
-
|
|
316
|
-
# pnpm
|
|
317
349
|
pnpm remove -g @kaitranntt/ccs
|
|
318
|
-
|
|
319
|
-
# bun
|
|
320
350
|
bun remove -g @kaitranntt/ccs
|
|
321
351
|
```
|
|
322
352
|
|
|
323
353
|
**Official Uninstaller**
|
|
324
|
-
|
|
325
|
-
**macOS / Linux**
|
|
326
354
|
```bash
|
|
355
|
+
# macOS / Linux
|
|
327
356
|
curl -fsSL ccs.kaitran.ca/uninstall | bash
|
|
328
|
-
```
|
|
329
357
|
|
|
330
|
-
|
|
331
|
-
```powershell
|
|
358
|
+
# Windows
|
|
332
359
|
irm ccs.kaitran.ca/uninstall | iex
|
|
333
360
|
```
|
|
334
361
|
|
|
@@ -348,6 +375,7 @@ irm ccs.kaitran.ca/uninstall | iex
|
|
|
348
375
|
- [Installation Guide](./docs/en/installation.md)
|
|
349
376
|
- [Configuration](./docs/en/configuration.md)
|
|
350
377
|
- [Usage Examples](./docs/en/usage.md)
|
|
378
|
+
- [System Architecture](./docs/system-architecture.md)
|
|
351
379
|
- [Troubleshooting](./docs/en/troubleshooting.md)
|
|
352
380
|
- [Contributing](./CONTRIBUTING.md)
|
|
353
381
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.4.0
|
package/bin/ccs.js
CHANGED
|
@@ -108,6 +108,8 @@ function handleHelpCommand() {
|
|
|
108
108
|
console.log(colored('Model Switching:', 'cyan'));
|
|
109
109
|
console.log(` ${colored('ccs', 'yellow')} Use default Claude account`);
|
|
110
110
|
console.log(` ${colored('ccs glm', 'yellow')} Switch to GLM 4.6 model`);
|
|
111
|
+
console.log(` ${colored('ccs glmt', 'yellow')} Switch to GLM with thinking mode`);
|
|
112
|
+
console.log(` ${colored('ccs glmt --verbose', 'yellow')} Enable debug logging`);
|
|
111
113
|
console.log(` ${colored('ccs kimi', 'yellow')} Switch to Kimi for Coding`);
|
|
112
114
|
console.log(` ${colored('ccs glm', 'yellow')} "debug this code" Use GLM and run command`);
|
|
113
115
|
console.log('');
|
|
@@ -212,6 +214,114 @@ function detectProfile(args) {
|
|
|
212
214
|
}
|
|
213
215
|
}
|
|
214
216
|
|
|
217
|
+
// Execute Claude CLI with embedded proxy (for GLMT profile)
|
|
218
|
+
async function execClaudeWithProxy(claudeCli, profileName, args) {
|
|
219
|
+
const { getSettingsPath } = require('./config-manager');
|
|
220
|
+
|
|
221
|
+
// 1. Read settings to get API key
|
|
222
|
+
const settingsPath = getSettingsPath(profileName);
|
|
223
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
224
|
+
const apiKey = settings.env.ANTHROPIC_AUTH_TOKEN;
|
|
225
|
+
|
|
226
|
+
if (!apiKey || apiKey === 'YOUR_GLM_API_KEY_HERE') {
|
|
227
|
+
console.error('[X] GLMT profile requires Z.AI API key');
|
|
228
|
+
console.error(' Edit ~/.ccs/glmt.settings.json and set ANTHROPIC_AUTH_TOKEN');
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Detect verbose flag
|
|
233
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
234
|
+
|
|
235
|
+
// 2. Spawn embedded proxy with verbose flag
|
|
236
|
+
const proxyPath = path.join(__dirname, 'glmt-proxy.js');
|
|
237
|
+
const proxyArgs = verbose ? ['--verbose'] : [];
|
|
238
|
+
const proxy = spawn('node', [proxyPath, ...proxyArgs], {
|
|
239
|
+
stdio: ['ignore', 'pipe', verbose ? 'pipe' : 'inherit']
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// 3. Wait for proxy ready signal (with timeout)
|
|
243
|
+
let port;
|
|
244
|
+
try {
|
|
245
|
+
port = await new Promise((resolve, reject) => {
|
|
246
|
+
const timeout = setTimeout(() => {
|
|
247
|
+
reject(new Error('Proxy startup timeout (5s)'));
|
|
248
|
+
}, 5000);
|
|
249
|
+
|
|
250
|
+
proxy.stdout.on('data', (data) => {
|
|
251
|
+
const match = data.toString().match(/PROXY_READY:(\d+)/);
|
|
252
|
+
if (match) {
|
|
253
|
+
clearTimeout(timeout);
|
|
254
|
+
resolve(parseInt(match[1]));
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
proxy.on('error', (error) => {
|
|
259
|
+
clearTimeout(timeout);
|
|
260
|
+
reject(error);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
proxy.on('exit', (code) => {
|
|
264
|
+
if (code !== 0 && code !== null) {
|
|
265
|
+
clearTimeout(timeout);
|
|
266
|
+
reject(new Error(`Proxy exited with code ${code}`));
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error('[X] Failed to start GLMT proxy:', error.message);
|
|
272
|
+
console.error('');
|
|
273
|
+
console.error('Possible causes:');
|
|
274
|
+
console.error(' 1. Port conflict (unlikely with random port)');
|
|
275
|
+
console.error(' 2. Node.js permission issue');
|
|
276
|
+
console.error(' 3. Firewall blocking localhost');
|
|
277
|
+
console.error('');
|
|
278
|
+
console.error('Workarounds:');
|
|
279
|
+
console.error(' - Use non-thinking mode: ccs glm "prompt"');
|
|
280
|
+
console.error(' - Enable verbose logging: ccs glmt --verbose "prompt"');
|
|
281
|
+
console.error(' - Check proxy logs in ~/.ccs/logs/ (if debug enabled)');
|
|
282
|
+
console.error('');
|
|
283
|
+
proxy.kill();
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 4. Spawn Claude CLI with proxy URL
|
|
288
|
+
const envVars = {
|
|
289
|
+
...process.env,
|
|
290
|
+
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
|
|
291
|
+
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
292
|
+
ANTHROPIC_MODEL: 'glm-4.6'
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const claude = spawn(claudeCli, args, {
|
|
296
|
+
stdio: 'inherit',
|
|
297
|
+
env: envVars
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// 5. Cleanup: kill proxy when Claude exits
|
|
301
|
+
claude.on('exit', (code, signal) => {
|
|
302
|
+
proxy.kill('SIGTERM');
|
|
303
|
+
if (signal) process.kill(process.pid, signal);
|
|
304
|
+
else process.exit(code || 0);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
claude.on('error', (error) => {
|
|
308
|
+
console.error('[X] Claude CLI error:', error);
|
|
309
|
+
proxy.kill('SIGTERM');
|
|
310
|
+
process.exit(1);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Also handle parent process termination (use .once to avoid duplicates)
|
|
314
|
+
process.once('SIGTERM', () => {
|
|
315
|
+
proxy.kill('SIGTERM');
|
|
316
|
+
claude.kill('SIGTERM');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
process.once('SIGINT', () => {
|
|
320
|
+
proxy.kill('SIGTERM');
|
|
321
|
+
claude.kill('SIGTERM');
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
215
325
|
// Main execution
|
|
216
326
|
async function main() {
|
|
217
327
|
const args = process.argv.slice(2);
|
|
@@ -284,10 +394,16 @@ async function main() {
|
|
|
284
394
|
const profileInfo = detector.detectProfileType(profile);
|
|
285
395
|
|
|
286
396
|
if (profileInfo.type === 'settings') {
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
397
|
+
// Check if this is GLMT profile (requires proxy)
|
|
398
|
+
if (profileInfo.name === 'glmt') {
|
|
399
|
+
// GLMT FLOW: Settings-based with embedded proxy for thinking support
|
|
400
|
+
await execClaudeWithProxy(claudeCli, profileInfo.name, remainingArgs);
|
|
401
|
+
} else {
|
|
402
|
+
// EXISTING FLOW: Settings-based profile (glm, kimi)
|
|
403
|
+
// Use --settings flag (backward compatible)
|
|
404
|
+
const expandedSettingsPath = getSettingsPath(profileInfo.name);
|
|
405
|
+
execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs]);
|
|
406
|
+
}
|
|
291
407
|
} else if (profileInfo.type === 'account') {
|
|
292
408
|
// NEW FLOW: Account-based profile (work, personal)
|
|
293
409
|
// All platforms: Use instance isolation with CLAUDE_CONFIG_DIR
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* DeltaAccumulator - Maintain state across streaming deltas
|
|
6
|
+
*
|
|
7
|
+
* Tracks:
|
|
8
|
+
* - Message metadata (id, model, role)
|
|
9
|
+
* - Content blocks (thinking, text)
|
|
10
|
+
* - Current block index
|
|
11
|
+
* - Accumulated content
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const acc = new DeltaAccumulator(thinkingConfig);
|
|
15
|
+
* const events = transformer.transformDelta(openaiEvent, acc);
|
|
16
|
+
*/
|
|
17
|
+
class DeltaAccumulator {
|
|
18
|
+
constructor(thinkingConfig = {}, options = {}) {
|
|
19
|
+
this.thinkingConfig = thinkingConfig;
|
|
20
|
+
this.messageId = 'msg_' + Date.now() + '_' + Math.random().toString(36).substring(7);
|
|
21
|
+
this.model = null;
|
|
22
|
+
this.role = 'assistant';
|
|
23
|
+
|
|
24
|
+
// Content blocks
|
|
25
|
+
this.contentBlocks = [];
|
|
26
|
+
this.currentBlockIndex = -1;
|
|
27
|
+
|
|
28
|
+
// Buffers
|
|
29
|
+
this.thinkingBuffer = '';
|
|
30
|
+
this.textBuffer = '';
|
|
31
|
+
|
|
32
|
+
// C-02 Fix: Limits to prevent unbounded accumulation
|
|
33
|
+
this.maxBlocks = options.maxBlocks || 100;
|
|
34
|
+
this.maxBufferSize = options.maxBufferSize || 10 * 1024 * 1024; // 10MB
|
|
35
|
+
|
|
36
|
+
// State flags
|
|
37
|
+
this.messageStarted = false;
|
|
38
|
+
this.finalized = false;
|
|
39
|
+
|
|
40
|
+
// Statistics
|
|
41
|
+
this.inputTokens = 0;
|
|
42
|
+
this.outputTokens = 0;
|
|
43
|
+
this.finishReason = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get current content block
|
|
48
|
+
* @returns {Object|null} Current block or null
|
|
49
|
+
*/
|
|
50
|
+
getCurrentBlock() {
|
|
51
|
+
if (this.currentBlockIndex >= 0 && this.currentBlockIndex < this.contentBlocks.length) {
|
|
52
|
+
return this.contentBlocks[this.currentBlockIndex];
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Start new content block
|
|
59
|
+
* @param {string} type - Block type ('thinking' or 'text')
|
|
60
|
+
* @returns {Object} New block
|
|
61
|
+
*/
|
|
62
|
+
startBlock(type) {
|
|
63
|
+
// C-02 Fix: Enforce max blocks limit
|
|
64
|
+
if (this.contentBlocks.length >= this.maxBlocks) {
|
|
65
|
+
throw new Error(`Maximum ${this.maxBlocks} content blocks exceeded (DoS protection)`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.currentBlockIndex++;
|
|
69
|
+
const block = {
|
|
70
|
+
index: this.currentBlockIndex,
|
|
71
|
+
type: type,
|
|
72
|
+
content: '',
|
|
73
|
+
started: true,
|
|
74
|
+
stopped: false
|
|
75
|
+
};
|
|
76
|
+
this.contentBlocks.push(block);
|
|
77
|
+
|
|
78
|
+
// Reset buffer for new block
|
|
79
|
+
if (type === 'thinking') {
|
|
80
|
+
this.thinkingBuffer = '';
|
|
81
|
+
} else if (type === 'text') {
|
|
82
|
+
this.textBuffer = '';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return block;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Add delta to current block
|
|
90
|
+
* @param {string} delta - Content delta
|
|
91
|
+
*/
|
|
92
|
+
addDelta(delta) {
|
|
93
|
+
const block = this.getCurrentBlock();
|
|
94
|
+
if (block) {
|
|
95
|
+
if (block.type === 'thinking') {
|
|
96
|
+
// C-02 Fix: Enforce buffer size limit
|
|
97
|
+
if (this.thinkingBuffer.length + delta.length > this.maxBufferSize) {
|
|
98
|
+
throw new Error(`Thinking buffer exceeded ${this.maxBufferSize} bytes (DoS protection)`);
|
|
99
|
+
}
|
|
100
|
+
this.thinkingBuffer += delta;
|
|
101
|
+
block.content = this.thinkingBuffer;
|
|
102
|
+
} else if (block.type === 'text') {
|
|
103
|
+
// C-02 Fix: Enforce buffer size limit
|
|
104
|
+
if (this.textBuffer.length + delta.length > this.maxBufferSize) {
|
|
105
|
+
throw new Error(`Text buffer exceeded ${this.maxBufferSize} bytes (DoS protection)`);
|
|
106
|
+
}
|
|
107
|
+
this.textBuffer += delta;
|
|
108
|
+
block.content = this.textBuffer;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Mark current block as stopped
|
|
115
|
+
*/
|
|
116
|
+
stopCurrentBlock() {
|
|
117
|
+
const block = this.getCurrentBlock();
|
|
118
|
+
if (block) {
|
|
119
|
+
block.stopped = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Update usage statistics
|
|
125
|
+
* @param {Object} usage - Usage object from OpenAI
|
|
126
|
+
*/
|
|
127
|
+
updateUsage(usage) {
|
|
128
|
+
if (usage) {
|
|
129
|
+
this.inputTokens = usage.prompt_tokens || usage.input_tokens || 0;
|
|
130
|
+
this.outputTokens = usage.completion_tokens || usage.output_tokens || 0;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get summary of accumulated state
|
|
136
|
+
* @returns {Object} Summary
|
|
137
|
+
*/
|
|
138
|
+
getSummary() {
|
|
139
|
+
return {
|
|
140
|
+
messageId: this.messageId,
|
|
141
|
+
model: this.model,
|
|
142
|
+
role: this.role,
|
|
143
|
+
blockCount: this.contentBlocks.length,
|
|
144
|
+
currentIndex: this.currentBlockIndex,
|
|
145
|
+
messageStarted: this.messageStarted,
|
|
146
|
+
finalized: this.finalized,
|
|
147
|
+
usage: {
|
|
148
|
+
input_tokens: this.inputTokens,
|
|
149
|
+
output_tokens: this.outputTokens
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = DeltaAccumulator;
|