@tina_summer/codepocket 2.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.
package/bin/run.js ADDED
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CodePocket Connector — 一行命令连接小程序
5
+ *
6
+ * 用法:
7
+ * 首次: npx codepocket --key YOUR_KEY
8
+ * 之后: npx codepocket
9
+ *
10
+ * 也可通过环境变量配置:
11
+ * RELAY_KEY=xxx codepocket
12
+ */
13
+
14
+ const { WebSocket } = require('ws')
15
+ const fs = require('fs')
16
+ const path = require('path')
17
+ const os = require('os')
18
+ const qrcode = require('qrcode-terminal')
19
+
20
+ // ── Config ──
21
+
22
+ const CONFIG_DIR = path.join(os.homedir(), '.codepocket', 'hub')
23
+ const CONFIG_FILE = path.join(path.join(os.homedir(), '.codepocket'), 'config.json')
24
+ const SESSION_FILE = path.join(CONFIG_DIR, 'wechat-session.json')
25
+
26
+ function loadConfig() {
27
+ try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')) } catch { return {} }
28
+ }
29
+
30
+ function saveConfig(cfg) {
31
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
32
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2))
33
+ }
34
+
35
+ // Parse args
36
+ const args = process.argv.slice(2)
37
+ let hubUrl = process.env.CPKT_HUB_URL || process.env.HAPI_HUB_URL || 'http://127.0.0.1:3006'
38
+ let relayUrl = process.env.RELAY_WS_URL || ''
39
+ let relayKey = process.env.RELAY_KEY || ''
40
+
41
+ for (let i = 0; i < args.length; i++) {
42
+ const arg = args[i]
43
+ if (arg.startsWith('--hub=')) hubUrl = arg.slice(6).replace(/\/+$/, '')
44
+ if (arg === '--hub' && args[i + 1]) { hubUrl = args[++i].replace(/\/+$/, '') }
45
+ if (arg.startsWith('--relay=')) relayUrl = arg.slice(8)
46
+ if (arg === '--relay' && args[i + 1]) { relayUrl = args[++i] }
47
+ if (arg.startsWith('--key=')) relayKey = arg.slice(6)
48
+ if (arg === '--key' && args[i + 1]) { relayKey = args[++i] }
49
+ }
50
+
51
+ // Load saved config, merge with args
52
+ const saved = loadConfig()
53
+ if (relayKey) {
54
+ // New key provided — save it
55
+ saveConfig({ ...saved, relayKey, relayUrl: relayUrl || saved.relayUrl || 'wss://bkstory.cn/ws' })
56
+ } else {
57
+ relayKey = saved.relayKey || ''
58
+ }
59
+ if (!relayUrl) relayUrl = saved.relayUrl || 'wss://bkstory.cn/ws'
60
+
61
+ let jwt = ''
62
+ let relayWs = null
63
+ let relaySessionToken = ''
64
+ let sseResponse = null
65
+
66
+ // ── Find CLI token ──
67
+
68
+ async function findHubToken() {
69
+ const hubHome = process.env.HAPI_HOME || path.join(os.homedir(), '.codepocket', 'hub')
70
+ const settingsPath = path.join(hubHome, 'settings.json')
71
+ try {
72
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))
73
+ if (settings.cliApiToken) return settings.cliApiToken
74
+ } catch {}
75
+ if (process.env.CLI_API_TOKEN) return process.env.CLI_API_TOKEN
76
+ console.error('[!] 找不到 Hub token,请先运行: cpkt')
77
+ process.exit(1)
78
+ }
79
+
80
+ // ── Authenticate ──
81
+
82
+ async function authenticateHub(token) {
83
+ const res = await fetch(`${hubUrl}/api/auth`, {
84
+ method: 'POST',
85
+ headers: { 'Content-Type': 'application/json' },
86
+ body: JSON.stringify({ accessToken: token }),
87
+ })
88
+ if (!res.ok) {
89
+ const data = await res.json().catch(() => ({}))
90
+ throw new Error(`Hub auth failed: ${data.error || res.status}`)
91
+ }
92
+ return (await res.json()).token
93
+ }
94
+
95
+ // ── SSE stream ──
96
+
97
+ async function startSSE() {
98
+ const url = `${hubUrl}/api/events?token=${encodeURIComponent(jwt)}&all=true`
99
+ try {
100
+ const res = await fetch(url, { headers: { Accept: 'text/event-stream' } })
101
+ if (!res.ok || !res.body) {
102
+ console.error(`[SSE] failed: ${res.status}, retrying in 5s...`)
103
+ setTimeout(startSSE, 5000)
104
+ return
105
+ }
106
+ const reader = res.body.getReader()
107
+ const decoder = new TextDecoder()
108
+ let buffer = ''
109
+ async function pump() {
110
+ while (true) {
111
+ const { done, value } = await reader.read()
112
+ if (done) break
113
+ buffer += decoder.decode(value, { stream: true })
114
+ const lines = buffer.split('\n')
115
+ buffer = lines.pop() || ''
116
+ for (const line of lines) {
117
+ if (line.startsWith('data:')) {
118
+ const raw = line.slice(5).trim()
119
+ if (!raw) continue
120
+ try { sendToRelay({ type: 'sse-event', event: JSON.parse(raw) }) } catch {}
121
+ }
122
+ }
123
+ }
124
+ console.error('[SSE] stream ended, reconnecting in 5s...')
125
+ setTimeout(startSSE, 5000)
126
+ }
127
+ sseResponse = res
128
+ pump().catch(() => { setTimeout(startSSE, 5000) })
129
+ } catch {
130
+ console.error('[SSE] connection error, retrying in 5s...')
131
+ setTimeout(startSSE, 5000)
132
+ }
133
+ }
134
+
135
+ // ── Relay connection ──
136
+
137
+ function connectRelay() {
138
+ let savedSession = null
139
+ try { savedSession = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8')) } catch {}
140
+
141
+ relayWs = new WebSocket(relayUrl)
142
+
143
+ relayWs.on('open', () => {
144
+ console.log('[Relay] connected')
145
+ relayWs.send(JSON.stringify({
146
+ type: 'register',
147
+ key: relayKey,
148
+ sessionToken: savedSession?.sessionToken || undefined,
149
+ }))
150
+ })
151
+
152
+ relayWs.on('message', (raw) => {
153
+ let msg
154
+ try { msg = JSON.parse(raw.toString()) } catch { return }
155
+ handleRelayMessage(msg)
156
+ })
157
+
158
+ relayWs.on('close', () => {
159
+ console.log('[Relay] disconnected, reconnecting in 3s...')
160
+ relayWs = null
161
+ setTimeout(connectRelay, 3000)
162
+ })
163
+
164
+ relayWs.on('error', () => { relayWs?.close() })
165
+ }
166
+
167
+ function handleRelayMessage(msg) {
168
+ switch (msg.type) {
169
+ case 'registered': {
170
+ relaySessionToken = msg.sessionToken
171
+ try {
172
+ fs.mkdirSync(path.dirname(SESSION_FILE), { recursive: true })
173
+ fs.writeFileSync(SESSION_FILE, JSON.stringify({ sessionToken: relaySessionToken, hubUrl, relayUrl }))
174
+ } catch {}
175
+ if (msg.reconnected) {
176
+ console.log('\n [✓] 已重新连接\n')
177
+ } else {
178
+ const qrContent = `codepocket://pair?token=${msg.token}`
179
+ console.log()
180
+ qrcode.generate(qrContent, { small: true }, (qrStr) => {
181
+ console.log(qrStr)
182
+ })
183
+ console.log(' 打开小程序 → 扫码连接\n')
184
+ }
185
+ break
186
+ }
187
+ case 'client-connected': {
188
+ console.log(' [✓] 微信小程序已连接')
189
+ break
190
+ }
191
+ case 'rest-request': {
192
+ forwardToHub(msg.requestId, msg.method, msg.path, msg.body)
193
+ break
194
+ }
195
+ case 'auth-request': {
196
+ handleAuthRequest(msg.requestId, msg.accessToken)
197
+ break
198
+ }
199
+ case 'register-error': {
200
+ console.error(` [!] ${msg.error}`)
201
+ console.error(' 请检查 key 是否正确')
202
+ console.error(' 获取 key: curl -H "Authorization: Bearer ADMIN_KEY" http://127.0.0.1:3010/admin/keys')
203
+ break
204
+ }
205
+ }
206
+ }
207
+
208
+ function sendToRelay(msg) {
209
+ if (relayWs?.readyState === WebSocket.OPEN) {
210
+ relayWs.send(JSON.stringify(msg))
211
+ }
212
+ }
213
+
214
+ async function forwardToHub(requestId, method, p, body) {
215
+ try {
216
+ const init = {
217
+ method,
218
+ headers: {
219
+ 'Content-Type': 'application/json',
220
+ ...(jwt ? { Authorization: `Bearer ${jwt}` } : {}),
221
+ },
222
+ }
223
+ if (body && method !== 'GET') init.body = JSON.stringify(body)
224
+ const res = await fetch(`${hubUrl}${p}`, init)
225
+ const data = await res.json().catch(() => ({}))
226
+ sendToRelay({ type: 'rest-response', requestId, status: res.status, data })
227
+ } catch (err) {
228
+ sendToRelay({ type: 'rest-response', requestId, status: 502, data: { error: err.message } })
229
+ }
230
+ }
231
+
232
+ async function handleAuthRequest(requestId, accessToken) {
233
+ try {
234
+ const res = await fetch(`${hubUrl}/api/auth`, {
235
+ method: 'POST',
236
+ headers: { 'Content-Type': 'application/json' },
237
+ body: JSON.stringify({ accessToken }),
238
+ })
239
+ const data = await res.json()
240
+ sendToRelay({ type: 'auth-response', requestId, ok: res.ok, data })
241
+ } catch (err) {
242
+ sendToRelay({ type: 'auth-response', requestId, ok: false, data: { error: err.message } })
243
+ }
244
+ }
245
+
246
+ // ── Main ──
247
+
248
+ async function main() {
249
+ console.log('╔══════════════════════════════════════╗')
250
+ console.log('║ CodePocket Connect ║')
251
+ console.log('╚══════════════════════════════════════╝')
252
+ console.log()
253
+
254
+ if (!relayKey) {
255
+ console.error('[!] 需要 relay 密钥(首次使用只需设置一次)')
256
+ console.error()
257
+ console.error(' 用法:')
258
+ console.error(' codepocket --key YOUR_KEY')
259
+ console.error()
260
+ console.error(' 获取 key:')
261
+ console.error(' curl -H "Authorization: Bearer ADMIN_KEY" http://YOUR_SERVER:3010/admin/keys')
262
+ console.error()
263
+ console.error(' 或设置环境变量:')
264
+ console.error(' export RELAY_KEY=xxx')
265
+ console.error()
266
+ process.exit(1)
267
+ }
268
+
269
+ console.log(` Hub: ${hubUrl}`)
270
+ console.log(` Relay: ${relayUrl}`)
271
+ console.log(` Key: ${relayKey.slice(0, 8)}...`)
272
+ console.log()
273
+
274
+ console.log('[1/3] 连接 Hub...')
275
+ const token = await findHubToken()
276
+
277
+ console.log('[2/3] 认证中...')
278
+ jwt = await authenticateHub(token)
279
+ console.log('[✓] Hub 认证成功')
280
+
281
+ console.log('[3/3] 连接中继服务器...')
282
+ startSSE()
283
+ connectRelay()
284
+ }
285
+
286
+ main().catch((err) => {
287
+ console.error('[!] 启动失败:', err.message)
288
+ process.exit(1)
289
+ })
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@tina_summer/codepocket",
3
+ "version": "2.0.0",
4
+ "description": "CodePocket — 你的全能 AI 随身控制台",
5
+ "bin": {
6
+ "codepocket": "bin/codepocket.js",
7
+ "cpkt": "bin/codepocket.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "vendor"
13
+ ],
14
+ "scripts": {
15
+ "start": "node bin/codepocket.js"
16
+ },
17
+ "dependencies": {
18
+ "qrcode-terminal": "^0.12.0",
19
+ "ws": "^8.18.0"
20
+ },
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "keywords": [
25
+ "codepocket",
26
+ "claude",
27
+ "wechat",
28
+ "ai"
29
+ ],
30
+ "license": "MIT"
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,304 @@
1
+ /**
2
+ * CodePocket Connector — 在用户机器上运行,桥接本地 Hapi Hub 和远程 Relay
3
+ *
4
+ * 用法:
5
+ * bun run src/index.ts # 自动发现本地 hub
6
+ * bun run src/index.ts --hub http://localhost:3006 --relay wss://relay.example.com/ws
7
+ *
8
+ * 工作流程:
9
+ * 1. 连接本地 Hapi Hub REST API,获取 CLI_API_TOKEN
10
+ * 2. 用 token 调用 /api/auth 获取 JWT
11
+ * 3. 开启 SSE 流,接收实时事件
12
+ * 4. 连接远程 Relay WebSocket
13
+ * 5. 注册并获取配对码,显示给用户
14
+ * 6. 双向转发: SSE→Relay, Relay REST→Hub REST
15
+ */
16
+
17
+ const HUB_URL = (process.env.CPKT_HUB_URL || process.env.HAPI_HUB_URL || 'http://127.0.0.1:3006').replace(/\/+$/, '')
18
+ const RELAY_WS = process.env.RELAY_WS_URL || 'ws://127.0.0.1:3010/ws'
19
+ const HUB_HOME = process.env.HAPI_HOME || `${process.env.HOME}/.codepocket/hub`
20
+ const SESSION_FILE = `${HUB_HOME}/wechat-session.json`
21
+
22
+ let hubToken = '' // CLI_API_TOKEN
23
+ let jwt = '' // JWT from hub
24
+ let relayWs: any = null
25
+ let relaySessionToken = ''
26
+ let sseController: AbortController | null = null
27
+
28
+ // --- Parse args ---
29
+ for (const arg of process.argv.slice(2)) {
30
+ if (arg.startsWith('--hub=')) process.env.HAPI_HUB_URL = arg.slice(6)
31
+ if (arg.startsWith('--relay=')) process.env.RELAY_WS_URL = arg.slice(8)
32
+ }
33
+
34
+ const hubUrl = (process.env.HAPI_HUB_URL || HUB_URL).replace(/\/+$/, '')
35
+ const relayUrl = process.env.RELAY_WS_URL || RELAY_WS
36
+
37
+ // --- Step 1: Find CLI token ---
38
+
39
+ async function findHubToken(): Promise<string> {
40
+ // Try settings file
41
+ const settingsPath = `${HUB_HOME}/settings.json`
42
+ try {
43
+ const settings = await Bun.file(settingsPath).json()
44
+ if (settings.cliApiToken) return settings.cliApiToken
45
+ } catch {}
46
+
47
+ // Try env
48
+ if (process.env.CLI_API_TOKEN) return process.env.CLI_API_TOKEN
49
+
50
+ console.error('[!] 无法找到 CLI_API_TOKEN')
51
+ console.error(' 请确保 Hub 正在运行,或设置 CLI_API_TOKEN 环境变量')
52
+ process.exit(1)
53
+ }
54
+
55
+ // --- Step 2: Authenticate with hub ---
56
+
57
+ async function authenticateHub(token: string): Promise<string> {
58
+ const res = await fetch(`${hubUrl}/api/auth`, {
59
+ method: 'POST',
60
+ headers: { 'Content-Type': 'application/json' },
61
+ body: JSON.stringify({ accessToken: token }),
62
+ })
63
+
64
+ if (!res.ok) {
65
+ const data = await res.json().catch(() => ({}))
66
+ throw new Error(`Hub auth failed: ${data.error || res.status}`)
67
+ }
68
+
69
+ const data = await res.json()
70
+ return data.token
71
+ }
72
+
73
+ // --- Step 3: Start SSE from hub ---
74
+
75
+ function startSSE() {
76
+ sseController?.abort()
77
+ sseController = new AbortController()
78
+
79
+ const url = `${hubUrl}/api/events?token=${encodeURIComponent(jwt)}&all=true`
80
+
81
+ ;(async () => {
82
+ try {
83
+ const res = await fetch(url, {
84
+ signal: sseController!.signal,
85
+ headers: { Accept: 'text/event-stream' },
86
+ })
87
+
88
+ if (!res.ok || !res.body) {
89
+ console.error(`[SSE] connection failed: ${res.status}`)
90
+ return
91
+ }
92
+
93
+ const reader = res.body.getReader()
94
+ const decoder = new TextDecoder()
95
+ let buffer = ''
96
+
97
+ while (true) {
98
+ const { done, value } = await reader.read()
99
+ if (done) break
100
+
101
+ buffer += decoder.decode(value, { stream: true })
102
+ const lines = buffer.split('\n')
103
+ buffer = lines.pop() || ''
104
+
105
+ for (const line of lines) {
106
+ if (line.startsWith('data:')) {
107
+ const raw = line.slice(5).trim()
108
+ if (!raw) continue
109
+ try {
110
+ const event = JSON.parse(raw)
111
+ forwardToRelay({ type: 'sse-event', event })
112
+ } catch {}
113
+ }
114
+ }
115
+ }
116
+ } catch (err: any) {
117
+ if (err.name !== 'AbortError') {
118
+ console.error('[SSE] stream ended, reconnecting in 5s...')
119
+ setTimeout(startSSE, 5000)
120
+ }
121
+ }
122
+ })()
123
+ }
124
+
125
+ // --- Step 4: Connect to relay ---
126
+
127
+ function connectRelay() {
128
+ try {
129
+ relayWs = new WebSocket(relayUrl)
130
+ } catch (err) {
131
+ console.error(`[Relay] connect failed, retrying in 5s...`)
132
+ setTimeout(connectRelay, 5000)
133
+ return
134
+ }
135
+
136
+ relayWs.onopen = () => {
137
+ console.log('[Relay] connected')
138
+
139
+ // Try to reconnect with existing session
140
+ let savedSession: any = null
141
+ try { savedSession = JSON.parse(require('fs').readFileSync(SESSION_FILE, 'utf-8')) } catch {}
142
+
143
+ relayWs.send(JSON.stringify({
144
+ type: 'register',
145
+ sessionToken: savedSession?.sessionToken || undefined,
146
+ }))
147
+ }
148
+
149
+ relayWs.onmessage = (event: MessageEvent) => {
150
+ let msg: any
151
+ try { msg = JSON.parse(event.data as string) } catch { return }
152
+ handleRelayMessage(msg)
153
+ }
154
+
155
+ relayWs.onclose = () => {
156
+ console.log('[Relay] disconnected, reconnecting in 3s...')
157
+ relayWs = null
158
+ setTimeout(connectRelay, 3000)
159
+ }
160
+
161
+ relayWs.onerror = () => {
162
+ relayWs?.close()
163
+ }
164
+ }
165
+
166
+ function handleRelayMessage(msg: any) {
167
+ switch (msg.type) {
168
+ case 'registered': {
169
+ relaySessionToken = msg.sessionToken
170
+
171
+ // Save session for reconnection
172
+ try {
173
+ require('fs').mkdirSync(require('path').dirname(SESSION_FILE), { recursive: true })
174
+ require('fs').writeFileSync(SESSION_FILE, JSON.stringify({
175
+ sessionToken: relaySessionToken,
176
+ hubUrl,
177
+ relayUrl,
178
+ }))
179
+ } catch {}
180
+
181
+ if (msg.reconnected) {
182
+ console.log(`\n[✓] 已重新连接\n`)
183
+ } else {
184
+ console.log(`\n${'═'.repeat(40)}`)
185
+ console.log(` 微信小程序配对码: ${msg.code}`)
186
+ console.log(`${'═'.repeat(40)}\n`)
187
+ console.log(' 打开小程序 → 输入配对码 → 即可连接')
188
+ console.log(' 配对码 10 分钟内有效\n')
189
+ }
190
+ break
191
+ }
192
+
193
+ case 'client-connected': {
194
+ console.log('[✓] 微信小程序已连接')
195
+ break
196
+ }
197
+
198
+ // Client REST request → forward to local hub
199
+ case 'rest-request': {
200
+ forwardToHub(msg.requestId, msg.method, msg.path, msg.body)
201
+ break
202
+ }
203
+
204
+ // Client auth request → forward to local hub
205
+ case 'auth-request': {
206
+ handleAuthRequest(msg.requestId, msg.accessToken)
207
+ break
208
+ }
209
+ }
210
+ }
211
+
212
+ // --- Forwarding ---
213
+
214
+ function forwardToRelay(msg: any) {
215
+ if (relayWs && relayWs.readyState === WebSocket.OPEN) {
216
+ relayWs.send(JSON.stringify(msg))
217
+ }
218
+ }
219
+
220
+ async function forwardToHub(requestId: string, method: string, path: string, body?: any) {
221
+ try {
222
+ const init: RequestInit = {
223
+ method,
224
+ headers: {
225
+ 'Content-Type': 'application/json',
226
+ ...(jwt ? { Authorization: `Bearer ${jwt}` } : {}),
227
+ },
228
+ }
229
+ if (body && method !== 'GET') init.body = JSON.stringify(body)
230
+
231
+ const res = await fetch(`${hubUrl}${path}`, init)
232
+ const data = await res.json().catch(() => ({}))
233
+
234
+ forwardToRelay({
235
+ type: 'rest-response',
236
+ requestId,
237
+ status: res.status,
238
+ data,
239
+ })
240
+ } catch (err: any) {
241
+ console.error(`[REST] ${method} ${path} failed:`, err.message)
242
+ forwardToRelay({
243
+ type: 'rest-response',
244
+ requestId,
245
+ status: 502,
246
+ data: { error: err.message },
247
+ })
248
+ }
249
+ }
250
+
251
+ async function handleAuthRequest(requestId: string, accessToken: string) {
252
+ try {
253
+ const res = await fetch(`${hubUrl}/api/auth`, {
254
+ method: 'POST',
255
+ headers: { 'Content-Type': 'application/json' },
256
+ body: JSON.stringify({ accessToken }),
257
+ })
258
+ const data = await res.json()
259
+ forwardToRelay({
260
+ type: 'auth-response',
261
+ requestId,
262
+ ok: res.ok,
263
+ data,
264
+ })
265
+ } catch (err: any) {
266
+ forwardToRelay({
267
+ type: 'auth-response',
268
+ requestId,
269
+ ok: false,
270
+ data: { error: err.message },
271
+ })
272
+ }
273
+ }
274
+
275
+ // --- Main ---
276
+
277
+ async function main() {
278
+ console.log('╔══════════════════════════════════════╗')
279
+ console.log('║ CodePocket Connector v1.0 ║')
280
+ console.log('╚══════════════════════════════════════╝')
281
+ console.log()
282
+ console.log(`Hub: ${hubUrl}`)
283
+ console.log(`Relay: ${relayUrl}`)
284
+ console.log()
285
+
286
+ // 1. Find token
287
+ console.log('[1/3] 连接 Hub...')
288
+ hubToken = await findHubToken()
289
+
290
+ // 2. Authenticate
291
+ console.log('[2/3] 认证中...')
292
+ jwt = await authenticateHub(hubToken)
293
+ console.log('[✓] Hub 认证成功')
294
+
295
+ // 3. Start SSE + connect relay
296
+ console.log('[3/3] 连接中继服务器...')
297
+ startSSE()
298
+ connectRelay()
299
+ }
300
+
301
+ main().catch((err) => {
302
+ console.error('[!] 启动失败:', err.message)
303
+ process.exit(1)
304
+ })