ccem 2.28.0 → 2.30.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.
Files changed (2) hide show
  1. package/dist/index.js +304 -26
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2702,16 +2702,70 @@ var config = new Conf({
2702
2702
  cwd: getCcemConfigDir()
2703
2703
  // 使用统一的配置目录
2704
2704
  });
2705
- var decryptWithSecret = (encryptedBase64, secret) => {
2706
- const key = crypto3.scryptSync(secret, "ccem-salt", 32);
2705
+ var REMOTE_CRYPTO_ALGORITHM_V2 = "aes-256-gcm";
2706
+ var REMOTE_CRYPTO_ALGORITHM_V1 = "aes-256-cbc";
2707
+ var isEnvelopeV2 = (value) => {
2708
+ if (typeof value !== "object" || value === null) return false;
2709
+ const v = value;
2710
+ return v.v === 2 && typeof v.nonce === "string" && typeof v.ciphertext === "string" && typeof v.tag === "string";
2711
+ };
2712
+ var deriveRemoteKey = (secret) => crypto3.scryptSync(secret, "ccem-salt", 32);
2713
+ var decryptV2Envelope = (envelope, key) => {
2714
+ const nonce = Buffer.from(envelope.nonce, "base64");
2715
+ const ciphertext = Buffer.from(envelope.ciphertext, "base64");
2716
+ const tag = Buffer.from(envelope.tag, "base64");
2717
+ if (nonce.length === 0 || ciphertext.length === 0 || tag.length === 0) {
2718
+ throw new Error("v2 envelope has empty nonce/ciphertext/tag");
2719
+ }
2720
+ const decipher = crypto3.createDecipheriv(
2721
+ REMOTE_CRYPTO_ALGORITHM_V2,
2722
+ key,
2723
+ nonce
2724
+ );
2725
+ decipher.setAuthTag(tag);
2726
+ let decrypted = decipher.update(ciphertext);
2727
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
2728
+ return decrypted.toString("utf8");
2729
+ };
2730
+ var decryptV1Legacy = (encryptedBase64, key) => {
2707
2731
  const combined = Buffer.from(encryptedBase64, "base64");
2732
+ if (combined.length < 16) {
2733
+ throw new Error("v1 payload too short");
2734
+ }
2708
2735
  const iv = combined.subarray(0, 16);
2709
2736
  const encryptedHex = combined.subarray(16).toString("hex");
2710
- const decipher = crypto3.createDecipheriv("aes-256-cbc", key, iv);
2737
+ const decipher = crypto3.createDecipheriv(REMOTE_CRYPTO_ALGORITHM_V1, key, iv);
2711
2738
  let decrypted = decipher.update(encryptedHex, "hex", "utf8");
2712
2739
  decrypted += decipher.final("utf8");
2713
2740
  return decrypted;
2714
2741
  };
2742
+ var decryptWithSecret = (encryptedBase64, secret) => {
2743
+ const key = deriveRemoteKey(secret);
2744
+ let parsedObj = null;
2745
+ try {
2746
+ const jsonStr = Buffer.from(encryptedBase64, "base64").toString("utf8");
2747
+ parsedObj = JSON.parse(jsonStr);
2748
+ } catch {
2749
+ }
2750
+ let envelopeV2 = null;
2751
+ if (parsedObj !== null) {
2752
+ if (isEnvelopeV2(parsedObj)) {
2753
+ envelopeV2 = parsedObj;
2754
+ } else if (parsedObj && typeof parsedObj === "object" && "v" in parsedObj) {
2755
+ const version = parsedObj.v;
2756
+ if (version === 2) {
2757
+ throw new Error(
2758
+ "Malformed v2 envelope: missing required fields (nonce, ciphertext, tag)"
2759
+ );
2760
+ }
2761
+ throw new Error(`Unsupported remote envelope version: ${version}`);
2762
+ }
2763
+ }
2764
+ if (envelopeV2) {
2765
+ return decryptV2Envelope(envelopeV2, key);
2766
+ }
2767
+ return decryptV1Legacy(encryptedBase64, key);
2768
+ };
2715
2769
  var getUniqueName = (baseName, existingNames) => {
2716
2770
  if (!existingNames.has(baseName)) {
2717
2771
  return baseName;
@@ -3168,8 +3222,132 @@ function formatCronTaskTableRows(tasks) {
3168
3222
  // src/desktopControl.ts
3169
3223
  import fs10 from "fs";
3170
3224
  import path8 from "path";
3225
+ var DEFAULT_REQUEST_TIMEOUT_MS = 5e3;
3226
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
3227
+ var ENDPOINT_UNREACHABLE_CODES = /* @__PURE__ */ new Set([
3228
+ "ECONNREFUSED",
3229
+ "ECONNRESET",
3230
+ "ENOTCONN",
3231
+ "EHOSTUNREACH",
3232
+ "ECONNABORTED"
3233
+ ]);
3234
+ var StaleDesktopControlDescriptorError = class extends Error {
3235
+ name = "StaleDesktopControlDescriptorError";
3236
+ reason;
3237
+ descriptorPath;
3238
+ pid;
3239
+ cleanedUp;
3240
+ cause;
3241
+ constructor(details) {
3242
+ const subject = details.pid !== null ? `process ${details.pid}` : "the publishing process";
3243
+ const symptom = details.reason === "dead-pid" ? `${subject} is no longer running` : details.reason === "endpoint-unreachable" ? "the control endpoint refused the connection" : `the control request timed out after ${details.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS}ms`;
3244
+ const remedy = details.cleanedUp ? "The stale descriptor was removed automatically; start CCEM Desktop and rerun the command." : details.pid !== null ? "Restart CCEM Desktop so it republishes a fresh control endpoint." : "Restart CCEM Desktop to refresh the descriptor, or remove the stale file manually if it is no longer managed.";
3245
+ super(
3246
+ `CCEM Desktop control descriptor at ${details.descriptorPath} is stale: ${symptom}. ${remedy}`
3247
+ );
3248
+ this.reason = details.reason;
3249
+ this.descriptorPath = details.descriptorPath;
3250
+ this.pid = details.pid;
3251
+ this.cleanedUp = details.cleanedUp;
3252
+ if (details.cause !== void 0) {
3253
+ this.cause = details.cause;
3254
+ }
3255
+ }
3256
+ };
3171
3257
  function getDesktopControlDescriptorPath() {
3172
- return process.env.CCEM_CONTROL_FILE?.trim() || path8.join(getCcemConfigDir(), "control.json");
3258
+ return process.env.CCEM_CONTROL_FILE?.trim() || getDefaultControlDescriptorPath();
3259
+ }
3260
+ function getDefaultControlDescriptorPath() {
3261
+ return path8.join(getCcemConfigDir(), "control.json");
3262
+ }
3263
+ function isDefaultDescriptorPath(descriptorPath) {
3264
+ try {
3265
+ return path8.resolve(descriptorPath) === path8.resolve(getDefaultControlDescriptorPath());
3266
+ } catch {
3267
+ return false;
3268
+ }
3269
+ }
3270
+ function safeRemoveStaleDescriptor(descriptorPath) {
3271
+ if (!isDefaultDescriptorPath(descriptorPath)) {
3272
+ return false;
3273
+ }
3274
+ try {
3275
+ fs10.rmSync(descriptorPath);
3276
+ return true;
3277
+ } catch {
3278
+ return false;
3279
+ }
3280
+ }
3281
+ function readErrnoCode(error) {
3282
+ if (!error || typeof error !== "object") return void 0;
3283
+ const candidate = error;
3284
+ return candidate.code ?? candidate.cause?.code;
3285
+ }
3286
+ function readPid(value) {
3287
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
3288
+ }
3289
+ function isLoopbackHost(host) {
3290
+ const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
3291
+ if (LOOPBACK_HOSTS.has(normalized)) {
3292
+ return true;
3293
+ }
3294
+ if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(normalized)) {
3295
+ return true;
3296
+ }
3297
+ if (normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") {
3298
+ return true;
3299
+ }
3300
+ return false;
3301
+ }
3302
+ function extractHost(endpoint) {
3303
+ const withoutScheme = endpoint.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "");
3304
+ const hostPort = withoutScheme.split(/[\/?#]/)[0] || "";
3305
+ if (hostPort.startsWith("[")) {
3306
+ const end = hostPort.indexOf("]");
3307
+ if (end === -1) return null;
3308
+ return hostPort.slice(1, end);
3309
+ }
3310
+ const colonIndex = hostPort.lastIndexOf(":");
3311
+ if (colonIndex === -1) return hostPort;
3312
+ return hostPort.slice(0, colonIndex);
3313
+ }
3314
+ function validateLoopbackEndpoint(endpoint) {
3315
+ const host = extractHost(endpoint);
3316
+ if (!host) {
3317
+ throw new Error(
3318
+ `CCEM Desktop control endpoint '${redactEndpoint(endpoint)}' is missing a host. Refusing to continue.`
3319
+ );
3320
+ }
3321
+ if (!isLoopbackHost(host)) {
3322
+ throw new Error(
3323
+ `CCEM Desktop control endpoint '${redactEndpoint(endpoint)}' is not bound to loopback. Only 127.0.0.1, localhost, or ::1 are allowed.`
3324
+ );
3325
+ }
3326
+ }
3327
+ function isPidAlive(pid) {
3328
+ if (!Number.isFinite(pid) || pid <= 0) return false;
3329
+ try {
3330
+ process.kill(pid, 0);
3331
+ return true;
3332
+ } catch (error) {
3333
+ const code = error.code;
3334
+ if (code === "ESRCH") return false;
3335
+ if (code === "EPERM") return true;
3336
+ return false;
3337
+ }
3338
+ }
3339
+ function redactEndpoint(endpoint) {
3340
+ try {
3341
+ const url = new URL(endpoint);
3342
+ return `${url.protocol}//${url.host}${url.pathname}`;
3343
+ } catch {
3344
+ return "<invalid endpoint>";
3345
+ }
3346
+ }
3347
+ function isDescriptor(value) {
3348
+ return Boolean(
3349
+ value && typeof value === "object" && typeof value.endpoint === "string" && typeof value.token === "string"
3350
+ );
3173
3351
  }
3174
3352
  function resolveDesktopControlDescriptor(descriptorPath = getDesktopControlDescriptorPath()) {
3175
3353
  if (!fs10.existsSync(descriptorPath)) {
@@ -3181,27 +3359,93 @@ function resolveDesktopControlDescriptor(descriptorPath = getDesktopControlDescr
3181
3359
  if (!endpoint || !token) {
3182
3360
  throw new Error(`Invalid CCEM Desktop control descriptor at ${descriptorPath}`);
3183
3361
  }
3184
- return {
3185
- endpoint,
3186
- token,
3187
- pid: typeof parsed.pid === "number" ? parsed.pid : null
3188
- };
3362
+ validateLoopbackEndpoint(endpoint);
3363
+ const pid = readPid(parsed.pid);
3364
+ if (pid !== null && !isPidAlive(pid)) {
3365
+ const cleanedUp = safeRemoveStaleDescriptor(descriptorPath);
3366
+ throw new StaleDesktopControlDescriptorError({
3367
+ reason: "dead-pid",
3368
+ descriptorPath,
3369
+ pid,
3370
+ cleanedUp
3371
+ });
3372
+ }
3373
+ return { endpoint, token, pid };
3189
3374
  }
3190
- async function requestDesktopControl(method, params, descriptor = resolveDesktopControlDescriptor()) {
3375
+ async function requestDesktopControl(method, params, descriptorOrOptions, maybeOptions = {}) {
3376
+ const descriptorPath = getDesktopControlDescriptorPath();
3377
+ const hasInjectedDescriptor = isDescriptor(descriptorOrOptions);
3378
+ const descriptor = hasInjectedDescriptor ? descriptorOrOptions : resolveDesktopControlDescriptor(descriptorPath);
3379
+ const options = hasInjectedDescriptor ? maybeOptions : descriptorOrOptions ?? maybeOptions ?? {};
3380
+ const timeoutMs = options.timeoutMs ?? options.fetchTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
3381
+ const pid = readPid(descriptor.pid);
3382
+ validateLoopbackEndpoint(descriptor.endpoint);
3383
+ if (pid !== null && !isPidAlive(pid)) {
3384
+ const cleanedUp = hasInjectedDescriptor ? false : safeRemoveStaleDescriptor(descriptorPath);
3385
+ throw new StaleDesktopControlDescriptorError({
3386
+ reason: "dead-pid",
3387
+ descriptorPath,
3388
+ pid,
3389
+ cleanedUp
3390
+ });
3391
+ }
3191
3392
  const id = `ccem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3192
- const response = await fetch(descriptor.endpoint, {
3193
- method: "POST",
3194
- headers: {
3195
- authorization: `Bearer ${descriptor.token}`,
3196
- "content-type": "application/json"
3197
- },
3198
- body: JSON.stringify({
3199
- jsonrpc: "2.0",
3200
- id,
3201
- method,
3202
- params: params ?? {}
3203
- })
3204
- });
3393
+ const controller = new AbortController();
3394
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3395
+ const externalSignal = options.signal;
3396
+ const onExternalAbort = () => controller.abort();
3397
+ if (externalSignal) {
3398
+ if (externalSignal.aborted) {
3399
+ clearTimeout(timer);
3400
+ controller.abort();
3401
+ } else {
3402
+ externalSignal.addEventListener("abort", onExternalAbort, { once: true });
3403
+ }
3404
+ }
3405
+ let response;
3406
+ try {
3407
+ response = await fetch(descriptor.endpoint, {
3408
+ method: "POST",
3409
+ headers: {
3410
+ authorization: `Bearer ${descriptor.token}`,
3411
+ "content-type": "application/json"
3412
+ },
3413
+ body: JSON.stringify({
3414
+ jsonrpc: "2.0",
3415
+ id,
3416
+ method,
3417
+ params: params ?? {}
3418
+ }),
3419
+ signal: controller.signal
3420
+ });
3421
+ } catch (error) {
3422
+ if (controller.signal.aborted) {
3423
+ throw new StaleDesktopControlDescriptorError({
3424
+ reason: "request-timeout",
3425
+ descriptorPath,
3426
+ pid,
3427
+ cleanedUp: false,
3428
+ timeoutMs,
3429
+ cause: error
3430
+ });
3431
+ }
3432
+ const code = readErrnoCode(error);
3433
+ if (code && ENDPOINT_UNREACHABLE_CODES.has(code)) {
3434
+ throw new StaleDesktopControlDescriptorError({
3435
+ reason: "endpoint-unreachable",
3436
+ descriptorPath,
3437
+ pid,
3438
+ cleanedUp: false,
3439
+ cause: error
3440
+ });
3441
+ }
3442
+ throw error;
3443
+ } finally {
3444
+ clearTimeout(timer);
3445
+ if (externalSignal) {
3446
+ externalSignal.removeEventListener("abort", onExternalAbort);
3447
+ }
3448
+ }
3205
3449
  if (!response.ok) {
3206
3450
  throw new Error(`CCEM Desktop control request failed: HTTP ${response.status}`);
3207
3451
  }
@@ -3211,6 +3455,26 @@ async function requestDesktopControl(method, params, descriptor = resolveDesktop
3211
3455
  }
3212
3456
  return payload.result;
3213
3457
  }
3458
+ function parseSinceOption(raw) {
3459
+ if (raw === void 0 || raw === null || raw === "") return null;
3460
+ const value = Number(raw);
3461
+ if (!Number.isFinite(value) || value < 0 || !Number.isInteger(value)) {
3462
+ throw new Error(
3463
+ `Invalid --since value '${raw}'. Expected a non-negative integer sequence number (e.g. 0, 42).`
3464
+ );
3465
+ }
3466
+ return value;
3467
+ }
3468
+ function parseLimitOption(raw) {
3469
+ if (raw === void 0 || raw === null || raw === "") return null;
3470
+ const value = Number(raw);
3471
+ if (!Number.isFinite(value) || value <= 0 || !Number.isInteger(value)) {
3472
+ throw new Error(
3473
+ `Invalid --limit value '${raw}'. Expected a positive integer (e.g. 1, 100).`
3474
+ );
3475
+ }
3476
+ return value;
3477
+ }
3214
3478
  function printJson(value) {
3215
3479
  console.log(JSON.stringify(value, null, 2));
3216
3480
  }
@@ -4192,10 +4456,12 @@ desktopCmd.command("status <runtimeId>").description("Get one CCEM Desktop works
4192
4456
  });
4193
4457
  desktopCmd.command("events <runtimeId>").description("Read CCEM Desktop workspace session events").option("--since <seq>", "First event sequence after this value").option("--limit <count>", "Maximum events to return").option("--json", "Output JSON").action(async function(runtimeId) {
4194
4458
  const opts = this.opts();
4459
+ const sinceSeq = parseSinceOption(opts.since);
4460
+ const limit = parseLimitOption(opts.limit);
4195
4461
  const result = await requestDesktopControl("ccem.workspace.getEvents", {
4196
4462
  runtimeId,
4197
- sinceSeq: opts.since ? Number(opts.since) : null,
4198
- limit: opts.limit ? Number(opts.limit) : null
4463
+ sinceSeq,
4464
+ limit
4199
4465
  });
4200
4466
  outputDesktopResult(result, opts);
4201
4467
  });
@@ -4420,4 +4686,16 @@ Editing environment '${result.name}'`));
4420
4686
  }
4421
4687
  }
4422
4688
  });
4423
- program.parse(process.argv);
4689
+ function handleCliError(error) {
4690
+ if (error instanceof StaleDesktopControlDescriptorError) {
4691
+ console.error(chalk7.red(error.message));
4692
+ process.exit(1);
4693
+ }
4694
+ if (error instanceof Error) {
4695
+ console.error(chalk7.red(error.message));
4696
+ process.exit(1);
4697
+ }
4698
+ console.error(chalk7.red(String(error)));
4699
+ process.exit(1);
4700
+ }
4701
+ await program.parseAsync(process.argv).catch(handleCliError);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccem",
3
- "version": "2.28.0",
3
+ "version": "2.30.0",
4
4
  "type": "module",
5
5
  "description": "Claude Code Environment Manager",
6
6
  "repository": {