chatccc 0.2.124 → 0.2.126

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,53 @@
1
+ # Claude-Specific Injection Prompt
2
+
3
+ Use this prompt as the Claude CLI injection prompt for this project.
4
+
5
+ Project workspace:
6
+
7
+ ```text
8
+ f:\users\weizhangjian\feishuclauderprivate
9
+ ```
10
+
11
+ ## Repeated Successful Command Guard
12
+
13
+ When working in this project through Claude CLI, repeated successful shell commands are a completion signal, not a reason to keep using tools.
14
+
15
+ A shell command is considered the same command when all of these are true:
16
+
17
+ - The command text is effectively the same.
18
+ - The working directory is the same.
19
+ - The current task goal has not changed.
20
+ - There is no new user input that changes the task.
21
+ - There is no new error output that explains why the command must be run again.
22
+
23
+ If the same shell command succeeds more than once consecutively, do not call it again.
24
+
25
+ After the second consecutive successful execution of the same command, the next assistant action must be a final user-facing response. Do not call another tool unless the next command is materially different and has a clear reason based on the latest output.
26
+
27
+ This guard applies to all shell commands, including but not limited to:
28
+
29
+ - test commands
30
+ - build commands
31
+ - git commands
32
+ - install commands
33
+ - formatting commands
34
+ - project scripts
35
+ - status or inspection commands
36
+
37
+ Do not "verify one more time" by repeating the same successful command. If verification is needed, use a different command that checks a different fact, or explain the current result to the user.
38
+
39
+ If you notice that you are about to repeat a successful command with the same arguments, stop using tools and respond to the user with the current result.
40
+
41
+ If you produce or observe text like "I'm stuck in a loop", do not call any more tools. Immediately send the final user-facing response explaining:
42
+
43
+ - what has completed,
44
+ - what is still uncertain, if anything,
45
+ - and what the user can do next, if action is needed.
46
+
47
+ ## Hard Stop Rule
48
+
49
+ Never execute the same successful shell command three times for the same task in this project.
50
+
51
+ If the same command has already succeeded twice in a row, the next assistant action must be a final response to the user, not another tool call.
52
+
53
+ Successful repeated command execution is a terminal condition. Prefer a concise final response over further tool calls.
package/package.json CHANGED
@@ -1,63 +1,64 @@
1
- {
2
- "name": "chatccc",
3
- "version": "0.2.124",
4
- "description": "Feishu bot bridge for Claude Code",
5
- "license": "Apache-2.0",
6
- "type": "module",
7
- "main": "./src/index.ts",
8
- "bin": {
9
- "chatccc": "bin/chatccc.mjs"
10
- },
11
- "files": [
12
- "src/",
13
- "bin/",
14
- "scripts/postinstall-sharp-check.mjs",
15
- "demo/ilink_echo_probe.ts",
16
- "im-skills/",
17
- "images/img_readme_*.jpg",
18
- "images/img_readme_*.png",
19
- "images/avatars/status_*.png",
20
- "images/avatars/badges/",
21
- "package.json",
22
- "README.md",
23
- "config.sample.json"
24
- ],
25
- "scripts": {
26
- "dev": "tsx src/index.ts",
27
- "chatccc": "tsx src/index.ts",
28
- "start": "tsx src/index.ts",
29
- "demo:bot-test": "tsx demo/bot_test.ts",
30
- "demo:bot-test:local": "tsx demo/bot_test.ts --local",
31
- "demo:create-group": "tsx src/index.ts",
32
- "demo:create-group:local": "tsx src/index.ts --local",
33
- "demo:permission-check": "tsx demo/permission_check.ts",
34
- "demo:claude-hi": "tsx demo/claude_say_hi.ts",
35
- "demo:codex-hi": "tsx demo/codex_say_hi.ts",
36
- "demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
37
- "test": "vitest run",
38
- "test:watch": "vitest",
39
- "postinstall": "node scripts/postinstall-sharp-check.mjs"
40
- },
41
- "dependencies": {
42
- "@ai-sdk/openai-compatible": "^2.0.47",
43
- "@anthropic-ai/claude-agent-sdk": "0.2.133",
44
- "@larksuiteoapi/node-sdk": "^1.59.0",
45
- "@openilink/openilink-sdk-node": "^0.6.0",
46
- "ai": "^6.0.184",
47
- "nodemailer": "^8.0.7",
48
- "qrcode-terminal": "^0.12.0",
49
- "sharp": "^0.34.5",
50
- "tsx": "^4.0.0",
51
- "ws": "^8.18.0"
52
- },
53
- "devDependencies": {
54
- "@types/node": "^20.0.0",
55
- "@types/qrcode-terminal": "^0.12.2",
56
- "@types/ws": "^8.18.1",
57
- "typescript": "^5.0.0",
58
- "vitest": "^3.2.4"
59
- },
60
- "engines": {
61
- "node": ">=20"
62
- }
63
- }
1
+ {
2
+ "name": "chatccc",
3
+ "version": "0.2.126",
4
+ "description": "Feishu bot bridge for Claude Code",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./src/index.ts",
8
+ "bin": {
9
+ "chatccc": "bin/chatccc.mjs"
10
+ },
11
+ "files": [
12
+ "src/",
13
+ "bin/",
14
+ "scripts/postinstall-sharp-check.mjs",
15
+ "demo/ilink_echo_probe.ts",
16
+ "agent-prompts/",
17
+ "im-skills/",
18
+ "images/img_readme_*.jpg",
19
+ "images/img_readme_*.png",
20
+ "images/avatars/status_*.png",
21
+ "images/avatars/badges/",
22
+ "package.json",
23
+ "README.md",
24
+ "config.sample.json"
25
+ ],
26
+ "scripts": {
27
+ "dev": "tsx src/index.ts",
28
+ "chatccc": "tsx src/index.ts",
29
+ "start": "tsx src/index.ts",
30
+ "demo:bot-test": "tsx demo/bot_test.ts",
31
+ "demo:bot-test:local": "tsx demo/bot_test.ts --local",
32
+ "demo:create-group": "tsx src/index.ts",
33
+ "demo:create-group:local": "tsx src/index.ts --local",
34
+ "demo:permission-check": "tsx demo/permission_check.ts",
35
+ "demo:claude-hi": "tsx demo/claude_say_hi.ts",
36
+ "demo:codex-hi": "tsx demo/codex_say_hi.ts",
37
+ "demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest",
40
+ "postinstall": "node scripts/postinstall-sharp-check.mjs"
41
+ },
42
+ "dependencies": {
43
+ "@ai-sdk/openai-compatible": "^2.0.47",
44
+ "@anthropic-ai/claude-agent-sdk": "0.2.133",
45
+ "@larksuiteoapi/node-sdk": "^1.59.0",
46
+ "@openilink/openilink-sdk-node": "^0.6.0",
47
+ "ai": "^6.0.184",
48
+ "nodemailer": "^8.0.7",
49
+ "qrcode-terminal": "^0.12.0",
50
+ "sharp": "^0.34.5",
51
+ "tsx": "^4.0.0",
52
+ "ws": "^8.18.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^20.0.0",
56
+ "@types/qrcode-terminal": "^0.12.2",
57
+ "@types/ws": "^8.18.1",
58
+ "typescript": "^5.0.0",
59
+ "vitest": "^3.2.4"
60
+ },
61
+ "engines": {
62
+ "node": ">=20"
63
+ }
64
+ }
@@ -1,5 +1,9 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { normalizeSdkMessage, createClaudeAdapter } from "../adapters/claude-adapter.ts";
2
+ import {
3
+ normalizeSdkMessage,
4
+ createClaudeAdapter,
5
+ buildClaudePromptText,
6
+ } from "../adapters/claude-adapter.ts";
3
7
  import type { UnifiedStreamMessage } from "../adapters/adapter-interface.ts";
4
8
  import type {
5
9
  ClaudeSessionMeta,
@@ -439,4 +443,23 @@ describe("createClaudeAdapter", () => {
439
443
  });
440
444
  expect(await adapter.getSessionInfo("sid-C")).toEqual({ sessionId: "sid-C" });
441
445
  });
442
- });
446
+ });
447
+
448
+ describe("buildClaudePromptText", () => {
449
+ it("prepends the Claude-specific injection prompt when present", () => {
450
+ const result = buildClaudePromptText(
451
+ "[User message]\nhello\n[/User message]",
452
+ "Never repeat successful commands.",
453
+ );
454
+
455
+ expect(result).toContain("[ChatCCC Claude-specific injection prompt]");
456
+ expect(result).toContain("Never repeat successful commands.");
457
+ expect(result).toContain("[/ChatCCC Claude-specific injection prompt]");
458
+ expect(result.endsWith("[User message]\nhello\n[/User message]")).toBe(true);
459
+ });
460
+
461
+ it("leaves user text unchanged when the injection prompt is empty", () => {
462
+ expect(buildClaudePromptText("hello", " ")).toBe("hello");
463
+ expect(buildClaudePromptText("hello", null)).toBe("hello");
464
+ });
465
+ });
@@ -177,7 +177,7 @@ describe("handleCommand WeChat processing ack", () => {
177
177
  blocks: [{ type: "text" as const, text: `收到: ${userText}` }],
178
178
  };
179
179
  });
180
- _setAdapterForToolForTest("cursor", {
180
+ _setAdapterForToolForTest("claude", {
181
181
  displayName: "Claude",
182
182
  sessionDescPrefix: "Claude Session:",
183
183
  createSession: vi.fn(async () => ({ sessionId: "sid-feishu-new" })),
@@ -5,12 +5,13 @@
5
5
  // 以确保 --mcp-config 参数能正确传递给 CLI 子进程,加载用户 MCP 服务器。
6
6
  // =============================================================================
7
7
 
8
- import { spawn, type ChildProcess } from "node:child_process";
9
- import { createInterface } from "node:readline";
10
- import { existsSync, readFileSync } from "node:fs";
11
- import { join, dirname } from "node:path";
12
- import { homedir } from "node:os";
13
- import { createRequire } from "node:module";
8
+ import { spawn, type ChildProcess } from "node:child_process";
9
+ import { createInterface } from "node:readline";
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { join, dirname } from "node:path";
12
+ import { homedir } from "node:os";
13
+ import { createRequire } from "node:module";
14
+ import { fileURLToPath } from "node:url";
14
15
 
15
16
  import type {
16
17
  ToolAdapter,
@@ -71,7 +72,13 @@ function resolveClaudeBinary(): string {
71
72
  );
72
73
  }
73
74
 
74
- const CLAUDE_EXE = resolveClaudeBinary();
75
+ const CLAUDE_EXE = resolveClaudeBinary();
76
+ const PROJECT_ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
77
+ const CLAUDE_SPECIFIC_PROMPT_PATH = join(
78
+ PROJECT_ROOT,
79
+ "agent-prompts",
80
+ "claude_specific.md",
81
+ );
75
82
 
76
83
  // ---------------------------------------------------------------------------
77
84
  // 类型别名(CLI JSONL 消息格式,与旧 SDK 消息格式兼容)
@@ -359,19 +366,45 @@ async function* readJsonLines(
359
366
  }
360
367
 
361
368
  /** 构造 stream-json 格式的 stdin 输入行 */
362
- function buildStreamJsonInput(text: string): string {
363
- return JSON.stringify({
364
- type: "user",
365
- message: {
366
- role: "user",
369
+ function buildStreamJsonInput(text: string): string {
370
+ return JSON.stringify({
371
+ type: "user",
372
+ message: {
373
+ role: "user",
367
374
  content: [{ type: "text", text }],
368
375
  },
369
- }) + "\n";
370
- }
371
-
372
- // ---------------------------------------------------------------------------
373
- // ClaudeAdapter
374
- // ---------------------------------------------------------------------------
376
+ }) + "\n";
377
+ }
378
+
379
+ export function readClaudeSpecificInjectionPrompt(): string | null {
380
+ try {
381
+ if (!existsSync(CLAUDE_SPECIFIC_PROMPT_PATH)) return null;
382
+ const prompt = readFileSync(CLAUDE_SPECIFIC_PROMPT_PATH, "utf-8").trim();
383
+ return prompt.length > 0 ? prompt : null;
384
+ } catch {
385
+ return null;
386
+ }
387
+ }
388
+
389
+ export function buildClaudePromptText(
390
+ userText: string,
391
+ injectionPrompt: string | null = readClaudeSpecificInjectionPrompt(),
392
+ ): string {
393
+ const prompt = injectionPrompt?.trim();
394
+ if (!prompt) return userText;
395
+
396
+ return [
397
+ "[ChatCCC Claude-specific injection prompt]",
398
+ prompt,
399
+ "[/ChatCCC Claude-specific injection prompt]",
400
+ "",
401
+ userText,
402
+ ].join("\n");
403
+ }
404
+
405
+ // ---------------------------------------------------------------------------
406
+ // ClaudeAdapter
407
+ // ---------------------------------------------------------------------------
375
408
 
376
409
  class ClaudeAdapter implements ToolAdapter {
377
410
  readonly displayName = "Claude Code";
@@ -437,13 +470,13 @@ class ClaudeAdapter implements ToolAdapter {
437
470
  const proc = spawnCli(args, cwd, env, true);
438
471
  if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
439
472
 
440
- const onAbort = () => { void killProcessTree(proc.pid); };
441
- signal?.addEventListener("abort", onAbort, { once: true });
442
-
443
- proc.stdin!.write(buildStreamJsonInput(userText));
444
- proc.stdin!.end();
445
-
446
- try {
473
+ const onAbort = () => { void killProcessTree(proc.pid); };
474
+ signal?.addEventListener("abort", onAbort, { once: true });
475
+
476
+ proc.stdin!.write(buildStreamJsonInput(buildClaudePromptText(userText)));
477
+ proc.stdin!.end();
478
+
479
+ try {
447
480
  for await (const raw of readJsonLines(proc, signal)) {
448
481
  if (signal?.aborted) break;
449
482
 
@@ -493,4 +526,4 @@ export function createClaudeAdapter(
493
526
  options: ClaudeAdapterOptions,
494
527
  ): ToolAdapter {
495
528
  return new ClaudeAdapter(options);
496
- }
529
+ }
@@ -151,8 +151,6 @@ export interface DisplayCardState {
151
151
  lastSentFinalReply?: string;
152
152
  /** 点点点动画计数器(统一 display loop 每个卡片独立计数) */
153
153
  dotCount: number;
154
- /** stream-state 连续读取失败次数,超过阈值才删除 display entry */
155
- readFailCount?: number;
156
154
  }
157
155
 
158
156
  export const displayCards = new Map<string, DisplayCardState>();
package/src/session.ts CHANGED
@@ -1279,21 +1279,15 @@ export function startUnifiedDisplayLoop(): void {
1279
1279
  const sessionId = display.sessionId;
1280
1280
  const state = await readStreamState(sessionId);
1281
1281
  if (!state) {
1282
- display.readFailCount = (display.readFailCount ?? 0) + 1;
1283
- if (display.readFailCount >= 3) {
1284
- console.log(`[${ts()}] [DISPLAY] readStreamState ${display.readFailCount} consecutive failures for ${sessionId}, removing display entry for chat ${chatId}`);
1285
- displayCards.delete(chatId);
1286
- }
1282
+ displayCards.delete(chatId);
1287
1283
  continue;
1288
1284
  }
1289
- display.readFailCount = 0;
1290
1285
 
1291
1286
  // 交叉验证:chat 当前绑定的 session 是否仍是 display 记录的 session。
1292
1287
  // 若 chat 已被切换到其他 session(如 /newh),旧 display 必须停推。
1293
1288
  const currentSessionForChat = sessionInfoMap.get(chatId)?.sessionId;
1294
1289
  if (currentSessionForChat && currentSessionForChat !== sessionId) {
1295
1290
  if (state.status !== "running") {
1296
- console.log(`[${ts()}] [DISPLAY] chat ${chatId} now bound to ${currentSessionForChat}, removing stale display for ${sessionId} (terminal: ${state.status})`);
1297
1291
  displayCards.delete(chatId);
1298
1292
  }
1299
1293
  continue;
@@ -1303,7 +1297,6 @@ export function startUnifiedDisplayLoop(): void {
1303
1297
  const lastActive = getLastActiveChat(sessionId);
1304
1298
  if (lastActive !== chatId) {
1305
1299
  if (state.status !== "running") {
1306
- console.log(`[${ts()}] [DISPLAY] lastActive mismatch for ${sessionId}: display chat=${chatId} lastActive=${lastActive ?? "undefined"}, removing display entry (terminal: ${state.status})`);
1307
1300
  displayCards.delete(chatId);
1308
1301
  }
1309
1302
  continue;