@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 CHANGED
@@ -30,7 +30,7 @@ Stop hitting rate limits. Keep working continuously.
30
30
  claude /login
31
31
  ```
32
32
 
33
- ### Primary Installation Methods
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, you need to update your API keys in their respective settings files:
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
- # Use Claude subscription (default) for high-level planning
115
- ccs "Plan the implementation of a microservices architecture"
116
+ # Default Claude subscription
117
+ ccs "Plan microservices architecture"
116
118
 
117
- # Switch to GLM for cost-optimized tasks
118
- ccs glm "Create a simple REST API"
119
+ # Switch to GLM (cost-optimized)
120
+ ccs glm "Create REST API"
119
121
 
120
- # Switch to Kimi for its thinking capabilities
121
- ccs kimi "Write integration tests with proper error handling"
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
- ## 📁 Shared Data Architecture
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
- **v3.1 Shared Global Data**: Commands and skills are symlinked across all profiles via `~/.ccs/shared/`, eliminating duplication.
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
- │ ├── commands/ # Custom slash commands
175
- └── skills/ # Claude Code skills
178
+ │ ├── agents/
179
+ ├── commands/
180
+ │ └── skills/
176
181
  ├── instances/ # Profile-specific data
177
- ├── work/
178
- ├── commands@ → ~/.ccs/shared/commands/ # Symlink
179
- ├── skills@ → ~/.ccs/shared/skills/ # Symlink
180
- ├── settings.json # Profile-specific config
181
- │ └── sessions/ # Profile-specific sessions
182
- └── personal/
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
- **Benefits**:
187
- - No duplication of commands/skills across profiles
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
- **What's Shared**:
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
- ## 🏗️ Architecture Overview
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
- **v3.0 Login-Per-Profile Model**: Each profile is an isolated Claude instance where users login directly. No credential copying or vault encryption.
202
+ ### GLM vs GLMT
207
203
 
208
- ```mermaid
209
- flowchart TD
210
- subgraph "User Input"
211
- USER["User runs: ccs &lt;profile&gt; [args...]"]
212
- end
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
- subgraph "Profile Detection Engine"
215
- DETECT[ProfileDetector]
216
- PROFILE_CHECK{Profile exists?}
212
+ ### Streaming Support (v3.4)
217
213
 
218
- subgraph "Profile Types"
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
- subgraph "CCS Core Processing"
225
- CONFIG["Read config.json<br/>and profiles.json"]
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
- subgraph "Profile Handlers"
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
- subgraph "Claude CLI Execution"
234
- CLAUDE_DETECT["Claude CLI Detection<br/>CCS_CLAUDE_PATH support"]
222
+ ### How It Works
235
223
 
236
- subgraph "Execution Methods"
237
- SETTINGS_EXEC["claude --settings &lt;path&gt;"]
238
- INSTANCE_EXEC["CLAUDE_CONFIG_DIR=&lt;instance&gt; claude"]
239
- end
240
- end
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
- subgraph "API Layer"
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
- %% Flow connections
247
- USER --> DETECT
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
- SETTINGS --> CONFIG
253
- ACCOUNT --> CONFIG
235
+ ### API Key Setup
254
236
 
255
- CONFIG --> SETTINGS_MGR
256
- CONFIG --> INSTANCE_MGR
237
+ ```bash
238
+ # Edit GLMT settings
239
+ nano ~/.ccs/glmt.settings.json
257
240
 
258
- SETTINGS_MGR --> SETTINGS_EXEC
259
- INSTANCE_MGR --> INSTANCE_EXEC
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
- SETTINGS_EXEC --> CLAUDE_DETECT
262
- INSTANCE_EXEC --> CLAUDE_DETECT
249
+ ### Security Limits
263
250
 
264
- CLAUDE_DETECT --> API
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
- ## Features
271
+ **Check streaming mode**:
272
+ ```bash
273
+ # Disable streaming for debugging
274
+ CCS_GLMT_STREAMING=disabled ccs glmt "test"
275
+ ```
270
276
 
271
- - **Instant Switching** - `ccs glm` switches to GLM, no config editing
272
- - **Concurrent Sessions** - Run multiple profiles simultaneously in different terminals
273
- - **Isolated Instances** - Each profile gets own config (`~/.ccs/instances/<profile>/`)
274
- - **Cross-Platform** - macOS, Linux, Windows - identical behavior
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
- ## 💻 Usage Examples
287
+ ## Usage Examples
281
288
 
282
- ### Basic Profile Switching
289
+ ### Basic Switching
283
290
  ```bash
284
- ccs # Use Claude subscription (default)
285
- ccs glm # Use GLM fallback
286
- ccs kimi # Use Kimi for Coding
287
- ccs --version # Show CCS version and install location
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
- ### Concurrent Sessions (Multi-Account)
298
+ ### Multi-Account Setup
291
299
  ```bash
292
- # Create multiple Claude accounts
293
- ccs auth create work # Company account
294
- ccs auth create personal # Personal account
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 - Work account
304
+ # Terminal 1
298
305
  ccs work "implement feature"
299
306
 
300
- # Terminal 2 - Personal account (runs concurrently)
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
- ### 🗑️ Uninstall
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
- **Windows PowerShell**
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.2.0
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
- // EXISTING FLOW: Settings-based profile (glm, kimi)
288
- // Use --settings flag (backward compatible)
289
- const expandedSettingsPath = getSettingsPath(profileInfo.name);
290
- execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs]);
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;