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.
- package/.env.example +73 -0
- package/.github/workflows/ci.yml +99 -0
- package/.github/workflows/release-macos-trusted.yml +103 -0
- package/README.md +336 -0
- package/bin/idlewatch-agent.js +1053 -0
- package/docs/onboarding-external.md +58 -0
- package/docs/packaging/macos-dmg.md +199 -0
- package/docs/packaging/macos-launch-agent.md +70 -0
- package/docs/qa/archive/mac-qa-log-2026-02-17.md +5838 -0
- package/docs/qa/mac-qa-log.md +2864 -0
- package/docs/telemetry/idle-stale-policy.md +57 -0
- package/docs/telemetry/openclaw-mapping.md +80 -0
- package/package.json +76 -0
- package/scripts/build-dmg.sh +65 -0
- package/scripts/install-macos-launch-agent.sh +78 -0
- package/scripts/lib/telemetry-row-parser.mjs +100 -0
- package/scripts/package-macos.sh +228 -0
- package/scripts/uninstall-macos-launch-agent.sh +30 -0
- package/scripts/validate-all.sh +142 -0
- package/scripts/validate-bin.mjs +25 -0
- package/scripts/validate-dmg-checksum.sh +37 -0
- package/scripts/validate-dmg-install.sh +155 -0
- package/scripts/validate-dry-run-schema.mjs +257 -0
- package/scripts/validate-onboarding.mjs +63 -0
- package/scripts/validate-openclaw-cache-recovery-e2e.mjs +113 -0
- package/scripts/validate-openclaw-release-gates.mjs +51 -0
- package/scripts/validate-openclaw-stats-ingestion.mjs +372 -0
- package/scripts/validate-openclaw-usage-health.mjs +95 -0
- package/scripts/validate-packaged-artifact.mjs +233 -0
- package/scripts/validate-packaged-bundled-runtime.sh +191 -0
- package/scripts/validate-packaged-metadata.sh +43 -0
- package/scripts/validate-packaged-openclaw-cache-recovery-e2e.mjs +153 -0
- package/scripts/validate-packaged-openclaw-release-gates.mjs +72 -0
- package/scripts/validate-packaged-openclaw-stats-ingestion.mjs +402 -0
- package/scripts/validate-packaged-sourcemaps.mjs +82 -0
- package/scripts/validate-packaged-usage-alert-rate-e2e.mjs +98 -0
- package/scripts/validate-packaged-usage-probe-noise-e2e.mjs +87 -0
- package/scripts/validate-packaged-usage-recovery-e2e.mjs +90 -0
- package/scripts/validate-trusted-prereqs.sh +44 -0
- package/scripts/validate-usage-alert-rate-e2e.mjs +91 -0
- package/scripts/validate-usage-freshness-e2e.mjs +81 -0
- package/skill/SKILL.md +43 -0
- package/src/config.js +100 -0
- package/src/enrollment.js +176 -0
- package/src/gpu.js +115 -0
- package/src/memory.js +67 -0
- package/src/openclaw-cache.js +51 -0
- package/src/openclaw-usage.js +1020 -0
- package/src/telemetry-mapping.js +54 -0
- package/src/usage-alert.js +41 -0
- package/src/usage-freshness.js +31 -0
- package/test/config.test.mjs +112 -0
- package/test/fixtures/gpu-agx.txt +2 -0
- package/test/fixtures/gpu-iogpu.txt +2 -0
- package/test/fixtures/gpu-top-grep.txt +2 -0
- package/test/fixtures/openclaw-fleet-sample-v1.json +68 -0
- package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-iso-ts.txt +2 -0
- package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-newest.txt +2 -0
- package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-string-ts.txt +2 -0
- package/test/fixtures/openclaw-mixed-status-then-generic-output.txt +2 -0
- package/test/fixtures/openclaw-stats-current-wrapper.json +12 -0
- package/test/fixtures/openclaw-stats-current-wrapper2.json +15 -0
- package/test/fixtures/openclaw-stats-data-wrapper.json +21 -0
- package/test/fixtures/openclaw-stats-nested-session-wrapper.json +23 -0
- package/test/fixtures/openclaw-stats-payload-wrapper.json +1 -0
- package/test/fixtures/openclaw-stats-status-current-wrapper.json +19 -0
- package/test/fixtures/openclaw-stats.json +17 -0
- package/test/fixtures/openclaw-status-ansi-complex-noise.txt +3 -0
- package/test/fixtures/openclaw-status-ansi-noise.txt +2 -0
- package/test/fixtures/openclaw-status-control-noise.txt +1 -0
- package/test/fixtures/openclaw-status-data-wrapper.json +20 -0
- package/test/fixtures/openclaw-status-dcs-noise.txt +1 -0
- package/test/fixtures/openclaw-status-epoch-seconds.json +15 -0
- package/test/fixtures/openclaw-status-mixed-noise.txt +1 -0
- package/test/fixtures/openclaw-status-multi-json.txt +3 -0
- package/test/fixtures/openclaw-status-nested-recent.json +19 -0
- package/test/fixtures/openclaw-status-noisy-default-then-usage.txt +2 -0
- package/test/fixtures/openclaw-status-noisy.txt +3 -0
- package/test/fixtures/openclaw-status-osc-noise.txt +1 -0
- package/test/fixtures/openclaw-status-result-session.json +15 -0
- package/test/fixtures/openclaw-status-session-map-with-defaults.json +23 -0
- package/test/fixtures/openclaw-status-session-map.json +28 -0
- package/test/fixtures/openclaw-status-session-model-name.json +18 -0
- package/test/fixtures/openclaw-status-snake-session-wrapper.json +13 -0
- package/test/fixtures/openclaw-status-stats-current-sessions-snake-tokens.json +25 -0
- package/test/fixtures/openclaw-status-stats-current-sessions.json +28 -0
- package/test/fixtures/openclaw-status-stats-current-usage-time-camelcase.json +19 -0
- package/test/fixtures/openclaw-status-stats-session-default-model.json +27 -0
- package/test/fixtures/openclaw-status-status-wrapper.json +13 -0
- package/test/fixtures/openclaw-status-strings.json +38 -0
- package/test/fixtures/openclaw-status-ts-ms-alias.json +14 -0
- package/test/fixtures/openclaw-status-updated-at-ms-alias.json +14 -0
- package/test/fixtures/openclaw-status-usage-timestamp-ms-alias.json +14 -0
- package/test/fixtures/openclaw-status-usage-ts-alias.json +14 -0
- package/test/fixtures/openclaw-status-wrap-session-object.json +24 -0
- package/test/fixtures/openclaw-status.json +41 -0
- package/test/fixtures/openclaw-usage-model-name-generic.json +9 -0
- package/test/gpu.test.mjs +58 -0
- package/test/memory.test.mjs +35 -0
- package/test/openclaw-cache.test.mjs +48 -0
- package/test/openclaw-env.test.mjs +365 -0
- package/test/openclaw-usage.test.mjs +555 -0
- package/test/telemetry-mapping.test.mjs +69 -0
- package/test/telemetry-row-parser.test.mjs +44 -0
- package/test/usage-alert.test.mjs +73 -0
- package/test/usage-freshness.test.mjs +63 -0
- package/test/validate-dry-run-schema.test.mjs +146 -0
- package/tui/Cargo.lock +801 -0
- package/tui/Cargo.toml +11 -0
- 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
|
+
}
|