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
package/src/gpu.js ADDED
@@ -0,0 +1,115 @@
1
+ import { execSync } from 'child_process'
2
+
3
+ function clampPct(value) {
4
+ if (!Number.isFinite(value)) return null
5
+ return Math.max(0, Math.min(100, Number(value.toFixed(2))))
6
+ }
7
+
8
+ function parseFirstPercent(text) {
9
+ if (!text) return null
10
+ const m = text.match(/(\d+\.?\d*)\s*%/)
11
+ return m ? clampPct(Number(m[1])) : null
12
+ }
13
+
14
+ function parseIoregGpuUtilization(text) {
15
+ if (!text) return null
16
+
17
+ const preferredKeys = [
18
+ 'Device Utilization %',
19
+ 'Renderer Utilization %',
20
+ 'Tiler Utilization %'
21
+ ]
22
+
23
+ for (const key of preferredKeys) {
24
+ const re = new RegExp(`"${key.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}"\\s*=\\s*(\\d+\\.?\\d*)`, 'i')
25
+ const m = text.match(re)
26
+ if (m) return clampPct(Number(m[1]))
27
+ }
28
+
29
+ return null
30
+ }
31
+
32
+ function parseGpuPercent(text, source) {
33
+ if (source.startsWith('ioreg')) return parseIoregGpuUtilization(text)
34
+
35
+ if (source === 'top-grep') {
36
+ const gpuLines = text
37
+ .split(/\r?\n/)
38
+ .map((line) => line.trim())
39
+ .filter((line) => /gpu/i.test(line) && /%/.test(line))
40
+ .join('\n')
41
+
42
+ return parseFirstPercent(gpuLines)
43
+ }
44
+
45
+ return parseFirstPercent(text)
46
+ }
47
+
48
+ export function gpuSampleDarwin(exec = execSync) {
49
+ const probes = [
50
+ {
51
+ cmd: '/usr/sbin/ioreg -r -d 1 -w0 -c AGXAccelerator',
52
+ source: 'ioreg-agx',
53
+ confidence: 'high',
54
+ sampleWindowMs: null,
55
+ timeoutMs: 1500
56
+ },
57
+ {
58
+ cmd: '/usr/sbin/ioreg -r -d 1 -w0 -c IOGPU',
59
+ source: 'ioreg-iogpu',
60
+ confidence: 'medium',
61
+ sampleWindowMs: null,
62
+ timeoutMs: 1500
63
+ },
64
+ {
65
+ cmd: 'powermetrics --samplers gpu_power -n 1 -i 1000',
66
+ source: 'powermetrics',
67
+ confidence: 'high',
68
+ sampleWindowMs: 1000,
69
+ timeoutMs: 1800
70
+ },
71
+ {
72
+ cmd: "top -l 1 | grep -i 'GPU'",
73
+ source: 'top-grep',
74
+ confidence: 'low',
75
+ sampleWindowMs: null,
76
+ timeoutMs: 1200
77
+ }
78
+ ]
79
+
80
+ for (const probe of probes) {
81
+ try {
82
+ const out = exec(probe.cmd, {
83
+ encoding: 'utf8',
84
+ stdio: ['ignore', 'pipe', 'ignore'],
85
+ timeout: probe.timeoutMs
86
+ })
87
+
88
+ const pct = parseGpuPercent(out, probe.source)
89
+ if (pct !== null) {
90
+ return {
91
+ pct,
92
+ source: probe.source,
93
+ confidence: probe.confidence,
94
+ sampleWindowMs: probe.sampleWindowMs
95
+ }
96
+ }
97
+ } catch {
98
+ // ignore and continue
99
+ }
100
+ }
101
+
102
+ return {
103
+ pct: null,
104
+ source: 'unavailable',
105
+ confidence: 'none',
106
+ sampleWindowMs: null
107
+ }
108
+ }
109
+
110
+ export const __gpuTestUtils = {
111
+ parseFirstPercent,
112
+ parseIoregGpuUtilization,
113
+ parseGpuPercent,
114
+ clampPct
115
+ }
package/src/memory.js ADDED
@@ -0,0 +1,67 @@
1
+ import os from 'os'
2
+ import { execSync } from 'child_process'
3
+
4
+ function clampPct(value) {
5
+ if (!Number.isFinite(value)) return null
6
+ return Math.max(0, Math.min(100, Number(value.toFixed(2))))
7
+ }
8
+
9
+ function classifyPressure(pct) {
10
+ if (!Number.isFinite(pct)) return 'unavailable'
11
+ if (pct >= 90) return 'critical'
12
+ if (pct >= 75) return 'warning'
13
+ return 'normal'
14
+ }
15
+
16
+ export function parseMemoryPressurePct(raw) {
17
+ if (!raw) return null
18
+ const freeMatch = raw.match(/System-wide memory free percentage:\s*(\d+\.?\d*)%/i)
19
+ if (freeMatch) {
20
+ const freePct = Number(freeMatch[1])
21
+ if (Number.isFinite(freePct)) {
22
+ return clampPct(100 - freePct)
23
+ }
24
+ }
25
+
26
+ const pressureMatch = raw.match(/System-wide memory pressure:\s*(\d+\.?\d*)%/i)
27
+ if (pressureMatch) {
28
+ return clampPct(Number(pressureMatch[1]))
29
+ }
30
+
31
+ return null
32
+ }
33
+
34
+ export function memUsedPct() {
35
+ return clampPct(((os.totalmem() - os.freemem()) / os.totalmem()) * 100)
36
+ }
37
+
38
+ export function memoryPressureDarwin(exec = execSync) {
39
+ try {
40
+ const out = exec('/usr/bin/memory_pressure -Q', {
41
+ encoding: 'utf8',
42
+ stdio: ['ignore', 'pipe', 'ignore'],
43
+ timeout: 1200
44
+ })
45
+ const pct = parseMemoryPressurePct(out)
46
+ if (pct !== null) {
47
+ return {
48
+ pct,
49
+ cls: classifyPressure(pct),
50
+ source: 'memory_pressure'
51
+ }
52
+ }
53
+ } catch {
54
+ // ignore and return unavailable
55
+ }
56
+
57
+ return {
58
+ pct: null,
59
+ cls: 'unavailable',
60
+ source: 'unavailable'
61
+ }
62
+ }
63
+
64
+ export const __memoryTestUtils = {
65
+ clampPct,
66
+ classifyPressure
67
+ }
@@ -0,0 +1,51 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ function isFiniteNumber(value) {
5
+ return typeof value === 'number' && Number.isFinite(value)
6
+ }
7
+
8
+ export function loadLastGoodUsageSnapshot(cachePath, nowMs = Date.now()) {
9
+ if (!cachePath) return null
10
+
11
+ try {
12
+ if (!fs.existsSync(cachePath)) return null
13
+ const raw = fs.readFileSync(cachePath, 'utf8')
14
+ const parsed = JSON.parse(raw)
15
+ if (!isFiniteNumber(parsed?.at) || typeof parsed?.usage !== 'object' || parsed.usage === null) return null
16
+
17
+ return {
18
+ at: parsed.at,
19
+ usage: parsed.usage,
20
+ ageMs: Math.max(0, nowMs - parsed.at)
21
+ }
22
+ } catch {
23
+ return null
24
+ }
25
+ }
26
+
27
+ export function persistLastGoodUsageSnapshot(cachePath, payload) {
28
+ if (!cachePath || !payload || !isFiniteNumber(payload.at) || typeof payload.usage !== 'object' || payload.usage === null) {
29
+ return false
30
+ }
31
+
32
+ const destination = cachePath
33
+ const dir = path.dirname(destination)
34
+ const tempPath = `${destination}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`
35
+ const serialized = JSON.stringify({ at: payload.at, usage: payload.usage })
36
+
37
+ try {
38
+ fs.mkdirSync(dir, { recursive: true })
39
+ fs.writeFileSync(tempPath, serialized, 'utf8')
40
+ fs.renameSync(tempPath, destination)
41
+ return true
42
+ } catch {
43
+ return false
44
+ } finally {
45
+ try {
46
+ if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath)
47
+ } catch {
48
+ // best effort cleanup
49
+ }
50
+ }
51
+ }