rainskills 0.1.29 → 0.1.30

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
@@ -239,7 +239,7 @@ node ~/.rainbond/lib/rainskills/bin/rainskills.js platform install --onboarding-
239
239
 
240
240
  Rainskills 会在用户下一次发起业务动作时,由本地运行时立即返回环境查询结果,并另行启动后台任务静默检查更新。更新只跟随 npm `latest` 指向的正式版。当前版本是 RC 或其他预发布版本时不会查询、不会自动升级;npm 上的新 RC 版本也不参与正式版自动升级。
241
241
 
242
- 发现更高的正式版后,后台任务只委托到经过校验的精确版本,例如 `rainskills@0.1.29`,不会执行浮动的 `@latest` 业务代码。新版本原子刷新已经安装的 Rainskills Skills;当前业务继续使用启动时已经加载的版本,最迟从下一条新任务开始使用新版。npm 超时、版本检查失败、安装位置不安全或文件迁移失败时会保留旧版本,且不会阻塞或改变当前操作。
242
+ 发现更高的正式版后,后台任务只委托到经过校验的精确版本,例如 `rainskills@0.1.30`,不会执行浮动的 `@latest` 业务代码。新版本原子刷新已经安装的 Rainskills Skills;当前业务继续使用启动时已经加载的版本,最迟从下一条新任务开始使用新版。npm 超时、版本检查失败、安装位置不安全或文件迁移失败时会保留旧版本,且不会阻塞或改变当前操作。
243
243
 
244
244
  升级只更新 Rainskills 自身,不触发 Rainbond 安装、运行环境选择、登录授权或重新对接,也不会新增 Agent MCP 配置。更新内容仅包括 Skills 和本地 CLI。原始业务操作会继续执行;只有该业务操作本身需要运行环境时,才按既有门禁检查当前连接。可用连接直接复用,401 只重新授权一次,403 立即停止,从未连接过运行环境时才进入环境选择。
245
245
 
package/SKILL.md CHANGED
@@ -50,7 +50,7 @@ Codex 中命令工具一旦返回 `session_id`,必须立即对该 `session_id`
50
50
 
51
51
  DeepSeek Harness 和 WorkBuddy 也必须保持原命令附着直到退出;如果宿主把长命令转成持久终端或后台任务,只能轮询该原始命令句柄,不得另起 `runtime status` 推测授权结果。WorkBuddy 安装后若当前任务的 Skill 清单尚未刷新,在下一条业务动作前先刷新 Skill 列表或新建任务,不得退回内置 Sites 代替 Rainbond 部署。
52
52
 
53
- Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.29`):
53
+ Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.30`):
54
54
 
55
55
  - 状态:执行 `runtime status --json`。
56
56
  - 首次连接:执行 `runtime connect <target> --saas` 或 `runtime connect <target> --rainbond-url <Console origin>`。
@@ -449,6 +449,56 @@ function skillManifestPath(config) {
449
449
  return path.join(config.homeDir || os.homedir(), CONFIG_DIRECTORY, "bin", SKILL_MANIFEST_FILENAME);
450
450
  }
451
451
 
452
+ function loadTelemetryPackageVersion(config) {
453
+ try {
454
+ const manifest = JSON.parse(fs.readFileSync(skillManifestPath(config), "utf8"));
455
+ return typeof manifest.package_version === "string" && manifest.package_version
456
+ ? manifest.package_version
457
+ : "unknown";
458
+ } catch {
459
+ return "unknown";
460
+ }
461
+ }
462
+
463
+ function createBridgeTelemetry(config) {
464
+ try {
465
+ const directory = path.join(config.homeDir || os.homedir(), CONFIG_DIRECTORY, "rainskills", "telemetry");
466
+ const { createResultTelemetry } = requireRuntimeModule("result-telemetry.js");
467
+ return createResultTelemetry({
468
+ directory,
469
+ packageVersion: loadTelemetryPackageVersion(config),
470
+ });
471
+ } catch {
472
+ return null;
473
+ }
474
+ }
475
+
476
+ async function executeWithUsageTelemetry(command, config) {
477
+ const tracksUsage = command.command === "read" || command.command === "call";
478
+ const telemetry = tracksUsage ? createBridgeTelemetry(config) : null;
479
+ if (telemetry) await telemetry.flushPending(3).catch(() => {});
480
+ try {
481
+ const output = await execute(command, config);
482
+ if (telemetry) {
483
+ const firstUse = telemetry.recordFirstUse("success");
484
+ const active = telemetry.recordActiveDaily();
485
+ await Promise.allSettled([firstUse.delivery, active.delivery]);
486
+ }
487
+ return output;
488
+ } catch (error) {
489
+ if (telemetry) {
490
+ const firstUse = telemetry.recordFirstUse("failed", {
491
+ error_stage: "first_use",
492
+ error_code: error instanceof BridgeError && error.exitCode === EXIT.TRANSPORT
493
+ ? "transport_failed"
494
+ : "tool_call_failed",
495
+ });
496
+ await firstUse.delivery.catch(() => false);
497
+ }
498
+ throw error;
499
+ }
500
+ }
501
+
452
502
  function loadSkillBinding(config, skillId, rootSkillId) {
453
503
  const target = skillManifestPath(config);
454
504
  let manifest;
@@ -1370,7 +1420,7 @@ async function main(args = process.argv.slice(2)) {
1370
1420
  if (config.isInsecureHttp) {
1371
1421
  process.stderr.write('{"warning":"using insecure HTTP transport"}\n');
1372
1422
  }
1373
- const output = await execute(command, config);
1423
+ const output = await executeWithUsageTelemetry(command, config);
1374
1424
  process.stdout.write(`${JSON.stringify(fitOutput(output))}\n`);
1375
1425
  return;
1376
1426
  }
@@ -1380,7 +1430,7 @@ async function main(args = process.argv.slice(2)) {
1380
1430
  if (config.isInsecureHttp) {
1381
1431
  process.stderr.write('{"warning":"using insecure HTTP transport"}\n');
1382
1432
  }
1383
- const output = await execute(command, config);
1433
+ const output = await executeWithUsageTelemetry(command, config);
1384
1434
  process.stdout.write(`${JSON.stringify(fitOutput(output))}\n`);
1385
1435
  } catch (error) {
1386
1436
  const bridgeError = error instanceof BridgeError
package/install.sh CHANGED
@@ -176,6 +176,7 @@ VALIDATED_TOKEN=""
176
176
  OBTAINED_RAINBOND_TOKEN=""
177
177
  RAINSKILLS_INSTALL_REPORT_URL="https://log.rainbond.com/api/rainskills/installations"
178
178
  RAINSKILLS_LIFECYCLE_REPORT_URL="https://log.rainbond.com/api/rainskills/lifecycle-events"
179
+ RAINSKILLS_TELEMETRY_REPORT_URL="${RAINSKILLS_TELEMETRY_REPORT_URL:-https://log.rainbond.com/api/rainskills/events}"
179
180
  RAINSKILLS_INSTALL_ATTEMPT_ID="${RAINSKILLS_INSTALL_ATTEMPT_ID:-}"
180
181
  RAINSKILLS_INSTALL_EID=""
181
182
  RAINSKILLS_INSTALL_CLIENT="unknown"
@@ -183,6 +184,8 @@ RAINSKILLS_INSTALL_ACTION="install"
183
184
  RAINSKILLS_INSTALL_FAILURE_STAGE="bootstrap"
184
185
  RAINSKILLS_INSTALL_FAILURE_CATEGORY="invalid_arguments"
185
186
  RAINSKILLS_INSTALL_TERMINAL_REPORTED=0
187
+ RAINSKILLS_V2_TERMINAL_REPORTED=0
188
+ RAINSKILLS_V2_CURRENT_AGENT=""
186
189
  RAINSKILLS_TELEMETRY_SEQUENCE=0
187
190
  RAINSKILLS_BROWSER_LOGIN_SERVER_PID=""
188
191
  RAINSKILLS_BROWSER_LOGIN_READER_PID=""
@@ -286,13 +289,282 @@ new_rainskills_install_attempt_id() {
286
289
  python3 - <<'PY'
287
290
  import uuid
288
291
 
289
- print(uuid.uuid4().hex)
292
+ print(str(uuid.uuid4()))
290
293
  PY
291
294
  return 0
292
295
  fi
293
296
  printf '%s-%s-%s\n' "$(date +%s)" "$$" "${RANDOM:-0}"
294
297
  }
295
298
 
299
+ rainskills_v2_enabled() {
300
+ [[ "${RAINSKILLS_TELEMETRY_DISABLED:-0}" != "1" ]]
301
+ }
302
+
303
+ initialize_rainskills_v2_identity() {
304
+ rainskills_v2_enabled || return 0
305
+ command -v python3 >/dev/null 2>&1 || return 0
306
+ local telemetry_dir package_file
307
+ telemetry_dir="${RAINSKILLS_TELEMETRY_DIR:-${HOME:-/tmp}/.rainbond/rainskills/telemetry}"
308
+ package_file="$SCRIPT_DIR/package.json"
309
+ RAINSKILLS_TELEMETRY_INSTALLATION_ID="$({
310
+ python3 - "$telemetry_dir" "${RAINSKILLS_TELEMETRY_INSTALLATION_ID:-}" <<'PY'
311
+ import os
312
+ import re
313
+ import sys
314
+ import uuid
315
+
316
+ directory, provided = sys.argv[1:]
317
+ pattern = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", re.I)
318
+ os.makedirs(directory, mode=0o700, exist_ok=True)
319
+ os.chmod(directory, 0o700)
320
+ path = os.path.join(directory, "installation-id")
321
+ value = provided.strip()
322
+ if not pattern.match(value):
323
+ try:
324
+ value = open(path, encoding="utf-8").read().strip()
325
+ except Exception:
326
+ value = ""
327
+ if not pattern.match(value):
328
+ value = str(uuid.uuid4())
329
+ temporary = "{}.{}.tmp".format(path, os.getpid())
330
+ with open(temporary, "x", encoding="utf-8") as stream:
331
+ stream.write(value + "\n")
332
+ os.chmod(temporary, 0o600)
333
+ os.replace(temporary, path)
334
+ os.chmod(path, 0o600)
335
+ print(value)
336
+ PY
337
+ } 2>/dev/null || true)"
338
+ if [[ -z "${RAINSKILLS_PACKAGE_VERSION:-}" && -f "$package_file" ]]; then
339
+ RAINSKILLS_PACKAGE_VERSION="$({
340
+ python3 - "$package_file" <<'PY'
341
+ import json
342
+ import sys
343
+
344
+ try:
345
+ value = json.load(open(sys.argv[1], encoding="utf-8")).get("version")
346
+ except Exception:
347
+ value = None
348
+ print(value if isinstance(value, str) and value else "unknown")
349
+ PY
350
+ } 2>/dev/null || printf 'unknown')"
351
+ fi
352
+ export RAINSKILLS_TELEMETRY_INSTALLATION_ID RAINSKILLS_PACKAGE_VERSION
353
+ }
354
+
355
+ rainskills_v2_action() {
356
+ case "$RAINSKILLS_INSTALL_ACTION" in
357
+ install|refresh|upgrade|repair) printf '%s\n' "$RAINSKILLS_INSTALL_ACTION" ;;
358
+ connect) printf 'repair\n' ;;
359
+ *) printf 'install\n' ;;
360
+ esac
361
+ }
362
+
363
+ rainskills_v2_agent() {
364
+ case "$1" in
365
+ codex) printf 'codex\n' ;;
366
+ claude|claude_code) printf 'claude_code\n' ;;
367
+ pi) printf 'pi\n' ;;
368
+ dsh|deepseek_harness|deepseek) printf 'deepseek\n' ;;
369
+ workbuddy) printf 'workbuddy\n' ;;
370
+ other) printf 'other\n' ;;
371
+ *) printf 'unknown\n' ;;
372
+ esac
373
+ }
374
+
375
+ rainskills_v2_os_type() {
376
+ case "$(uname -s 2>/dev/null || true)" in
377
+ Darwin) printf 'darwin\n' ;;
378
+ MINGW*|MSYS*|CYGWIN*) printf 'windows\n' ;;
379
+ *) printf 'linux\n' ;;
380
+ esac
381
+ }
382
+
383
+ rainskills_v2_os_arch() {
384
+ case "$(uname -m 2>/dev/null || true)" in
385
+ x86_64|amd64) printf 'amd64\n' ;;
386
+ arm64|aarch64) printf 'arm64\n' ;;
387
+ "") printf 'unknown\n' ;;
388
+ *) printf 'other\n' ;;
389
+ esac
390
+ }
391
+
392
+ rainskills_v2_execution_environment() {
393
+ if [[ -n "${WSL_DISTRO_NAME:-}" || -n "${WSL_INTEROP:-}" ]]; then
394
+ printf 'wsl\n'
395
+ elif [[ -n "${SSH_CONNECTION:-}" || -n "${SSH_CLIENT:-}" ]]; then
396
+ printf 'ssh\n'
397
+ elif [[ -n "${container:-}" ]]; then
398
+ printf 'container\n'
399
+ else
400
+ printf 'native\n'
401
+ fi
402
+ }
403
+
404
+ rainskills_v2_error_stage() {
405
+ case "${RAINSKILLS_INSTALL_FAILURE_STAGE:-install}" in
406
+ bootstrap) printf 'preflight\n' ;;
407
+ download) printf 'download\n' ;;
408
+ skill_installation) printf 'agent_configuration\n' ;;
409
+ authorization) printf 'authorization\n' ;;
410
+ verification) printf 'verification\n' ;;
411
+ configuration) printf 'agent_configuration\n' ;;
412
+ *) printf 'install\n' ;;
413
+ esac
414
+ }
415
+
416
+ rainskills_v2_error_code() {
417
+ case "${RAINSKILLS_INSTALL_FAILURE_CATEGORY:-unknown}" in
418
+ invalid_arguments) printf 'invalid_arguments\n' ;;
419
+ tarball_unavailable|download_failed) printf 'download_failed\n' ;;
420
+ authorization_failed) printf 'authorization_failed\n' ;;
421
+ mcp_verification_failed) printf 'verification_failed\n' ;;
422
+ mcp_configuration_failed|cli_configuration_failed|skill_installation_failed) printf 'agent_config_failed\n' ;;
423
+ network_unreachable) printf 'network_unreachable\n' ;;
424
+ *) printf 'unknown\n' ;;
425
+ esac
426
+ }
427
+
428
+ report_rainskills_v2_event() {
429
+ local event_type="$1"
430
+ local status="${2:-}"
431
+ local agent="${3:-}"
432
+ local error_stage="${4:-}"
433
+ local error_code="${5:-}"
434
+ rainskills_v2_enabled || return 0
435
+ [[ -n "${RAINSKILLS_TELEMETRY_INSTALLATION_ID:-}" ]] || return 0
436
+ command -v curl >/dev/null 2>&1 || return 0
437
+ command -v python3 >/dev/null 2>&1 || return 0
438
+
439
+ local payload telemetry_dir
440
+ telemetry_dir="${RAINSKILLS_TELEMETRY_DIR:-${HOME:-/tmp}/.rainbond/rainskills/telemetry}"
441
+ payload="$({
442
+ python3 - \
443
+ "$event_type" \
444
+ "$RAINSKILLS_TELEMETRY_INSTALLATION_ID" \
445
+ "$RAINSKILLS_INSTALL_ATTEMPT_ID" \
446
+ "${RAINSKILLS_PACKAGE_VERSION:-unknown}" \
447
+ "$(rainskills_v2_action)" \
448
+ "$(rainskills_v2_agent "$agent")" \
449
+ "$(rainskills_v2_os_type)" \
450
+ "$(rainskills_v2_os_arch)" \
451
+ "$(rainskills_v2_execution_environment)" \
452
+ "$status" \
453
+ "$error_stage" \
454
+ "$error_code" \
455
+ "$telemetry_dir" <<'PY'
456
+ import datetime
457
+ import json
458
+ import os
459
+ import sys
460
+ import time
461
+ import uuid
462
+
463
+ (event_type, installation_id, attempt_id, package_version, action, agent,
464
+ os_type, os_arch, execution_environment, status, error_stage, error_code,
465
+ telemetry_dir) = sys.argv[1:]
466
+ event = {
467
+ "schema": "rainskills.telemetry-event.v2",
468
+ "event_id": str(uuid.uuid4()),
469
+ "event_type": event_type,
470
+ "installation_id": installation_id,
471
+ "package_version": package_version,
472
+ "occurred_at": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"),
473
+ }
474
+ if event_type in {"install_result", "agent_config_result"}:
475
+ event["install_attempt_id"] = attempt_id
476
+ event["action"] = action
477
+ if event_type == "install_result":
478
+ event.update({
479
+ "os_type": os_type,
480
+ "os_arch": os_arch,
481
+ "execution_environment": execution_environment,
482
+ "status": status,
483
+ })
484
+ elif event_type == "agent_config_result":
485
+ event.update({"agent_type": agent, "status": status})
486
+ if status == "failed":
487
+ event["error_stage"] = error_stage
488
+ event["error_code"] = error_code
489
+ pending_dir = os.path.join(telemetry_dir, "pending-v2")
490
+ try:
491
+ os.makedirs(pending_dir, mode=0o700, exist_ok=True)
492
+ os.chmod(telemetry_dir, 0o700)
493
+ os.chmod(pending_dir, 0o700)
494
+ now = time.time()
495
+ entries = []
496
+ for name in os.listdir(pending_dir):
497
+ if not name.endswith(".json"):
498
+ continue
499
+ file_path = os.path.join(pending_dir, name)
500
+ try:
501
+ entries.append((os.stat(file_path).st_mtime, file_path))
502
+ except OSError:
503
+ pass
504
+ entries.sort()
505
+ expired = [entry for entry in entries if now - entry[0] > 7 * 24 * 60 * 60]
506
+ remaining = [entry for entry in entries if now - entry[0] <= 7 * 24 * 60 * 60]
507
+ overflow = remaining[:max(0, len(remaining) - 99)]
508
+ for _, file_path in expired + overflow:
509
+ try:
510
+ os.remove(file_path)
511
+ except OSError:
512
+ pass
513
+ event_path = os.path.join(pending_dir, event["event_id"] + ".json")
514
+ with open(event_path, "x", encoding="utf-8") as stream:
515
+ stream.write(json.dumps(event, separators=(",", ":")) + "\n")
516
+ os.chmod(event_path, 0o600)
517
+ except Exception:
518
+ pass
519
+ print(json.dumps(event, separators=(",", ":")))
520
+ PY
521
+ } 2>/dev/null || true)"
522
+ [[ -n "$payload" ]] || return 0
523
+ local event_id pending_file
524
+ event_id="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["event_id"])' <<<"$payload" 2>/dev/null || true)"
525
+ pending_file="$telemetry_dir/pending-v2/${event_id}.json"
526
+ (
527
+ if curl --silent --show-error --connect-timeout 2 --max-time 3 \
528
+ -X POST "$RAINSKILLS_TELEMETRY_REPORT_URL" \
529
+ -H 'Content-Type: application/json' \
530
+ --data-binary "$payload" >/dev/null 2>&1; then
531
+ [[ -n "$event_id" ]] && rm -f "$pending_file"
532
+ fi
533
+ ) &
534
+ }
535
+
536
+ record_rainskills_v2_configured_agent() {
537
+ local agent
538
+ agent="$(rainskills_v2_agent "$1")"
539
+ [[ "$agent" != "unknown" ]] || return 0
540
+ rainskills_v2_enabled || return 0
541
+ command -v python3 >/dev/null 2>&1 || return 0
542
+ local telemetry_dir
543
+ telemetry_dir="${RAINSKILLS_TELEMETRY_DIR:-${HOME:-/tmp}/.rainbond/rainskills/telemetry}"
544
+ python3 - "$telemetry_dir" "$agent" <<'PY' >/dev/null 2>&1 || true
545
+ import json
546
+ import os
547
+ import sys
548
+
549
+ directory, agent = sys.argv[1:]
550
+ os.makedirs(directory, mode=0o700, exist_ok=True)
551
+ os.chmod(directory, 0o700)
552
+ path = os.path.join(directory, "configured-agents.json")
553
+ try:
554
+ current = json.load(open(path, encoding="utf-8"))
555
+ except Exception:
556
+ current = []
557
+ agents = sorted(set(value for value in current + [agent] if isinstance(value, str)))
558
+ temporary = "{}.{}.tmp".format(path, os.getpid())
559
+ with open(temporary, "x", encoding="utf-8") as stream:
560
+ json.dump(agents, stream, separators=(",", ":"))
561
+ stream.write("\n")
562
+ os.chmod(temporary, 0o600)
563
+ os.replace(temporary, path)
564
+ os.chmod(path, 0o600)
565
+ PY
566
+ }
567
+
296
568
  rainskills_install_client_for_target() {
297
569
  case "$1" in
298
570
  codex)
@@ -325,6 +597,7 @@ report_rainskills_installation() {
325
597
  local failure_stage="${3:-}"
326
598
  local failure_category="${4:-}"
327
599
 
600
+ [[ "${RAINSKILLS_LEGACY_TELEMETRY_ENABLED:-0}" == "1" ]] || return 0
328
601
  [[ -n "$RAINSKILLS_INSTALL_ATTEMPT_ID" ]] || return 0
329
602
  if [[ "$phase" == "authorized" || "$phase" == "configured" ]]; then
330
603
  [[ -n "$RAINSKILLS_INSTALL_EID" ]] || return 0
@@ -423,6 +696,7 @@ report_rainskills_lifecycle_event() {
423
696
  local blocked_reason="${6:-}"
424
697
  local auth_method="${7:-}"
425
698
 
699
+ [[ "${RAINSKILLS_LEGACY_TELEMETRY_ENABLED:-0}" == "1" ]] || return 0
426
700
  [[ -n "$RAINSKILLS_INSTALL_ATTEMPT_ID" ]] || return 0
427
701
  command -v python3 >/dev/null 2>&1 || return 0
428
702
  RAINSKILLS_TELEMETRY_SEQUENCE=$((RAINSKILLS_TELEMETRY_SEQUENCE + 1))
@@ -536,6 +810,7 @@ initialize_rainskills_installation_reporting() {
536
810
  esac
537
811
  done
538
812
  RAINSKILLS_INSTALL_CLIENT="$(rainskills_install_client_for_target "$target")"
813
+ initialize_rainskills_v2_identity
539
814
  report_rainskills_lifecycle_event "bootstrap" "resume" "resume" "started"
540
815
  report_rainskills_installation "started" "started"
541
816
  }
@@ -634,6 +909,25 @@ set_rainskills_failure_context() {
634
909
 
635
910
  report_unhandled_rainskills_installation_failure() {
636
911
  local exit_status="$1"
912
+ if [[ "$exit_status" -ne 0 && \
913
+ "${BASH_SUBSHELL:-0}" -eq 0 && \
914
+ "$RAINSKILLS_V2_TERMINAL_REPORTED" -eq 0 ]]; then
915
+ RAINSKILLS_V2_TERMINAL_REPORTED=1
916
+ if [[ -n "$RAINSKILLS_V2_CURRENT_AGENT" ]]; then
917
+ report_rainskills_v2_event \
918
+ "agent_config_result" \
919
+ "failed" \
920
+ "$RAINSKILLS_V2_CURRENT_AGENT" \
921
+ "agent_configuration" \
922
+ "agent_config_failed"
923
+ fi
924
+ report_rainskills_v2_event \
925
+ "install_result" \
926
+ "failed" \
927
+ "" \
928
+ "$(rainskills_v2_error_stage)" \
929
+ "$(rainskills_v2_error_code)"
930
+ fi
637
931
  if [[ "$exit_status" -ne 0 && \
638
932
  "${BASH_SUBSHELL:-0}" -eq 0 && \
639
933
  "$RAINSKILLS_INSTALL_TERMINAL_REPORTED" -eq 0 ]]; then
@@ -716,6 +1010,23 @@ handle_installer_exit() {
716
1010
  }
717
1011
 
718
1012
  die() {
1013
+ if [[ "${BASH_SUBSHELL:-0}" -eq 0 && "$RAINSKILLS_V2_TERMINAL_REPORTED" -eq 0 ]]; then
1014
+ RAINSKILLS_V2_TERMINAL_REPORTED=1
1015
+ if [[ -n "$RAINSKILLS_V2_CURRENT_AGENT" ]]; then
1016
+ report_rainskills_v2_event \
1017
+ "agent_config_result" \
1018
+ "failed" \
1019
+ "$RAINSKILLS_V2_CURRENT_AGENT" \
1020
+ "agent_configuration" \
1021
+ "agent_config_failed"
1022
+ fi
1023
+ report_rainskills_v2_event \
1024
+ "install_result" \
1025
+ "failed" \
1026
+ "" \
1027
+ "$(rainskills_v2_error_stage)" \
1028
+ "$(rainskills_v2_error_code)"
1029
+ fi
719
1030
  if [[ "${BASH_SUBSHELL:-0}" -eq 0 && "$RAINSKILLS_INSTALL_TERMINAL_REPORTED" -eq 0 ]]; then
720
1031
  RAINSKILLS_INSTALL_TERMINAL_REPORTED=1
721
1032
  report_rainskills_installation \
@@ -1104,6 +1415,24 @@ collect_destinations() {
1104
1415
  printf '%s\n' "${destinations[@]}"
1105
1416
  }
1106
1417
 
1418
+ collect_target_agents() {
1419
+ if [[ -n "$CUSTOM_DEST" ]]; then
1420
+ printf 'unknown\n'
1421
+ return 0
1422
+ fi
1423
+ case "$TARGET" in
1424
+ claude) printf 'claude\n' ;;
1425
+ codex) printf 'codex\n' ;;
1426
+ pi) printf 'pi\n' ;;
1427
+ dsh) printf 'dsh\n' ;;
1428
+ workbuddy) printf 'workbuddy\n' ;;
1429
+ all)
1430
+ printf '%s\n' claude codex pi dsh workbuddy
1431
+ ;;
1432
+ *) printf 'unknown\n' ;;
1433
+ esac
1434
+ }
1435
+
1107
1436
  normalize_rainbond_url() {
1108
1437
  local raw
1109
1438
  raw="$(trim "$1")"
@@ -2549,6 +2878,8 @@ do_refresh() {
2549
2878
  log "如果想立刻在当前终端使用,请执行:source ${ACTIVE_SHELL_RC}"
2550
2879
  fi
2551
2880
  RAINSKILLS_INSTALL_TERMINAL_REPORTED=1
2881
+ RAINSKILLS_V2_TERMINAL_REPORTED=1
2882
+ report_rainskills_v2_event "install_result" "success"
2552
2883
  report_rainskills_lifecycle_event "configure_cli" "install_cli" "configure_cli" "completed"
2553
2884
  report_rainskills_installation "configured" "success"
2554
2885
  }
@@ -2629,6 +2960,10 @@ configure_runtime_connection() {
2629
2960
  report_rainskills_lifecycle_event "configure_cli" "install_cli" "configure_cli" "started"
2630
2961
  install_local_cli
2631
2962
  RAINSKILLS_INSTALL_TERMINAL_REPORTED=1
2963
+ RAINSKILLS_V2_TERMINAL_REPORTED=1
2964
+ report_rainskills_v2_event "agent_config_result" "success" "$TARGET"
2965
+ record_rainskills_v2_configured_agent "$TARGET"
2966
+ report_rainskills_v2_event "install_result" "success"
2632
2967
  report_rainskills_lifecycle_event "configure_cli" "install_cli" "configure_cli" "completed"
2633
2968
  report_rainskills_installation "configured" "success"
2634
2969
 
@@ -2690,16 +3025,28 @@ main() {
2690
3025
  done
2691
3026
 
2692
3027
  local destinations=()
3028
+ local target_agents=()
2693
3029
  local dest
2694
3030
  while IFS= read -r dest; do
2695
3031
  destinations+=("$dest")
2696
3032
  done < <(collect_destinations)
2697
3033
 
2698
- for dest in "${destinations[@]}"; do
3034
+ local target_agent
3035
+ while IFS= read -r target_agent; do
3036
+ target_agents+=("$target_agent")
3037
+ done < <(collect_target_agents)
3038
+
3039
+ local destination_index
3040
+ for destination_index in "${!destinations[@]}"; do
3041
+ dest="${destinations[$destination_index]}"
3042
+ RAINSKILLS_V2_CURRENT_AGENT="${target_agents[$destination_index]:-unknown}"
2699
3043
  install_detail_log "安装到:$dest"
2700
3044
  for skill_dir in "${skills[@]}"; do
2701
3045
  copy_skill "$skill_dir" "$dest"
2702
3046
  done
3047
+ report_rainskills_v2_event "agent_config_result" "success" "$RAINSKILLS_V2_CURRENT_AGENT"
3048
+ record_rainskills_v2_configured_agent "$RAINSKILLS_V2_CURRENT_AGENT"
3049
+ RAINSKILLS_V2_CURRENT_AGENT=""
2703
3050
  done
2704
3051
 
2705
3052
  install_local_cli
@@ -2708,6 +3055,9 @@ main() {
2708
3055
  install_detail_log "安装完成。本次:${INSTALL_COUNT_NEW} 项新装 / ${INSTALL_COUNT_UPDATED} 项已更新 / ${INSTALL_COUNT_UNCHANGED} 项已是最新 / ${INSTALL_COUNT_FORCED} 项强制覆盖"
2709
3056
  install_detail_log ""
2710
3057
  print_capability_summary
3058
+ RAINSKILLS_INSTALL_TERMINAL_REPORTED=1
3059
+ RAINSKILLS_V2_TERMINAL_REPORTED=1
3060
+ report_rainskills_v2_event "install_result" "success"
2711
3061
  }
2712
3062
 
2713
3063
  if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rainskills",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "Install the complete Rainskills AI deployment skill suite as one product.",
5
5
  "author": {
6
6
  "name": "Goodrain",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rainskills",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "Install the complete Rainskills AI deployment skill suite as one product.",
5
5
  "author": {
6
6
  "name": "Goodrain",
@@ -14,7 +14,7 @@ This is the single marketplace entry for the complete Rainskills product. The in
14
14
  3. Keep stdin, stdout, and stderr attached. When `RAINSKILLS_USER_INPUT_REQUIRED` appears, pause for that installer choice. If the installer emits `rainskills.next-action.v1`, execute only its fixed `argv` through the same launcher; never evaluate output as a shell command. If the adjacent `bin/rainskills.js` exists, use it for fixed next actions; otherwise use the same versioned npm package fallback described below.
15
15
  4. Stay attached until every independent Skill is installed. Do not select, connect, or configure an application runtime during installation. In the user-facing response, output only the fixed completion message below.
16
16
 
17
- If the adjacent installer is missing, check the local Node.js version before choosing the fallback. With `npx` and Node.js 18 or newer, use `npx --yes rainskills@0.1.29 <target>`. With no Node.js or a version below 18, use `bash <(curl -fsSL https://get.rainbond.com/rainskills/install.sh) <target>` instead. Omit `<target>` only when the host cannot be determined reliably. Keep either command attached to the interactive terminal. For an update or repair, refresh this marketplace Skill first, then run the installer again; it compares and updates every independent internal Skill.
17
+ If the adjacent installer is missing, check the local Node.js version before choosing the fallback. With `npx` and Node.js 18 or newer, use `npx --yes rainskills@0.1.30 <target>`. With no Node.js or a version below 18, use `bash <(curl -fsSL https://get.rainbond.com/rainskills/install.sh) <target>` instead. Omit `<target>` only when the host cannot be determined reliably. Keep either command attached to the interactive terminal. For an update or repair, refresh this marketplace Skill first, then run the installer again; it compares and updates every independent internal Skill.
18
18
 
19
19
  Skills-only 安装不需要 Node.js;CDN fallback 只负责安装 Skill 文件,不代表运行环境连接、应用部署或平台安装已经可执行。用户首次提出需要运行环境的动作时,对应业务 Skill 才检查 Node.js;固定 Rainskills launcher 需要 Node.js 18 或更高版本。缺失或版本过低时保留原始 intent 并停止,等待用户或 agent 明确同意安装或升级 Node.js,安装完成消息不得提前提示 Node.js。
20
20
 
@@ -50,7 +50,7 @@ Codex 中命令工具一旦返回 `session_id`,必须立即对该 `session_id`
50
50
 
51
51
  DeepSeek Harness 和 WorkBuddy 也必须保持原命令附着直到退出;如果宿主把长命令转成持久终端或后台任务,只能轮询该原始命令句柄,不得另起 `runtime status` 推测授权结果。WorkBuddy 安装后若当前任务的 Skill 清单尚未刷新,在下一条业务动作前先刷新 Skill 列表或新建任务,不得退回内置 Sites 代替 Rainbond 部署。
52
52
 
53
- Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.29`):
53
+ Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.30`):
54
54
 
55
55
  - 状态:执行 `runtime status --json`。
56
56
  - 首次连接:执行 `runtime connect <target> --saas` 或 `runtime connect <target> --rainbond-url <Console origin>`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rainskills",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "Interactive Rainbond skill installer for Codex, Claude Code, Pi Agent, DeepSeek Harness, and WorkBuddy",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/goodrain/rainskills#readme",
@@ -22,7 +22,7 @@ Codex 中命令工具一旦返回 `session_id`,必须立即对该 `session_id`
22
22
  ```json
23
23
  {
24
24
  "schema": "rainskills.single-runtime-contract.v1",
25
- "package_version": "rainskills@0.1.29",
25
+ "package_version": "rainskills@0.1.30",
26
26
  "runtime_status": [
27
27
  "node",
28
28
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -34,7 +34,7 @@ Codex 中命令工具一旦返回 `session_id`,必须立即对该 `session_id`
34
34
  ```json
35
35
  {
36
36
  "schema": "rainskills.single-runtime-contract.v1",
37
- "package_version": "rainskills@0.1.29",
37
+ "package_version": "rainskills@0.1.30",
38
38
  "runtime_status": [
39
39
  "node",
40
40
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -57,7 +57,7 @@ Codex 中命令工具一旦返回 `session_id`,必须立即对该 `session_id`
57
57
  ```json
58
58
  {
59
59
  "schema": "rainskills.single-runtime-contract.v1",
60
- "package_version": "rainskills@0.1.29",
60
+ "package_version": "rainskills@0.1.30",
61
61
  "runtime_status": [
62
62
  "node",
63
63
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -34,7 +34,7 @@ Codex 中命令工具一旦返回 `session_id`,必须立即对该 `session_id`
34
34
  ```json
35
35
  {
36
36
  "schema": "rainskills.single-runtime-contract.v1",
37
- "package_version": "rainskills@0.1.29",
37
+ "package_version": "rainskills@0.1.30",
38
38
  "runtime_status": [
39
39
  "node",
40
40
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -34,7 +34,7 @@ Codex 中命令工具一旦返回 `session_id`,必须立即对该 `session_id`
34
34
  ```json
35
35
  {
36
36
  "schema": "rainskills.single-runtime-contract.v1",
37
- "package_version": "rainskills@0.1.29",
37
+ "package_version": "rainskills@0.1.30",
38
38
  "runtime_status": [
39
39
  "node",
40
40
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -57,7 +57,7 @@ Codex 中命令工具一旦返回 `session_id`,必须立即对该 `session_id`
57
57
  ```json
58
58
  {
59
59
  "schema": "rainskills.single-runtime-contract.v1",
60
- "package_version": "rainskills@0.1.29",
60
+ "package_version": "rainskills@0.1.30",
61
61
  "runtime_status": [
62
62
  "node",
63
63
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -22,7 +22,7 @@ Codex 中命令工具一旦返回 `session_id`,必须立即对该 `session_id`
22
22
  ```json
23
23
  {
24
24
  "schema": "rainskills.single-runtime-contract.v1",
25
- "package_version": "rainskills@0.1.29",
25
+ "package_version": "rainskills@0.1.30",
26
26
  "runtime_status": [
27
27
  "node",
28
28
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -32,7 +32,7 @@ Do not use it to deploy an application to an existing Rainbond. Route those requ
32
32
  ## Workflow
33
33
 
34
34
  1. Read [installation-policy.md](references/installation-policy.md).
35
- 2. Use the installed local launcher `["node", "<home>/.rainbond/lib/rainskills/bin/rainskills.js"]`; its protected runtime package marker is `rainskills@0.1.29` and must equal this package's `package.json`. For a Rainskills marker, first validate schema `rainskills.next-action.v1`, action, onboarding id, and the bounded `argv` array, then append that array to the launcher. Never use `latest` or evaluate a shell string from output.
35
+ 2. Use the installed local launcher `["node", "<home>/.rainbond/lib/rainskills/bin/rainskills.js"]`; its protected runtime package marker is `rainskills@0.1.30` and must equal this package's `package.json`. For a Rainskills marker, first validate schema `rainskills.next-action.v1`, action, onboarding id, and the bounded `argv` array, then append that array to the launcher. Never use `latest` or evaluate a shell string from output.
36
36
  3. 业务 Skill 的四项运行环境菜单会把本机或独立服务器选择写入 `rainskills.next-action.v1` 的显式 `--location`;收到这类 next-action 后直接执行固定 argv,不得再次调用 `private-deployment-location`。只有用户直接要求安装 Rainbond 平台且尚未选择部署位置时,才执行 launcher + `["runtime", "message", "--id", "private-deployment-location"]` 并原样输出固定的三项部署位置消息:选择 1 后执行带 `["--location", "local", "--mode", "single-node"]` 的 `platform install`;选择 2 后执行带 `["--location", "server"]` 的 `platform install`,由 helper 继续显示固定的服务器类型消息;选择 3 后执行 launcher + `["runtime", "message", "--id", "private-console-origin"]` 并进入已有环境连接,不得执行 `platform install`。平台安装 onboarding 只保存安装断点,不保存或恢复业务 intent。
37
37
  4. Let the helper perform one read-only preflight against the already selected local or remote target, then show resources, blockers, and applicable host changes. Never invoke `platform install` without an explicit `--location`; the helper must not ask for the deployment location again.
38
38
  5. 主机集群开始前必须获得 explicit confirmation,并使用受限 AI 交接:首次调用在固定安装 argv 后追加 `--agent-handoff`,记录该子进程会话;用户确认后,使用相同固定 argv 追加 `--agent-handoff --yes` 一次。不得向等待进程写入 `y`、不得启动第二个竞争安装、不得 `kill` 安装进程,也不得让用户复制完整安装或恢复命令。用户取消时,仅使用同一 argv 追加 `--agent-handoff --cancel`;它只能清除匹配的待确认状态,不能建立 SSH 连接或修改服务器。
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
 
3
3
  module.exports = Object.freeze({
4
- version: "0.1.29",
4
+ version: "0.1.30",
5
5
  });
@@ -0,0 +1,285 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+ const { randomUUID: defaultRandomUUID } = require("node:crypto");
7
+
8
+ const SCHEMA = "rainskills.telemetry-event.v2";
9
+ const DEFAULT_REPORT_URL = "https://log.rainbond.com/api/rainskills/events";
10
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
11
+ const AGENTS = new Set(["codex", "pi", "claude_code", "deepseek", "workbuddy", "other", "unknown"]);
12
+ const MAX_PENDING_EVENTS = 100;
13
+ const MAX_PENDING_AGE_MS = 7 * 24 * 60 * 60 * 1000;
14
+
15
+ function defaultDirectory() {
16
+ return path.join(os.homedir(), ".rainbond", "rainskills", "telemetry");
17
+ }
18
+
19
+ function safeMkdir(directory) {
20
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
21
+ try { fs.chmodSync(directory, 0o700); } catch { /* best effort */ }
22
+ }
23
+
24
+ function readPrivateText(file) {
25
+ try {
26
+ const info = fs.lstatSync(file);
27
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) return null;
28
+ return fs.readFileSync(file, "utf8");
29
+ } catch (error) {
30
+ if (error.code === "ENOENT") return null;
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ function writePrivateFile(file, content) {
36
+ safeMkdir(path.dirname(file));
37
+ const temporary = path.join(
38
+ path.dirname(file),
39
+ `.${path.basename(file)}.${process.pid}.${defaultRandomUUID()}.tmp`
40
+ );
41
+ try {
42
+ fs.writeFileSync(temporary, content, { encoding: "utf8", flag: "wx", mode: 0o600 });
43
+ fs.renameSync(temporary, file);
44
+ fs.chmodSync(file, 0o600);
45
+ } finally {
46
+ try { fs.rmSync(temporary, { force: true }); } catch { /* keep original error */ }
47
+ }
48
+ }
49
+
50
+ function ensureInstallationId(directory = defaultDirectory(), randomUUID = defaultRandomUUID) {
51
+ const file = path.join(directory, "installation-id");
52
+ const existing = (readPrivateText(file) || "").trim();
53
+ if (UUID_PATTERN.test(existing)) return existing;
54
+ const generated = randomUUID();
55
+ if (!UUID_PATTERN.test(generated)) throw new Error("telemetry installation id must be a UUID");
56
+ writePrivateFile(file, `${generated}\n`);
57
+ return generated;
58
+ }
59
+
60
+ function normalizeAgent(value) {
61
+ const aliases = {
62
+ claude: "claude_code",
63
+ dsh: "deepseek",
64
+ deepseek_harness: "deepseek",
65
+ };
66
+ const normalized = aliases[value] || value;
67
+ return AGENTS.has(normalized) ? normalized : "unknown";
68
+ }
69
+
70
+ function readConfiguredAgent(directory = defaultDirectory()) {
71
+ try {
72
+ const raw = readPrivateText(path.join(directory, "configured-agents.json"));
73
+ const agents = JSON.parse(raw || "[]").map(normalizeAgent);
74
+ const unique = [...new Set(agents.filter((agent) => agent !== "unknown"))];
75
+ return unique.length === 1 ? unique[0] : "unknown";
76
+ } catch {
77
+ return "unknown";
78
+ }
79
+ }
80
+
81
+ function writeConfiguredAgents(directory, agents) {
82
+ let existing = [];
83
+ try {
84
+ existing = JSON.parse(readPrivateText(path.join(directory, "configured-agents.json")) || "[]");
85
+ } catch { /* replace invalid state with the validated targets */ }
86
+ const normalized = [...new Set([...existing, ...(agents || [])]
87
+ .map(normalizeAgent)
88
+ .filter((agent) => agent !== "unknown"))].sort();
89
+ writePrivateFile(path.join(directory, "configured-agents.json"), `${JSON.stringify(normalized)}\n`);
90
+ return normalized;
91
+ }
92
+
93
+ function loadState(directory) {
94
+ try {
95
+ const raw = readPrivateText(path.join(directory, "v2-state.json"));
96
+ const parsed = JSON.parse(raw || "{}");
97
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
98
+ } catch {
99
+ return {};
100
+ }
101
+ }
102
+
103
+ function saveState(directory, state) {
104
+ writePrivateFile(path.join(directory, "v2-state.json"), `${JSON.stringify(state)}\n`);
105
+ }
106
+
107
+ function pendingDirectory(directory) {
108
+ return path.join(directory, "pending-v2");
109
+ }
110
+
111
+ function prunePending(directory, nowMs) {
112
+ const pending = pendingDirectory(directory);
113
+ let entries;
114
+ try {
115
+ entries = fs.readdirSync(pending)
116
+ .filter((name) => name.endsWith(".json"))
117
+ .map((name) => {
118
+ const file = path.join(pending, name);
119
+ return { file, mtimeMs: fs.lstatSync(file).mtimeMs };
120
+ })
121
+ .sort((left, right) => left.mtimeMs - right.mtimeMs);
122
+ } catch (error) {
123
+ if (error.code === "ENOENT") return;
124
+ throw error;
125
+ }
126
+ const expired = entries.filter((entry) => nowMs - entry.mtimeMs > MAX_PENDING_AGE_MS);
127
+ const remaining = entries.filter((entry) => nowMs - entry.mtimeMs <= MAX_PENDING_AGE_MS);
128
+ const overflow = remaining.slice(0, Math.max(0, remaining.length - MAX_PENDING_EVENTS + 1));
129
+ for (const entry of [...expired, ...overflow]) {
130
+ try { fs.rmSync(entry.file, { force: true }); } catch { /* best effort */ }
131
+ }
132
+ }
133
+
134
+ function buildEvent({ installationId, packageVersion, agentType, randomUUID, now }, input) {
135
+ const event = {
136
+ schema: SCHEMA,
137
+ event_id: randomUUID(),
138
+ event_type: input.event_type,
139
+ installation_id: installationId,
140
+ package_version: packageVersion,
141
+ occurred_at: now().toISOString(),
142
+ };
143
+ const fields = [
144
+ "install_attempt_id", "action", "agent_type", "os_type", "os_arch",
145
+ "execution_environment", "status", "error_stage", "error_code",
146
+ ];
147
+ for (const field of fields) {
148
+ const usesDefaultAgent = ["first_use_result", "active_daily"].includes(input.event_type);
149
+ const value = field === "agent_type"
150
+ ? (input[field] || (usesDefaultAgent ? agentType : ""))
151
+ : input[field];
152
+ if (typeof value === "string" && value.length > 0) event[field] = value;
153
+ }
154
+ if (event.agent_type) event.agent_type = normalizeAgent(event.agent_type);
155
+ return event;
156
+ }
157
+
158
+ function createResultTelemetry({
159
+ directory = defaultDirectory(),
160
+ reportUrl = process.env.RAINSKILLS_TELEMETRY_REPORT_URL || DEFAULT_REPORT_URL,
161
+ fetchImpl = globalThis.fetch,
162
+ randomUUID = defaultRandomUUID,
163
+ now = () => new Date(),
164
+ timeoutMs = 1500,
165
+ installationId,
166
+ packageVersion = "unknown",
167
+ agentType,
168
+ disabled = process.env.RAINSKILLS_TELEMETRY_DISABLED === "1",
169
+ } = {}) {
170
+ const resolvedAgent = normalizeAgent(agentType || readConfiguredAgent(directory));
171
+ const resolvedInstallationId = installationId || (disabled ? null : ensureInstallationId(directory, randomUUID));
172
+
173
+ async function send(event, file) {
174
+ if (typeof fetchImpl !== "function" || !reportUrl) return false;
175
+ const controller = new AbortController();
176
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
177
+ try {
178
+ const response = await fetchImpl(reportUrl, {
179
+ method: "POST",
180
+ headers: {
181
+ accept: "application/json",
182
+ "content-type": "application/json",
183
+ "idempotency-key": event.event_id,
184
+ },
185
+ body: JSON.stringify(event),
186
+ signal: controller.signal,
187
+ });
188
+ const status = Number(response?.status || 0);
189
+ const shouldDiscard = Boolean(response?.ok)
190
+ || (status >= 400 && status < 500 && status !== 429);
191
+ if (shouldDiscard && file) {
192
+ try { fs.rmSync(file, { force: true }); } catch { /* best effort */ }
193
+ }
194
+ return Boolean(response?.ok);
195
+ } catch {
196
+ return false;
197
+ } finally {
198
+ clearTimeout(timer);
199
+ }
200
+ }
201
+
202
+ function skippedResult() {
203
+ return { event: null, delivery: Promise.resolve(false), recorded: false };
204
+ }
205
+
206
+ function record(input, onceKey = null) {
207
+ if (disabled) return skippedResult();
208
+ const state = loadState(directory);
209
+ if (onceKey && state[onceKey]) return skippedResult();
210
+
211
+ const event = buildEvent({
212
+ installationId: resolvedInstallationId,
213
+ packageVersion,
214
+ agentType: resolvedAgent,
215
+ randomUUID,
216
+ now,
217
+ }, input);
218
+ const pending = pendingDirectory(directory);
219
+ safeMkdir(pending);
220
+ prunePending(directory, now().getTime());
221
+ const file = path.join(pending, `${event.event_id}.json`);
222
+ writePrivateFile(file, `${JSON.stringify(event)}\n`);
223
+ if (onceKey) saveState(directory, { ...state, [onceKey]: event.event_id });
224
+ return { event, delivery: send(event, file), recorded: true };
225
+ }
226
+
227
+ function recordFirstUse(status, error = {}) {
228
+ if (!new Set(["success", "failed"]).has(status)) return skippedResult();
229
+ return record({
230
+ event_type: "first_use_result",
231
+ status,
232
+ ...(status === "failed" ? {
233
+ error_stage: error.error_stage,
234
+ error_code: error.error_code,
235
+ } : {}),
236
+ }, `first_use:${resolvedAgent}:${status}`);
237
+ }
238
+
239
+ function recordActiveDaily() {
240
+ const activeDate = now().toISOString().slice(0, 10);
241
+ return record({ event_type: "active_daily" }, `active:${resolvedAgent}:${activeDate}`);
242
+ }
243
+
244
+ async function flushPending(limit = MAX_PENDING_EVENTS) {
245
+ if (disabled) return;
246
+ let files;
247
+ try {
248
+ files = fs.readdirSync(pendingDirectory(directory))
249
+ .filter((name) => name.endsWith(".json"))
250
+ .sort()
251
+ .slice(0, Math.max(0, Math.min(limit, MAX_PENDING_EVENTS)))
252
+ .map((name) => path.join(pendingDirectory(directory), name));
253
+ } catch (error) {
254
+ if (error.code === "ENOENT") return;
255
+ throw error;
256
+ }
257
+ for (const file of files) {
258
+ try {
259
+ const event = JSON.parse(readPrivateText(file));
260
+ await send(event, file);
261
+ } catch {
262
+ try { fs.rmSync(file, { force: true }); } catch { /* best effort */ }
263
+ }
264
+ }
265
+ }
266
+
267
+ return {
268
+ installationId: resolvedInstallationId,
269
+ agentType: resolvedAgent,
270
+ record,
271
+ recordFirstUse,
272
+ recordActiveDaily,
273
+ flushPending,
274
+ };
275
+ }
276
+
277
+ module.exports = {
278
+ DEFAULT_REPORT_URL,
279
+ SCHEMA,
280
+ createResultTelemetry,
281
+ ensureInstallationId,
282
+ normalizeAgent,
283
+ readConfiguredAgent,
284
+ writeConfiguredAgents,
285
+ };
@@ -98,6 +98,7 @@ function createLifecycleTelemetry({
98
98
  randomUUID = crypto.randomUUID,
99
99
  now = () => new Date().toISOString(),
100
100
  timeoutMs = 1500,
101
+ enabled = process.env.RAINSKILLS_LEGACY_TELEMETRY_ENABLED === "1",
101
102
  } = {}) {
102
103
  let sequence = 0;
103
104
  const deliveries = new Set();
@@ -213,6 +214,7 @@ function createLifecycleTelemetry({
213
214
  }
214
215
 
215
216
  async function send(event) {
217
+ if (!enabled) return false;
216
218
  const result = await sendRequest(reportUrl, event);
217
219
  if (result.ok) return true;
218
220
  if (![400, 404, 415, 422].includes(result.status)) return false;
@@ -224,6 +226,7 @@ function createLifecycleTelemetry({
224
226
 
225
227
  function record(input = {}) {
226
228
  const event = buildEvent(input);
229
+ if (!enabled) return { event, delivery: Promise.resolve(false) };
227
230
  writeLocalEvent(directory, event);
228
231
  const delivery = send(event).catch(() => false);
229
232
  deliveries.add(delivery);
@@ -74,7 +74,6 @@ async function validateMcp({
74
74
  timedOut = true;
75
75
  controller.abort();
76
76
  }, timeoutMs);
77
- timeout.unref?.();
78
77
  try {
79
78
  const response = await fetchImpl(url, {
80
79
  method: "POST",
@@ -18,6 +18,11 @@ const {
18
18
  } = require("./windows-auth.js");
19
19
  const { validateMcp } = require("./windows-client-config.js");
20
20
  const { createLifecycleTelemetry } = require("./telemetry.js");
21
+ const {
22
+ createResultTelemetry,
23
+ normalizeAgent,
24
+ writeConfiguredAgents,
25
+ } = require("./result-telemetry.js");
21
26
  const {
22
27
  destinationsForHostTarget,
23
28
  isHostTarget,
@@ -619,21 +624,100 @@ async function main(argv, dependencies = {}) {
619
624
  const skills = discoverSkills(packageRoot);
620
625
  const logger = dependencies.logger || ((message) => stdout.write(`${message}\n`));
621
626
  const detailLogger = options.verbose ? logger : () => {};
622
- const counts = copySkills({
623
- skills,
624
- destinations,
625
- force: options.force,
626
- logger: detailLogger,
627
- });
628
- if (!options.customDest) {
629
- await (dependencies.installLocalCli || installLocalCli)({ packageRoot, home });
627
+ const telemetryDirectory = path.join(home, ".rainbond", "rainskills", "telemetry");
628
+ const packageVersion = (() => {
629
+ try {
630
+ const value = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version;
631
+ return typeof value === "string" && value ? value : "unknown";
632
+ } catch {
633
+ return "unknown";
634
+ }
635
+ })();
636
+ const installAttemptId = UUID_PATTERN.test(environment.RAINSKILLS_INSTALL_ATTEMPT_ID || "")
637
+ ? environment.RAINSKILLS_INSTALL_ATTEMPT_ID
638
+ : crypto.randomUUID();
639
+ const osArch = os.arch() === "x64" ? "amd64" : os.arch() === "arm64" ? "arm64" : "other";
640
+ const resultTelemetry = (() => {
641
+ try {
642
+ return (dependencies.resultTelemetryFactory || createResultTelemetry)({
643
+ directory: telemetryDirectory,
644
+ packageVersion,
645
+ agentType: "unknown",
646
+ disabled: environment.RAINSKILLS_TELEMETRY_DISABLED === "1",
647
+ installAttemptId,
648
+ osArch,
649
+ });
650
+ } catch {
651
+ return {
652
+ record: () => ({ recorded: false, delivery: Promise.resolve(false) }),
653
+ };
654
+ }
655
+ })();
656
+ const targets = options.customDest
657
+ ? ["unknown"]
658
+ : (target === "all" ? ["claude", "codex", "pi", "dsh", "workbuddy"] : [target]);
659
+ const deliveries = [];
660
+ try {
661
+ const counts = copySkills({
662
+ skills,
663
+ destinations,
664
+ force: options.force,
665
+ logger: detailLogger,
666
+ });
667
+ if (!options.customDest) {
668
+ await (dependencies.installLocalCli || installLocalCli)({ packageRoot, home });
669
+ }
670
+ for (const configuredTarget of targets) {
671
+ const agent = normalizeAgent(configuredTarget);
672
+ const result = resultTelemetry.record({
673
+ event_type: "agent_config_result",
674
+ install_attempt_id: installAttemptId,
675
+ action: "install",
676
+ agent_type: agent,
677
+ status: "success",
678
+ });
679
+ deliveries.push(result.delivery);
680
+ }
681
+ const installResult = resultTelemetry.record({
682
+ event_type: "install_result",
683
+ install_attempt_id: installAttemptId,
684
+ action: "install",
685
+ os_type: "windows",
686
+ os_arch: osArch,
687
+ execution_environment: "native",
688
+ status: "success",
689
+ });
690
+ deliveries.push(installResult.delivery);
691
+ if (environment.RAINSKILLS_TELEMETRY_DISABLED !== "1") {
692
+ try {
693
+ (dependencies.configuredAgentsWriter || writeConfiguredAgents)(
694
+ telemetryDirectory,
695
+ targets.map(normalizeAgent)
696
+ );
697
+ } catch { /* telemetry state must not block installation */ }
698
+ }
699
+ await Promise.allSettled(deliveries);
700
+ detailLogger("");
701
+ detailLogger(`安装完成。本次:${counts.installed} 项新装 / ${counts.updated} 项已更新 / ${counts.unchanged} 项已是最新 / ${counts.forced} 项强制覆盖`);
702
+ detailLogger("");
703
+ logger(CAPABILITY_SUMMARY);
704
+ logger(AGENT_SUMMARY_REQUIREMENT);
705
+ return { status: "skills-installed", counts };
706
+ } catch (error) {
707
+ const failure = resultTelemetry.record({
708
+ event_type: "install_result",
709
+ install_attempt_id: installAttemptId,
710
+ action: "install",
711
+ os_type: "windows",
712
+ os_arch: osArch,
713
+ execution_environment: "native",
714
+ status: "failed",
715
+ error_stage: "agent_configuration",
716
+ error_code: "agent_config_failed",
717
+ });
718
+ await failure.delivery.catch(() => false);
719
+ throw error;
630
720
  }
631
- detailLogger("");
632
- detailLogger(`安装完成。本次:${counts.installed} 项新装 / ${counts.updated} 项已更新 / ${counts.unchanged} 项已是最新 / ${counts.forced} 项强制覆盖`);
633
- detailLogger("");
634
- logger(CAPABILITY_SUMMARY);
635
- logger(AGENT_SUMMARY_REQUIREMENT);
636
- return { status: "skills-installed", counts };
637
721
  }
638
722
 
639
723
  module.exports = {
@@ -25,7 +25,7 @@ Codex 中命令工具一旦返回 `session_id`,必须立即对该 `session_id`
25
25
  ```json
26
26
  {
27
27
  "schema": "rainskills.single-runtime-contract.v1",
28
- "package_version": "rainskills@0.1.29",
28
+ "package_version": "rainskills@0.1.30",
29
29
  "runtime_status": [
30
30
  "node",
31
31
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -34,7 +34,7 @@ Codex 中命令工具一旦返回 `session_id`,必须立即对该 `session_id`
34
34
  ```json
35
35
  {
36
36
  "schema": "rainskills.single-runtime-contract.v1",
37
- "package_version": "rainskills@0.1.29",
37
+ "package_version": "rainskills@0.1.30",
38
38
  "runtime_status": [
39
39
  "node",
40
40
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -34,7 +34,7 @@ Codex 中命令工具一旦返回 `session_id`,必须立即对该 `session_id`
34
34
  ```json
35
35
  {
36
36
  "schema": "rainskills.single-runtime-contract.v1",
37
- "package_version": "rainskills@0.1.29",
37
+ "package_version": "rainskills@0.1.30",
38
38
  "runtime_status": [
39
39
  "node",
40
40
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",