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
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
|
+
}
|