chatcc-agent 0.3.4 → 0.3.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatcc-agent",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "CCLink Agent - bridges Claude Code CLI with instant messaging",
5
5
  "bin": {
6
6
  "chatcc": "src/cli.js"
@@ -44,8 +44,8 @@
44
44
  "node": ">=16.0.0"
45
45
  },
46
46
  "dependencies": {
47
- "@tencentcloud/chat": "^3.6.6",
48
- "ws": "^8.0.0"
47
+ "@tencentcloud/chat": "3.6.6",
48
+ "ws": "8.20.1"
49
49
  },
50
50
  "devDependencies": {}
51
51
  }
@@ -6,6 +6,7 @@ const os = require('os');
6
6
  const path = require('path');
7
7
  const { StreamBuffer } = require('./stream-buffer');
8
8
  const { ProcessQueue } = require('./process-queue');
9
+ const { PERMISSION_MODES, PERMISSION_ACTIONS } = require('./constants');
9
10
 
10
11
  class ClaudeBridge {
11
12
 
@@ -75,7 +76,7 @@ class ClaudeBridge {
75
76
  }
76
77
 
77
78
  _requiresApproval(toolName, permissions) {
78
- if (permissions?.defaultMode === '自动全部允许') return false;
79
+ if (permissions?.defaultMode === PERMISSION_MODES.AUTO_ALL) return false;
79
80
 
80
81
  const TOOL_CATEGORY = {
81
82
  'Read': 'fileRead', 'read': 'fileRead',
@@ -87,11 +88,11 @@ class ClaudeBridge {
87
88
 
88
89
  // Per-tool setting overrides default mode
89
90
  if (category) {
90
- return (permissions?.[category] || '需要确认') !== '自动允许';
91
+ return (permissions?.[category] || PERMISSION_ACTIONS.CONFIRM) !== PERMISSION_ACTIONS.AUTO_ALLOW;
91
92
  }
92
93
 
93
- // Unknown tools: auto-allow in "自动允许读取" mode, otherwise require approval
94
- return permissions?.defaultMode !== '自动允许读取';
94
+ // Unknown tools: auto-allow in "auto_read" mode, otherwise require approval
95
+ return permissions?.defaultMode !== PERMISSION_MODES.AUTO_READ;
95
96
  }
96
97
 
97
98
  _writeToStdin(proc, msg) {
@@ -298,7 +299,7 @@ class ClaudeBridge {
298
299
 
299
300
  _spawnCompact(replyTo, sessionID, requestID, options) {
300
301
  const { cwd, claudeSessionId, workspaceRestricted, permissions } = options;
301
- const usePermissionControl = permissions?.defaultMode !== '自动全部允许';
302
+ const usePermissionControl = permissions?.defaultMode !== PERMISSION_MODES.AUTO_ALL;
302
303
  const useStreamInput = usePermissionControl;
303
304
  const args = ['--output-format', 'stream-json', '--verbose'];
304
305
 
@@ -403,7 +404,7 @@ class ClaudeBridge {
403
404
 
404
405
  _spawnClaude(msgID, replyTo, content, sessionID, options = {}) {
405
406
  const { cwd, claudeSessionId, images, onClaudeSessionId, onResumeFailed, onSessionTitle, workspaceRestricted = false, permissions = null } = options;
406
- const usePermissionControl = permissions?.defaultMode !== '自动全部允许';
407
+ const usePermissionControl = permissions?.defaultMode !== PERMISSION_MODES.AUTO_ALL;
407
408
  const useStreamInput = usePermissionControl;
408
409
  const args = [
409
410
  '--output-format', 'stream-json',
@@ -582,6 +583,8 @@ class ClaudeBridge {
582
583
  case 'error':
583
584
  this._handleErrorEvent(event, replyTo, msgID, sessionID, streamBuffer, sendStreamEnd);
584
585
  break;
586
+ default:
587
+ console.warn(`[Bridge] Unknown Claude event type: ${event.type}`, JSON.stringify(event).slice(0, 200));
585
588
  }
586
589
  }
587
590
 
@@ -611,6 +614,11 @@ class ClaudeBridge {
611
614
  session_id: sessionID,
612
615
  tool_use_id: block.id,
613
616
  questions: block.input?.questions || [],
617
+ }, {
618
+ title: 'Claude 有问题',
619
+ desc: '需要您的选择',
620
+ ext: JSON.stringify({ session_id: sessionID, push_type: 'agent_reply' }),
621
+ ignoreBadge: false,
614
622
  }).catch(e => console.error('[Bridge] user_question:', e.message));
615
623
  } else {
616
624
  this._handleToolUseBlock(block, replyTo, msgID, streamBuffer, sessionID, cwd, workspaceRestricted, permissions, useControl, sendStreamEnd);
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Permission mode identifiers — used in IM protocol between Agent and iOS.
3
+ * Chinese display labels live on the iOS side only.
4
+ */
5
+
6
+ // Protocol versioning — bump CURRENT when introducing breaking changes.
7
+ // MIN is the oldest client/agent version we still support.
8
+ const CURRENT_PROTOCOL_VERSION = 1;
9
+ const MIN_PROTOCOL_VERSION = 1;
10
+
11
+ // Default permission modes (sent as default_mode in session settings)
12
+ const PERMISSION_MODES = {
13
+ CONFIRM_EVERY: 'confirm_every', // 每次确认
14
+ AUTO_READ: 'auto_read', // 自动允许读取
15
+ AUTO_ALL: 'auto_all', // 自动全部允许
16
+ };
17
+
18
+ // Per-category permission actions (sent as file_read / file_edit / code_exec)
19
+ const PERMISSION_ACTIONS = {
20
+ AUTO_ALLOW: 'auto_allow', // 自动允许
21
+ CONFIRM: 'confirm', // 需要确认
22
+ };
23
+
24
+ // Map legacy Chinese strings to new identifiers for backward compatibility
25
+ const LEGACY_MODE_MAP = {
26
+ '每次确认': PERMISSION_MODES.CONFIRM_EVERY,
27
+ '自动允许读取': PERMISSION_MODES.AUTO_READ,
28
+ '自动全部允许': PERMISSION_MODES.AUTO_ALL,
29
+ '自动允许': PERMISSION_ACTIONS.AUTO_ALLOW,
30
+ '需要确认': PERMISSION_ACTIONS.CONFIRM,
31
+ };
32
+
33
+ /**
34
+ * Normalize a permission value — accepts both new identifiers and legacy Chinese strings.
35
+ * Passes through unknown values (already in new format or custom).
36
+ */
37
+ function normalizePermission(value) {
38
+ if (!value) return value;
39
+ return LEGACY_MODE_MAP[value] || value;
40
+ }
41
+
42
+ module.exports = { CURRENT_PROTOCOL_VERSION, MIN_PROTOCOL_VERSION, PERMISSION_MODES, PERMISSION_ACTIONS, LEGACY_MODE_MAP, normalizePermission };
package/src/im-client.js CHANGED
@@ -1,10 +1,8 @@
1
1
  const WebSocket = require('ws');
2
2
  global.WebSocket = WebSocket;
3
3
 
4
- // Save original console references at module level (never changes)
5
- const _origLog = console.log;
6
- const _origWarn = console.warn;
7
- const _origErr = console.error;
4
+ const { CURRENT_PROTOCOL_VERSION, MIN_PROTOCOL_VERSION } = require('./constants');
5
+ const PROTOCOL_VERSION = CURRENT_PROTOCOL_VERSION;
8
6
 
9
7
  class IMClient {
10
8
  constructor(sdkAppID, userID, userSig, options = {}) {
@@ -15,19 +13,10 @@ class IMClient {
15
13
  this._onFatal = options.onFatal || (() => process.exit(1));
16
14
  this._onUserSigExpired = options.onUserSigExpired || null;
17
15
 
18
- // Suppress SDK noise only during require/create, restore immediately
19
- console.log = () => {};
20
- console.warn = () => {};
21
- console.error = () => {};
22
-
23
16
  const TIM = require('@tencentcloud/chat');
24
17
  this.TIM = TIM;
25
18
  this.tim = TIM.create({ SDKAppID: sdkAppID });
26
- this.tim.setLogLevel(4);
27
-
28
- console.log = _origLog;
29
- console.warn = _origWarn;
30
- console.error = _origErr;
19
+ this.tim.setLogLevel(4); // Suppress all SDK noise
31
20
 
32
21
  this._messageHandlers = [];
33
22
 
@@ -48,9 +37,9 @@ class IMClient {
48
37
 
49
38
  this.tim.on(TIM.EVENT.NET_STATE_CHANGE, (event) => {
50
39
  if (event.data.state === this.TIM.NET_STATE_DISCONNECTED) {
51
- _origLog('[IM] Network disconnected');
40
+ console.log('[IM] Network disconnected');
52
41
  } else if (event.data.state === this.TIM.NET_STATE_CONNECTED) {
53
- _origLog('[IM] Network connected');
42
+ console.log('[IM] Network connected');
54
43
  }
55
44
  });
56
45
 
@@ -64,7 +53,7 @@ class IMClient {
64
53
  this._lastKickoutTimes = this._lastKickoutTimes.filter(t => now - t < 60000);
65
54
 
66
55
  if (reason === this.TIM.KICKED_OUT_MULTIPLE_ACCOUNT) {
67
- _origLog('[IM] Kicked out: UserSig expired, requesting refresh...');
56
+ console.log('[IM] Kicked out: UserSig expired, requesting refresh...');
68
57
  if (this._onUserSigExpired) {
69
58
  this._onUserSigExpired().then(newSig => {
70
59
  if (newSig) {
@@ -73,7 +62,7 @@ class IMClient {
73
62
  this._reconnect();
74
63
  }
75
64
  }).catch(e => {
76
- _origLog('[IM] UserSig refresh failed:', e.message);
65
+ console.log('[IM] UserSig refresh failed:', e.message);
77
66
  });
78
67
  return;
79
68
  }
@@ -82,7 +71,7 @@ class IMClient {
82
71
  // Detect kickout loop: 5+ kickouts in 60s means something is wrong
83
72
  if (this._lastKickoutTimes.length >= 5) {
84
73
  const backoff = 15000;
85
- _origLog(`[IM] Kickout loop detected (${this._lastKickoutTimes.length} in 60s). Cooling down ${backoff/1000}s...`);
74
+ console.log(`[IM] Kickout loop detected (${this._lastKickoutTimes.length} in 60s). Cooling down ${backoff/1000}s...`);
86
75
  this._lastKickoutTimes = [];
87
76
  setTimeout(() => {
88
77
  this._reconnectAttempts = 0;
@@ -91,10 +80,10 @@ class IMClient {
91
80
  return;
92
81
  }
93
82
 
94
- _origLog('[IM] Kicked out! Attempting reconnect...');
83
+ console.log('[IM] Kicked out! Attempting reconnect...');
95
84
  this._reconnectAttempts++;
96
85
  if (this._reconnectAttempts > this._maxReconnectAttempts) {
97
- _origLog('[IM] Max reconnect attempts reached. Waiting before retry...');
86
+ console.log('[IM] Max reconnect attempts reached. Waiting before retry...');
98
87
  this._reconnectAttempts = this._maxReconnectAttempts - 3;
99
88
  }
100
89
  const delay = Math.min(2000 * this._reconnectAttempts, 30000);
@@ -122,15 +111,15 @@ class IMClient {
122
111
 
123
112
  async _reconnect() {
124
113
  try {
125
- _origLog(`[IM] Reconnecting (attempt ${this._reconnectAttempts}/${this._maxReconnectAttempts})...`);
114
+ console.log(`[IM] Reconnecting (attempt ${this._reconnectAttempts}/${this._maxReconnectAttempts})...`);
126
115
  try { await this.tim.logout(); } catch {}
127
116
  await this.login();
128
- _origLog('[IM] Reconnected successfully');
117
+ console.log('[IM] Reconnected successfully');
129
118
  } catch (e) {
130
- _origLog('[IM] Reconnect failed:', e.message);
119
+ console.log('[IM] Reconnect failed:', e.message);
131
120
  this._reconnectAttempts++;
132
121
  if (this._reconnectAttempts > this._maxReconnectAttempts) {
133
- _origLog('[IM] Max reconnect attempts reached. Backing off...');
122
+ console.log('[IM] Max reconnect attempts reached. Backing off...');
134
123
  this._reconnectAttempts = this._maxReconnectAttempts - 3;
135
124
  }
136
125
  const delay = Math.min(2000 * this._reconnectAttempts, 30000);
@@ -146,18 +135,33 @@ class IMClient {
146
135
  this._messageHandlers.push(handler);
147
136
  }
148
137
 
149
- async sendCustomMessage(to, ccType, payload = {}) {
138
+ async sendCustomMessage(to, ccType, payload = {}, pushOptions = null) {
150
139
  const message = this.tim.createCustomMessage({
151
140
  to,
152
141
  conversationType: this.TIM.TYPES.CONV_C2C,
153
142
  payload: {
154
- data: JSON.stringify({ cc_type: ccType, ...payload }),
143
+ data: JSON.stringify({ cc_type: ccType, v: PROTOCOL_VERSION, min_v: MIN_PROTOCOL_VERSION, ...payload }),
155
144
  description: 'CCLink',
156
145
  },
157
146
  });
158
- await this.tim.sendMessage(message);
147
+
148
+ // Default: silent (disablePush). Only messages with explicit pushOptions trigger notifications.
149
+ const sendOpts = {};
150
+ if (pushOptions) {
151
+ sendOpts.offlinePushInfo = {
152
+ title: pushOptions.title || 'CCLink',
153
+ description: pushOptions.desc || '',
154
+ extension: pushOptions.ext || '',
155
+ disablePush: false,
156
+ apnsInfo: { ignoreIOSBadge: pushOptions.ignoreBadge ?? false },
157
+ };
158
+ } else {
159
+ sendOpts.offlinePushInfo = { disablePush: true };
160
+ }
161
+
162
+ await this.tim.sendMessage(message, sendOpts);
159
163
  return message.ID;
160
164
  }
161
165
  }
162
166
 
163
- module.exports = { IMClient };
167
+ module.exports = { IMClient, PROTOCOL_VERSION };
package/src/index.js CHANGED
@@ -133,11 +133,34 @@ async function shutdown(signal) {
133
133
 
134
134
  process.on('exit', cleanupTempFiles);
135
135
 
136
+ async function checkForUpdates(agentID, pairedClientIDs) {
137
+ try {
138
+ const { execSync } = require('child_process');
139
+ const currentVersion = getVersion();
140
+ const latestVersion = execSync('npm view chatcc-agent version', {
141
+ encoding: 'utf-8', timeout: 15000,
142
+ }).trim();
143
+ if (latestVersion && latestVersion !== currentVersion) {
144
+ console.log(`[Update] New version available: ${currentVersion} -> ${latestVersion}`);
145
+ for (const clientID of pairedClientIDs) {
146
+ imClient.sendCustomMessage(clientID, 'update_available', {
147
+ current_version: currentVersion,
148
+ latest_version: latestVersion,
149
+ }).catch(() => {});
150
+ }
151
+ }
152
+ } catch (e) {
153
+ // Non-fatal: network issues, npm unavailable, etc.
154
+ console.log('[Update] Check failed:', e.message);
155
+ }
156
+ }
157
+
136
158
  async function main() {
137
159
  // Pre-flight: check Claude CLI is available
138
160
  const { execFileSync } = require('child_process');
139
161
  try {
140
- execFileSync('claude', ['--version'], { stdio: 'pipe' });
162
+ const versionOutput = execFileSync('claude', ['--version'], { encoding: 'utf-8', stdio: 'pipe' }).trim();
163
+ console.log('[Preflight] Claude CLI version:', versionOutput);
141
164
  } catch {
142
165
  console.error('[Fatal] Claude CLI not found. Install it first: npm install -g @anthropic-ai/claude-code');
143
166
  process.exit(1);
@@ -207,6 +230,13 @@ async function main() {
207
230
  }
208
231
  }
209
232
 
233
+ // Auto-update check: on startup and every 24h
234
+ checkForUpdates(config.agentUserID, pairedClientIDs);
235
+ const updateInterval = setInterval(() => {
236
+ checkForUpdates(config.agentUserID, pairedClientIDs);
237
+ }, 24 * 60 * 60 * 1000);
238
+ updateInterval.unref();
239
+
210
240
  // Setup services
211
241
  sessions = new SessionStore(path.join(CHATCC_DIR, 'sessions.json'), {
212
242
  maxSessions: MAX_SESSIONS,
@@ -231,6 +261,11 @@ async function main() {
231
261
  request_id: reqId,
232
262
  path: filePath,
233
263
  operation,
264
+ }, {
265
+ title: '权限请求',
266
+ desc: `${operation}: ${filePath.length > 40 ? '...' + filePath.slice(-37) : filePath}`,
267
+ ext: JSON.stringify({ push_type: 'tool_confirm' }),
268
+ ignoreBadge: false,
234
269
  }).catch(() => {});
235
270
  const timer = setTimeout(() => {
236
271
  fileService._pendingPermissions.delete(reqId);
@@ -408,6 +443,26 @@ async function main() {
408
443
  break;
409
444
  }
410
445
 
446
+ case 'clear_request': {
447
+ const sid = data.session_id;
448
+ const sess = sessions.get(sid) || null;
449
+ if (!sess || sess.clientID !== from) {
450
+ imClient.sendCustomMessage(from, 'clear_response', {
451
+ request_id: data.request_id, success: false, error: '会话不存在或无权限',
452
+ }).catch(() => {});
453
+ break;
454
+ }
455
+ // Clear Claude session so next message starts fresh
456
+ sess.claudeSessionId = null;
457
+ sess.lastActiveAt = Date.now();
458
+ sessions.set(sid, sess);
459
+ console.log(`[Session] Clear context for ${sid}, claudeSessionId reset`);
460
+ imClient.sendCustomMessage(from, 'clear_response', {
461
+ request_id: data.request_id, success: true,
462
+ }).catch(e => console.error('[Clear] Reply failed:', e.message));
463
+ break;
464
+ }
465
+
411
466
  case 'compact_request': {
412
467
  const sid = data.session_id;
413
468
  const sess = sessions.get(sid) || null;
@@ -476,7 +531,11 @@ async function main() {
476
531
  }
477
532
 
478
533
  default:
479
- console.log('[Message] Unknown cc_type:', data.cc_type);
534
+ console.log('[Message] Unknown cc_type:', data.cc_type, 'v=', data.v || 0);
535
+ imClient.sendCustomMessage(from, 'unknown_type_error', {
536
+ original_type: data.cc_type,
537
+ message: `Unknown cc_type: ${data.cc_type}`,
538
+ }).catch(() => {});
480
539
  }
481
540
  });
482
541
 
package/src/security.js CHANGED
@@ -163,11 +163,12 @@ function validateTerminalCommand(input) {
163
163
 
164
164
  function buildPermissions(data) {
165
165
  if (!data) return null;
166
+ const { normalizePermission, PERMISSION_MODES, PERMISSION_ACTIONS } = require('./constants');
166
167
  return {
167
- defaultMode: data.default_mode || '每次确认',
168
- fileRead: data.file_read || '自动允许',
169
- fileEdit: data.file_edit || '需要确认',
170
- codeExec: data.code_exec || '需要确认',
168
+ defaultMode: normalizePermission(data.default_mode || PERMISSION_MODES.CONFIRM_EVERY),
169
+ fileRead: normalizePermission(data.file_read || PERMISSION_ACTIONS.AUTO_ALLOW),
170
+ fileEdit: normalizePermission(data.file_edit || PERMISSION_ACTIONS.CONFIRM),
171
+ codeExec: normalizePermission(data.code_exec || PERMISSION_ACTIONS.CONFIRM),
171
172
  };
172
173
  }
173
174
 
@@ -2,6 +2,7 @@ const os = require('os');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { getVersion } = require('./version');
5
+ const { CURRENT_PROTOCOL_VERSION, MIN_PROTOCOL_VERSION } = require('./constants');
5
6
 
6
7
  function collectServerMeta() {
7
8
  const suggestedWorkspaces = [];
@@ -23,6 +24,8 @@ function collectServerMeta() {
23
24
  cpuCount: os.cpus().length,
24
25
  memoryGB: Math.round(os.totalmem() / 1024 / 1024 / 1024),
25
26
  agentVersion: getVersion(),
27
+ protocol_version: CURRENT_PROTOCOL_VERSION,
28
+ min_protocol_version: MIN_PROTOCOL_VERSION,
26
29
  suggestedWorkspaces: suggestedWorkspaces,
27
30
  };
28
31
  }