ccjk 11.1.0 → 11.1.2

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,495 @@
1
+ import { existsSync, readFileSync, mkdirSync, unlinkSync } from 'node:fs';
2
+ import os__default, { homedir } from 'node:os';
3
+ import { join } from 'pathe';
4
+ import { writeFileAtomic } from '../chunks/fs-operations.mjs';
5
+ import { Buffer } from 'node:buffer';
6
+ import crypto from 'node:crypto';
7
+
8
+ const TOKEN_PREFIX = "ccjk_";
9
+ const TOKEN_LENGTH = 64;
10
+ const TOKEN_VERSION = 1;
11
+ function generateDeviceToken() {
12
+ const randomBytes = crypto.randomBytes(TOKEN_LENGTH / 2);
13
+ const randomHex = randomBytes.toString("hex");
14
+ return `${TOKEN_PREFIX}${TOKEN_VERSION}${randomHex}`;
15
+ }
16
+ function isValidTokenFormat(token) {
17
+ if (!token || typeof token !== "string") {
18
+ return false;
19
+ }
20
+ if (!token.startsWith(TOKEN_PREFIX)) {
21
+ return false;
22
+ }
23
+ const expectedLength = TOKEN_PREFIX.length + 1 + TOKEN_LENGTH;
24
+ if (token.length !== expectedLength) {
25
+ return false;
26
+ }
27
+ const version = token[TOKEN_PREFIX.length];
28
+ if (!/^\d$/.test(version)) {
29
+ return false;
30
+ }
31
+ const hexPart = token.slice(TOKEN_PREFIX.length + 1);
32
+ if (!/^[a-f0-9]+$/i.test(hexPart)) {
33
+ return false;
34
+ }
35
+ return true;
36
+ }
37
+ function getDeviceInfo() {
38
+ return {
39
+ name: os__default.hostname(),
40
+ platform: os__default.platform(),
41
+ osVersion: os__default.release(),
42
+ arch: os__default.arch(),
43
+ username: os__default.userInfo().username,
44
+ machineId: generateMachineId()
45
+ };
46
+ }
47
+ function generateMachineId() {
48
+ const components = [
49
+ os__default.hostname(),
50
+ os__default.platform(),
51
+ os__default.arch(),
52
+ os__default.cpus()[0]?.model || "unknown",
53
+ os__default.userInfo().username,
54
+ // Add network interface MAC addresses for uniqueness
55
+ ...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)
56
+ // Limit to first 3 MACs
57
+ ];
58
+ const combined = components.join("|");
59
+ return crypto.createHash("sha256").update(combined).digest("hex").slice(0, 32);
60
+ }
61
+ function deriveEncryptionKey() {
62
+ const machineId = generateMachineId();
63
+ const salt = "ccjk-notification-token-v1";
64
+ return crypto.pbkdf2Sync(machineId, salt, 1e5, 32, "sha256");
65
+ }
66
+ function encryptToken(token) {
67
+ const key = deriveEncryptionKey();
68
+ const iv = crypto.randomBytes(16);
69
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
70
+ let encrypted = cipher.update(token, "utf8", "hex");
71
+ encrypted += cipher.final("hex");
72
+ const authTag = cipher.getAuthTag();
73
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
74
+ }
75
+ function decryptToken(encryptedToken) {
76
+ try {
77
+ const parts = encryptedToken.split(":");
78
+ if (parts.length !== 3) {
79
+ return null;
80
+ }
81
+ const [ivHex, authTagHex, encrypted] = parts;
82
+ const key = deriveEncryptionKey();
83
+ const iv = Buffer.from(ivHex, "hex");
84
+ const authTag = Buffer.from(authTagHex, "hex");
85
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
86
+ decipher.setAuthTag(authTag);
87
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
88
+ decrypted += decipher.final("utf8");
89
+ return decrypted;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+ function maskToken(token) {
95
+ if (!token || token.length < 12) {
96
+ return "***";
97
+ }
98
+ const prefix = token.slice(0, TOKEN_PREFIX.length + 1);
99
+ const suffix = token.slice(-4);
100
+ return `${prefix}***...***${suffix}`;
101
+ }
102
+
103
+ const CLOUD_API_BASE_URL = "https://api.claudehome.cn";
104
+ const DEFAULT_TIMEOUT = 3e4;
105
+ const POLL_TIMEOUT = 6e4;
106
+ const TOKEN_FILE_PATH = join(homedir(), ".ccjk", "cloud-token.json");
107
+ class CCJKCloudClient {
108
+ baseUrl;
109
+ deviceToken = null;
110
+ deviceId = null;
111
+ /**
112
+ * Create a new CCJKCloudClient instance
113
+ *
114
+ * @param baseUrl - Cloud API base URL (default: https://api.claudehome.cn)
115
+ */
116
+ constructor(baseUrl = CLOUD_API_BASE_URL) {
117
+ this.baseUrl = baseUrl;
118
+ this.loadToken();
119
+ }
120
+ // ==========================================================================
121
+ // Token Management
122
+ // ==========================================================================
123
+ /**
124
+ * Load token from storage file
125
+ */
126
+ loadToken() {
127
+ try {
128
+ if (existsSync(TOKEN_FILE_PATH)) {
129
+ const data = readFileSync(TOKEN_FILE_PATH, "utf-8");
130
+ const storage = JSON.parse(data);
131
+ this.deviceToken = storage.deviceToken;
132
+ this.deviceId = storage.deviceId || null;
133
+ }
134
+ } catch {
135
+ this.deviceToken = null;
136
+ this.deviceId = null;
137
+ }
138
+ }
139
+ /**
140
+ * Save token to storage file
141
+ */
142
+ saveToken(storage) {
143
+ try {
144
+ const dir = join(homedir(), ".ccjk");
145
+ if (!existsSync(dir)) {
146
+ mkdirSync(dir, { recursive: true });
147
+ }
148
+ writeFileAtomic(TOKEN_FILE_PATH, JSON.stringify(storage, null, 2));
149
+ } catch (error) {
150
+ throw new Error(`Failed to save token: ${error instanceof Error ? error.message : String(error)}`);
151
+ }
152
+ }
153
+ /**
154
+ * Check if device is bound
155
+ */
156
+ isBound() {
157
+ return this.deviceToken !== null && this.deviceToken.length > 0;
158
+ }
159
+ /**
160
+ * Get current device token
161
+ */
162
+ getDeviceToken() {
163
+ return this.deviceToken;
164
+ }
165
+ /**
166
+ * Get current device ID
167
+ */
168
+ getDeviceId() {
169
+ return this.deviceId;
170
+ }
171
+ /**
172
+ * Clear stored token (unbind device)
173
+ */
174
+ clearToken() {
175
+ this.deviceToken = null;
176
+ this.deviceId = null;
177
+ try {
178
+ if (existsSync(TOKEN_FILE_PATH)) {
179
+ unlinkSync(TOKEN_FILE_PATH);
180
+ }
181
+ } catch {
182
+ }
183
+ }
184
+ // ==========================================================================
185
+ // Device Binding
186
+ // ==========================================================================
187
+ /**
188
+ * Bind device using a binding code
189
+ *
190
+ * The binding code is obtained from the CCJK mobile app or web dashboard.
191
+ * Once bound, the device can send and receive notifications.
192
+ *
193
+ * @param code - Binding code from mobile app
194
+ * @param deviceInfo - Optional device information (auto-detected if not provided)
195
+ * @returns Bind response with device token
196
+ *
197
+ * @example
198
+ * ```typescript
199
+ * const client = new CCJKCloudClient()
200
+ * const result = await client.bind('ABC123')
201
+ * if (result.success) {
202
+ * console.log('Device bound successfully!')
203
+ * }
204
+ * ```
205
+ */
206
+ async bind(code, deviceInfo) {
207
+ const info = deviceInfo ? { ...getDeviceInfo(), ...deviceInfo } : getDeviceInfo();
208
+ const response = await this.request("/bind/use", {
209
+ method: "POST",
210
+ body: JSON.stringify({
211
+ code,
212
+ deviceInfo: info
213
+ })
214
+ });
215
+ if (response.success && response.data) {
216
+ this.deviceToken = response.data.deviceToken;
217
+ this.deviceId = response.data.deviceId;
218
+ this.saveToken({
219
+ deviceToken: response.data.deviceToken,
220
+ deviceId: response.data.deviceId,
221
+ bindingCode: code,
222
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
223
+ deviceInfo: info
224
+ });
225
+ return {
226
+ success: true,
227
+ deviceToken: response.data.deviceToken,
228
+ deviceId: response.data.deviceId
229
+ };
230
+ }
231
+ return {
232
+ success: false,
233
+ error: response.error || "Failed to bind device",
234
+ code: response.code
235
+ };
236
+ }
237
+ // ==========================================================================
238
+ // Notification Sending
239
+ // ==========================================================================
240
+ /**
241
+ * Send a notification to the user
242
+ *
243
+ * @param options - Notification options
244
+ * @returns Notification response
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * const client = new CCJKCloudClient()
249
+ * await client.notify({
250
+ * title: 'Build Complete',
251
+ * body: 'Your project has been built successfully!',
252
+ * type: 'success'
253
+ * })
254
+ * ```
255
+ */
256
+ async notify(options) {
257
+ if (!this.deviceToken) {
258
+ return {
259
+ success: false,
260
+ error: 'Device not bound. Please run "ccjk notification bind <code>" first.',
261
+ code: "NOT_BOUND"
262
+ };
263
+ }
264
+ const response = await this.request("/notify", {
265
+ method: "POST",
266
+ body: JSON.stringify({
267
+ title: options.title,
268
+ body: options.body,
269
+ type: options.type || "info",
270
+ taskId: options.taskId,
271
+ metadata: options.metadata,
272
+ actions: options.actions
273
+ })
274
+ });
275
+ if (response.success && response.data) {
276
+ return {
277
+ success: true,
278
+ notificationId: response.data.notificationId
279
+ };
280
+ }
281
+ return {
282
+ success: false,
283
+ error: response.error || "Failed to send notification",
284
+ code: response.code
285
+ };
286
+ }
287
+ // ==========================================================================
288
+ // Reply Polling
289
+ // ==========================================================================
290
+ /**
291
+ * Wait for a reply from the user
292
+ *
293
+ * Uses long-polling to wait for a user reply. The timeout parameter
294
+ * controls how long to wait before returning null.
295
+ *
296
+ * @param timeout - Timeout in milliseconds (default: 30000)
297
+ * @returns User reply or null if timeout
298
+ *
299
+ * @example
300
+ * ```typescript
301
+ * const client = new CCJKCloudClient()
302
+ * const reply = await client.waitForReply(60000) // Wait up to 60 seconds
303
+ * if (reply) {
304
+ * console.log('User replied:', reply.content)
305
+ * }
306
+ * ```
307
+ */
308
+ async waitForReply(timeout = POLL_TIMEOUT) {
309
+ if (!this.deviceToken) {
310
+ throw new Error('Device not bound. Please run "ccjk notification bind <code>" first.');
311
+ }
312
+ const response = await this.request(`/reply/poll?timeout=${timeout}`, {
313
+ method: "GET",
314
+ timeout
315
+ });
316
+ if (response.success && response.data?.reply) {
317
+ return {
318
+ content: response.data.reply.content,
319
+ timestamp: new Date(response.data.reply.timestamp),
320
+ notificationId: response.data.reply.notificationId,
321
+ actionId: response.data.reply.actionId
322
+ };
323
+ }
324
+ return null;
325
+ }
326
+ // ==========================================================================
327
+ // Ask and Wait
328
+ // ==========================================================================
329
+ /**
330
+ * Ask the user a question and wait for their reply
331
+ *
332
+ * This is a convenience method that combines notify() and waitForReply().
333
+ * It sends a notification with the question and waits for the user to respond.
334
+ *
335
+ * @param question - Question to ask the user
336
+ * @param options - Additional notification options
337
+ * @param timeout - Timeout in milliseconds (default: 60000)
338
+ * @returns User reply
339
+ *
340
+ * @example
341
+ * ```typescript
342
+ * const client = new CCJKCloudClient()
343
+ * const reply = await client.ask('Deploy to production?', {
344
+ * actions: [
345
+ * { id: 'yes', label: 'Yes', value: 'yes' },
346
+ * { id: 'no', label: 'No', value: 'no' }
347
+ * ]
348
+ * })
349
+ * if (reply.actionId === 'yes') {
350
+ * // Proceed with deployment
351
+ * }
352
+ * ```
353
+ */
354
+ async ask(question, options, timeout = POLL_TIMEOUT) {
355
+ const notifyResult = await this.notify({
356
+ title: options?.title || "CCJK Question",
357
+ body: question,
358
+ type: "info",
359
+ ...options
360
+ });
361
+ if (!notifyResult.success) {
362
+ throw new Error(notifyResult.error || "Failed to send question");
363
+ }
364
+ const reply = await this.waitForReply(timeout);
365
+ if (!reply) {
366
+ throw new Error("No reply received within timeout");
367
+ }
368
+ return reply;
369
+ }
370
+ // ==========================================================================
371
+ // Status Check
372
+ // ==========================================================================
373
+ /**
374
+ * Get binding status and device information
375
+ *
376
+ * @returns Binding status information
377
+ */
378
+ async getStatus() {
379
+ if (!this.deviceToken) {
380
+ return { bound: false };
381
+ }
382
+ try {
383
+ if (existsSync(TOKEN_FILE_PATH)) {
384
+ const data = readFileSync(TOKEN_FILE_PATH, "utf-8");
385
+ const storage = JSON.parse(data);
386
+ return {
387
+ bound: true,
388
+ deviceId: storage.deviceId,
389
+ deviceInfo: storage.deviceInfo,
390
+ lastUsed: storage.lastUsedAt
391
+ };
392
+ }
393
+ } catch {
394
+ }
395
+ return {
396
+ bound: true,
397
+ deviceId: this.deviceId || void 0
398
+ };
399
+ }
400
+ // ==========================================================================
401
+ // HTTP Request Helper
402
+ // ==========================================================================
403
+ /**
404
+ * Make an HTTP request to the cloud service
405
+ */
406
+ async request(path, options) {
407
+ const url = `${this.baseUrl}${path}`;
408
+ const timeout = options.timeout || DEFAULT_TIMEOUT;
409
+ const controller = new AbortController();
410
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
411
+ try {
412
+ const headers = {
413
+ "Content-Type": "application/json"
414
+ };
415
+ if (this.deviceToken) {
416
+ headers["X-Device-Token"] = this.deviceToken;
417
+ }
418
+ const response = await fetch(url, {
419
+ method: options.method,
420
+ headers,
421
+ body: options.body,
422
+ signal: controller.signal
423
+ });
424
+ clearTimeout(timeoutId);
425
+ const data = await response.json();
426
+ if (!response.ok) {
427
+ return {
428
+ success: false,
429
+ error: data.error || `HTTP ${response.status}: ${response.statusText}`,
430
+ code: data.code || `HTTP_${response.status}`
431
+ };
432
+ }
433
+ if (this.deviceToken && existsSync(TOKEN_FILE_PATH)) {
434
+ try {
435
+ const storageData = readFileSync(TOKEN_FILE_PATH, "utf-8");
436
+ const storage = JSON.parse(storageData);
437
+ storage.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
438
+ writeFileAtomic(TOKEN_FILE_PATH, JSON.stringify(storage, null, 2));
439
+ } catch {
440
+ }
441
+ }
442
+ return data;
443
+ } catch (error) {
444
+ clearTimeout(timeoutId);
445
+ if (error instanceof Error) {
446
+ if (error.name === "AbortError") {
447
+ return {
448
+ success: false,
449
+ error: "Request timeout",
450
+ code: "TIMEOUT"
451
+ };
452
+ }
453
+ return {
454
+ success: false,
455
+ error: error.message,
456
+ code: "NETWORK_ERROR"
457
+ };
458
+ }
459
+ return {
460
+ success: false,
461
+ error: String(error),
462
+ code: "UNKNOWN_ERROR"
463
+ };
464
+ }
465
+ }
466
+ }
467
+ let cloudClientInstance = null;
468
+ function getCloudNotificationClient() {
469
+ if (!cloudClientInstance) {
470
+ cloudClientInstance = new CCJKCloudClient();
471
+ }
472
+ return cloudClientInstance;
473
+ }
474
+ async function bindDevice(code, deviceInfo) {
475
+ const client = getCloudNotificationClient();
476
+ return client.bind(code, deviceInfo);
477
+ }
478
+ async function sendNotification(options) {
479
+ const client = getCloudNotificationClient();
480
+ return client.notify(options);
481
+ }
482
+ function isDeviceBound() {
483
+ const client = getCloudNotificationClient();
484
+ return client.isBound();
485
+ }
486
+ async function getBindingStatus() {
487
+ const client = getCloudNotificationClient();
488
+ return client.getStatus();
489
+ }
490
+ function unbindDevice() {
491
+ const client = getCloudNotificationClient();
492
+ client.clearToken();
493
+ }
494
+
495
+ export { isValidTokenFormat as a, bindDevice as b, generateDeviceToken as c, decryptToken as d, encryptToken as e, getDeviceInfo as f, getBindingStatus as g, isDeviceBound as i, maskToken as m, sendNotification as s, unbindDevice as u };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ccjk",
3
3
  "type": "module",
4
- "version": "11.1.0",
4
+ "version": "11.1.2",
5
5
  "packageManager": "pnpm@10.17.1",
6
6
  "description": "CLI toolkit for Claude Code and Codex setup. Simplifies MCP service installation, API configuration, workflow management, and multi-provider support with guided interactive setup.",
7
7
  "author": {
@@ -86,7 +86,7 @@
86
86
  "build": "unbuild",
87
87
  "start": "node bin/ccjk.mjs",
88
88
  "typecheck": "tsc --noEmit",
89
- "prepublishOnly": "node scripts/validate-prepublish.mjs && pnpm build && pnpm test:run",
89
+ "prepublishOnly": "node scripts/validate-prepublish.mjs && pnpm contract:check && pnpm build && pnpm test:run",
90
90
  "lint": "eslint",
91
91
  "lint:fix": "eslint --fix",
92
92
  "test": "vitest",
@@ -137,7 +137,8 @@
137
137
  "benchmark:server": "npx http-server docs/v2 -p 8080 -o dashboard.html",
138
138
  "benchmark:open": "open docs/v2/dashboard.html || xdg-open docs/v2/dashboard.html || start docs/v2/dashboard.html",
139
139
  "i18n:check": "tsx scripts/check-i18n.ts",
140
- "i18n:report": "tsx scripts/check-i18n.ts --report"
140
+ "i18n:report": "tsx scripts/check-i18n.ts --report",
141
+ "contract:check": "node scripts/check-remote-contract.mjs"
141
142
  },
142
143
  "dependencies": {
143
144
  "@anthropic-ai/sdk": "^0.52.0",
@@ -1,53 +1,37 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
3
  "description": "CCJK settings template — auto-migrated on upgrade",
4
-
5
4
  "model": "default",
6
-
7
5
  "env": {
8
6
  "ANTHROPIC_MODEL": "",
9
7
  "ANTHROPIC_DEFAULT_HAIKU_MODEL": "",
10
8
  "ANTHROPIC_DEFAULT_SONNET_MODEL": "",
11
9
  "ANTHROPIC_DEFAULT_OPUS_MODEL": ""
12
10
  },
13
-
14
11
  "language": "",
15
-
16
12
  "showTurnDuration": false,
17
-
18
13
  "respectGitignore": true,
19
-
20
14
  "auto": {
21
15
  "mcp": 0
22
16
  },
23
-
24
17
  "agent": "",
25
-
26
18
  "thinking": {
27
19
  "enabled": false,
28
20
  "budgetTokens": 10240
29
21
  },
30
-
31
22
  "fileSuggestion": {
32
23
  "type": "command",
33
24
  "command": "git"
34
25
  },
35
-
36
26
  "allowUnsandboxedCommands": false,
37
-
38
27
  "disallowedTools": [],
39
-
40
28
  "attribution": {},
41
-
42
29
  "index": {
43
30
  "maxFiles": 10000,
44
31
  "maxFileSize": 2097152
45
32
  },
46
-
47
33
  "allowBrowser": false,
48
-
49
34
  "statusLine": {},
50
-
51
35
  "plansDirectory": ".claude/plans",
52
36
  "hooks": {
53
37
  "PreToolUse": [],
@@ -85,13 +69,37 @@
85
69
  "Read(*)",
86
70
  "Edit(*)",
87
71
  "Write(*)",
88
- "NotebookEdit(*)"
72
+ "NotebookEdit(*)",
73
+ "Bash(cd *)",
74
+ "Bash(for *)",
75
+ "Bash(while *)",
76
+ "Bash(if *)",
77
+ "Bash(source *)",
78
+ "Bash(export *)",
79
+ "Bash(unset *)",
80
+ "Bash(rm *)",
81
+ "Bash(rmdir *)",
82
+ "Bash(tee *)",
83
+ "Bash(env *)",
84
+ "Bash(time *)",
85
+ "Bash(printf *)",
86
+ "Bash(sed *)",
87
+ "Bash(awk *)",
88
+ "Bash(tr *)",
89
+ "Bash(xargs *)",
90
+ "Bash(cut *)",
91
+ "Bash(curl *)",
92
+ "Bash(node *)",
93
+ "Bash(python *)",
94
+ "Bash(python3 *)",
95
+ "Bash(pip *)",
96
+ "Bash(pip3 *)",
97
+ "WebFetch(*)",
98
+ "MCP(*)"
89
99
  ]
90
100
  },
91
-
92
101
  "chat": {
93
102
  "alwaysApprove": []
94
103
  },
95
-
96
104
  "experimental": {}
97
- }
105
+ }