idlewatch 0.1.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 (110) hide show
  1. package/.env.example +73 -0
  2. package/.github/workflows/ci.yml +99 -0
  3. package/.github/workflows/release-macos-trusted.yml +103 -0
  4. package/README.md +336 -0
  5. package/bin/idlewatch-agent.js +1053 -0
  6. package/docs/onboarding-external.md +58 -0
  7. package/docs/packaging/macos-dmg.md +199 -0
  8. package/docs/packaging/macos-launch-agent.md +70 -0
  9. package/docs/qa/archive/mac-qa-log-2026-02-17.md +5838 -0
  10. package/docs/qa/mac-qa-log.md +2864 -0
  11. package/docs/telemetry/idle-stale-policy.md +57 -0
  12. package/docs/telemetry/openclaw-mapping.md +80 -0
  13. package/package.json +76 -0
  14. package/scripts/build-dmg.sh +65 -0
  15. package/scripts/install-macos-launch-agent.sh +78 -0
  16. package/scripts/lib/telemetry-row-parser.mjs +100 -0
  17. package/scripts/package-macos.sh +228 -0
  18. package/scripts/uninstall-macos-launch-agent.sh +30 -0
  19. package/scripts/validate-all.sh +142 -0
  20. package/scripts/validate-bin.mjs +25 -0
  21. package/scripts/validate-dmg-checksum.sh +37 -0
  22. package/scripts/validate-dmg-install.sh +155 -0
  23. package/scripts/validate-dry-run-schema.mjs +257 -0
  24. package/scripts/validate-onboarding.mjs +63 -0
  25. package/scripts/validate-openclaw-cache-recovery-e2e.mjs +113 -0
  26. package/scripts/validate-openclaw-release-gates.mjs +51 -0
  27. package/scripts/validate-openclaw-stats-ingestion.mjs +372 -0
  28. package/scripts/validate-openclaw-usage-health.mjs +95 -0
  29. package/scripts/validate-packaged-artifact.mjs +233 -0
  30. package/scripts/validate-packaged-bundled-runtime.sh +191 -0
  31. package/scripts/validate-packaged-metadata.sh +43 -0
  32. package/scripts/validate-packaged-openclaw-cache-recovery-e2e.mjs +153 -0
  33. package/scripts/validate-packaged-openclaw-release-gates.mjs +72 -0
  34. package/scripts/validate-packaged-openclaw-stats-ingestion.mjs +402 -0
  35. package/scripts/validate-packaged-sourcemaps.mjs +82 -0
  36. package/scripts/validate-packaged-usage-alert-rate-e2e.mjs +98 -0
  37. package/scripts/validate-packaged-usage-probe-noise-e2e.mjs +87 -0
  38. package/scripts/validate-packaged-usage-recovery-e2e.mjs +90 -0
  39. package/scripts/validate-trusted-prereqs.sh +44 -0
  40. package/scripts/validate-usage-alert-rate-e2e.mjs +91 -0
  41. package/scripts/validate-usage-freshness-e2e.mjs +81 -0
  42. package/skill/SKILL.md +43 -0
  43. package/src/config.js +100 -0
  44. package/src/enrollment.js +176 -0
  45. package/src/gpu.js +115 -0
  46. package/src/memory.js +67 -0
  47. package/src/openclaw-cache.js +51 -0
  48. package/src/openclaw-usage.js +1020 -0
  49. package/src/telemetry-mapping.js +54 -0
  50. package/src/usage-alert.js +41 -0
  51. package/src/usage-freshness.js +31 -0
  52. package/test/config.test.mjs +112 -0
  53. package/test/fixtures/gpu-agx.txt +2 -0
  54. package/test/fixtures/gpu-iogpu.txt +2 -0
  55. package/test/fixtures/gpu-top-grep.txt +2 -0
  56. package/test/fixtures/openclaw-fleet-sample-v1.json +68 -0
  57. package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-iso-ts.txt +2 -0
  58. package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-newest.txt +2 -0
  59. package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-string-ts.txt +2 -0
  60. package/test/fixtures/openclaw-mixed-status-then-generic-output.txt +2 -0
  61. package/test/fixtures/openclaw-stats-current-wrapper.json +12 -0
  62. package/test/fixtures/openclaw-stats-current-wrapper2.json +15 -0
  63. package/test/fixtures/openclaw-stats-data-wrapper.json +21 -0
  64. package/test/fixtures/openclaw-stats-nested-session-wrapper.json +23 -0
  65. package/test/fixtures/openclaw-stats-payload-wrapper.json +1 -0
  66. package/test/fixtures/openclaw-stats-status-current-wrapper.json +19 -0
  67. package/test/fixtures/openclaw-stats.json +17 -0
  68. package/test/fixtures/openclaw-status-ansi-complex-noise.txt +3 -0
  69. package/test/fixtures/openclaw-status-ansi-noise.txt +2 -0
  70. package/test/fixtures/openclaw-status-control-noise.txt +1 -0
  71. package/test/fixtures/openclaw-status-data-wrapper.json +20 -0
  72. package/test/fixtures/openclaw-status-dcs-noise.txt +1 -0
  73. package/test/fixtures/openclaw-status-epoch-seconds.json +15 -0
  74. package/test/fixtures/openclaw-status-mixed-noise.txt +1 -0
  75. package/test/fixtures/openclaw-status-multi-json.txt +3 -0
  76. package/test/fixtures/openclaw-status-nested-recent.json +19 -0
  77. package/test/fixtures/openclaw-status-noisy-default-then-usage.txt +2 -0
  78. package/test/fixtures/openclaw-status-noisy.txt +3 -0
  79. package/test/fixtures/openclaw-status-osc-noise.txt +1 -0
  80. package/test/fixtures/openclaw-status-result-session.json +15 -0
  81. package/test/fixtures/openclaw-status-session-map-with-defaults.json +23 -0
  82. package/test/fixtures/openclaw-status-session-map.json +28 -0
  83. package/test/fixtures/openclaw-status-session-model-name.json +18 -0
  84. package/test/fixtures/openclaw-status-snake-session-wrapper.json +13 -0
  85. package/test/fixtures/openclaw-status-stats-current-sessions-snake-tokens.json +25 -0
  86. package/test/fixtures/openclaw-status-stats-current-sessions.json +28 -0
  87. package/test/fixtures/openclaw-status-stats-current-usage-time-camelcase.json +19 -0
  88. package/test/fixtures/openclaw-status-stats-session-default-model.json +27 -0
  89. package/test/fixtures/openclaw-status-status-wrapper.json +13 -0
  90. package/test/fixtures/openclaw-status-strings.json +38 -0
  91. package/test/fixtures/openclaw-status-ts-ms-alias.json +14 -0
  92. package/test/fixtures/openclaw-status-updated-at-ms-alias.json +14 -0
  93. package/test/fixtures/openclaw-status-usage-timestamp-ms-alias.json +14 -0
  94. package/test/fixtures/openclaw-status-usage-ts-alias.json +14 -0
  95. package/test/fixtures/openclaw-status-wrap-session-object.json +24 -0
  96. package/test/fixtures/openclaw-status.json +41 -0
  97. package/test/fixtures/openclaw-usage-model-name-generic.json +9 -0
  98. package/test/gpu.test.mjs +58 -0
  99. package/test/memory.test.mjs +35 -0
  100. package/test/openclaw-cache.test.mjs +48 -0
  101. package/test/openclaw-env.test.mjs +365 -0
  102. package/test/openclaw-usage.test.mjs +555 -0
  103. package/test/telemetry-mapping.test.mjs +69 -0
  104. package/test/telemetry-row-parser.test.mjs +44 -0
  105. package/test/usage-alert.test.mjs +73 -0
  106. package/test/usage-freshness.test.mjs +63 -0
  107. package/test/validate-dry-run-schema.test.mjs +146 -0
  108. package/tui/Cargo.lock +801 -0
  109. package/tui/Cargo.toml +11 -0
  110. package/tui/src/main.rs +368 -0
@@ -0,0 +1,57 @@
1
+ # OpenClaw Usage Staleness & Idle Policy
2
+
3
+ ## Overview
4
+
5
+ When IdleWatch monitors OpenClaw usage via `openclaw status --json`, the
6
+ `openclawUsageAgeMs` field reflects how long ago the most recent session
7
+ activity occurred. During periods of user inactivity (no prompts, no agent
8
+ turns), this age grows continuously.
9
+
10
+ ## Expected behavior during idle
11
+
12
+ | Condition | `usageFreshnessState` | `usageAlertLevel` | `usageAlertReason` |
13
+ |---|---|---|---|
14
+ | Age < `IDLEWATCH_USAGE_NEAR_STALE_MS` | `fresh` | `ok` | `healthy` |
15
+ | Age ≥ near-stale threshold | `fresh` (aging) | `ok` | `healthy` |
16
+ | Age ≥ `IDLEWATCH_USAGE_STALE_MS` + grace | `stale` | `warning` | `activity-past-threshold` |
17
+ | Age ≥ `IDLEWATCH_USAGE_IDLE_AFTER_MS` (6h default) | `stale` | `idle` | `idle-expected` |
18
+
19
+ ## Why stale/warning is normal during idle
20
+
21
+ OpenClaw's `status --json` reports the timestamp of the **last session
22
+ activity**. If no one is using the assistant, the age naturally grows past the
23
+ stale threshold (default 60s + 10s grace). This does **not** indicate a
24
+ monitoring failure — it indicates the assistant is simply idle.
25
+
26
+ The `warning` alert level means "usage data is stale relative to the
27
+ freshness SLO" — it is an **informational signal**, not an error. Operators
28
+ should treat sustained `warning` during known idle windows (nights,
29
+ weekends) as normal.
30
+
31
+ ## When to investigate
32
+
33
+ - `warning` persisting during **active** usage sessions (the assistant is
34
+ being used but IdleWatch still reports stale) → probe or parse failure.
35
+ - `usageIngestionStatus` is not `ok` → the probe itself is broken.
36
+ - `usageProbeResult` shows `command-error` or `parse-error` → OpenClaw CLI
37
+ issue.
38
+
39
+ ## Thresholds
40
+
41
+ | Env var | Default | Description |
42
+ |---|---|---|
43
+ | `IDLEWATCH_USAGE_STALE_MS` | `max(interval×3, 60000)` | Age beyond which usage is stale |
44
+ | `IDLEWATCH_USAGE_NEAR_STALE_MS` | `floor((stale+grace)×0.85)` | Pre-stale aging signal |
45
+ | `IDLEWATCH_USAGE_STALE_GRACE_MS` | `min(interval, 10000)` | Grace window before stale alert |
46
+ | `IDLEWATCH_USAGE_IDLE_AFTER_MS` | `21600000` (6h) | Downgrade stale→idle after this age |
47
+ | `IDLEWATCH_MAX_OPENCLAW_USAGE_AGE_MS` | `600000` (10m) | SLO validator threshold |
48
+
49
+ ## Dashboard guidance
50
+
51
+ - Filter alerts: suppress `warning` when `usageAlertReason` is
52
+ `activity-past-threshold` during off-hours.
53
+ - Use `idle` alert level (after 6h) as a positive signal that the host is
54
+ inactive rather than broken.
55
+ - Key reliability signals: `usageIngestionStatus` and `usageProbeResult`.
56
+ If both are `ok`, the monitoring pipeline is healthy regardless of
57
+ freshness state.
@@ -0,0 +1,80 @@
1
+ # OpenClaw Telemetry Mapping for Fleet Aggregation
2
+
3
+ ## Purpose
4
+
5
+ This spec defines a stable, versioned OpenClaw-focused telemetry envelope for fleet dashboards while preserving the existing flat IdleWatch row shape.
6
+
7
+ ## Goals
8
+
9
+ - Keep existing flat fields fully backward compatible.
10
+ - Add a canonical `fleet` object for aggregation across hosts.
11
+ - Carry explicit provenance so operators can trust/triage usage data.
12
+ - Version the schema to support additive evolution.
13
+
14
+ ## Schema contract (v1)
15
+
16
+ - `schemaFamily`: `idlewatch.openclaw.fleet`
17
+ - `schemaVersion`: `1.0.0`
18
+ - `schemaCompat`: `["0.x-flat-row"]`
19
+
20
+ ### Backward compatibility
21
+
22
+ All prior top-level fields are retained (`cpuPct`, `memPct`, `tokensPerMin`, `openclaw*`, `source.*`, etc.).
23
+ Consumers using old selectors continue to work.
24
+
25
+ ## Canonical mapping
26
+
27
+ ### Fleet identity
28
+
29
+ - `fleet.host` ← `host`
30
+ - `fleet.collectedAtMs` ← `ts`
31
+
32
+ ### Fleet resources (high-value host load)
33
+
34
+ - `fleet.resources.cpuPct` ← `cpuPct`
35
+ - `fleet.resources.memUsedPct` ← `memUsedPct` (fallback `memPct`)
36
+ - `fleet.resources.memPressurePct` ← `memPressurePct`
37
+ - `fleet.resources.memPressureClass` ← `memPressureClass`
38
+
39
+ ### Fleet usage (OpenClaw)
40
+
41
+ - `fleet.usage.model` ← `openclawModel`
42
+ - `fleet.usage.totalTokens` ← `openclawTotalTokens`
43
+ - `fleet.usage.tokensPerMin` ← `tokensPerMin`
44
+ - `fleet.usage.sessionId` ← `openclawSessionId`
45
+ - `fleet.usage.agentId` ← `openclawAgentId`
46
+ - `fleet.usage.usageTimestampMs` ← `openclawUsageTs`
47
+ - `fleet.usage.usageAgeMs` ← `openclawUsageAgeMs`
48
+ - `fleet.usage.freshnessState` ← `source.usageFreshnessState`
49
+ - `fleet.usage.integrationStatus` ← `source.usageIntegrationStatus`
50
+ - `fleet.usage.ingestionStatus` ← `source.usageIngestionStatus`
51
+ - `fleet.usage.activityStatus` ← `source.usageActivityStatus`
52
+ - `fleet.usage.alertLevel` ← `source.usageAlertLevel`
53
+ - `fleet.usage.alertReason` ← `source.usageAlertReason`
54
+
55
+ ### Provenance
56
+
57
+ - `fleet.provenance.collector` ← static `idlewatch-agent`
58
+ - `fleet.provenance.collectorVersion` ← package version
59
+ - `fleet.provenance.usageSource` ← `source.usage`
60
+ - `fleet.provenance.usageCommand` ← `source.usageCommand`
61
+ - `fleet.provenance.usageProbeResult` ← `source.usageProbeResult`
62
+ - `fleet.provenance.usageProbeAttempts` ← `source.usageProbeAttempts`
63
+ - `fleet.provenance.usageUsedFallbackCache` ← `source.usageUsedFallbackCache`
64
+ - `fleet.provenance.usageFallbackCacheSource` ← `source.usageFallbackCacheSource`
65
+
66
+ ## Enrichment rollout plan
67
+
68
+ 1. Emit `schemaFamily/schemaVersion/schemaCompat` + `fleet` object (additive only).
69
+ 2. Keep legacy flat fields indefinitely during migration.
70
+ 3. Dashboards/alerts migrate to `fleet.*` paths by domain:
71
+ - capacity: `fleet.resources.*`
72
+ - usage: `fleet.usage.*`
73
+ - reliability: `fleet.provenance.*` + `fleet.usage.ingestionStatus`
74
+ 4. Future changes are additive under v1; breaking path changes require v2.
75
+
76
+ ## Fixtures and tests
77
+
78
+ - Fixture sample: `test/fixtures/openclaw-fleet-sample-v1.json`
79
+ - Mapping unit tests: `test/telemetry-mapping.test.mjs`
80
+ - Dry-run schema gate includes v1 checks: `scripts/validate-dry-run-schema.mjs`
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "idlewatch",
3
+ "version": "0.1.0",
4
+ "description": "Host telemetry collector for IdleWatch",
5
+ "type": "module",
6
+ "bin": {
7
+ "idlewatch": "bin/idlewatch-agent.js",
8
+ "idlewatch-agent": "bin/idlewatch-agent.js",
9
+ "idlewatch-skill": "bin/idlewatch-agent.js"
10
+ },
11
+ "scripts": {
12
+ "start": "node bin/idlewatch-agent.js",
13
+ "quickstart": "node bin/idlewatch-agent.js quickstart",
14
+ "validate:bin": "node scripts/validate-bin.mjs",
15
+ "validate:onboarding": "node scripts/validate-onboarding.mjs",
16
+ "test:unit": "node --test 'test/*.test.mjs'",
17
+ "smoke:help": "node bin/idlewatch-agent.js --help",
18
+ "smoke:dry-run": "node bin/idlewatch-agent.js --dry-run",
19
+ "smoke:once": "IDLEWATCH_OPENCLAW_USAGE=off node bin/idlewatch-agent.js --once",
20
+ "validate:dry-run-schema": "node scripts/validate-dry-run-schema.mjs node bin/idlewatch-agent.js --dry-run",
21
+ "validate:packaged-dry-run-schema": "[ \"${IDLEWATCH_SKIP_PACKAGE_MACOS:-}\" = \"1\" ] || npm run package:macos --silent && node scripts/validate-dry-run-schema.mjs ./dist/IdleWatch.app/Contents/MacOS/IdleWatch --dry-run",
22
+ "validate:packaged-dry-run-schema:reuse-artifact": "npm run validate:packaged-artifact --silent && IDLEWATCH_SKIP_PACKAGE_MACOS=1 npm run validate:packaged-dry-run-schema --silent",
23
+ "validate:packaged-usage-health": "[ \"${IDLEWATCH_SKIP_PACKAGE_MACOS:-}\" = \"1\" ] || npm run package:macos --silent && IDLEWATCH_REQUIRE_OPENCLAW_USAGE=1 node scripts/validate-dry-run-schema.mjs ./dist/IdleWatch.app/Contents/MacOS/IdleWatch --dry-run",
24
+ "validate:packaged-usage-health:reuse-artifact": "npm run validate:packaged-artifact --silent && IDLEWATCH_SKIP_PACKAGE_MACOS=1 npm run validate:packaged-usage-health --silent",
25
+ "validate:packaged-usage-age-slo": "[ \"${IDLEWATCH_SKIP_PACKAGE_MACOS:-}\" = \"1\" ] || npm run package:macos --silent && IDLEWATCH_REQUIRE_OPENCLAW_USAGE=1 IDLEWATCH_MAX_OPENCLAW_USAGE_AGE_MS=600000 node scripts/validate-dry-run-schema.mjs ./dist/IdleWatch.app/Contents/MacOS/IdleWatch --dry-run",
26
+ "validate:packaged-usage-age-slo:reuse-artifact": "npm run validate:packaged-artifact --silent && IDLEWATCH_SKIP_PACKAGE_MACOS=1 npm run validate:packaged-usage-age-slo --silent",
27
+ "validate:firebase-emulator-mode": "FIREBASE_PROJECT_ID=idlewatch-dev FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 IDLEWATCH_OPENCLAW_USAGE=off node bin/idlewatch-agent.js --dry-run",
28
+ "validate:firebase-write-once": "IDLEWATCH_OPENCLAW_USAGE=off node bin/idlewatch-agent.js --once",
29
+ "validate:firebase-write-required-once": "IDLEWATCH_REQUIRE_FIREBASE_WRITES=1 IDLEWATCH_OPENCLAW_USAGE=off node bin/idlewatch-agent.js --once",
30
+ "validate:dmg-install": "./scripts/validate-dmg-install.sh",
31
+ "validate:packaged-bundled-runtime": "./scripts/validate-packaged-bundled-runtime.sh",
32
+ "validate:packaged-bundled-runtime:reuse-artifact": "IDLEWATCH_SKIP_PACKAGE_MACOS=1 npm run validate:packaged-bundled-runtime --silent",
33
+ "validate:dmg-checksum": "./scripts/validate-dmg-checksum.sh",
34
+ "validate:usage-freshness-e2e": "node scripts/validate-usage-freshness-e2e.mjs",
35
+ "validate:usage-alert-rate-e2e": "node scripts/validate-usage-alert-rate-e2e.mjs",
36
+ "validate:packaged-usage-recovery-e2e": "node scripts/validate-packaged-usage-recovery-e2e.mjs",
37
+ "validate:packaged-usage-alert-rate-e2e": "node scripts/validate-packaged-usage-alert-rate-e2e.mjs",
38
+ "validate:openclaw-cache-recovery-e2e": "node scripts/validate-openclaw-cache-recovery-e2e.mjs",
39
+ "validate:openclaw-stats-ingestion": "node scripts/validate-openclaw-stats-ingestion.mjs",
40
+ "validate:openclaw-usage-health": "node scripts/validate-openclaw-usage-health.mjs",
41
+ "validate:packaged-usage-probe-noise-e2e": "node scripts/validate-packaged-usage-probe-noise-e2e.mjs",
42
+ "validate:packaged-openclaw-stats-ingestion": "node scripts/validate-packaged-openclaw-stats-ingestion.mjs",
43
+ "validate:packaged-openclaw-cache-recovery-e2e": "node scripts/validate-packaged-openclaw-cache-recovery-e2e.mjs",
44
+ "validate:trusted-prereqs": "./scripts/validate-trusted-prereqs.sh",
45
+ "package:macos": "./scripts/package-macos.sh",
46
+ "package:dmg": "./scripts/build-dmg.sh",
47
+ "validate:packaged-metadata": "./scripts/validate-packaged-metadata.sh",
48
+ "package:trusted": "IDLEWATCH_REQUIRE_TRUSTED_DISTRIBUTION=1 npm run validate:trusted-prereqs --silent && IDLEWATCH_REQUIRE_TRUSTED_DISTRIBUTION=1 ./scripts/package-macos.sh && IDLEWATCH_REQUIRE_TRUSTED_DISTRIBUTION=1 ./scripts/build-dmg.sh",
49
+ "package:release": "npm run package:trusted && npm run validate:dmg-checksum",
50
+ "install:macos-launch-agent": "./scripts/install-macos-launch-agent.sh",
51
+ "uninstall:macos-launch-agent": "./scripts/uninstall-macos-launch-agent.sh",
52
+ "test": "npm run validate:bin && npm run test:unit && npm run smoke:help && npm run smoke:dry-run && npm run smoke:once",
53
+ "validate:all": "./scripts/validate-all.sh",
54
+ "validate:all:quick": "./scripts/validate-all.sh --skip-packaging",
55
+ "validate:packaged-openclaw-release-gates": "node scripts/validate-packaged-openclaw-release-gates.mjs",
56
+ "validate:openclaw-release-gates": "node scripts/validate-openclaw-release-gates.mjs",
57
+ "validate:openclaw-release-gates:all": "npm run validate:openclaw-release-gates --silent && if [ \"$(uname -s)\" = \"Darwin\" ]; then npm run validate:packaged-openclaw-release-gates:reuse-artifact --silent; fi",
58
+ "validate:packaged-openclaw-release-gates:all": "npm run validate:packaged-openclaw-release-gates --silent && npm run validate:packaged-openclaw-release-gates:reuse-artifact --silent",
59
+ "validate:packaged-openclaw-robustness": "npm run validate:packaged-usage-age-slo --silent && npm run validate:packaged-usage-alert-rate-e2e --silent && npm run validate:packaged-usage-probe-noise-e2e --silent && npm run validate:packaged-openclaw-release-gates --silent",
60
+ "validate:packaged-openclaw-robustness:reuse-artifact": "npm run validate:packaged-artifact --silent && IDLEWATCH_SKIP_PACKAGE_MACOS=1 npm run validate:packaged-usage-age-slo --silent && IDLEWATCH_SKIP_PACKAGE_MACOS=1 npm run validate:packaged-usage-alert-rate-e2e --silent && IDLEWATCH_SKIP_PACKAGE_MACOS=1 npm run validate:packaged-usage-probe-noise-e2e --silent && IDLEWATCH_SKIP_PACKAGE_MACOS=1 npm run validate:packaged-openclaw-release-gates --silent",
61
+ "validate:packaged-openclaw-release-gates:reuse-artifact": "npm run validate:packaged-artifact --silent && IDLEWATCH_SKIP_PACKAGE_MACOS=1 npm run validate:packaged-openclaw-release-gates --silent",
62
+ "validate:packaged-openclaw-cache-recovery-e2e:reuse-artifact": "npm run validate:packaged-artifact --silent && IDLEWATCH_SKIP_PACKAGE_MACOS=1 npm run validate:packaged-openclaw-cache-recovery-e2e --silent",
63
+ "validate:packaged-openclaw-stats-ingestion:reuse-artifact": "npm run validate:packaged-artifact --silent && IDLEWATCH_SKIP_PACKAGE_MACOS=1 npm run validate:packaged-openclaw-stats-ingestion --silent",
64
+ "validate:packaged-usage-recovery-e2e:reuse-artifact": "npm run validate:packaged-artifact --silent && IDLEWATCH_SKIP_PACKAGE_MACOS=1 npm run validate:packaged-usage-recovery-e2e --silent",
65
+ "validate:packaged-usage-probe-noise-e2e:reuse-artifact": "npm run validate:packaged-artifact --silent && IDLEWATCH_SKIP_PACKAGE_MACOS=1 npm run validate:packaged-usage-probe-noise-e2e --silent",
66
+ "validate:packaged-usage-alert-rate-e2e:reuse-artifact": "npm run validate:packaged-artifact --silent && IDLEWATCH_SKIP_PACKAGE_MACOS=1 npm run validate:packaged-usage-alert-rate-e2e --silent",
67
+ "validate:openclaw-release-gates:reuse-artifact": "IDLEWATCH_SKIP_PACKAGE_MACOS=1 npm run validate:openclaw-release-gates --silent",
68
+ "validate:release-gate-all": "npm run validate:openclaw-release-gates:all --silent && if [ \"$(uname -s)\" = \"Darwin\" ]; then npm run validate:packaged-openclaw-robustness --silent; fi",
69
+ "validate:release-gate": "npm run validate:openclaw-release-gates --silent && if [ \"$(uname -s)\" = \"Darwin\" ]; then npm run validate:packaged-openclaw-robustness:reuse-artifact --silent; fi",
70
+ "validate:packaged-artifact": "node scripts/validate-packaged-artifact.mjs",
71
+ "validate:packaged-artifact:bundled-runtime": "IDLEWATCH_REQUIRE_NODE_RUNTIME_BUNDLED=1 npm run validate:packaged-artifact --silent"
72
+ },
73
+ "dependencies": {
74
+ "firebase-admin": "^12.7.0"
75
+ }
76
+ }
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ DIST_DIR="$ROOT_DIR/dist"
6
+ DMG_ROOT="$DIST_DIR/dmg-root"
7
+ VERSION="$(node -p "require('./package.json').version" 2>/dev/null || node -e "import('./package.json',{with:{type:'json'}}).then(m=>console.log(m.default.version))")"
8
+ NOTARY_PROFILE="${MACOS_NOTARY_PROFILE:-}"
9
+ REQUIRE_TRUSTED="${IDLEWATCH_REQUIRE_TRUSTED_DISTRIBUTION:-0}"
10
+ ALLOW_UNSIGNED_TAG_RELEASE="${IDLEWATCH_ALLOW_UNSIGNED_TAG_RELEASE:-0}"
11
+ if [[ "$REQUIRE_TRUSTED" != "1" && "${GITHUB_ACTIONS:-}" == "true" ]]; then
12
+ REF_NAME="${GITHUB_REF:-}"
13
+ REF_TYPE="${GITHUB_REF_TYPE:-}"
14
+ if [[ "$REF_TYPE" == "tag" || "$REF_NAME" == refs/tags/* ]]; then
15
+ if [[ "$ALLOW_UNSIGNED_TAG_RELEASE" != "1" ]]; then
16
+ REQUIRE_TRUSTED="1"
17
+ echo "Detected CI tag build; enforcing trusted distribution requirements (set IDLEWATCH_ALLOW_UNSIGNED_TAG_RELEASE=1 to bypass intentionally)."
18
+ fi
19
+ fi
20
+ fi
21
+ DMG_SUFFIX="unsigned"
22
+ if [[ -n "${MACOS_CODESIGN_IDENTITY:-}" ]]; then
23
+ DMG_SUFFIX="signed"
24
+ fi
25
+
26
+ if [[ "$REQUIRE_TRUSTED" == "1" ]]; then
27
+ if [[ "$DMG_SUFFIX" != "signed" ]]; then
28
+ echo "IDLEWATCH_REQUIRE_TRUSTED_DISTRIBUTION=1 requires MACOS_CODESIGN_IDENTITY to be set." >&2
29
+ exit 1
30
+ fi
31
+ if [[ -z "$NOTARY_PROFILE" ]]; then
32
+ echo "IDLEWATCH_REQUIRE_TRUSTED_DISTRIBUTION=1 requires MACOS_NOTARY_PROFILE to be set." >&2
33
+ exit 1
34
+ fi
35
+ fi
36
+ OUT_DMG="$DIST_DIR/IdleWatch-${VERSION}-${DMG_SUFFIX}.dmg"
37
+
38
+ if [[ ! -d "$DMG_ROOT" ]]; then
39
+ echo "Missing $DMG_ROOT. Run ./scripts/package-macos.sh first." >&2
40
+ exit 1
41
+ fi
42
+
43
+ rm -f "$OUT_DMG"
44
+
45
+ hdiutil create \
46
+ -volname "IdleWatch" \
47
+ -srcfolder "$DMG_ROOT" \
48
+ -ov \
49
+ -format UDZO \
50
+ "$OUT_DMG"
51
+
52
+ if command -v shasum >/dev/null 2>&1; then
53
+ shasum -a 256 "$OUT_DMG" > "$OUT_DMG.sha256"
54
+ echo "Wrote checksum: $OUT_DMG.sha256"
55
+ fi
56
+
57
+ if [[ -n "$NOTARY_PROFILE" ]]; then
58
+ echo "Submitting DMG for notarization with profile: $NOTARY_PROFILE"
59
+ xcrun notarytool submit "$OUT_DMG" --keychain-profile "$NOTARY_PROFILE" --wait
60
+ xcrun stapler staple "$OUT_DMG"
61
+ echo "Created notarized DMG: $OUT_DMG"
62
+ else
63
+ echo "Created DMG: $OUT_DMG"
64
+ echo "MACOS_NOTARY_PROFILE not set; skipped notarization/staple."
65
+ fi
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ APP_PATH="${IDLEWATCH_APP_PATH:-/Applications/IdleWatch.app}"
5
+ BIN_PATH="${IDLEWATCH_APP_BIN:-$APP_PATH/Contents/MacOS/IdleWatch}"
6
+ PLIST_LABEL="${IDLEWATCH_LAUNCH_AGENT_LABEL:-com.idlewatch.agent}"
7
+ PLIST_ROOT="${IDLEWATCH_LAUNCH_AGENT_PLIST_ROOT:-$HOME/Library/LaunchAgents}"
8
+ LOG_DIR="${IDLEWATCH_LAUNCH_AGENT_LOG_DIR:-$HOME/Library/Logs/IdleWatch}"
9
+ INTERVAL_MS="${IDLEWATCH_INTERVAL_MS:-10000}"
10
+ START_INTERVAL_SEC=$(( (INTERVAL_MS / 1000) ))
11
+ if [[ $START_INTERVAL_SEC -lt 60 ]]; then
12
+ START_INTERVAL_SEC=60
13
+ fi
14
+
15
+ PLIST_PATH="$PLIST_ROOT/$PLIST_LABEL.plist"
16
+
17
+ if [[ ! -d "$PLIST_ROOT" ]]; then
18
+ mkdir -p "$PLIST_ROOT"
19
+ fi
20
+
21
+ if [[ ! -d "$LOG_DIR" ]]; then
22
+ mkdir -p "$LOG_DIR"
23
+ fi
24
+
25
+ if ! command -v launchctl >/dev/null 2>&1; then
26
+ echo "launchctl not available; this script must be run on macOS." >&2
27
+ exit 1
28
+ fi
29
+
30
+ if [[ ! -x "$BIN_PATH" ]]; then
31
+ echo "IdleWatch launcher executable not found or not executable: $BIN_PATH" >&2
32
+ echo "Set IDLEWATCH_APP_BIN to the correct binary path before running this script." >&2
33
+ exit 1
34
+ fi
35
+
36
+ cat > "$PLIST_PATH" <<'PLIST'
37
+ <?xml version="1.0" encoding="UTF-8"?>
38
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
39
+ <plist version="1.0">
40
+ <dict>
41
+ <key>Label</key>
42
+ <string>{{PLIST_LABEL}}</string>
43
+ <key>ProgramArguments</key>
44
+ <array>
45
+ <string>{{BIN_PATH}}</string>
46
+ </array>
47
+ <key>RunAtLoad</key>
48
+ <true/>
49
+ <key>KeepAlive</key>
50
+ <true/>
51
+ <key>ProcessType</key>
52
+ <string>Background</string>
53
+ <key>StandardOutPath</key>
54
+ <string>{{LOG_DIR}}/idlewatch.out.log</string>
55
+ <key>StandardErrorPath</key>
56
+ <string>{{LOG_DIR}}/idlewatch.err.log</string>
57
+ <key>StartInterval</key>
58
+ <integer>{{START_INTERVAL_SEC}}</integer>
59
+ </dict>
60
+ </plist>
61
+ PLIST
62
+
63
+ sed -i '' "s|{{PLIST_LABEL}}|$PLIST_LABEL|g" "$PLIST_PATH"
64
+ sed -i '' "s|{{BIN_PATH}}|$BIN_PATH|g" "$PLIST_PATH"
65
+ sed -i '' "s|{{LOG_DIR}}|$LOG_DIR|g" "$PLIST_PATH"
66
+ sed -i '' "s|{{START_INTERVAL_SEC}}|$START_INTERVAL_SEC|g" "$PLIST_PATH"
67
+
68
+ USER_GUID="$(id -u)"
69
+ PLIST_ID="gui/$USER_GUID/$PLIST_LABEL"
70
+ if launchctl print "$PLIST_ID" >/dev/null 2>&1; then
71
+ echo "LaunchAgent already loaded. Replacing configuration for $PLIST_ID."
72
+ launchctl bootout "$PLIST_ID" || true
73
+ fi
74
+
75
+ launchctl bootstrap "gui/$USER_GUID" "$PLIST_PATH"
76
+ launchctl enable "$PLIST_ID"
77
+ echo "Installed LaunchAgent: $PLIST_ID"
78
+ echo "Plist: $PLIST_PATH"
@@ -0,0 +1,100 @@
1
+ export function stripControlNoise(raw) {
2
+ return String(raw)
3
+ .replace(/\x1b\][^\x07\x1b]*\x07/g, '')
4
+ .replace(/\x1b\][^\x07\x1b]*\x1b\\/g, '')
5
+ .replace(/\x9b\][^\x07\x1b]*\x07/g, '')
6
+ .replace(/\x9b[^\x07\x1b]*\x1b\\/g, '')
7
+ .replace(/\x1b[PXZ^_].*?(?:\x1b\\|\x9c)/gs, '')
8
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
9
+ .replace(new RegExp(`\x1b[^m]*m`, 'g'), '')
10
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
11
+ .replace(/\r/g, '')
12
+ }
13
+
14
+ export function extractJsonCandidates(raw) {
15
+ const text = stripControlNoise(raw)
16
+ const candidates = []
17
+
18
+ for (let start = 0; start < text.length; start += 1) {
19
+ const open = text[start]
20
+ if (open !== '{' && open !== '[') continue
21
+ if (open === '[') {
22
+ const rest = text.slice(start + 1)
23
+ const firstToken = rest.match(/^\s*([\[{])/)
24
+ if (!firstToken) continue
25
+ }
26
+
27
+ const close = open === '{' ? '}' : ']'
28
+ let depth = 0
29
+ let inString = false
30
+ let escaped = false
31
+ let end = -1
32
+
33
+ for (let i = start; i < text.length; i++) {
34
+ const ch = text[i]
35
+
36
+ if (inString) {
37
+ if (escaped) {
38
+ escaped = false
39
+ continue
40
+ }
41
+ if (ch === '\\') {
42
+ escaped = true
43
+ continue
44
+ }
45
+ if (ch === '"') {
46
+ inString = false
47
+ continue
48
+ }
49
+ continue
50
+ }
51
+
52
+ if (ch === '"') {
53
+ inString = true
54
+ continue
55
+ }
56
+
57
+ if (ch === open) {
58
+ depth += 1
59
+ continue
60
+ }
61
+
62
+ if (ch === close) {
63
+ depth -= 1
64
+ if (depth === 0) {
65
+ end = i
66
+ break
67
+ }
68
+
69
+ if (depth < 0) {
70
+ break
71
+ }
72
+ }
73
+ }
74
+
75
+ if (end > start) {
76
+ candidates.push(text.slice(start, end + 1))
77
+ start = end
78
+ }
79
+ }
80
+
81
+ return candidates
82
+ }
83
+
84
+ export function readTelemetryJsonRow(raw) {
85
+ const candidates = extractJsonCandidates(raw)
86
+ let lastErr
87
+
88
+ for (let i = candidates.length - 1; i >= 0; i -= 1) {
89
+ try {
90
+ return JSON.parse(candidates[i])
91
+ } catch (err) {
92
+ lastErr = err
93
+ }
94
+ }
95
+
96
+ const message = `No telemetry JSON row found in command output (${candidates.length} JSON candidates)`
97
+ const error = new Error(message)
98
+ if (lastErr) error.cause = lastErr
99
+ throw error
100
+ }