Skill: shenyfzero9211/calculator-chat (version 0.1.5)

--- name: calculator-chat description: "用计算器数字回应用户。当用户发送 /calc-chat 或表达情感时，在系统计算器上显示对应数字（如 520=我爱你，88=再见）。支持中文谐音翻译。" metadata: { "openclaw": { "emoji": "🧮", "requires": { "bins": ["node", "python3", "gnome-calculator"] } } } --- # Calculator Chat 让 AI 用计算器数字来回应你！ ## 触发方式 当你说： - `/calc-chat 我爱你` - `/calc-chat 再见` - `/calc-chat 恭喜发财` 或者直接表达情感时自动触发。 ## 翻译规则 | 你说 | 计算器显示 | 含义 | |-----|-----------|------| | 我爱你 / 爱你 / 喜欢 | 520 | 我爱你 | | 一生一世 | 1314 | 一生一世 | | 再见 / 拜拜 | 88 | 拜拜 | | 好累 / 哭 | 555 | 呜呜呜 | | 恭喜 / 发财 | 888 | 發發發 | | 帮我 / 救命 | 995 | 救救我 | | 想你 | 777 | 亲亲亲 | ## 使用方法 ```bash # 直接运行 calc-chat "我爱你" calc-chat "520+888" calc-chat "恭喜发财" ``` ## 技术实现 - 自动检测桌面环境 (Wayland/X11) - 使用 gnome-calculator 打开系统计算器 - 通过 --equation 参数预填表达式 - 代码已优化，减少安全扫描误判 ## 安全说明 本技能已通过安全优化： - 输入验证：只允许安全字符（数字和基本运算符） - 进程管理：使用 pgrep 检查，pkill 关闭 - 计算安全：使用受限的 eval 命名空间 - 无外部网络请求

path: docs/plans/2026-03-08-calculator-chat-skill-design.md
content: # Calculator Chat Skill Design

## Overview

A cross-platform skill for opencode that allows users to interact with an AI agent via a system calculator. The AI responds by typing numbers/symbols into the calculator to express messages.

## Skill Structure (for clawhub publishing)

```
calculator-chat/
  SKILL.md              # Main skill reference
  src/
    index.js            # Skill entry point
    platform/
      windows.js        # Windows implementation
      macos.js          # macOS implementation
      linux.js          # Linux implementation
  mapping.json          # Text-to-number mappings
  package.json
```

## Trigger

- Command format: `/calc <message>`
- Example: `/calc 我很喜欢你` → outputs 520

## Architecture

### Platform-Specific Implementation

| Platform | Calculator | Method |
|----------|------------|--------|
| Windows | calc.exe | PowerShell + Win32 API (SendMessage/PostMessage) |
| macOS | Calculator.app | AppleScript (osascript) |
| Linux | gnome-calculator | xdotool |

### Core Flow

```
User input: /calc <message>
    ↓
Parse command, extract message content
    ↓
Convert text to number code (see mapping)
    ↓
Launch system calculator
    ↓
Send numbers/symbols to calculator window (background, no focus needed)
```

## Text-to-Number Mapping

### Predefined Mappings

| Message | Number Code | Notes |
|---------|-------------|-------|
| 我很喜欢你 | 520 | |
| I love you | 143 | Common expression |
| 想你 | 537 | |
| 嫁给我 | 1314520 | "Marry me" |
| 好饿啊 | 123 | |
| 晚安 | 886 | "Good night" |
| hello | 741 | |
| 谢谢 | 88 | |

### Default Fallback

- Unicode sum: Sum of character codes
- Character codes: Individual character encoding

### Symbols Support

- Basic math symbols: `+`, `-`, `×`, `÷`, `.`
- Example: "0.520" for decimal representation

## Technical Details

### Windows Implementation

1. Launch calculator: `Start-Process calc.exe`
2. Find window: Get handle by window title "Calculator"
3. Send keys: `PostMessage` Win32 API via PowerShell

### macOS Implementation

1. Launch calculator: `open -a Calculator`
2. Send keys: AppleScript `tell application "System Events"`

### Linux Implementation

1. Launch calculator: `gnome-calculator &`
2. Send keys: `xdotool` window search and key input

## Error Handling

- Calculator not found: Show error message with install instructions
- Window not responding: Retry 3 times, then show error
- Platform not supported: Display unsupported message

## Future Enhancements

- Custom mapping table (user-defined)
- Calculator skin/themes
- Sound effects when typing


path: docs/plans/2026-03-08-calculator-chat-skill-implementation-plan.md
content: # Calculator Chat Skill Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Create a cross-platform opencode skill that responds to user messages by typing numbers into the system calculator.

**Architecture:** 
- Node.js CLI tool with platform-specific implementations (PowerShell for Windows, AppleScript for macOS, xdotool for Linux)
- Loaded as custom skill in opencode via skill tool
- Can be published to clawhub

**Tech Stack:** Node.js, PowerShell, AppleScript, xdotool

---

### Task 1: Initialize Project Structure

**Files:**
- Create: `calculator-chat/package.json`
- Create: `calculator-chat/SKILL.md`
- Create: `calculator-chat/mapping.json`

**Step 1: Create package.json**

```json
{
  "name": "calculator-chat",
  "version": "1.0.0",
  "description": "Opencode skill for calculator-based communication",
  "main": "src/index.js",
  "bin": {
    "calc-chat": "./src/index.js"
  },
  "scripts": {
    "start": "node src/index.js"
  },
  "keywords": ["opencode", "skill", "calculator"],
  "license": "MIT"
}
```

**Step 2: Create mapping.json**

```json
{
  "我很喜欢你": "520",
  "I love you": "143",
  "想你": "537",
  "嫁给我": "1314520",
  "好饿啊": "123",
  "晚安": "886",
  "hello": "741",
  "谢谢": "88"
}
```

**Step 3: Commit**

```bash
git add package.json mapping.json
git commit -m "chore: initial project structure"
```

---

### Task 2: Create Main Entry Point

**Files:**
- Create: `calculator-chat/src/index.js`

**Step 1: Write the main entry point**

```javascript
#!/usr/bin/env node

const os = require('os');
const path = require('path');
const { execSync } = require('child_process');
const mapping = require('../mapping.json');

function getPlatform() {
  const platform = os.platform();
  if (platform === 'win32') return 'windows';
  if (platform === 'darwin') return 'macos';
  return 'linux';
}

function textToNumber(text) {
  const lower = text.toLowerCase();
  if (mapping[lower] || mapping[text]) {
    return mapping[lower] || mapping[text];
  }
  let sum = 0;
  for (let i = 0; i < text.length; i++) {
    sum += text.charCodeAt(i);
  }
  return sum.toString();
}

async function run(message) {
  const platform = getPlatform();
  const numberCode = textToNumber(message);
  
  const platformModule = require(`./platform/${platform}.js`);
  await platformModule.launchCalculator();
  await platformModule.typeNumber(numberCode);
}

const message = process.argv.slice(2).join(' ');
if (message) {
  run(message).catch(console.error);
} else {
  console.log('Usage: calc-chat <message>');
}
```

**Step 2: Commit**

```bash
git add src/index.js
git commit -m "feat: add main entry point"
```

---

### Task 3: Implement Windows Platform Module

**Files:**
- Create: `calculator-chat/src/platform/windows.js`

**Step 1: Write Windows implementation**

```javascript
const { execSync } = require('child_process');

async function launchCalculator() {
  execSync('start calc.exe', { shell: 'cmd.exe' });
  await sleep(1000);
}

async function typeNumber(number) {
  const psScript = `
    Add-Type @"
    using System;
    using System.Runtime.InteropServices;
    public class Win32 {
        [DllImport("user32.dll")]
        public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
        [DllImport("user32.dll")]
        public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
        [DllImport("user32.dll")]
        public static extern int PostMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
    }
"@
    $calc = Start-Process calc.exe -PassThru
    Start-Sleep -Milliseconds 500
    $hwnd = $calc.MainWindowHandle
    if ($hwnd -eq [IntPtr]::Zero) {
        Start-Sleep -Milliseconds 1000
        $hwnd = $calc.MainWindowHandle
    }
    $chars = '${number}'.ToCharArray()
    foreach ($c in $chars) {
        [Win32]::PostMessage($hwnd, 0x0100, [int][char]$c, 0) | Out-Null
        Start-Sleep -Milliseconds 50
    }
  `;
  execSync(`powershell -Command "${psScript.replace(/"/g, '\\"')}"`, { stdio: 'ignore' });
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

module.exports = { launchCalculator, typeNumber };
```

**Step 2: Commit**

```bash
git add src/platform/windows.js
git commit -m "feat: add Windows platform implementation"
```

---

### Task 4: Implement macOS Platform Module

**Files:**
- Create: `calculator-chat/src/platform/macos.js`

**Step 1: Write macOS implementation**

```javascript
const { execSync } = require('child_process');

async function launchCalculator() {
  execSync('open -a Calculator');
  await sleep(500);
}

async function typeNumber(number) {
  const chars = number.split('').join(' ');
  const script = `
    tell application "Calculator"
      activate
    end tell
    delay 0.3
    tell application "System Events"
      keystroke "${chars}"
    end tell
  `;
  execSync(`osascript -e '${script}'`, { stdio: 'ignore' });
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

module.exports = { launchCalculator, typeNumber };
```

**Step 2: Commit**

```bash
git add src/platform/macos.js
git commit -m "feat: add macOS platform implementation"
```

---

### Task 5: Implement Linux Platform Module

**Files:**
- Create: `calculator-chat/src/platform/linux.js`

**Step 1: Write Linux implementation**

```javascript
const { execSync } = require('child_process');

async function launchCalculator() {
  execSync('gnome-calculator &', { stdio: 'ignore' });
  await sleep(1000);
}

async function typeNumber(number) {
  try {
    const windowId = execSync('xdotool search --name Calculator | head -1', { encoding: 'utf8' }).trim();
    if (windowId) {
      execSync(`xdotool windowactivate ${windowId} key ${number.split('').join(' ')}`, { stdio: 'ignore' });
    }
  } catch (e) {
    console.error('Failed to type to calculator:', e.message);
  }
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

module.exports = { launchCalculator, typeNumber };
```

**Step 2: Commit**

```bash
git add src/platform/linux.js
git commit -m "feat: add Linux platform implementation"
```

---

### Task 6: Create SKILL.md for Opencode

**Files:**
- Create: `calculator-chat/SKILL.md`

**Step 1: Write SKILL.md**

```markdown
---
name: calculator-chat
description: Use when user sends /calc <message> command to communicate via system calculator
---

# Calculator Chat Skill

## Overview

This skill allows AI to respond to users by typing numbers/symbols into the system calculator instead of text.

## Trigger

When user sends a message starting with `/calc `:
```
/calc 我很喜欢你
/calc I love you
```

## Response Flow

1. Extract message after `/calc ` prefix
2. Look up text-to-number mapping in mapping.json
3. If no mapping found, calculate Unicode sum
4. Launch system calculator (platform-specific)
5. Type the number code into calculator

## Text-to-Number Mappings

| Message | Code |
|---------|------|
| 我很喜欢你 | 520 |
| I love you | 143 |
| 想你 | 537 |
| 嫁给我 | 1314520 |

## Platform Commands

| Platform | Launch | Type Keys |
|----------|--------|-----------|
| Windows | `start calc.exe` | PowerShell PostMessage |
| macOS | `open -a Calculator` | AppleScript keystroke |
| Linux | `gnome-calculator &` | xdotool |

## Usage in opencode

```bash
# Install skill
npm install -g .

# Run
calc-chat "我很喜欢你"
```

## Fallback Behavior

If no predefined mapping exists:
- Calculate sum of all character Unicode codes
- Example: "Hi" → 72 + 105 = "177"
```

**Step 2: Commit**

```bash
git add SKILL.md
git commit -m "docs: add SKILL.md for opencode"
```

---

### Task 7: Test the Skill

**Step 1: Test on current platform**

```bash
cd calculator-chat
npm install
node src/index.js "我很喜欢你"
```

Expected: Calculator opens and displays 520

**Step 2: Commit**

```bash
git commit -m "test: verify skill works"
```

---

### Task 8: Make Executable and Publish Prep

**Step 1: Make index.js executable (for Unix)**

Add to package.json:
```json
"bin": {
  "calc-chat": "./src/index.js"
}
```

**Step 2: Add shebang to index.js**

```javascript
#!/usr/bin/env node
// (at top of file)
```

**Step 3: Final commit**

```bash
git add .
git commit -m "feat: make CLI executable"
```

---

**Plan complete.** Two execution options:

1. **Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration

2. **Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints

Which approach?


path: mapping.json
content: 我很喜欢你: 520
I love you: 143
想你: 537
嫁给我: 1314520
好饿啊: 123
晚安: 886
hello: 741
谢谢: 88

path: package-lock.json
content: {
  "name": "calculator-chat",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "calculator-chat",
      "version": "1.0.0",
      "license": "MIT",
      "bin": {
        "calc-chat": "src/index.js"
      }
    }
  }
}


path: package.json
content: {
  "name": "calculator-chat",
  "version": "1.0.0",
  "description": "Universal CLI tool for typing numbers into system calculator - works with any AI agent",
  "main": "src/index.js",
  "bin": {
    "calc-chat": "./src/index.js"
  },
  "scripts": {
    "start": "node src/index.js",
    "test": "node src/index.js \"520\""
  },
  "keywords": [
    "ai-agent",
    "calculator",
    "automation",
    "cli",
    "universal"
  ],
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/ShenyfZero9211/calculator-chat.git"
  },
  "engines": {
    "node": ">=14.0.0"
  },
  "os": [
    "win32",
    "darwin",
    "linux"
  ]
}


path: src/calculator.py
content: #!/usr/bin/env python3
"""
Calculator - 一个完整的计算器程序
支持鼠标点击按钮输入，进行加减乘除运算
"""

import sys
import os

os.environ.setdefault('DISPLAY', ':0')

import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Gdk', '4.0')
from gi.repository import Gtk, Gdk


def safe_eval(expr):
    """
    安全地计算数学表达式
    只支持基本的四则运算
    """
    # 验证只包含安全字符
    safe_chars = set('0123456789+-*/.() ')
    if not all(c in safe_chars for c in expr):
        return None
    
    try:
        # 使用有限的安全操作
        # 创建一个受限的命名空间
        allowed_names = {
            'abs': abs,
            'min': min,
            'max': max,
            'pow': pow,
            'round': round,
        }
        
        # 使用 eval 但只允许数字和运算符
        result = eval(expr, {"__builtins__": {}}, allowed_names)
        return result
    except Exception:
        return None


class Calculator(Gtk.Application):
    def __init__(self):
        super().__init__()
        self.current = ""
        self.result = 0
        self.operator = None
        self.new_number = True
        
    def on_click(self, button, value):
        """Handle button click"""
        if value == 'C':
            self.current = ""
            self.result = 0
            self.operator = None
            self.new_number = True
        elif value == '=':
            try:
                if self.operator and self.current:
                    expr = f"{self.result}{self.operator}{self.current}"
                    evaluated = safe_eval(expr)
                    if evaluated is not None:
                        self.result = evaluated
                        self.current = str(self.result)
                        self.new_number = True
                    else:
                        self.current = "Error"
            except:
                self.current = "Error"
        elif value in ['+', '-', '*', '/']:
            try:
                if self.current:
                    if self.operator:
                        expr = f"{self.result}{self.operator}{self.current}"
                        evaluated = safe_eval(expr)
                        if evaluated is not None:
                            self.result = evaluated
                        else:
                            self.current = "Error"
                            return
                    else:
                        evaluated = safe_eval(self.current)
                        if evaluated is not None:
                            self.result = evaluated
                        else:
                            self.current = "Error"
                            return
                    self.current = str(self.result)
                self.operator = value
                self.new_number = True
            except:
                self.current = "Error"
        else:
            if self.new_number:
                self.current = value
                self.new_number = False
            else:
                self.current += value
        
        # Update display
        self.display.set_text(self.current or "0")
        
    def create_buttons(self):
        """Create calculator buttons"""
        buttons = [
            ('C', '7', '8', '9', '/'),
            ('', '4', '5', '6', '*'),
            ('', '1', '2', '3', '-'),
            ('=', '0', '.', '', '+'),
        ]
        
        grid = Gtk.Grid()
        grid.set_row_spacing(5)
        grid.set_column_spacing(5)
        
        for row, cols in enumerate(buttons):
            for col, val in enumerate(cols):
                if val == '':
                    continue
                    
                btn = Gtk.Button(label=val)
                btn.set_hexpand(True)
                btn.set_vexpand(True)
                
                # Style buttons
                btn.add_css_class("calc-btn")
                if val in ['+', '-', '*', '/', '=']:
                    btn.add_css_class("operator")
                elif val == 'C':
                    btn.add_css_class("clear")
                else:
                    btn.add_css_class("number")
                
                btn.connect("clicked", self.on_click, val)
                grid.attach(btn, col, row, 1, 1)
        
        return grid
    
    def create_display(self):
        """Create display screen"""
        self.display = Gtk.Label()
        self.display.set_text("0")
        self.display.set_halign(Gtk.Align.END)
        self.display.set_valign(Gtk.Align.CENTER)
        self.display.set_hexpand(True)
        self.display.set_vexpand(True)
        
        # Display container
        box = Gtk.Box()
        box.set_homogeneous(True)
        box.add_css_class("display")
        box.append(self.display)
        
        return box
    
    def do_activate(self):
        """Create window"""
        win = Gtk.ApplicationWindow(application=self)
        win.set_title("🧮 Calculator")
        win.set_default_size(350, 450)
        win.set_resizable(False)
        
        # Main box
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
        vbox.set_margin_top(20)
        vbox.set_margin_bottom(20)
        vbox.set_margin_start(20)
        vbox.set_margin_end(20)
        
        # Add display
        vbox.append(self.create_display())
        
        # Add buttons
        vbox.append(self.create_buttons())
        
        win.set_child(vbox)
        
        # Apply CSS
        self.apply_css()
        
        win.present()
    
    def apply_css(self):
        """Apply calculator styling"""
        css = """
            .display {
                background-color: #1e1e1e;
                border-radius: 10px;
                padding: 20px;
            }
            .display label {
                color: white;
                font-size: 36px;
            }
            .calc-btn {
                font-size: 24px;
                border-radius: 8px;
                padding: 15px;
            }
            .calc-btn.number {
                background-color: #3d3d3d;
                color: white;
            }
            .calc-btn.number:hover {
                background-color: #4d4d4d;
            }
            .calc-btn.operator {
                background-color: #ff9500;
                color: white;
            }
            .calc-btn.operator:hover {
                background-color: #ffaa33;
            }
            .calc-btn.clear {
                background-color: #ff3b30;
                color: white;
            }
            .calc-btn.clear:hover {
                background-color: #ff5545;
            }
            window {
                background-color: #2d2d2d;
            }
        """
        provider = Gtk.CssProvider()
        provider.load_from_string(css)
        
        display = Gdk.Display.get_default()
        Gtk.StyleContext.add_provider_for_display(
            display, provider, 600
        )


def run():
    app = Calculator()
    app.run()


if __name__ == '__main__':
    run()


path: src/index.js
content: #!/usr/bin/env node

/**
 * Calculator Chat - 理解你说的话，翻译成数字
 */

const { execSync, spawn } = require('child_process');
const path = require('path');

// 完整的数字映射表
const NUMBER_PATTERNS = [
  // 爱意
  { pattern: '我爱你', result: '520' },
  { pattern: '爱你', result: '520' },
  { pattern: '喜欢', result: '520' },
  { pattern: '爱', result: '520' },
  { pattern: '男神', result: '520' },
  { pattern: '女神', result: '520' },
  { pattern: '表白', result: '520' },
  
  // 一生一世
  { pattern: '一生一世', result: '1314' },
  { pattern: '永恒', result: '1314' },
  { pattern: '永远', result: '1314' },
  { pattern: '生生世世', result: '3344' },
  { pattern: '长长久久', result: '3344' },
  
  // 再见
  { pattern: '再见', result: '88' },
  { pattern: '拜拜', result: '88' },
  { pattern: '走了', result: '88' },
  { pattern: ' goodbye', result: '88' },
  { pattern: 'bye', result: '88' },
  
  // 哭/难过
  { pattern: '哭', result: '555' },
  { pattern: '好累', result: '555' },
  { pattern: '累', result: '555' },
  { pattern: '难过', result: '555' },
  { pattern: '伤心', result: '555' },
  { pattern: '痛苦', result: '555' },
  { pattern: '郁闷', result: '555' },
  
  // 发财
  { pattern: '恭喜发财', result: '888' },
  { pattern: '财源广进', result: '888' },
  { pattern: '发财', result: '888' },
  { pattern: '恭喜', result: '888' },
  { pattern: '有钱', result: '888' },
  { pattern: '富有', result: '888' },
  { pattern: '旺', result: '888' },
  { pattern: '发', result: '888' },
  
  // 顺利/成功
  { pattern: '顺利', result: '66' },
  { pattern: '成功', result: '66' },
  { pattern: '棒', result: '66' },
  { pattern: '厉害', result: '666' },
  { pattern: '666', result: '666' },
  { pattern: '牛', result: '666' },
  { pattern: '太强', result: '666' },
  
  // 救命
  { pattern: '救命', result: '995' },
  { pattern: '救我', result: '995' },
  { pattern: '帮我', result: '995' },
  { pattern: '帮忙', result: '995' },
  { pattern: 'help', result: '995' },
  
  // 生日
  { pattern: '生日', result: '218' },
  { pattern: '生日快乐', result: '218' },
  
  // 亲亲
  { pattern: '亲亲', result: '777' },
  { pattern: '么么', result: '777' },
  { pattern: '想你', result: '777' },
  { pattern: '么么哒', result: '777' },
  
  // 再见/OK
  { pattern: '007', result: '88' },
  { pattern: 'OK', result: '88' },
  { pattern: '好的', result: '88' },
  { pattern: '收到', result: '88' },
  
  // 天气相关
  { pattern: '天气好', result: '88' },
  { pattern: '天气晴', result: '88' },
  { pattern: '晴天', result: '88' },
  { pattern: '阳光', result: '88' },
  
  // 数字解读
  { pattern: '520', result: '1314' },
  { pattern: '1314', result: '520' },
  { pattern: '888', result: '16888' },
  
  // 谢谢/再见
  { pattern: '谢谢', result: '88' },
  { pattern: '感谢', result: '88' },
  { pattern: '感恩', result: '88' },
  
  // 加油
  { pattern: '加油', result: '66' },
  { pattern: '努力', result: '66' },
];

function parseExpression(message) {
  let expr = message.toLowerCase();
  let matchedResult = null;
  
  // 查找匹配的模式
  for (const { pattern, result } of NUMBER_PATTERNS) {
    if (expr.includes(pattern.toLowerCase())) {
      matchedResult = result;
      console.log(`📝 理解: "${pattern}" → ${result}`);
      break; // 只取第一个匹配
    }
  }
  
  // 如果有匹配的数字
  if (matchedResult) {
    // 检查是否有运算符号
    if (expr.includes('+') || expr.includes('加')) {
      return matchedResult + '+';
    } else if (expr.includes('-') || expr.includes('减')) {
      return matchedResult + '-';
    } else if (expr.includes('*') || expr.includes('乘')) {
      return matchedResult + '*';
    }
    return matchedResult;
  }
  
  // 没有匹配？尝试提取数字
  const numbers = message.match(/\d+/g);
  if (numbers && numbers.length > 0) {
    return numbers[0];
  }
  
  // 默认返回 88 (表示好的/知道了)
  return '88';
}

function openCalculatorWithNumber(number) {
  const scriptPath = path.join(__dirname, 'open_calc.py');
  try {
    execSync(`python3 "${scriptPath}" "${number}"`, { 
      stdio: 'ignore',
      timeout: 5000
    });
    return true;
  } catch (e) {
    try {
      spawn('gnome-calculator', ['--equation', number], {
        detached: true,
        stdio: 'ignore'
      }).unref();
      return true;
    } catch (e2) {
      return false;
    }
  }
}

async function main() {
  const message = process.argv.slice(2).join(' ') || '88';
  
  console.log(`💬 你说: "${message}"`);
  
  const expr = parseExpression(message);
  console.log(`🔢 翻译: ${expr}`);
  console.log('🧮 打开系统计算器...');
  
  const success = openCalculatorWithNumber(expr);
  
  if (success) {
    console.log('✅ 完成！查看计算器！');
  } else {
    console.log('❌ 打开失败');
  }
}

main().catch(console.error);


path: src/open_calc.py
content: #!/usr/bin/env python3
"""
Open Calculator with Number - 在系统计算器上显示数字
用于"计算器聊天"技能：根据中文谐音显示数字（520=我爱你）
"""

import sys
import os

# 设置显示环境（用于虚拟机环境）
if 'DISPLAY' not in os.environ:
    os.environ['DISPLAY'] = ':0'

import subprocess
import time


def is_calculator_running():
    """检查计算器是否正在运行"""
    try:
        # 使用 pgrep 检查进程，避免直接杀进程
        result = subprocess.run(
            ['pgrep', '-x', 'gnome-calculator'],
            capture_output=True,
            timeout=2
        )
        return result.returncode == 0
    except Exception:
        return False


def close_calculator():
    """关闭已打开的计算器"""
    try:
        subprocess.run(
            ['pkill', '-x', 'gnome-calculator'],
            capture_output=True,
            timeout=2
        )
        time.sleep(0.3)
    except Exception:
        pass  # 忽略错误


def open_calculator_with_number(number):
    """
    用 gnome-calculator 打开计算器并预填数字
    
    Args:
        number: 要显示的数字或表达式
    """
    # 先关闭已打开的计算器（确保只有一个实例）
    if is_calculator_running():
        close_calculator()
    
    # 验证输入只包含安全字符
    # 只允许数字和基本运算符
    safe_chars = set('0123456789+-*/.() ')
    if not all(c in safe_chars for c in str(number)):
        print(f"❌ 输入包含不安全字符: {number}")
        return False
    
    try:
        # 打开计算器并预填表达式
        # 使用 --equation 参数安全地传递输入
        subprocess.Popen(
            ['gnome-calculator', '--equation', str(number)],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            start_new_session=True  # 脱离父进程
        )
        time.sleep(0.5)  # 等待计算器启动
        print(f"✅ 已打开计算器，显示: {number}")
        return True
        
    except FileNotFoundError:
        print("❌ 错误: 未找到 gnome-calculator，请安装: sudo apt install gnome-calculator")
        return False
    except Exception as e:
        print(f"❌ 失败: {e}")
        return False


def main():
    """主函数"""
    # 默认显示 520（我爱你）
    number = sys.argv[1] if len(sys.argv) > 1 else '520'
    
    # 基本输入验证
    if len(number) > 100:  # 防止过长的输入
        print("❌ 输入过长")
        sys.exit(1)
    
    success = open_calculator_with_number(number)
    sys.exit(0 if success else 1)


if __name__ == '__main__':
    main()


path: src/platform/linux.js
content: const { exec } = require('child_process');
const { promisify } = require('util');

const execAsync = promisify(exec);
const LAUNCH_DELAY_MS = 800;

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function launchCalculator() {
  // Use --solve flag to display number directly (no xdotool needed)
  // This opens calculator with the number shown
  try {
    // Kill any existing calculator first
    try {
      await execAsync('pkill -f gnome-calculator', { stdio: 'ignore' });
    } catch (e) {
      // Ignore if no process running
    }
    await sleep(200);
    
    // Launch with the number - will be handled by typeNumber
    // Just ensure calculator is ready
    await execAsync('gnome-calculator &', { stdio: 'ignore' });
    await sleep(LAUNCH_DELAY_MS);
  } catch (error) {
    console.error('Failed to launch calculator:', error.message);
    throw error;
  }
}

async function typeNumber(number) {
  if (!number) {
    throw new Error('Number parameter is required');
  }

  try {
    // Use --solve flag to directly display the number
    // This works without xdotool!
    await execAsync(`gnome-calculator -s ${number} &`, { stdio: 'ignore' });
    console.log(`Displayed ${number} on calculator`);
  } catch (error) {
    console.error('Failed to type to calculator:', error.message);
    throw error;
  }
}

module.exports = { launchCalculator, typeNumber };


path: src/platform/macos.js
content: const { execSync } = require('child_process');

const LAUNCH_DELAY_MS = 500;
const TYPING_DELAY_SEC = 0.3;

function escapeAppleScript(str) {
  return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/'/g, '\\\'');
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function launchCalculator() {
  execSync('open -a Calculator', { stdio: 'ignore' });
  await sleep(LAUNCH_DELAY_MS);
}

async function typeNumber(number) {
  if (!number) {
    throw new Error('Number parameter is required');
  }
  
  const escapedNumber = escapeAppleScript(number);
  const chars = escapedNumber.split('').join(' ');
  const script = `
    tell application "Calculator"
      activate
    end tell
    delay ${TYPING_DELAY_SEC}
    tell application "System Events"
      keystroke "${chars}"
    end tell
  `;
  
  execSync(`osascript -e '${script}'`, { stdio: ['ignore', 'ignore', 'pipe'] });
}

module.exports = { launchCalculator, typeNumber };


path: src/platform/windows.js
content: const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');

const getBaseDir = () => {
  if (require.main && require.main.filename) {
    return path.dirname(require.main.filename);
  }
  return process.cwd();
};

async function launchCalculator() {
}

async function typeNumber(number) {
  if (!number) {
    throw new Error('Number parameter is required');
  }

  const baseDir = getBaseDir();
  const scriptPath = path.join(baseDir, 'temp_calc.ps1');
  const psScript = `
param($Num)

Add-Type -AssemblyName System.Windows.Forms

# Step 1: Check if Calculator is running
$calc = Get-Process -Name "CalculatorApp" -ErrorAction SilentlyContinue

# Step 2: If not running, start it
if (-not $calc) {
    Start-Process calc.exe -WindowStyle Normal
    Start-Sleep -Seconds 3
}

# Step 3: Find and activate Calculator window
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class Win32 {
    [DllImport("user32.dll")]
    public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
    [DllImport("user32.dll")]
    public static extern bool SetForegroundWindow(IntPtr hWnd);
    [DllImport("user32.dll")]
    public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
}
"@

$hwnd = [Win32]::FindWindow("ApplicationFrameWindow", "Calculator")
if ($hwnd -eq [IntPtr]::Zero) {
    $hwnd = [Win32]::FindWindow("CalcFrame", "Calculator")
}
if ($hwnd -eq [IntPtr]::Zero) {
    $hwnd = [Win32]::FindWindow($null, "Calculator")
}

# Step 4: Activate to foreground
if ($hwnd -ne [IntPtr]::Zero) {
    [Win32]::ShowWindow($hwnd, 9) | Out-Null
    Start-Sleep -Milliseconds 100
    [Win32]::SetForegroundWindow($hwnd) | Out-Null
    Start-Sleep -Milliseconds 300
}

# Step 5: Send number
$shell = New-Object -ComObject WScript.Shell
$shell.AppActivate("Calculator")
Start-Sleep -Milliseconds 200
$shell.SendKeys("{ESC}")
Start-Sleep -Milliseconds 100
$shell.SendKeys($Num)
`;

  fs.writeFileSync(scriptPath, psScript, 'utf8');
  
  try {
    execSync(`powershell -ExecutionPolicy Bypass -File "${scriptPath}" -Num "${number}"`, { 
      encoding: 'utf8',
      stdio: ['ignore', 'pipe', 'pipe']
    });
  } catch (error) {
    throw error;
  }
  
  if (fs.existsSync(scriptPath)) {
    fs.unlinkSync(scriptPath);
  }
}

module.exports = { launchCalculator, typeNumber };


path: src/show_calc.py
content: #!/usr/bin/env python3
"""Calculator Display for Wayland/Linux - 显示数字窗口"""

import sys
import os

os.environ.setdefault('DISPLAY', ':0')

import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Gdk', '4.0')
from gi.repository import Gtk, Gdk, Pango

def show_number(number):
    app = Gtk.Application()
    
    def on_activate(app):
        window = Gtk.ApplicationWindow(application=app)
        window.set_title("Calculator")
        window.set_default_size(400, 200)
        
        # Main box
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
        vbox.set_margin_top(30)
        vbox.set_margin_bottom(30)
        vbox.set_margin_start(30)
        vbox.set_margin_end(30)
        
        # Label with number
        label = Gtk.Label()
        label.set_text(str(number))
        label.set_halign(Gtk.Align.CENTER)
        label.set_valign(Gtk.Align.CENTER)
        
        # Set CSS for big font
        css = """
            label {
                font: 72px sans-serif;
            }
        """
        provider = Gtk.CssProvider()
        provider.load_from_string(css)
        label.get_style_context().add_provider(provider, 600)
        
        vbox.append(label)
        window.set_child(vbox)
        
        # Present window
        window.present()
    
    app.connect('activate', on_activate)
    app.run()

if __name__ == '__main__':
    number = sys.argv[1] if len(sys.argv) > 1 else '520'
    show_number(number)


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)