@raolin2025/claude-code-node 2.2.7 → 2.2.9

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": "@raolin2025/claude-code-node",
3
- "version": "2.2.7",
3
+ "version": "2.2.9",
4
4
  "description": "Node.js AI Code Agent CLI - Zero dependencies, pure JavaScript, security hardened, multi-channel notifications",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/core/cli.js CHANGED
@@ -116,14 +116,102 @@ function startSocketServer(engine, session, sessionManager, channelManager, verb
116
116
  // Banner & Help
117
117
  // ============================================================
118
118
 
119
- const BANNER = `
120
- ╔═══════════════════════════════════════════════╗
121
- ║ AI Code Agent — Node.js Edition ║
122
- ║ OpenAI-Compatible · DeepSeek Default ║
123
- ║ Type '/help' for commands ║
124
- ║ Type '/exit' or Ctrl+C to quit ║
125
- ╚═══════════════════════════════════════════════╝
126
- `.trim()
119
+ // ============================================================
120
+ // Banner & Help
121
+ // ============================================================
122
+
123
+ // 版本号(从 package.json 读取或手动更新)
124
+ let CC_NODE_VERSION = '2.2.7'
125
+ try {
126
+ const pkgPath = new URL('../../package.json', import.meta.url)
127
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
128
+ CC_NODE_VERSION = pkg.version || CC_NODE_VERSION
129
+ } catch {}
130
+
131
+ // 生成 Claude Code 风格的三栏 Banner
132
+ function buildBanner({ model, permissionMode, session, maxTokens }) {
133
+ const width = 112
134
+ const inner = width - 2
135
+ const leftLabel = ` CC-Node v${CC_NODE_VERSION} `
136
+ const top = `╭${leftLabel}${'─'.repeat(inner - leftLabel.length)}╮`
137
+ const bottom = `╰${'─'.repeat(inner)}╯`
138
+
139
+ const col1 = 30 // 机器人列
140
+ const col2 = 42 // 标题列
141
+ const col3 = inner - col1 - col2 - 2 // 信息列
142
+
143
+ // ANSI 颜色
144
+ const BLUE = '\x1b[34m'
145
+ const CYAN = '\x1b[36m'
146
+ const RESET = '\x1b[0m'
147
+
148
+ // 原始机器人(宽21)
149
+ const robotRaw = [
150
+ ' ╭───────╮ ',
151
+ '┌───────────────────┐',
152
+ '│ ██ ██ │',
153
+ '│ │',
154
+ '│ ██████ │',
155
+ '└───────────────────┘'
156
+ ]
157
+
158
+ // 颜色化:边框蓝,眼睛/嘴巴青
159
+ const colorize = (line) =>
160
+ line
161
+ .replace(/[┌└─╭╮]/g, BLUE + '$&' + RESET)
162
+ .replace(/[█]/g, CYAN + '$&' + RESET)
163
+
164
+ const robotColored = robotRaw.map(colorize)
165
+
166
+ // 在 col1 内居中(考虑 ESC 序列不看长度,按可见长度21 计算)
167
+ const leftPad = 4 // (30 - 21) / 2 = 4.5 => 4 left, 5 right
168
+ const rightPad = 5
169
+ const robotLines = robotColored.map(line => ' '.repeat(leftPad) + line + ' '.repeat(rightPad))
170
+
171
+ // 标题列(居中)
172
+ const pad = (s, w, align = 'center') => {
173
+ if (s.length >= w) return s
174
+ const sp = w - s.length
175
+ if (align === 'center') {
176
+ const l = Math.floor(sp / 2)
177
+ return ' '.repeat(l) + s + ' '.repeat(sp - l)
178
+ }
179
+ return s + ' '.repeat(sp)
180
+ }
181
+
182
+ const titleLines = [
183
+ pad('AI Code Agent', col2, 'center'),
184
+ pad('Node.js Edition', col2, 'center'),
185
+ pad('', col2, 'center'),
186
+ pad('─'.repeat(col2 - 2), col2, 'center'),
187
+ pad('/help — commands · /exit — quit', col2, 'center'),
188
+ pad('', col2, 'center')
189
+ ]
190
+
191
+ // 信息列(右对齐)
192
+ const sessionId = session?.id || '??????'
193
+ const infoLines = [
194
+ pad(`Turns: ${session?.state?.turnCount ?? 0} • Tools: 0`, col3, 'right'),
195
+ pad(`Model: ${model}`, col3, 'right'),
196
+ pad(`Permission: ${permissionMode}`, col3, 'right'),
197
+ pad(`Budget: 0 / ${maxTokens ?? 200000}`, col3, 'right'),
198
+ pad(`Session: ${sessionId?.toString().slice(-6)}`, col3, 'right'),
199
+ pad('', col3, 'right')
200
+ ]
201
+
202
+ // 构建每行:│ col1 │ col2 │ col3 │
203
+ const lines = [top]
204
+ const empty = `│${' '.repeat(inner)}│`
205
+ lines.push(empty)
206
+
207
+ for (let i = 0; i < robotLines.length; i++) {
208
+ lines.push(`│${robotLines[i]}│${titleLines[i]}│${infoLines[i]}│`)
209
+ }
210
+
211
+ lines.push(empty)
212
+ lines.push(bottom)
213
+ return lines.join('\n')
214
+ }
127
215
 
128
216
  const HELP_TEXT = `
129
217
  Commands:
@@ -140,7 +228,10 @@ Commands:
140
228
  /cost — Show API cost report
141
229
  /compact — Manually compact conversation context
142
230
  /cd PATH — Change working directory
143
- /allow [tool] — Allow a tool for the current session (default: all)
231
+ /allow [tool|all|reset] — Allow tools for this session (default: all)
232
+ /allow all — automatically allow all subsequent tools
233
+ /allow reset — reset to ask mode
234
+ /allow <tool> — allow specific tool (e.g. Bash)
144
235
  /exit — Exit (also Ctrl+C)
145
236
  /quit — Same as /exit
146
237
 
@@ -174,7 +265,7 @@ const DETAILED_HELP = {
174
265
 
175
266
  cd: "/cd <path>\n Change the working directory of cc-node.\n Affects all subsequent tool executions (Bash, Read, Write, etc.).\n\n Without path: show the current working directory.\n\n Example: /cd /home/raolin/projects\n Example: /cd ..",
176
267
 
177
- allow: "/allow [tool_name]\n Allow a tool to execute without confirmation for this session.\n Without tool name: allows ALL tools.\n\n Example: /allow\n Example: /allow Bash",
268
+ allow: "/allow [tool|all|reset]\n Manage tool permissions for this session.\n\n Options:\n <tool> — allow a specific tool (e.g. Bash, Write, Read)\n all — automatically allow ALL remaining tools for this session\n reset — reset to ask mode (ask for each tool)\n (no arg) — same as /allow all\n\n When asked to confirm a tool, you can also type:\n y — allow this once\n a — allow all for the rest of the session\n\n Example: /allow Bash\n Example: /allow all\n Example: /allow reset",
178
269
 
179
270
  exit: "/exit\n Exit cc-node. Same as Ctrl+C or /quit.",
180
271
  quit: "/quit\n Exit cc-node. Same as Ctrl+C or /exit.",
@@ -354,17 +445,26 @@ export async function main() {
354
445
  // 将 readline 注入引擎配置,用于 ask 模式确认和 AskUserQuestion 工具
355
446
  if (permissionMode === 'ask') {
356
447
  engine.config.onConfirmTool = async (toolName, input) => {
448
+ // 如果已经启用会话全局自动允许,直接通过
449
+ if (engine.permissionChecker.sessionAllowAll) return true
450
+
451
+ const snippet = JSON.stringify(input).slice(0, 120) || '(no params)'
357
452
  return new Promise((resolve) => {
358
- const snippet = JSON.stringify(input).slice(0, 120) || '(no params)'
359
- rl.question(`\n⚠️ Allow tool "${toolName}"?\n Input: ${snippet}\n (y/N) `, (answer) => {
360
- resolve(answer.toLowerCase().startsWith('y'))
453
+ rl.question(`\n⚠️ Allow tool "${toolName}"?\n Input: ${snippet}\n (y/N/a) a=all session `, (answer) => {
454
+ const a = answer.toLowerCase()
455
+ if (a === 'a') {
456
+ engine.permissionChecker.allowAllForSession()
457
+ resolve(true)
458
+ } else {
459
+ resolve(a === 'y')
460
+ }
361
461
  })
362
462
  })
363
463
  }
364
464
  }
365
465
  engine.config.readline = rl
366
466
 
367
- console.log(BANNER)
467
+ console.log(buildBanner({ model, permissionMode, session, maxTokens: tokenBudget.maxTokens }))
368
468
  console.log(`Model: ${model} | Permission: ${permissionMode} | Tools: ${registry.getNames().join(', ')}`)
369
469
  console.log(`Socket: ${SOCK_PATH} (cc-notify can connect)`)
370
470
  if (channelManager.list().length > 0) {
@@ -495,9 +595,18 @@ export async function main() {
495
595
  break
496
596
  }
497
597
  case 'allow': {
498
- const allowTool = rest.join(' ') || '*'
499
- engine.permissionChecker.allowForSession(allowTool, '*')
500
- console.log(`✅ Tool "${allowTool}" allowed for this session`)
598
+ const arg = rest.join(' ').toLowerCase()
599
+ if (arg === 'all') {
600
+ engine.permissionChecker.allowAllForSession()
601
+ console.log('✅ All tools allowed for the rest of this session')
602
+ } else if (arg === 'reset') {
603
+ engine.permissionChecker.resetSessionAllow()
604
+ console.log('✅ Session permission reset to normal (ask mode)')
605
+ } else {
606
+ const tool = arg || '*'
607
+ engine.permissionChecker.allow(tool)
608
+ console.log(`✅ Tool "${tool}" allowed for this session`)
609
+ }
501
610
  break
502
611
  }
503
612
  case 'cost':
@@ -9,21 +9,20 @@ export class PermissionChecker {
9
9
  this.alwaysAllow = new Set()
10
10
  this.alwaysDeny = new Set()
11
11
  this.askRules = new Set()
12
+ this.sessionAllowAll = false // 整个会话自动允许
12
13
  }
13
14
 
14
15
  /**
15
16
  * 检查工具调用是否被允许
16
17
  */
17
18
  async check(toolName, input = {}) {
18
- if (this.mode === 'always-allow') return true
19
+ if (this.mode === 'always-allow' || this.sessionAllowAll) return true
19
20
  if (this.mode === 'deny') return false
20
21
 
21
22
  if (this.alwaysAllow.has(toolName)) return true
22
23
  if (this.alwaysDeny.has(toolName)) return false
23
24
 
24
- // 'ask' 模式 需要用户确认(简化版直接允许)
25
- // 完整版应该在终端显示确认对话框
26
- return true
25
+ return true // 'ask' 模式下由 onConfirmTool 决定
27
26
  }
28
27
 
29
28
  allow(toolName) {
@@ -33,5 +32,15 @@ export class PermissionChecker {
33
32
  deny(toolName) {
34
33
  this.alwaysDeny.add(toolName)
35
34
  }
35
+
36
+ // 会话剩余时间全部工具自动允许
37
+ allowAllForSession() {
38
+ this.sessionAllowAll = true
39
+ }
40
+
41
+ // 重置会话允许状态
42
+ resetSessionAllow() {
43
+ this.sessionAllowAll = false
44
+ }
36
45
  }
37
46