claude-buddy-cli 1.0.0

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,30 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node:*)",
5
+ "Bash(npm install:*)",
6
+ "Bash(npm test:*)",
7
+ "Bash(git init:*)",
8
+ "Bash(git commit -m ':*)",
9
+ "Bash(git show:*)",
10
+ "Bash(git:*)",
11
+ "Bash(npm link:*)",
12
+ "Bash(buddy-cli --help)",
13
+ "Bash(pkill -f \"node.*index.js\")",
14
+ "Bash(pkill -f \"node.*test\")",
15
+ "Bash(echo \"Exit code: $?\")",
16
+ "mcp__plugin_context7_context7__resolve-library-id",
17
+ "mcp__plugin_context7_context7__query-docs",
18
+ "WebSearch",
19
+ "Bash(unzip -l /Users/ichigoichie/ClaudeCode/buddy-cli.zip)",
20
+ "Bash(unzip -p /Users/ichigoichie/ClaudeCode/buddy-cli.zip buddy-cli/index.js)",
21
+ "Bash(mitmproxy --version)",
22
+ "Bash(claude --version)",
23
+ "Bash(grep -n '$S1\\\\|YS1' /Users/ichigoichie/.nvm/versions/node/v22.22.0/lib/node_modules/@anthropic-ai/claude-code/cli.js)",
24
+ "Bash(ps -p 73878 -o pid,ppid,command)",
25
+ "Bash(osascript -e 'tell application \"Terminal\" to get properties of front window')",
26
+ "Bash(osascript:*)",
27
+ "Bash(buddy-cli:*)"
28
+ ]
29
+ }
30
+ }
package/CLAUDE.md ADDED
@@ -0,0 +1,33 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## 项目概述
6
+
7
+ `buddy-cli` 是一个 CLI 工具,用于生成基于种子随机数的确定性用户 ID,目标是获得 "legendary" 稀有度结果。生成的 ID 会写入 `~/.claude.json` 的 `userID` 字段。
8
+
9
+ ## 命令
10
+
11
+ ```bash
12
+ # 运行 CLI(暴力搜索 legendary ID)
13
+ node index.js
14
+
15
+ # 或全局安装后运行
16
+ npm link
17
+ buddy-cli
18
+ ```
19
+
20
+ ## 架构
21
+
22
+ 单文件应用 (`index.js`),包含三个核心组件:
23
+
24
+ 1. **哈希函数** (`fnv1a`) - FNV-1a 哈希算法,用于生成确定性种子
25
+ 2. **PRNG** (`mulberry32`) - 种子随机数生成器
26
+ 3. **掷骰逻辑** (`roll`) - 将随机值映射到稀有度等级:
27
+ - legendary: >= 99%(1% 概率)
28
+ - epic: >= 95%(4% 概率)
29
+ - rare: >= 85%(10% 概率)
30
+ - uncommon: >= 60%(25% 概率)
31
+ - common: < 60%(60% 概率)
32
+
33
+ `findLegendary()` 函数暴力搜索随机 ID,直到产生 "legendary" 结果。获胜的 ID 会写入 `~/.claude.json`。
package/index.js ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs"
4
+ import os from "os"
5
+ import path from "path"
6
+ import { fileURLToPath } from 'url'
7
+
8
+ const SALT = "friend-2026-401"
9
+ const CLAUDE_CONFIG_PATH = path.join(os.homedir(), ".claude.json")
10
+
11
+ // FNV-1a hash
12
+ function fnv1a(str) {
13
+ let hash = 0x811c9dc5
14
+ for (let i = 0; i < str.length; i++) {
15
+ hash ^= str.charCodeAt(i)
16
+ hash = (hash * 0x01000193) >>> 0
17
+ }
18
+ return hash
19
+ }
20
+
21
+ // PRNG (Mulberry32)
22
+ function mulberry32(a) {
23
+ return function () {
24
+ let t = (a += 0x6d2b79f5)
25
+ t = Math.imul(t ^ (t >>> 15), t | 1)
26
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
27
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
28
+ }
29
+ }
30
+
31
+ // 稀有度判定
32
+ function getRarityFromRandom(r) {
33
+ if (r >= 0.99) return 'legendary'
34
+ if (r >= 0.95) return 'epic'
35
+ if (r >= 0.85) return 'rare'
36
+ if (r >= 0.60) return 'uncommon'
37
+ return 'common'
38
+ }
39
+
40
+ // 掷骰
41
+ function roll(userId) {
42
+ const seed = fnv1a(userId + SALT)
43
+ const rand = mulberry32(seed)
44
+ return getRarityFromRandom(rand())
45
+ }
46
+
47
+ // 生成随机 ID
48
+ function generateDrawId() {
49
+ return 'user_' + Math.random().toString(36).slice(2, 10)
50
+ }
51
+
52
+ // 更新 Claude Code 配置
53
+ function updateClaudeBuddy(userId) {
54
+ let config = {}
55
+ if (fs.existsSync(CLAUDE_CONFIG_PATH)) {
56
+ try {
57
+ config = JSON.parse(fs.readFileSync(CLAUDE_CONFIG_PATH, 'utf-8'))
58
+ } catch {
59
+ config = {}
60
+ }
61
+ }
62
+ config.userID = userId
63
+ fs.writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2))
64
+ }
65
+
66
+ // 主逻辑
67
+ function findLegendary() {
68
+ let drawCount = 0
69
+
70
+ console.log('\n🎰 Buddy Gacha')
71
+ console.log('Searching for legendary...\n')
72
+
73
+ while (true) {
74
+ const id = generateDrawId()
75
+ drawCount++
76
+
77
+ if (roll(id) === 'legendary') {
78
+ updateClaudeBuddy(id)
79
+ console.log(`\n✨ LEGENDARY FOUND!`)
80
+ console.log(`ID: ${id}`)
81
+ console.log(`Draws: ${drawCount}`)
82
+ console.log(`\nUpdated ~/.claude.json\n`)
83
+ return
84
+ }
85
+
86
+ if (drawCount % 100 === 0) {
87
+ process.stdout.write(`\rTried ${drawCount} candidates...`)
88
+ }
89
+ }
90
+ }
91
+
92
+ // 执行
93
+ const __filename = fileURLToPath(import.meta.url)
94
+ if (process.argv[1] && fs.existsSync(process.argv[1])) {
95
+ const realArgv1 = fs.realpathSync(process.argv[1])
96
+ if (__filename === realArgv1) {
97
+ findLegendary()
98
+ }
99
+ }
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "claude-buddy-cli",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Find legendary buddy ID for Claude Code",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "buddy-cli": "./index.js"
9
+ },
10
+ "keywords": [],
11
+ "author": "",
12
+ "license": "ISC"
13
+ }
@@ -0,0 +1,225 @@
1
+ import { describe, it, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import fs from 'fs'
4
+ import os from 'os'
5
+ import path from 'path'
6
+
7
+ // 测试辅助函数
8
+ const testCollectionPath = path.join(os.homedir(), '.buddy-collection-test.json')
9
+
10
+ function cleanupTestCollection() {
11
+ if (fs.existsSync(testCollectionPath)) {
12
+ fs.unlinkSync(testCollectionPath)
13
+ }
14
+ }
15
+
16
+ describe('Buddy CLI', () => {
17
+ beforeEach(() => {
18
+ cleanupTestCollection()
19
+ })
20
+
21
+ // Task 2 的测试将在这里添加
22
+ })
23
+
24
+ describe('Core Algorithms', () => {
25
+ it('fnv1a should produce consistent hash', async () => {
26
+ const { fnv1a } = await import('../index.js')
27
+ const hash1 = fnv1a('test-string')
28
+ const hash2 = fnv1a('test-string')
29
+ assert.equal(hash1, hash2)
30
+ assert.equal(typeof hash1, 'number')
31
+ })
32
+
33
+ it('fnv1a should produce different hashes for different inputs', async () => {
34
+ const { fnv1a } = await import('../index.js')
35
+ const hash1 = fnv1a('string-a')
36
+ const hash2 = fnv1a('string-b')
37
+ assert.notEqual(hash1, hash2)
38
+ })
39
+
40
+ it('mulberry32 should produce deterministic sequence', async () => {
41
+ const { mulberry32 } = await import('../index.js')
42
+ const rand1 = mulberry32(12345)
43
+ const rand2 = mulberry32(12345)
44
+ assert.equal(rand1(), rand2())
45
+ assert.equal(rand1(), rand2())
46
+ })
47
+
48
+ it('roll should return valid rarity', async () => {
49
+ const { roll } = await import('../index.js')
50
+ const rarities = ['common', 'uncommon', 'rare', 'epic', 'legendary']
51
+ // 测试多个 ID
52
+ for (let i = 0; i < 100; i++) {
53
+ const id = 'user_' + Math.random().toString(36).slice(2, 10)
54
+ const rarity = roll(id)
55
+ assert.ok(rarities.includes(rarity))
56
+ }
57
+ })
58
+ })
59
+
60
+ describe('Rarity Config', () => {
61
+ it('RARITY_COLORS should have all five rarities', async () => {
62
+ const { RARITY_COLORS } = await import('../index.js')
63
+ const rarities = ['common', 'uncommon', 'rare', 'epic', 'legendary']
64
+ for (const r of rarities) {
65
+ assert.ok(RARITY_COLORS[r])
66
+ }
67
+ })
68
+
69
+ it('RARITY_SIZES should define pixel art sizes', async () => {
70
+ const { RARITY_SIZES } = await import('../index.js')
71
+ assert.equal(RARITY_SIZES.common, 5)
72
+ assert.equal(RARITY_SIZES.uncommon, 7)
73
+ assert.equal(RARITY_SIZES.rare, 9)
74
+ assert.equal(RARITY_SIZES.epic, 11)
75
+ assert.equal(RARITY_SIZES.legendary, 15)
76
+ })
77
+
78
+ it('getRarityFromRandom should map probabilities correctly', async () => {
79
+ const { getRarityFromRandom } = await import('../index.js')
80
+ assert.equal(getRarityFromRandom(0.005), 'legendary') // < 1%
81
+ assert.equal(getRarityFromRandom(0.03), 'epic') // 1-5%
82
+ assert.equal(getRarityFromRandom(0.10), 'rare') // 5-15%
83
+ assert.equal(getRarityFromRandom(0.30), 'uncommon') // 15-40%
84
+ assert.equal(getRarityFromRandom(0.50), 'common') // >= 40%
85
+ })
86
+ })
87
+
88
+ describe('Pixel Art Generator', () => {
89
+ it('generatePixelArt should return string for common rarity', async () => {
90
+ const { generatePixelArt } = await import('../index.js')
91
+ const art = generatePixelArt('user_abc123', 'common')
92
+ assert.equal(typeof art, 'string')
93
+ assert.ok(art.length > 0)
94
+ })
95
+
96
+ it('generatePixelArt should produce larger art for higher rarity', async () => {
97
+ const { generatePixelArt } = await import('../index.js')
98
+ const commonArt = generatePixelArt('user_test1', 'common')
99
+ const legendaryArt = generatePixelArt('user_test2', 'legendary')
100
+ // legendary 应该有更多行
101
+ const commonLines = commonArt.split('\n').length
102
+ const legendaryLines = legendaryArt.split('\n').length
103
+ assert.ok(legendaryLines > commonLines)
104
+ })
105
+
106
+ it('generatePixelArt should produce different art for different IDs', async () => {
107
+ const { generatePixelArt } = await import('../index.js')
108
+ const art1 = generatePixelArt('user_aaaaaaa', 'legendary')
109
+ const art2 = generatePixelArt('user_bbbbbbb', 'legendary')
110
+ assert.notEqual(art1, art2)
111
+ })
112
+
113
+ it('extractFeatures should return valid feature object', async () => {
114
+ const { extractFeatures } = await import('../index.js')
115
+ const features = extractFeatures('user_test123')
116
+ assert.ok(typeof features.accessory === 'number')
117
+ assert.ok(features.accessory >= 0 && features.accessory < 8)
118
+ })
119
+ })
120
+
121
+ describe('Animation Helpers', () => {
122
+ it('sleep should resolve after specified time', async () => {
123
+ const { sleep } = await import('../index.js')
124
+ const start = Date.now()
125
+ await sleep(50)
126
+ const elapsed = Date.now() - start
127
+ assert.ok(elapsed >= 40 && elapsed < 100)
128
+ })
129
+
130
+ it('generateDrawId should produce valid user ID format', async () => {
131
+ const { generateDrawId } = await import('../index.js')
132
+ for (let i = 0; i < 10; i++) {
133
+ const id = generateDrawId()
134
+ assert.ok(id.startsWith('user_'))
135
+ assert.ok(id.length >= 9) // user_ (5) + at least 4 chars from slice
136
+ assert.ok(id.length <= 13) // user_ (5) + max 8 chars from slice
137
+ }
138
+ })
139
+ })
140
+
141
+ describe('Collection System', () => {
142
+ const testPath = path.join(os.homedir(), '.buddy-collection-test.json')
143
+
144
+ beforeEach(() => {
145
+ if (fs.existsSync(testPath)) {
146
+ fs.unlinkSync(testPath)
147
+ }
148
+ })
149
+
150
+ it('loadCollection should return empty object for non-existent file', async () => {
151
+ const { loadCollection } = await import('../index.js')
152
+ const collection = loadCollection(testPath)
153
+ assert.deepEqual(collection.buddies, [])
154
+ assert.deepEqual(collection.stats, { totalDraws: 0, legendaryCount: 0 })
155
+ })
156
+
157
+ it('addToCollection should save buddy correctly', async () => {
158
+ const { addToCollection, loadCollection } = await import('../index.js')
159
+ addToCollection('user_test123', 'legendary', 42, testPath)
160
+ const collection = loadCollection(testPath)
161
+ assert.equal(collection.buddies.length, 1)
162
+ assert.equal(collection.buddies[0].id, 'user_test123')
163
+ assert.equal(collection.buddies[0].rarity, 'legendary')
164
+ assert.equal(collection.buddies[0].drawCount, 42)
165
+ assert.equal(collection.stats.legendaryCount, 1)
166
+ })
167
+
168
+ it('addToCollection should append multiple buddies', async () => {
169
+ const { addToCollection, loadCollection } = await import('../index.js')
170
+ addToCollection('user_aaa', 'legendary', 10, testPath)
171
+ addToCollection('user_bbb', 'legendary', 20, testPath)
172
+ const collection = loadCollection(testPath)
173
+ assert.equal(collection.buddies.length, 2)
174
+ assert.equal(collection.stats.legendaryCount, 2)
175
+ })
176
+
177
+ it('updateStats should increment totalDraws', async () => {
178
+ const { updateStats, loadCollection } = await import('../index.js')
179
+ updateStats(testPath, 100)
180
+ const collection = loadCollection(testPath)
181
+ assert.equal(collection.stats.totalDraws, 100)
182
+ })
183
+
184
+ it('addToCollection should not count non-legendary in legendaryCount', async () => {
185
+ const { addToCollection, loadCollection } = await import('../index.js')
186
+ addToCollection('user_epic', 'epic', 10, testPath)
187
+ const collection = loadCollection(testPath)
188
+ assert.equal(collection.buddies.length, 1)
189
+ assert.equal(collection.stats.legendaryCount, 0)
190
+ })
191
+ })
192
+
193
+ describe('Gallery Command', () => {
194
+ const testPath = path.join(os.homedir(), '.buddy-collection-test.json')
195
+
196
+ beforeEach(() => {
197
+ if (fs.existsSync(testPath)) {
198
+ fs.unlinkSync(testPath)
199
+ }
200
+ })
201
+
202
+ it('getGalleryBuddies should return only legendary buddies', async () => {
203
+ const { addToCollection, getGalleryBuddies } = await import('../index.js')
204
+ addToCollection('user_leg1', 'legendary', 10, testPath)
205
+ addToCollection('user_leg2', 'legendary', 20, testPath)
206
+ addToCollection('user_epic', 'epic', 5, testPath)
207
+ const buddies = getGalleryBuddies(testPath)
208
+ assert.equal(buddies.length, 2)
209
+ assert.equal(buddies[0].rarity, 'legendary')
210
+ assert.equal(buddies[1].rarity, 'legendary')
211
+ })
212
+
213
+ it('formatBuddyDisplay should format buddy info', async () => {
214
+ const { formatBuddyDisplay } = await import('../index.js')
215
+ const buddy = {
216
+ id: 'user_test',
217
+ rarity: 'legendary',
218
+ foundAt: '2026-04-03T10:00:00Z',
219
+ drawCount: 42
220
+ }
221
+ const display = formatBuddyDisplay(buddy)
222
+ assert.ok(display.includes('user_test'))
223
+ assert.ok(display.includes('42'))
224
+ })
225
+ })