@kaitranntt/ccs 3.3.0 → 3.4.1

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,80 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * LocaleEnforcer - Force English output from GLM models
6
+ *
7
+ * Purpose: GLM models default to Chinese when prompts are ambiguous or contain Chinese context.
8
+ * This module injects "MUST respond in English" instruction into system prompt or first user message.
9
+ *
10
+ * Usage:
11
+ * const enforcer = new LocaleEnforcer({ forceEnglish: true });
12
+ * const modifiedMessages = enforcer.injectInstruction(messages);
13
+ *
14
+ * Configuration:
15
+ * CCS_GLMT_FORCE_ENGLISH=false - Disable locale enforcement (allow multilingual)
16
+ *
17
+ * Strategy:
18
+ * 1. If system prompt exists: Prepend instruction
19
+ * 2. If no system prompt: Prepend to first user message
20
+ * 3. Preserve message structure (string vs array content)
21
+ */
22
+ class LocaleEnforcer {
23
+ constructor(options = {}) {
24
+ this.forceEnglish = options.forceEnglish ?? true;
25
+ this.instruction = "CRITICAL: You MUST respond in English only, regardless of the input language or context. This is a strict requirement.";
26
+ }
27
+
28
+ /**
29
+ * Inject English instruction into messages
30
+ * @param {Array} messages - Messages array to modify
31
+ * @returns {Array} Modified messages array
32
+ */
33
+ injectInstruction(messages) {
34
+ if (!this.forceEnglish) {
35
+ return messages;
36
+ }
37
+
38
+ // Clone messages to avoid mutation
39
+ const modifiedMessages = JSON.parse(JSON.stringify(messages));
40
+
41
+ // Strategy 1: Inject into system prompt (preferred)
42
+ const systemIndex = modifiedMessages.findIndex(m => m.role === 'system');
43
+ if (systemIndex >= 0) {
44
+ const systemMsg = modifiedMessages[systemIndex];
45
+
46
+ if (typeof systemMsg.content === 'string') {
47
+ systemMsg.content = `${this.instruction}\n\n${systemMsg.content}`;
48
+ } else if (Array.isArray(systemMsg.content)) {
49
+ systemMsg.content.unshift({
50
+ type: 'text',
51
+ text: this.instruction
52
+ });
53
+ }
54
+
55
+ return modifiedMessages;
56
+ }
57
+
58
+ // Strategy 2: Prepend to first user message
59
+ const userIndex = modifiedMessages.findIndex(m => m.role === 'user');
60
+ if (userIndex >= 0) {
61
+ const userMsg = modifiedMessages[userIndex];
62
+
63
+ if (typeof userMsg.content === 'string') {
64
+ userMsg.content = `${this.instruction}\n\n${userMsg.content}`;
65
+ } else if (Array.isArray(userMsg.content)) {
66
+ userMsg.content.unshift({
67
+ type: 'text',
68
+ text: this.instruction
69
+ });
70
+ }
71
+
72
+ return modifiedMessages;
73
+ }
74
+
75
+ // No system or user messages found (edge case)
76
+ return modifiedMessages;
77
+ }
78
+ }
79
+
80
+ module.exports = LocaleEnforcer;
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * SSEParser - Parse Server-Sent Events (SSE) stream
6
+ *
7
+ * Handles:
8
+ * - Incomplete events across chunks
9
+ * - Multiple events in single chunk
10
+ * - Malformed data (skip gracefully)
11
+ * - [DONE] marker
12
+ *
13
+ * Usage:
14
+ * const parser = new SSEParser();
15
+ * stream.on('data', chunk => {
16
+ * const events = parser.parse(chunk);
17
+ * events.forEach(event => { ... });
18
+ * });
19
+ */
20
+ class SSEParser {
21
+ constructor(options = {}) {
22
+ this.buffer = '';
23
+ this.eventCount = 0;
24
+ this.maxBufferSize = options.maxBufferSize || 1024 * 1024; // 1MB default
25
+ }
26
+
27
+ /**
28
+ * Parse chunk and extract SSE events
29
+ * @param {Buffer|string} chunk - Data chunk from stream
30
+ * @returns {Array<Object>} Array of parsed events
31
+ */
32
+ parse(chunk) {
33
+ this.buffer += chunk.toString();
34
+
35
+ // C-01 Fix: Prevent unbounded buffer growth (DoS protection)
36
+ if (this.buffer.length > this.maxBufferSize) {
37
+ throw new Error(`SSE buffer exceeded ${this.maxBufferSize} bytes (DoS protection)`);
38
+ }
39
+
40
+ const lines = this.buffer.split('\n');
41
+
42
+ // Keep incomplete line in buffer
43
+ this.buffer = lines.pop() || '';
44
+
45
+ const events = [];
46
+ let currentEvent = { event: 'message', data: '' };
47
+
48
+ for (const line of lines) {
49
+ if (line.startsWith('event: ')) {
50
+ currentEvent.event = line.substring(7).trim();
51
+ } else if (line.startsWith('data: ')) {
52
+ const data = line.substring(6);
53
+
54
+ if (data === '[DONE]') {
55
+ this.eventCount++;
56
+ events.push({
57
+ event: 'done',
58
+ data: null,
59
+ index: this.eventCount
60
+ });
61
+ currentEvent = { event: 'message', data: '' };
62
+ } else {
63
+ try {
64
+ currentEvent.data = JSON.parse(data);
65
+ this.eventCount++;
66
+ currentEvent.index = this.eventCount;
67
+ events.push(currentEvent);
68
+ currentEvent = { event: 'message', data: '' };
69
+ } catch (e) {
70
+ // H-01 Fix: Log parse errors for debugging
71
+ if (typeof console !== 'undefined' && console.error) {
72
+ console.error('[SSEParser] Malformed JSON event:', e.message, 'Data:', data.substring(0, 100));
73
+ }
74
+ }
75
+ }
76
+ } else if (line.startsWith('id: ')) {
77
+ currentEvent.id = line.substring(4).trim();
78
+ } else if (line.startsWith('retry: ')) {
79
+ currentEvent.retry = parseInt(line.substring(7), 10);
80
+ }
81
+ // Empty lines separate events (already handled by JSON parsing)
82
+ }
83
+
84
+ return events;
85
+ }
86
+
87
+ /**
88
+ * Reset parser state (for reuse)
89
+ */
90
+ reset() {
91
+ this.buffer = '';
92
+ this.eventCount = 0;
93
+ }
94
+ }
95
+
96
+ module.exports = SSEParser;
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * TaskClassifier - Classify user prompts as reasoning, execution, or mixed tasks
6
+ *
7
+ * Purpose: Determine task type to inform thinking enable/disable decision.
8
+ * Uses keyword-based matching for fast, deterministic classification.
9
+ *
10
+ * Usage:
11
+ * const classifier = new TaskClassifier();
12
+ * const taskType = classifier.classify(messages);
13
+ *
14
+ * Task types:
15
+ * - reasoning: Planning, design, analysis (enable thinking)
16
+ * - execution: Implementation, fixes, debugging (disable thinking for speed)
17
+ * - mixed: Ambiguous or both (default to safe thinking mode)
18
+ *
19
+ * Classification strategy:
20
+ * 1. Extract text from all user messages
21
+ * 2. Score against reasoning and execution keyword lists
22
+ * 3. Return type with highest score (or 'mixed' if tied/no matches)
23
+ */
24
+ class TaskClassifier {
25
+ constructor(options = {}) {
26
+ this.keywords = {
27
+ reasoning: [
28
+ 'plan', 'design', 'analyze', 'architecture', 'strategy',
29
+ 'approach', 'consider', 'evaluate', 'research', 'explore',
30
+ 'brainstorm', 'think about', 'pros and cons', 'alternatives',
31
+ 'compare', 'recommend', 'assess', 'review', 'investigate'
32
+ ],
33
+ execution: [
34
+ 'fix', 'implement', 'debug', 'refactor', 'optimize',
35
+ 'add', 'remove', 'update', 'create', 'delete',
36
+ 'change', 'modify', 'replace', 'move', 'rename',
37
+ 'test', 'run', 'execute', 'deploy', 'build'
38
+ ]
39
+ };
40
+
41
+ // Allow custom keywords via options
42
+ if (options.customKeywords) {
43
+ this.keywords = { ...this.keywords, ...options.customKeywords };
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Classify messages as reasoning, execution, or mixed
49
+ * @param {Array} messages - Messages array
50
+ * @returns {string} 'reasoning', 'execution', or 'mixed'
51
+ */
52
+ classify(messages) {
53
+ if (!messages || messages.length === 0) {
54
+ return 'mixed'; // Default to safe mode
55
+ }
56
+
57
+ // Extract text from all user messages
58
+ const text = messages
59
+ .filter(m => m.role === 'user')
60
+ .map(m => this._extractText(m.content))
61
+ .join(' ')
62
+ .toLowerCase();
63
+
64
+ if (!text.trim()) {
65
+ return 'mixed'; // No text found
66
+ }
67
+
68
+ // Score against keyword lists
69
+ const reasoningScore = this._matchScore(text, this.keywords.reasoning);
70
+ const executionScore = this._matchScore(text, this.keywords.execution);
71
+
72
+ // Classify based on scores
73
+ if (reasoningScore > executionScore) {
74
+ return 'reasoning';
75
+ } else if (executionScore > reasoningScore) {
76
+ return 'execution';
77
+ } else {
78
+ return 'mixed'; // Tied or no matches
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Extract text from message content
84
+ * @param {string|Array} content - Message content
85
+ * @returns {string} Extracted text
86
+ * @private
87
+ */
88
+ _extractText(content) {
89
+ if (typeof content === 'string') {
90
+ return content;
91
+ }
92
+
93
+ if (Array.isArray(content)) {
94
+ return content
95
+ .filter(block => block.type === 'text')
96
+ .map(block => block.text || '')
97
+ .join(' ');
98
+ }
99
+
100
+ return '';
101
+ }
102
+
103
+ /**
104
+ * Calculate keyword match score
105
+ * @param {string} text - Text to search
106
+ * @param {Array} keywords - Keywords to match
107
+ * @returns {number} Number of matches
108
+ * @private
109
+ */
110
+ _matchScore(text, keywords) {
111
+ return keywords.reduce((score, keyword) => {
112
+ // Support both exact match and word boundary match
113
+ const regex = new RegExp(`\\b${this._escapeRegex(keyword)}\\b`, 'i');
114
+ return score + (regex.test(text) ? 1 : 0);
115
+ }, 0);
116
+ }
117
+
118
+ /**
119
+ * Escape special regex characters
120
+ * @param {string} str - String to escape
121
+ * @returns {string} Escaped string
122
+ * @private
123
+ */
124
+ _escapeRegex(str) {
125
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
126
+ }
127
+
128
+ /**
129
+ * Get classification details (for debugging)
130
+ * @param {Array} messages - Messages array
131
+ * @returns {Object} { type, reasoningScore, executionScore, text }
132
+ */
133
+ classifyWithDetails(messages) {
134
+ const text = messages
135
+ .filter(m => m.role === 'user')
136
+ .map(m => this._extractText(m.content))
137
+ .join(' ')
138
+ .toLowerCase();
139
+
140
+ const reasoningScore = this._matchScore(text, this.keywords.reasoning);
141
+ const executionScore = this._matchScore(text, this.keywords.execution);
142
+
143
+ let type;
144
+ if (reasoningScore > executionScore) {
145
+ type = 'reasoning';
146
+ } else if (executionScore > reasoningScore) {
147
+ type = 'execution';
148
+ } else {
149
+ type = 'mixed';
150
+ }
151
+
152
+ return {
153
+ type,
154
+ reasoningScore,
155
+ executionScore,
156
+ textLength: text.length,
157
+ textPreview: text.substring(0, 100) + (text.length > 100 ? '...' : '')
158
+ };
159
+ }
160
+ }
161
+
162
+ module.exports = TaskClassifier;
@@ -4,8 +4,8 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
6
  const { spawn } = require('child_process');
7
- const { colored } = require('./helpers');
8
- const { detectClaudeCli } = require('./claude-detector');
7
+ const { colored } = require('../utils/helpers');
8
+ const { detectClaudeCli } = require('../utils/claude-detector');
9
9
 
10
10
  /**
11
11
  * Health check results
package/lib/ccs CHANGED
@@ -2,7 +2,7 @@
2
2
  set -euo pipefail
3
3
 
4
4
  # Version (updated by scripts/bump-version.sh)
5
- CCS_VERSION="3.3.0"
5
+ CCS_VERSION="3.4.1"
6
6
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
7
  readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
8
8
  readonly PROFILES_JSON="$HOME/.ccs/profiles.json"
package/lib/ccs.ps1 CHANGED
@@ -12,7 +12,7 @@ param(
12
12
  $ErrorActionPreference = "Stop"
13
13
 
14
14
  # Version (updated by scripts/bump-version.sh)
15
- $CcsVersion = "3.3.0"
15
+ $CcsVersion = "3.4.1"
16
16
  $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
17
17
  $ConfigFile = if ($env:CCS_CONFIG) { $env:CCS_CONFIG } else { "$env:USERPROFILE\.ccs\config.json" }
18
18
  $ProfilesJson = "$env:USERPROFILE\.ccs\profiles.json"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaitranntt/ccs",
3
- "version": "3.3.0",
3
+ "version": "3.4.1",
4
4
  "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,35 @@
1
+ #!/bin/bash
2
+ # Auto-install CCS locally for testing changes
3
+
4
+ set -e
5
+
6
+ echo "[CCS Dev Install] Starting..."
7
+
8
+ # Get to the right directory
9
+ cd "$(dirname "$0")/.."
10
+
11
+ # Pack the npm package
12
+ echo "[CCS Dev Install] Creating package..."
13
+ npm pack
14
+
15
+ # Find the tarball
16
+ TARBALL=$(ls -t kaitranntt-ccs-*.tgz | head -1)
17
+
18
+ if [ -z "$TARBALL" ]; then
19
+ echo "[CCS Dev Install] ERROR: No tarball found"
20
+ exit 1
21
+ fi
22
+
23
+ echo "[CCS Dev Install] Found tarball: $TARBALL"
24
+
25
+ # Install globally
26
+ echo "[CCS Dev Install] Installing globally..."
27
+ npm install -g "$TARBALL"
28
+
29
+ # Clean up
30
+ echo "[CCS Dev Install] Cleaning up..."
31
+ rm "$TARBALL"
32
+
33
+ echo "[CCS Dev Install] ✓ Complete! CCS is now updated."
34
+ echo ""
35
+ echo "Test with: ccs glmt --version"