ccjk 11.1.0 → 11.1.1

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/README.md CHANGED
@@ -171,6 +171,12 @@ ccjk at --status # Check status
171
171
  ccjk cloud enable --provider github-gist
172
172
  ccjk cloud sync
173
173
 
174
+ # Remote Control (Web/App)
175
+ ccjk remote setup # One-command setup (interactive)
176
+ ccjk remote setup --non-interactive --server-url <url> --auth-token <token> --binding-code <code>
177
+ ccjk remote doctor # Diagnose remote readiness
178
+ ccjk remote status # Quick runtime status
179
+
174
180
  # MCP Services
175
181
  ccjk mcp install <service>
176
182
  ccjk mcp list
package/README.zh-CN.md CHANGED
@@ -98,6 +98,9 @@ npx ccjk cloud enable --provider github-gist
98
98
  ```bash
99
99
  npx ccjk # 交互式设置
100
100
  npx ccjk i # 完整初始化
101
+ npx ccjk remote setup # 一键远程初始化(推荐)
102
+ npx ccjk remote doctor # 远程体检
103
+ npx ccjk remote status # 远程状态
101
104
  npx ccjk u # 更新工作流
102
105
  npx ccjk sync # 云端同步
103
106
  npx ccjk doctor # 健康检查
@@ -1,512 +1,26 @@
1
1
  import ansis from 'ansis';
2
2
  import inquirer from 'inquirer';
3
3
  import { i18n } from './index2.mjs';
4
+ import { m as maskToken, a as isValidTokenFormat, c as generateDeviceToken, d as decryptToken, e as encryptToken, f as getDeviceInfo, i as isDeviceBound, g as getBindingStatus, u as unbindDevice, b as bindDevice, s as sendNotification } from '../shared/ccjk.CGcy7cNM.mjs';
5
+ import { exec } from 'node:child_process';
4
6
  import * as nodeFs from 'node:fs';
5
- import nodeFs__default, { existsSync, readFileSync, mkdirSync, unlinkSync } from 'node:fs';
7
+ import nodeFs__default from 'node:fs';
6
8
  import * as os from 'node:os';
7
- import os__default, { homedir } from 'node:os';
8
- import { join } from 'pathe';
9
- import { writeFileAtomic } from './fs-operations.mjs';
10
- import { Buffer } from 'node:buffer';
11
- import crypto from 'node:crypto';
12
- import { exec } from 'node:child_process';
9
+ import os__default from 'node:os';
13
10
  import * as path from 'node:path';
14
11
  import path__default from 'node:path';
15
12
  import process__default from 'node:process';
16
13
  import { promisify } from 'node:util';
14
+ import { writeFileAtomic } from './fs-operations.mjs';
17
15
  import { parse, stringify } from 'smol-toml';
18
16
  import 'node:url';
19
17
  import 'i18next';
20
18
  import 'i18next-fs-backend';
19
+ import 'pathe';
20
+ import 'node:buffer';
21
+ import 'node:crypto';
21
22
  import 'node:fs/promises';
22
23
 
23
- const TOKEN_PREFIX = "ccjk_";
24
- const TOKEN_LENGTH = 64;
25
- const TOKEN_VERSION = 1;
26
- function generateDeviceToken() {
27
- const randomBytes = crypto.randomBytes(TOKEN_LENGTH / 2);
28
- const randomHex = randomBytes.toString("hex");
29
- return `${TOKEN_PREFIX}${TOKEN_VERSION}${randomHex}`;
30
- }
31
- function isValidTokenFormat(token) {
32
- if (!token || typeof token !== "string") {
33
- return false;
34
- }
35
- if (!token.startsWith(TOKEN_PREFIX)) {
36
- return false;
37
- }
38
- const expectedLength = TOKEN_PREFIX.length + 1 + TOKEN_LENGTH;
39
- if (token.length !== expectedLength) {
40
- return false;
41
- }
42
- const version = token[TOKEN_PREFIX.length];
43
- if (!/^\d$/.test(version)) {
44
- return false;
45
- }
46
- const hexPart = token.slice(TOKEN_PREFIX.length + 1);
47
- if (!/^[a-f0-9]+$/i.test(hexPart)) {
48
- return false;
49
- }
50
- return true;
51
- }
52
- function getDeviceInfo() {
53
- return {
54
- name: os__default.hostname(),
55
- platform: os__default.platform(),
56
- osVersion: os__default.release(),
57
- arch: os__default.arch(),
58
- username: os__default.userInfo().username,
59
- machineId: generateMachineId()
60
- };
61
- }
62
- function generateMachineId() {
63
- const components = [
64
- os__default.hostname(),
65
- os__default.platform(),
66
- os__default.arch(),
67
- os__default.cpus()[0]?.model || "unknown",
68
- os__default.userInfo().username,
69
- // Add network interface MAC addresses for uniqueness
70
- ...Object.values(os__default.networkInterfaces()).flat().filter((iface) => iface && !iface.internal && iface.mac !== "00:00:00:00:00:00").map((iface) => iface?.mac).filter(Boolean).slice(0, 3)
71
- // Limit to first 3 MACs
72
- ];
73
- const combined = components.join("|");
74
- return crypto.createHash("sha256").update(combined).digest("hex").slice(0, 32);
75
- }
76
- function deriveEncryptionKey() {
77
- const machineId = generateMachineId();
78
- const salt = "ccjk-notification-token-v1";
79
- return crypto.pbkdf2Sync(machineId, salt, 1e5, 32, "sha256");
80
- }
81
- function encryptToken(token) {
82
- const key = deriveEncryptionKey();
83
- const iv = crypto.randomBytes(16);
84
- const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
85
- let encrypted = cipher.update(token, "utf8", "hex");
86
- encrypted += cipher.final("hex");
87
- const authTag = cipher.getAuthTag();
88
- return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
89
- }
90
- function decryptToken(encryptedToken) {
91
- try {
92
- const parts = encryptedToken.split(":");
93
- if (parts.length !== 3) {
94
- return null;
95
- }
96
- const [ivHex, authTagHex, encrypted] = parts;
97
- const key = deriveEncryptionKey();
98
- const iv = Buffer.from(ivHex, "hex");
99
- const authTag = Buffer.from(authTagHex, "hex");
100
- const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
101
- decipher.setAuthTag(authTag);
102
- let decrypted = decipher.update(encrypted, "hex", "utf8");
103
- decrypted += decipher.final("utf8");
104
- return decrypted;
105
- } catch {
106
- return null;
107
- }
108
- }
109
- function maskToken(token) {
110
- if (!token || token.length < 12) {
111
- return "***";
112
- }
113
- const prefix = token.slice(0, TOKEN_PREFIX.length + 1);
114
- const suffix = token.slice(-4);
115
- return `${prefix}***...***${suffix}`;
116
- }
117
-
118
- const CLOUD_API_BASE_URL = "https://api.claudehome.cn";
119
- const DEFAULT_TIMEOUT = 3e4;
120
- const POLL_TIMEOUT$1 = 6e4;
121
- const TOKEN_FILE_PATH = join(homedir(), ".ccjk", "cloud-token.json");
122
- class CCJKCloudClient {
123
- baseUrl;
124
- deviceToken = null;
125
- deviceId = null;
126
- /**
127
- * Create a new CCJKCloudClient instance
128
- *
129
- * @param baseUrl - Cloud API base URL (default: https://api.claudehome.cn)
130
- */
131
- constructor(baseUrl = CLOUD_API_BASE_URL) {
132
- this.baseUrl = baseUrl;
133
- this.loadToken();
134
- }
135
- // ==========================================================================
136
- // Token Management
137
- // ==========================================================================
138
- /**
139
- * Load token from storage file
140
- */
141
- loadToken() {
142
- try {
143
- if (existsSync(TOKEN_FILE_PATH)) {
144
- const data = readFileSync(TOKEN_FILE_PATH, "utf-8");
145
- const storage = JSON.parse(data);
146
- this.deviceToken = storage.deviceToken;
147
- this.deviceId = storage.deviceId || null;
148
- }
149
- } catch {
150
- this.deviceToken = null;
151
- this.deviceId = null;
152
- }
153
- }
154
- /**
155
- * Save token to storage file
156
- */
157
- saveToken(storage) {
158
- try {
159
- const dir = join(homedir(), ".ccjk");
160
- if (!existsSync(dir)) {
161
- mkdirSync(dir, { recursive: true });
162
- }
163
- writeFileAtomic(TOKEN_FILE_PATH, JSON.stringify(storage, null, 2));
164
- } catch (error) {
165
- throw new Error(`Failed to save token: ${error instanceof Error ? error.message : String(error)}`);
166
- }
167
- }
168
- /**
169
- * Check if device is bound
170
- */
171
- isBound() {
172
- return this.deviceToken !== null && this.deviceToken.length > 0;
173
- }
174
- /**
175
- * Get current device token
176
- */
177
- getDeviceToken() {
178
- return this.deviceToken;
179
- }
180
- /**
181
- * Get current device ID
182
- */
183
- getDeviceId() {
184
- return this.deviceId;
185
- }
186
- /**
187
- * Clear stored token (unbind device)
188
- */
189
- clearToken() {
190
- this.deviceToken = null;
191
- this.deviceId = null;
192
- try {
193
- if (existsSync(TOKEN_FILE_PATH)) {
194
- unlinkSync(TOKEN_FILE_PATH);
195
- }
196
- } catch {
197
- }
198
- }
199
- // ==========================================================================
200
- // Device Binding
201
- // ==========================================================================
202
- /**
203
- * Bind device using a binding code
204
- *
205
- * The binding code is obtained from the CCJK mobile app or web dashboard.
206
- * Once bound, the device can send and receive notifications.
207
- *
208
- * @param code - Binding code from mobile app
209
- * @param deviceInfo - Optional device information (auto-detected if not provided)
210
- * @returns Bind response with device token
211
- *
212
- * @example
213
- * ```typescript
214
- * const client = new CCJKCloudClient()
215
- * const result = await client.bind('ABC123')
216
- * if (result.success) {
217
- * console.log('Device bound successfully!')
218
- * }
219
- * ```
220
- */
221
- async bind(code, deviceInfo) {
222
- const info = deviceInfo ? { ...getDeviceInfo(), ...deviceInfo } : getDeviceInfo();
223
- const response = await this.request("/bind/use", {
224
- method: "POST",
225
- body: JSON.stringify({
226
- code,
227
- deviceInfo: info
228
- })
229
- });
230
- if (response.success && response.data) {
231
- this.deviceToken = response.data.deviceToken;
232
- this.deviceId = response.data.deviceId;
233
- this.saveToken({
234
- deviceToken: response.data.deviceToken,
235
- deviceId: response.data.deviceId,
236
- bindingCode: code,
237
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
238
- deviceInfo: info
239
- });
240
- return {
241
- success: true,
242
- deviceToken: response.data.deviceToken,
243
- deviceId: response.data.deviceId
244
- };
245
- }
246
- return {
247
- success: false,
248
- error: response.error || "Failed to bind device",
249
- code: response.code
250
- };
251
- }
252
- // ==========================================================================
253
- // Notification Sending
254
- // ==========================================================================
255
- /**
256
- * Send a notification to the user
257
- *
258
- * @param options - Notification options
259
- * @returns Notification response
260
- *
261
- * @example
262
- * ```typescript
263
- * const client = new CCJKCloudClient()
264
- * await client.notify({
265
- * title: 'Build Complete',
266
- * body: 'Your project has been built successfully!',
267
- * type: 'success'
268
- * })
269
- * ```
270
- */
271
- async notify(options) {
272
- if (!this.deviceToken) {
273
- return {
274
- success: false,
275
- error: 'Device not bound. Please run "ccjk notification bind <code>" first.',
276
- code: "NOT_BOUND"
277
- };
278
- }
279
- const response = await this.request("/notify", {
280
- method: "POST",
281
- body: JSON.stringify({
282
- title: options.title,
283
- body: options.body,
284
- type: options.type || "info",
285
- taskId: options.taskId,
286
- metadata: options.metadata,
287
- actions: options.actions
288
- })
289
- });
290
- if (response.success && response.data) {
291
- return {
292
- success: true,
293
- notificationId: response.data.notificationId
294
- };
295
- }
296
- return {
297
- success: false,
298
- error: response.error || "Failed to send notification",
299
- code: response.code
300
- };
301
- }
302
- // ==========================================================================
303
- // Reply Polling
304
- // ==========================================================================
305
- /**
306
- * Wait for a reply from the user
307
- *
308
- * Uses long-polling to wait for a user reply. The timeout parameter
309
- * controls how long to wait before returning null.
310
- *
311
- * @param timeout - Timeout in milliseconds (default: 30000)
312
- * @returns User reply or null if timeout
313
- *
314
- * @example
315
- * ```typescript
316
- * const client = new CCJKCloudClient()
317
- * const reply = await client.waitForReply(60000) // Wait up to 60 seconds
318
- * if (reply) {
319
- * console.log('User replied:', reply.content)
320
- * }
321
- * ```
322
- */
323
- async waitForReply(timeout = POLL_TIMEOUT$1) {
324
- if (!this.deviceToken) {
325
- throw new Error('Device not bound. Please run "ccjk notification bind <code>" first.');
326
- }
327
- const response = await this.request(`/reply/poll?timeout=${timeout}`, {
328
- method: "GET",
329
- timeout
330
- });
331
- if (response.success && response.data?.reply) {
332
- return {
333
- content: response.data.reply.content,
334
- timestamp: new Date(response.data.reply.timestamp),
335
- notificationId: response.data.reply.notificationId,
336
- actionId: response.data.reply.actionId
337
- };
338
- }
339
- return null;
340
- }
341
- // ==========================================================================
342
- // Ask and Wait
343
- // ==========================================================================
344
- /**
345
- * Ask the user a question and wait for their reply
346
- *
347
- * This is a convenience method that combines notify() and waitForReply().
348
- * It sends a notification with the question and waits for the user to respond.
349
- *
350
- * @param question - Question to ask the user
351
- * @param options - Additional notification options
352
- * @param timeout - Timeout in milliseconds (default: 60000)
353
- * @returns User reply
354
- *
355
- * @example
356
- * ```typescript
357
- * const client = new CCJKCloudClient()
358
- * const reply = await client.ask('Deploy to production?', {
359
- * actions: [
360
- * { id: 'yes', label: 'Yes', value: 'yes' },
361
- * { id: 'no', label: 'No', value: 'no' }
362
- * ]
363
- * })
364
- * if (reply.actionId === 'yes') {
365
- * // Proceed with deployment
366
- * }
367
- * ```
368
- */
369
- async ask(question, options, timeout = POLL_TIMEOUT$1) {
370
- const notifyResult = await this.notify({
371
- title: options?.title || "CCJK Question",
372
- body: question,
373
- type: "info",
374
- ...options
375
- });
376
- if (!notifyResult.success) {
377
- throw new Error(notifyResult.error || "Failed to send question");
378
- }
379
- const reply = await this.waitForReply(timeout);
380
- if (!reply) {
381
- throw new Error("No reply received within timeout");
382
- }
383
- return reply;
384
- }
385
- // ==========================================================================
386
- // Status Check
387
- // ==========================================================================
388
- /**
389
- * Get binding status and device information
390
- *
391
- * @returns Binding status information
392
- */
393
- async getStatus() {
394
- if (!this.deviceToken) {
395
- return { bound: false };
396
- }
397
- try {
398
- if (existsSync(TOKEN_FILE_PATH)) {
399
- const data = readFileSync(TOKEN_FILE_PATH, "utf-8");
400
- const storage = JSON.parse(data);
401
- return {
402
- bound: true,
403
- deviceId: storage.deviceId,
404
- deviceInfo: storage.deviceInfo,
405
- lastUsed: storage.lastUsedAt
406
- };
407
- }
408
- } catch {
409
- }
410
- return {
411
- bound: true,
412
- deviceId: this.deviceId || void 0
413
- };
414
- }
415
- // ==========================================================================
416
- // HTTP Request Helper
417
- // ==========================================================================
418
- /**
419
- * Make an HTTP request to the cloud service
420
- */
421
- async request(path, options) {
422
- const url = `${this.baseUrl}${path}`;
423
- const timeout = options.timeout || DEFAULT_TIMEOUT;
424
- const controller = new AbortController();
425
- const timeoutId = setTimeout(() => controller.abort(), timeout);
426
- try {
427
- const headers = {
428
- "Content-Type": "application/json"
429
- };
430
- if (this.deviceToken) {
431
- headers["X-Device-Token"] = this.deviceToken;
432
- }
433
- const response = await fetch(url, {
434
- method: options.method,
435
- headers,
436
- body: options.body,
437
- signal: controller.signal
438
- });
439
- clearTimeout(timeoutId);
440
- const data = await response.json();
441
- if (!response.ok) {
442
- return {
443
- success: false,
444
- error: data.error || `HTTP ${response.status}: ${response.statusText}`,
445
- code: data.code || `HTTP_${response.status}`
446
- };
447
- }
448
- if (this.deviceToken && existsSync(TOKEN_FILE_PATH)) {
449
- try {
450
- const storageData = readFileSync(TOKEN_FILE_PATH, "utf-8");
451
- const storage = JSON.parse(storageData);
452
- storage.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
453
- writeFileAtomic(TOKEN_FILE_PATH, JSON.stringify(storage, null, 2));
454
- } catch {
455
- }
456
- }
457
- return data;
458
- } catch (error) {
459
- clearTimeout(timeoutId);
460
- if (error instanceof Error) {
461
- if (error.name === "AbortError") {
462
- return {
463
- success: false,
464
- error: "Request timeout",
465
- code: "TIMEOUT"
466
- };
467
- }
468
- return {
469
- success: false,
470
- error: error.message,
471
- code: "NETWORK_ERROR"
472
- };
473
- }
474
- return {
475
- success: false,
476
- error: String(error),
477
- code: "UNKNOWN_ERROR"
478
- };
479
- }
480
- }
481
- }
482
- let cloudClientInstance = null;
483
- function getCloudNotificationClient() {
484
- if (!cloudClientInstance) {
485
- cloudClientInstance = new CCJKCloudClient();
486
- }
487
- return cloudClientInstance;
488
- }
489
- async function bindDevice(code, deviceInfo) {
490
- const client = getCloudNotificationClient();
491
- return client.bind(code, deviceInfo);
492
- }
493
- async function sendNotification(options) {
494
- const client = getCloudNotificationClient();
495
- return client.notify(options);
496
- }
497
- function isDeviceBound() {
498
- const client = getCloudNotificationClient();
499
- return client.isBound();
500
- }
501
- async function getBindingStatus() {
502
- const client = getCloudNotificationClient();
503
- return client.getStatus();
504
- }
505
- function unbindDevice() {
506
- const client = getCloudNotificationClient();
507
- client.clearToken();
508
- }
509
-
510
24
  const execAsync = promisify(exec);
511
25
  const DEFAULT_CONFIG = {
512
26
  shortcutName: "ClaudeNotify",
@@ -1547,6 +1061,7 @@ async function notificationCommand(action = "menu", args) {
1547
1061
  await configureThreshold();
1548
1062
  break;
1549
1063
  case "bind":
1064
+ console.log(ansis.yellow(i18n.t("notification:cloud.migrateToRemoteSetup")));
1550
1065
  await handleBind(args?.[0]);
1551
1066
  break;
1552
1067
  case "unbind":
@@ -1,4 +1,4 @@
1
- const version = "11.1.0";
1
+ const version = "11.1.1";
2
2
  const homepage = "https://github.com/miounet11/ccjk";
3
3
 
4
4
  export { homepage, version };