idlewatch 0.1.7 → 0.1.8
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/bin/idlewatch-agent.js +61 -32
- package/package.json +1 -1
- package/scripts/validate-dry-run-schema.mjs +4 -0
- package/src/enrollment.js +16 -5
- package/src/openclaw-usage.js +89 -37
- package/src/telemetry-mapping.js +6 -1
- package/src/thermal.js +113 -0
- package/tui/src/main.rs +73 -24
package/bin/idlewatch-agent.js
CHANGED
|
@@ -10,6 +10,7 @@ import { createRequire } from 'module'
|
|
|
10
10
|
import { parseOpenClawUsage } from '../src/openclaw-usage.js'
|
|
11
11
|
import { gpuSampleDarwin } from '../src/gpu.js'
|
|
12
12
|
import { memUsedPct, memoryPressureDarwin } from '../src/memory.js'
|
|
13
|
+
import { thermalSampleDarwin } from '../src/thermal.js'
|
|
13
14
|
import { deriveUsageFreshness } from '../src/usage-freshness.js'
|
|
14
15
|
import { deriveUsageAlert } from '../src/usage-alert.js'
|
|
15
16
|
import { loadLastGoodUsageSnapshot, persistLastGoodUsageSnapshot } from '../src/openclaw-cache.js'
|
|
@@ -104,9 +105,12 @@ function buildSetupTestEnv(enrolledEnv) {
|
|
|
104
105
|
|
|
105
106
|
const persistedEnv = loadPersistedEnvIntoProcess()
|
|
106
107
|
|
|
108
|
+
const OPENCLAW_AGENT_TARGETS = ['agent_activity', 'token_usage', 'runtime_state']
|
|
109
|
+
const OPENCLAW_DERIVED_TARGETS = [...OPENCLAW_AGENT_TARGETS]
|
|
110
|
+
|
|
107
111
|
function parseMonitorTargets(raw) {
|
|
108
|
-
const allowed = new Set(['cpu', 'memory', 'gpu', 'openclaw'])
|
|
109
|
-
const fallback = ['cpu', 'memory', '
|
|
112
|
+
const allowed = new Set(['cpu', 'memory', 'gpu', 'temperature', 'openclaw', ...OPENCLAW_DERIVED_TARGETS])
|
|
113
|
+
const fallback = ['cpu', 'memory', 'gpu', 'temperature', ...OPENCLAW_DERIVED_TARGETS]
|
|
110
114
|
|
|
111
115
|
if (!raw || typeof raw !== 'string') {
|
|
112
116
|
return new Set(fallback)
|
|
@@ -116,6 +120,7 @@ function parseMonitorTargets(raw) {
|
|
|
116
120
|
.split(',')
|
|
117
121
|
.map((item) => item.trim().toLowerCase())
|
|
118
122
|
.filter((item) => allowed.has(item))
|
|
123
|
+
.flatMap((item) => (item === 'openclaw' ? OPENCLAW_DERIVED_TARGETS : [item]))
|
|
119
124
|
|
|
120
125
|
if (parsed.length === 0) return new Set(fallback)
|
|
121
126
|
return new Set(parsed)
|
|
@@ -244,12 +249,20 @@ function buildRollingLoadSummary(logPath, nowMs = Date.now(), windowMs = DAY_WIN
|
|
|
244
249
|
return Number((valid.reduce((sum, value) => sum + value, 0) / valid.length).toFixed(2))
|
|
245
250
|
}
|
|
246
251
|
|
|
252
|
+
const maximum = (values) => {
|
|
253
|
+
const valid = values.filter((value) => Number.isFinite(value))
|
|
254
|
+
if (valid.length === 0) return null
|
|
255
|
+
return Number(Math.max(...valid).toFixed(1))
|
|
256
|
+
}
|
|
257
|
+
|
|
247
258
|
return {
|
|
248
259
|
windowMs,
|
|
249
260
|
sampleCount: rows.length,
|
|
250
261
|
cpuAvgPct: average(rows.map((row) => Number(row.cpuPct))),
|
|
251
262
|
memAvgPct: average(rows.map((row) => Number(row.memPct))),
|
|
252
263
|
gpuAvgPct: average(rows.map((row) => Number(row.gpuPct))),
|
|
264
|
+
tempAvgC: average(rows.map((row) => Number(row.deviceTempC))),
|
|
265
|
+
tempMaxC: maximum(rows.map((row) => Number(row.deviceTempC))),
|
|
253
266
|
tokensAvgPerMin: average(rows.map((row) => Number(row.tokensPerMin)))
|
|
254
267
|
}
|
|
255
268
|
}
|
|
@@ -493,8 +506,12 @@ const MONITOR_TARGETS = parseMonitorTargets(process.env.IDLEWATCH_MONITOR_TARGET
|
|
|
493
506
|
const MONITOR_CPU = MONITOR_TARGETS.has('cpu')
|
|
494
507
|
const MONITOR_MEMORY = MONITOR_TARGETS.has('memory')
|
|
495
508
|
const MONITOR_GPU = MONITOR_TARGETS.has('gpu')
|
|
496
|
-
const
|
|
497
|
-
const
|
|
509
|
+
const MONITOR_TEMPERATURE = MONITOR_TARGETS.has('temperature')
|
|
510
|
+
const MONITOR_AGENT_ACTIVITY = MONITOR_TARGETS.has('agent_activity')
|
|
511
|
+
const MONITOR_TOKEN_USAGE = MONITOR_TARGETS.has('token_usage')
|
|
512
|
+
const MONITOR_RUNTIME_STATE = MONITOR_TARGETS.has('runtime_state')
|
|
513
|
+
const MONITOR_OPENCLAW_USAGE = MONITOR_TOKEN_USAGE || MONITOR_RUNTIME_STATE
|
|
514
|
+
const EFFECTIVE_OPENCLAW_MODE = MONITOR_OPENCLAW_USAGE ? OPENCLAW_USAGE_MODE : 'off'
|
|
498
515
|
const REQUIRE_FIREBASE_WRITES = process.env.IDLEWATCH_REQUIRE_FIREBASE_WRITES === '1'
|
|
499
516
|
const CLOUD_INGEST_URL = (process.env.IDLEWATCH_CLOUD_INGEST_URL || '').trim()
|
|
500
517
|
const CLOUD_API_KEY = (process.env.IDLEWATCH_CLOUD_API_KEY || '').trim().replace(/^['"]|['"]$/g, '')
|
|
@@ -1320,8 +1337,8 @@ async function publish(row, retries = 2) {
|
|
|
1320
1337
|
async function collectSample() {
|
|
1321
1338
|
const sampleStartMs = Date.now()
|
|
1322
1339
|
const dayLoadSummary = buildRollingLoadSummary(LOCAL_LOG_PATH, sampleStartMs)
|
|
1323
|
-
const
|
|
1324
|
-
const activitySummary =
|
|
1340
|
+
const openclawUsageEnabled = EFFECTIVE_OPENCLAW_MODE !== 'off'
|
|
1341
|
+
const activitySummary = MONITOR_AGENT_ACTIVITY ? loadOpenClawActivitySummary({ nowMs: sampleStartMs }) : null
|
|
1325
1342
|
|
|
1326
1343
|
const disabledProbe = {
|
|
1327
1344
|
result: 'disabled',
|
|
@@ -1334,7 +1351,7 @@ async function collectSample() {
|
|
|
1334
1351
|
fallbackCacheSource: null
|
|
1335
1352
|
}
|
|
1336
1353
|
|
|
1337
|
-
let usageProbe =
|
|
1354
|
+
let usageProbe = openclawUsageEnabled ? loadOpenClawUsage() : { usage: null, probe: disabledProbe }
|
|
1338
1355
|
let usage = usageProbe.usage
|
|
1339
1356
|
let usageFreshness = deriveUsageFreshness(usage, sampleStartMs, USAGE_STALE_MS, USAGE_NEAR_STALE_MS, USAGE_STALE_GRACE_MS)
|
|
1340
1357
|
let usageRefreshAttempted = false
|
|
@@ -1346,7 +1363,7 @@ async function collectSample() {
|
|
|
1346
1363
|
const shouldRefreshForNearStale = USAGE_REFRESH_ON_NEAR_STALE === 1 && usageFreshness.isNearStale
|
|
1347
1364
|
const canRefreshFromCurrentState = usageProbe.probe.result === 'ok' || usageProbe.probe.result === 'fallback-cache'
|
|
1348
1365
|
|
|
1349
|
-
if (
|
|
1366
|
+
if (openclawUsageEnabled && usage && (usageFreshness.isPastStaleThreshold || shouldRefreshForNearStale) && canRefreshFromCurrentState) {
|
|
1350
1367
|
usageRefreshAttempted = true
|
|
1351
1368
|
usageRefreshStartMs = Date.now()
|
|
1352
1369
|
|
|
@@ -1390,6 +1407,9 @@ async function collectSample() {
|
|
|
1390
1407
|
: { pct: null, cls: 'disabled', source: 'disabled' }
|
|
1391
1408
|
|
|
1392
1409
|
const usedMemPct = MONITOR_MEMORY ? memUsedPct() : null
|
|
1410
|
+
const thermals = MONITOR_TEMPERATURE && process.platform === 'darwin'
|
|
1411
|
+
? thermalSampleDarwin()
|
|
1412
|
+
: { tempC: null, source: 'disabled', thermalLevel: null, thermalState: MONITOR_TEMPERATURE ? 'unavailable' : 'disabled' }
|
|
1393
1413
|
|
|
1394
1414
|
const usageIntegrationStatus = usage
|
|
1395
1415
|
? usageFreshness.isStale
|
|
@@ -1397,20 +1417,20 @@ async function collectSample() {
|
|
|
1397
1417
|
: usage?.integrationStatus === 'partial'
|
|
1398
1418
|
? 'ok'
|
|
1399
1419
|
: (usage?.integrationStatus ?? 'ok')
|
|
1400
|
-
: (
|
|
1420
|
+
: (openclawUsageEnabled ? 'unavailable' : 'disabled')
|
|
1401
1421
|
|
|
1402
1422
|
const source = {
|
|
1403
1423
|
monitorTargets: [...MONITOR_TARGETS],
|
|
1404
|
-
usage: usage ? 'openclaw' :
|
|
1424
|
+
usage: usage ? 'openclaw' : openclawUsageEnabled ? 'unavailable' : 'disabled',
|
|
1405
1425
|
usageIntegrationStatus,
|
|
1406
|
-
usageIngestionStatus:
|
|
1426
|
+
usageIngestionStatus: openclawUsageEnabled
|
|
1407
1427
|
? usage && ['ok', 'fallback-cache'].includes(usageProbe.probe.result)
|
|
1408
1428
|
? 'ok'
|
|
1409
1429
|
: 'unavailable'
|
|
1410
1430
|
: 'disabled',
|
|
1411
1431
|
usageActivityStatus: usage
|
|
1412
1432
|
? usageFreshness.freshnessState
|
|
1413
|
-
: (
|
|
1433
|
+
: (openclawUsageEnabled ? 'unavailable' : 'disabled'),
|
|
1414
1434
|
usageProbeResult: usageProbe.probe.result,
|
|
1415
1435
|
usageProbeAttempts: usageProbe.probe.attempts,
|
|
1416
1436
|
usageProbeSweeps: usageProbe.probe.sweeps,
|
|
@@ -1421,7 +1441,7 @@ async function collectSample() {
|
|
|
1421
1441
|
usageUsedFallbackCache: usageProbe.probe.usedFallbackCache,
|
|
1422
1442
|
usageFallbackCacheAgeMs: usageProbe.probe.fallbackAgeMs,
|
|
1423
1443
|
usageFallbackCacheSource: usageProbe.probe.fallbackCacheSource,
|
|
1424
|
-
usageFreshnessState:
|
|
1444
|
+
usageFreshnessState: openclawUsageEnabled
|
|
1425
1445
|
? usage
|
|
1426
1446
|
? usageFreshness.freshnessState
|
|
1427
1447
|
: null
|
|
@@ -1441,8 +1461,10 @@ async function collectSample() {
|
|
|
1441
1461
|
usageStaleMsThreshold: USAGE_STALE_MS,
|
|
1442
1462
|
usageNearStaleMsThreshold: USAGE_NEAR_STALE_MS,
|
|
1443
1463
|
usageStaleGraceMs: USAGE_STALE_GRACE_MS,
|
|
1444
|
-
activitySource: activitySummary?.source ?? (
|
|
1445
|
-
activityWindowMs: activitySummary?.windowMs ?? null,
|
|
1464
|
+
activitySource: activitySummary?.source ?? (MONITOR_AGENT_ACTIVITY ? 'unavailable' : 'disabled'),
|
|
1465
|
+
activityWindowMs: MONITOR_AGENT_ACTIVITY ? (activitySummary?.windowMs ?? null) : null,
|
|
1466
|
+
thermalSource: thermals.source,
|
|
1467
|
+
thermalState: thermals.thermalState,
|
|
1446
1468
|
memPressureSource: memPressure.source,
|
|
1447
1469
|
cloudIngestionStatus: CLOUD_INGEST_URL && CLOUD_API_KEY
|
|
1448
1470
|
? cloudIngestKickedOut ? 'kicked-out' : 'enabled'
|
|
@@ -1467,30 +1489,37 @@ async function collectSample() {
|
|
|
1467
1489
|
memPressurePct: MONITOR_MEMORY ? memPressure.pct : null,
|
|
1468
1490
|
memPressureClass: MONITOR_MEMORY ? memPressure.cls : 'disabled',
|
|
1469
1491
|
gpuPct: MONITOR_GPU ? gpu.pct : null,
|
|
1492
|
+
deviceTempC: MONITOR_TEMPERATURE ? thermals.tempC : null,
|
|
1493
|
+
thermalLevel: MONITOR_TEMPERATURE ? thermals.thermalLevel : null,
|
|
1494
|
+
thermalState: MONITOR_TEMPERATURE ? thermals.thermalState : 'disabled',
|
|
1470
1495
|
dayWindowMs: dayLoadSummary?.windowMs ?? DAY_WINDOW_MS,
|
|
1471
1496
|
dayCpuAvgPct: MONITOR_CPU ? (dayLoadSummary?.cpuAvgPct ?? null) : null,
|
|
1472
1497
|
dayMemAvgPct: MONITOR_MEMORY ? (dayLoadSummary?.memAvgPct ?? null) : null,
|
|
1473
1498
|
dayGpuAvgPct: MONITOR_GPU ? (dayLoadSummary?.gpuAvgPct ?? null) : null,
|
|
1474
|
-
|
|
1499
|
+
dayTempAvgC: MONITOR_TEMPERATURE ? (dayLoadSummary?.tempAvgC ?? null) : null,
|
|
1500
|
+
dayTempMaxC: MONITOR_TEMPERATURE ? (dayLoadSummary?.tempMaxC ?? null) : null,
|
|
1501
|
+
dayTokensAvgPerMin: MONITOR_TOKEN_USAGE ? (dayLoadSummary?.tokensAvgPerMin ?? null) : null,
|
|
1475
1502
|
gpuSource: gpu.source,
|
|
1476
1503
|
gpuConfidence: gpu.confidence,
|
|
1477
1504
|
gpuSampleWindowMs: gpu.sampleWindowMs,
|
|
1478
|
-
tokensPerMin:
|
|
1479
|
-
openclawModel:
|
|
1480
|
-
openclawProvider:
|
|
1481
|
-
openclawTotalTokens:
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1505
|
+
tokensPerMin: MONITOR_TOKEN_USAGE ? (usage?.tokensPerMin ?? null) : null,
|
|
1506
|
+
openclawModel: MONITOR_RUNTIME_STATE ? (usage?.model ?? null) : null,
|
|
1507
|
+
openclawProvider: MONITOR_RUNTIME_STATE ? (usage?.provider ?? null) : null,
|
|
1508
|
+
openclawTotalTokens: MONITOR_TOKEN_USAGE ? (usage?.totalTokens ?? null) : null,
|
|
1509
|
+
openclawInputTokens: MONITOR_TOKEN_USAGE ? (usage?.inputTokens ?? null) : null,
|
|
1510
|
+
openclawOutputTokens: MONITOR_TOKEN_USAGE ? (usage?.outputTokens ?? null) : null,
|
|
1511
|
+
openclawRemainingTokens: openclawUsageEnabled ? (usage?.remainingTokens ?? null) : null,
|
|
1512
|
+
openclawPercentUsed: openclawUsageEnabled ? (usage?.percentUsed ?? null) : null,
|
|
1513
|
+
openclawContextTokens: openclawUsageEnabled ? (usage?.contextTokens ?? null) : null,
|
|
1514
|
+
openclawBudgetKind: openclawUsageEnabled ? (usage?.budgetKind ?? null) : null,
|
|
1515
|
+
openclawSessionId: openclawUsageEnabled ? (usage?.sessionId ?? null) : null,
|
|
1516
|
+
openclawAgentId: openclawUsageEnabled ? (usage?.agentId ?? null) : null,
|
|
1517
|
+
openclawUsageTs: openclawUsageEnabled ? (usage?.usageTimestampMs ?? null) : null,
|
|
1518
|
+
openclawUsageAgeMs: openclawUsageEnabled ? usageFreshness.usageAgeMs : null,
|
|
1519
|
+
activityWindowMs: MONITOR_AGENT_ACTIVITY ? (activitySummary?.windowMs ?? null) : null,
|
|
1520
|
+
activityActiveSeconds: MONITOR_AGENT_ACTIVITY ? (activitySummary?.totalActiveSeconds ?? null) : null,
|
|
1521
|
+
activityIdleSeconds: MONITOR_AGENT_ACTIVITY ? (activitySummary?.idleSeconds ?? null) : null,
|
|
1522
|
+
activityJobs: MONITOR_AGENT_ACTIVITY ? (activitySummary?.jobs ?? []) : [],
|
|
1494
1523
|
localLogPath: LOCAL_LOG_PATH,
|
|
1495
1524
|
localLogBytes: null,
|
|
1496
1525
|
source
|
package/package.json
CHANGED
|
@@ -92,6 +92,9 @@ function validateRow(row) {
|
|
|
92
92
|
assert.equal(typeof row.gpuSource, 'string', 'gpuSource must be string')
|
|
93
93
|
assert.ok(['high', 'medium', 'low', 'none'].includes(row.gpuConfidence), 'gpuConfidence invalid')
|
|
94
94
|
assertNumberOrNull(row.gpuSampleWindowMs, 'gpuSampleWindowMs')
|
|
95
|
+
assertNumberOrNull(row.deviceTempC, 'deviceTempC')
|
|
96
|
+
assertNumberOrNull(row.dayTempAvgC, 'dayTempAvgC')
|
|
97
|
+
assertNumberOrNull(row.dayTempMaxC, 'dayTempMaxC')
|
|
95
98
|
|
|
96
99
|
assertNumberOrNull(row.tokensPerMin, 'tokensPerMin')
|
|
97
100
|
assert.ok(row.openclawModel === null || typeof row.openclawModel === 'string', 'openclawModel must be string or null')
|
|
@@ -115,6 +118,7 @@ function validateRow(row) {
|
|
|
115
118
|
assertNumberOrNull(row.fleet.resources.memUsedPct, 'fleet.resources.memUsedPct')
|
|
116
119
|
assertNumberOrNull(row.fleet.resources.memPressurePct, 'fleet.resources.memPressurePct')
|
|
117
120
|
assert.ok(['normal', 'warning', 'critical', 'unavailable'].includes(row.fleet.resources.memPressureClass), 'fleet.resources.memPressureClass invalid')
|
|
121
|
+
assertNumberOrNull(row.fleet.resources.tempC, 'fleet.resources.tempC')
|
|
118
122
|
assert.equal(typeof row.fleet.usage, 'object', 'fleet.usage must exist')
|
|
119
123
|
assert.ok(row.fleet.usage.model === null || typeof row.fleet.usage.model === 'string', 'fleet.usage.model invalid')
|
|
120
124
|
assertNumberOrNull(row.fleet.usage.totalTokens, 'fleet.usage.totalTokens')
|
package/src/enrollment.js
CHANGED
|
@@ -42,7 +42,9 @@ function writeSecureFile(filePath, content) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const
|
|
45
|
+
const OPENCLAW_AGENT_TARGETS = ['agent_activity', 'token_usage', 'runtime_state']
|
|
46
|
+
const OPENCLAW_DERIVED_TARGETS = [...OPENCLAW_AGENT_TARGETS]
|
|
47
|
+
const MONITOR_TARGET_CHOICES = ['cpu', 'memory', 'gpu', 'temperature', 'openclaw', ...OPENCLAW_DERIVED_TARGETS]
|
|
46
48
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
47
49
|
const PACKAGE_ROOT = path.resolve(MODULE_DIR, '..')
|
|
48
50
|
|
|
@@ -57,23 +59,28 @@ function detectAvailableMonitorTargets() {
|
|
|
57
59
|
if (process.platform === 'darwin' || commandExists('nvidia-smi', ['--help'])) {
|
|
58
60
|
available.add('gpu')
|
|
59
61
|
}
|
|
62
|
+
if (process.platform === 'darwin') {
|
|
63
|
+
available.add('temperature')
|
|
64
|
+
}
|
|
60
65
|
|
|
61
66
|
if (commandExists('openclaw', ['--help'])) {
|
|
62
|
-
available.add(
|
|
67
|
+
OPENCLAW_DERIVED_TARGETS.forEach((target) => available.add(target))
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
return [...available]
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
function normalizeMonitorTargets(raw, available) {
|
|
69
|
-
const fallback = ['cpu', 'memory', ...(available.includes('
|
|
74
|
+
const fallback = ['cpu', 'memory', ...(available.includes('gpu') ? ['gpu'] : []), ...(available.includes('temperature') ? ['temperature'] : []), ...OPENCLAW_DERIVED_TARGETS.filter((target) => available.includes(target))]
|
|
70
75
|
if (!raw) return fallback
|
|
71
76
|
|
|
72
77
|
const parsed = raw
|
|
73
78
|
.split(',')
|
|
74
79
|
.map((item) => item.trim().toLowerCase())
|
|
75
80
|
.filter(Boolean)
|
|
76
|
-
.filter((item) => MONITOR_TARGET_CHOICES.includes(item)
|
|
81
|
+
.filter((item) => MONITOR_TARGET_CHOICES.includes(item))
|
|
82
|
+
.flatMap((item) => (item === 'openclaw' ? OPENCLAW_DERIVED_TARGETS : [item]))
|
|
83
|
+
.filter((item) => available.includes(item))
|
|
77
84
|
|
|
78
85
|
if (parsed.length === 0) return fallback
|
|
79
86
|
return [...new Set(parsed)]
|
|
@@ -226,6 +233,10 @@ function promptModeText({ isReconfigure = false, currentMode = null } = {}) {
|
|
|
226
233
|
return `\n╭───────────────────────────────────────────────╮\n│${titleLine.padEnd(47)}│\n╰───────────────────────────────────────────────╯\n\nChoose setup mode:\n 1) Managed cloud (recommended)\n Link this device with an API key from idlewatch.com/api\n 2) Local-only (no cloud writes)\n`
|
|
227
234
|
}
|
|
228
235
|
|
|
236
|
+
function monitorTargetsNeedOpenClawUsage(monitorTargets) {
|
|
237
|
+
return monitorTargets.some((item) => OPENCLAW_AGENT_TARGETS.includes(item) && item !== 'agent_activity')
|
|
238
|
+
}
|
|
239
|
+
|
|
229
240
|
export async function runEnrollmentWizard(options = {}) {
|
|
230
241
|
const nonInteractive = options.nonInteractive || process.env.IDLEWATCH_ENROLL_NON_INTERACTIVE === '1'
|
|
231
242
|
const noTui = options.noTui || process.env.IDLEWATCH_NO_TUI === '1'
|
|
@@ -348,7 +359,7 @@ export async function runEnrollmentWizard(options = {}) {
|
|
|
348
359
|
`IDLEWATCH_DEVICE_NAME=${deviceName}`,
|
|
349
360
|
`IDLEWATCH_DEVICE_ID=${safeDeviceId}`,
|
|
350
361
|
`IDLEWATCH_MONITOR_TARGETS=${monitorTargets.join(',')}`,
|
|
351
|
-
`IDLEWATCH_OPENCLAW_USAGE=${monitorTargets
|
|
362
|
+
`IDLEWATCH_OPENCLAW_USAGE=${monitorTargetsNeedOpenClawUsage(monitorTargets) ? 'auto' : 'off'}`,
|
|
352
363
|
`IDLEWATCH_LOCAL_LOG_PATH=${localLogPath}`,
|
|
353
364
|
`IDLEWATCH_OPENCLAW_LAST_GOOD_CACHE_PATH=${localCachePath}`
|
|
354
365
|
]
|
package/src/openclaw-usage.js
CHANGED
|
@@ -166,6 +166,52 @@ function deriveTokensPerMinute(session) {
|
|
|
166
166
|
return Number((totalTokens / minutes).toFixed(2))
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
function pickTokenBreakdown(record, totals = {}) {
|
|
170
|
+
const inputTokens = pickNumber(
|
|
171
|
+
record?.inputTokens,
|
|
172
|
+
record?.input_tokens,
|
|
173
|
+
record?.promptTokens,
|
|
174
|
+
record?.prompt_tokens,
|
|
175
|
+
record?.tokens?.input,
|
|
176
|
+
record?.tokens?.prompt,
|
|
177
|
+
record?.tokenUsage?.input,
|
|
178
|
+
record?.token_usage?.input,
|
|
179
|
+
record?.usage?.inputTokens,
|
|
180
|
+
record?.usage?.input_tokens,
|
|
181
|
+
record?.usage?.tokens?.input,
|
|
182
|
+
record?.usage?.tokens?.prompt,
|
|
183
|
+
totals?.inputTokens,
|
|
184
|
+
totals?.input_tokens,
|
|
185
|
+
totals?.promptTokens,
|
|
186
|
+
totals?.prompt_tokens,
|
|
187
|
+
totals?.tokens?.input,
|
|
188
|
+
totals?.tokens?.prompt
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
const outputTokens = pickNumber(
|
|
192
|
+
record?.outputTokens,
|
|
193
|
+
record?.output_tokens,
|
|
194
|
+
record?.completionTokens,
|
|
195
|
+
record?.completion_tokens,
|
|
196
|
+
record?.tokens?.output,
|
|
197
|
+
record?.tokens?.completion,
|
|
198
|
+
record?.tokenUsage?.output,
|
|
199
|
+
record?.token_usage?.output,
|
|
200
|
+
record?.usage?.outputTokens,
|
|
201
|
+
record?.usage?.output_tokens,
|
|
202
|
+
record?.usage?.tokens?.output,
|
|
203
|
+
record?.usage?.tokens?.completion,
|
|
204
|
+
totals?.outputTokens,
|
|
205
|
+
totals?.output_tokens,
|
|
206
|
+
totals?.completionTokens,
|
|
207
|
+
totals?.completion_tokens,
|
|
208
|
+
totals?.tokens?.output,
|
|
209
|
+
totals?.tokens?.completion
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return { inputTokens, outputTokens }
|
|
213
|
+
}
|
|
214
|
+
|
|
169
215
|
function pickNewestSession(sessions = []) {
|
|
170
216
|
return sessions.reduce((best, candidate) => {
|
|
171
217
|
if (!best) return candidate
|
|
@@ -664,6 +710,7 @@ function parseFromStatusJson(parsed) {
|
|
|
664
710
|
session?.usage?.tokenCount,
|
|
665
711
|
session?.usage?.token_count
|
|
666
712
|
)
|
|
713
|
+
const { inputTokens, outputTokens } = pickTokenBreakdown(session, session?.usage)
|
|
667
714
|
const tokensPerMin = pickNumber(
|
|
668
715
|
session.tokensPerMinute,
|
|
669
716
|
session.tokens_per_minute,
|
|
@@ -768,26 +815,28 @@ function parseFromStatusJson(parsed) {
|
|
|
768
815
|
(Number.isFinite(sessionAgeMs) && sessionAgeMs >= 0 ? Date.now() - sessionAgeMs : pickTimestamp(parsed?.ts, parsed?.time, parsed?.updatedAt, parsed?.updated_at, parsed?.updatedAtMs, parsed?.timestamp))
|
|
769
816
|
|
|
770
817
|
const hasStrongUsage = model !== null || totalTokens !== null || tokensPerMin !== null
|
|
771
|
-
|
|
818
|
+
const provider = normalizeProviderName(
|
|
819
|
+
pickString(
|
|
820
|
+
session.provider,
|
|
821
|
+
session.providerName,
|
|
822
|
+
session.provider_name,
|
|
823
|
+
session?.usage?.provider,
|
|
824
|
+
session?.usage?.providerName,
|
|
825
|
+
session?.usage?.provider_name,
|
|
826
|
+
parsed?.provider,
|
|
827
|
+
parsed?.providerName,
|
|
828
|
+
parsed?.provider_name,
|
|
829
|
+
parsed?.status?.provider,
|
|
830
|
+
parsed?.status?.providerName,
|
|
831
|
+
parsed?.status?.provider_name
|
|
832
|
+
)
|
|
833
|
+
) ?? inferProviderFromModel(model)
|
|
772
834
|
return {
|
|
773
835
|
model,
|
|
774
|
-
provider
|
|
775
|
-
pickString(
|
|
776
|
-
session.provider,
|
|
777
|
-
session.providerName,
|
|
778
|
-
session.provider_name,
|
|
779
|
-
session?.usage?.provider,
|
|
780
|
-
session?.usage?.providerName,
|
|
781
|
-
session?.usage?.provider_name,
|
|
782
|
-
parsed?.provider,
|
|
783
|
-
parsed?.providerName,
|
|
784
|
-
parsed?.provider_name,
|
|
785
|
-
parsed?.status?.provider,
|
|
786
|
-
parsed?.status?.providerName,
|
|
787
|
-
parsed?.status?.provider_name
|
|
788
|
-
)
|
|
789
|
-
) ?? inferProviderFromModel(model),
|
|
836
|
+
provider,
|
|
790
837
|
totalTokens,
|
|
838
|
+
inputTokens,
|
|
839
|
+
outputTokens,
|
|
791
840
|
tokensPerMin,
|
|
792
841
|
remainingTokens,
|
|
793
842
|
percentUsed,
|
|
@@ -937,6 +986,7 @@ function parseGenericUsage(parsed) {
|
|
|
937
986
|
parsed?.totals?.tokenCount,
|
|
938
987
|
parsed?.totals?.total_tokens
|
|
939
988
|
)
|
|
989
|
+
const { inputTokens, outputTokens } = pickTokenBreakdown(usageRecord, usageTotals)
|
|
940
990
|
const tokensPerMin = pickNumber(
|
|
941
991
|
usageRecord?.tokensPerMinute,
|
|
942
992
|
usageRecord?.tokens_per_minute,
|
|
@@ -992,29 +1042,31 @@ function parseGenericUsage(parsed) {
|
|
|
992
1042
|
)
|
|
993
1043
|
|
|
994
1044
|
if (model === null && totalTokens === null && tokensPerMin === null) return null
|
|
995
|
-
|
|
1045
|
+
const provider = normalizeProviderName(
|
|
1046
|
+
pickString(
|
|
1047
|
+
usageRecord?.provider,
|
|
1048
|
+
usageRecord?.providerName,
|
|
1049
|
+
usageRecord?.provider_name,
|
|
1050
|
+
usageRecord?.usage?.provider,
|
|
1051
|
+
usageRecord?.usage?.providerName,
|
|
1052
|
+
usageRecord?.usage?.provider_name,
|
|
1053
|
+
usageTotals?.provider,
|
|
1054
|
+
usageTotals?.providerName,
|
|
1055
|
+
usageTotals?.provider_name,
|
|
1056
|
+
parsed?.provider,
|
|
1057
|
+
parsed?.providerName,
|
|
1058
|
+
parsed?.provider_name,
|
|
1059
|
+
parsed?.status?.provider,
|
|
1060
|
+
parsed?.status?.providerName,
|
|
1061
|
+
parsed?.status?.provider_name
|
|
1062
|
+
)
|
|
1063
|
+
) ?? inferProviderFromModel(model)
|
|
996
1064
|
return {
|
|
997
1065
|
model,
|
|
998
|
-
provider
|
|
999
|
-
pickString(
|
|
1000
|
-
usageRecord?.provider,
|
|
1001
|
-
usageRecord?.providerName,
|
|
1002
|
-
usageRecord?.provider_name,
|
|
1003
|
-
usageRecord?.usage?.provider,
|
|
1004
|
-
usageRecord?.usage?.providerName,
|
|
1005
|
-
usageRecord?.usage?.provider_name,
|
|
1006
|
-
usageTotals?.provider,
|
|
1007
|
-
usageTotals?.providerName,
|
|
1008
|
-
usageTotals?.provider_name,
|
|
1009
|
-
parsed?.provider,
|
|
1010
|
-
parsed?.providerName,
|
|
1011
|
-
parsed?.provider_name,
|
|
1012
|
-
parsed?.status?.provider,
|
|
1013
|
-
parsed?.status?.providerName,
|
|
1014
|
-
parsed?.status?.provider_name
|
|
1015
|
-
)
|
|
1016
|
-
) ?? inferProviderFromModel(model),
|
|
1066
|
+
provider,
|
|
1017
1067
|
totalTokens,
|
|
1068
|
+
inputTokens,
|
|
1069
|
+
outputTokens,
|
|
1018
1070
|
tokensPerMin,
|
|
1019
1071
|
remainingTokens,
|
|
1020
1072
|
percentUsed,
|
package/src/telemetry-mapping.js
CHANGED
|
@@ -22,12 +22,17 @@ export function enrichWithOpenClawFleetTelemetry(sample, context = {}) {
|
|
|
22
22
|
cpuPct: sample.cpuPct ?? null,
|
|
23
23
|
memUsedPct: sample.memUsedPct ?? sample.memPct ?? null,
|
|
24
24
|
memPressurePct: sample.memPressurePct ?? null,
|
|
25
|
-
memPressureClass: sample.memPressureClass ?? 'unavailable'
|
|
25
|
+
memPressureClass: sample.memPressureClass ?? 'unavailable',
|
|
26
|
+
tempC: sample.deviceTempC ?? null,
|
|
27
|
+
thermalLevel: sample.thermalLevel ?? null,
|
|
28
|
+
thermalState: sample.thermalState ?? 'unavailable'
|
|
26
29
|
},
|
|
27
30
|
usage: {
|
|
28
31
|
model: sample.openclawModel ?? null,
|
|
29
32
|
provider: sample.openclawProvider ?? null,
|
|
30
33
|
totalTokens: sample.openclawTotalTokens ?? null,
|
|
34
|
+
inputTokens: sample.openclawInputTokens ?? null,
|
|
35
|
+
outputTokens: sample.openclawOutputTokens ?? null,
|
|
31
36
|
remainingTokens: sample.openclawRemainingTokens ?? null,
|
|
32
37
|
percentUsed: sample.openclawPercentUsed ?? null,
|
|
33
38
|
contextTokens: sample.openclawContextTokens ?? null,
|
package/src/thermal.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { execSync } from 'child_process'
|
|
2
|
+
|
|
3
|
+
function clampTempC(value) {
|
|
4
|
+
if (!Number.isFinite(value)) return null
|
|
5
|
+
return Number(value.toFixed(1))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function parseIstatsTemperature(text) {
|
|
9
|
+
if (!text) return null
|
|
10
|
+
const match = text.match(/(-?\d+(?:\.\d+)?)\s*(?:°\s*[CF]|deg(?:rees)?\s*[CF]?)/i) || text.match(/(-?\d+(?:\.\d+)?)/)
|
|
11
|
+
return match ? clampTempC(Number(match[1])) : null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseOsxCpuTemp(text) {
|
|
15
|
+
if (!text) return null
|
|
16
|
+
const match = text.match(/(-?\d+(?:\.\d+)?)\s*°?\s*C/i)
|
|
17
|
+
return match ? clampTempC(Number(match[1])) : null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parsePmsetThermal(text) {
|
|
21
|
+
if (!text) return { level: null, state: 'unavailable' }
|
|
22
|
+
const normalized = text.toLowerCase()
|
|
23
|
+
|
|
24
|
+
if (normalized.includes('no thermal warning level has been recorded')) {
|
|
25
|
+
return { level: 0, state: 'nominal' }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const match = text.match(/thermal warning level[^0-9]*([0-9]+)/i)
|
|
29
|
+
if (!match) return { level: null, state: 'unavailable' }
|
|
30
|
+
|
|
31
|
+
const level = Number(match[1])
|
|
32
|
+
if (!Number.isFinite(level)) return { level: null, state: 'unavailable' }
|
|
33
|
+
|
|
34
|
+
if (level <= 0) return { level, state: 'nominal' }
|
|
35
|
+
if (level === 1) return { level, state: 'elevated' }
|
|
36
|
+
if (level === 2) return { level, state: 'high' }
|
|
37
|
+
return { level, state: 'critical' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function thermalSampleDarwin(exec = execSync) {
|
|
41
|
+
const probes = [
|
|
42
|
+
{
|
|
43
|
+
cmd: 'istats cpu temp',
|
|
44
|
+
source: 'istats',
|
|
45
|
+
parse: parseIstatsTemperature,
|
|
46
|
+
timeoutMs: 1200
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
cmd: 'osx-cpu-temp',
|
|
50
|
+
source: 'osx-cpu-temp',
|
|
51
|
+
parse: parseOsxCpuTemp,
|
|
52
|
+
timeoutMs: 1200
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
for (const probe of probes) {
|
|
57
|
+
try {
|
|
58
|
+
const out = exec(probe.cmd, {
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
61
|
+
timeout: probe.timeoutMs
|
|
62
|
+
})
|
|
63
|
+
const tempC = probe.parse(out)
|
|
64
|
+
if (tempC != null) {
|
|
65
|
+
const pressure = parsePmsetThermal(
|
|
66
|
+
exec('pmset -g therm', {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
69
|
+
timeout: 1000
|
|
70
|
+
})
|
|
71
|
+
)
|
|
72
|
+
return {
|
|
73
|
+
tempC,
|
|
74
|
+
source: probe.source,
|
|
75
|
+
thermalLevel: pressure.level,
|
|
76
|
+
thermalState: pressure.state
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// keep probing
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const pressure = parsePmsetThermal(
|
|
86
|
+
exec('pmset -g therm', {
|
|
87
|
+
encoding: 'utf8',
|
|
88
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
89
|
+
timeout: 1000
|
|
90
|
+
})
|
|
91
|
+
)
|
|
92
|
+
return {
|
|
93
|
+
tempC: null,
|
|
94
|
+
source: 'pmset-therm',
|
|
95
|
+
thermalLevel: pressure.level,
|
|
96
|
+
thermalState: pressure.state
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
return {
|
|
100
|
+
tempC: null,
|
|
101
|
+
source: 'unavailable',
|
|
102
|
+
thermalLevel: null,
|
|
103
|
+
thermalState: 'unavailable'
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const __thermalTestUtils = {
|
|
109
|
+
clampTempC,
|
|
110
|
+
parseIstatsTemperature,
|
|
111
|
+
parseOsxCpuTemp,
|
|
112
|
+
parsePmsetThermal
|
|
113
|
+
}
|
package/tui/src/main.rs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
use anyhow::{anyhow, Result};
|
|
2
2
|
use crossterm::{
|
|
3
|
-
event::{self, Event, KeyCode},
|
|
3
|
+
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
|
|
4
4
|
execute,
|
|
5
5
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
6
6
|
};
|
|
@@ -19,7 +19,9 @@ use std::{
|
|
|
19
19
|
#[derive(Clone)]
|
|
20
20
|
struct MonitorTarget {
|
|
21
21
|
key: &'static str,
|
|
22
|
+
group: &'static str,
|
|
22
23
|
label: &'static str,
|
|
24
|
+
description: &'static str,
|
|
23
25
|
available: bool,
|
|
24
26
|
selected: bool,
|
|
25
27
|
}
|
|
@@ -136,8 +138,9 @@ fn detect_monitor_targets(existing: &[String]) -> Vec<MonitorTarget> {
|
|
|
136
138
|
let gpu_available = cfg!(target_os = "macos") || command_exists("nvidia-smi", &["--help"]);
|
|
137
139
|
let has_existing = !existing.is_empty();
|
|
138
140
|
let wants = |key: &str, fallback: bool| {
|
|
141
|
+
let has_legacy_openclaw = existing.iter().any(|item| item == "openclaw");
|
|
139
142
|
if has_existing {
|
|
140
|
-
existing.iter().any(|item| item == key)
|
|
143
|
+
existing.iter().any(|item| item == key) || (has_legacy_openclaw && matches!(key, "agent_activity" | "token_usage" | "runtime_state"))
|
|
141
144
|
} else {
|
|
142
145
|
fallback
|
|
143
146
|
}
|
|
@@ -146,27 +149,59 @@ fn detect_monitor_targets(existing: &[String]) -> Vec<MonitorTarget> {
|
|
|
146
149
|
vec![
|
|
147
150
|
MonitorTarget {
|
|
148
151
|
key: "cpu",
|
|
152
|
+
group: "Compute",
|
|
149
153
|
label: "CPU usage",
|
|
154
|
+
description: "machine load",
|
|
150
155
|
available: true,
|
|
151
156
|
selected: wants("cpu", true),
|
|
152
157
|
},
|
|
153
158
|
MonitorTarget {
|
|
154
159
|
key: "memory",
|
|
160
|
+
group: "Compute",
|
|
155
161
|
label: "Memory usage",
|
|
162
|
+
description: "resident memory pressure",
|
|
156
163
|
available: true,
|
|
157
164
|
selected: wants("memory", true),
|
|
158
165
|
},
|
|
159
166
|
MonitorTarget {
|
|
160
167
|
key: "gpu",
|
|
168
|
+
group: "Compute",
|
|
161
169
|
label: "GPU usage",
|
|
170
|
+
description: "accelerator load",
|
|
162
171
|
available: gpu_available,
|
|
163
172
|
selected: wants("gpu", gpu_available),
|
|
164
173
|
},
|
|
165
174
|
MonitorTarget {
|
|
166
|
-
key: "
|
|
167
|
-
|
|
175
|
+
key: "temperature",
|
|
176
|
+
group: "Compute",
|
|
177
|
+
label: "Temperature",
|
|
178
|
+
description: "cpu temp when available, thermal state otherwise",
|
|
179
|
+
available: cfg!(target_os = "macos"),
|
|
180
|
+
selected: wants("temperature", cfg!(target_os = "macos")),
|
|
181
|
+
},
|
|
182
|
+
MonitorTarget {
|
|
183
|
+
key: "agent_activity",
|
|
184
|
+
group: "OpenClaw Agents",
|
|
185
|
+
label: "Idle / awake time",
|
|
186
|
+
description: "cron run history and idle wedges",
|
|
187
|
+
available: openclaw_available,
|
|
188
|
+
selected: wants("agent_activity", openclaw_available),
|
|
189
|
+
},
|
|
190
|
+
MonitorTarget {
|
|
191
|
+
key: "token_usage",
|
|
192
|
+
group: "OpenClaw Agents",
|
|
193
|
+
label: "Token usage",
|
|
194
|
+
description: "total, input, output, rate",
|
|
195
|
+
available: openclaw_available,
|
|
196
|
+
selected: wants("token_usage", openclaw_available),
|
|
197
|
+
},
|
|
198
|
+
MonitorTarget {
|
|
199
|
+
key: "runtime_state",
|
|
200
|
+
group: "OpenClaw Agents",
|
|
201
|
+
label: "Runtime metadata",
|
|
202
|
+
description: "provider, model, session state",
|
|
168
203
|
available: openclaw_available,
|
|
169
|
-
selected: wants("
|
|
204
|
+
selected: wants("runtime_state", openclaw_available),
|
|
170
205
|
},
|
|
171
206
|
]
|
|
172
207
|
}
|
|
@@ -232,7 +267,7 @@ fn render_mode_menu(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, selec
|
|
|
232
267
|
);
|
|
233
268
|
f.render_widget(path, chunks[2]);
|
|
234
269
|
|
|
235
|
-
let help = Paragraph::new("↑/↓ move • Enter select •
|
|
270
|
+
let help = Paragraph::new("↑/↓ move • Enter select • Ctrl+C quit")
|
|
236
271
|
.style(Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD));
|
|
237
272
|
f.render_widget(help, chunks[3]);
|
|
238
273
|
})?;
|
|
@@ -250,7 +285,7 @@ fn render_monitor_menu(
|
|
|
250
285
|
.margin(1)
|
|
251
286
|
.constraints([
|
|
252
287
|
Constraint::Length(4),
|
|
253
|
-
Constraint::Length(
|
|
288
|
+
Constraint::Length(16),
|
|
254
289
|
Constraint::Min(1),
|
|
255
290
|
])
|
|
256
291
|
.split(f.area());
|
|
@@ -265,15 +300,22 @@ fn render_monitor_menu(
|
|
|
265
300
|
);
|
|
266
301
|
f.render_widget(title, chunks[0]);
|
|
267
302
|
|
|
268
|
-
let items =
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
303
|
+
let mut items = Vec::new();
|
|
304
|
+
let mut last_group = "";
|
|
305
|
+
for (idx, target) in monitors.iter().enumerate() {
|
|
306
|
+
if target.group != last_group {
|
|
307
|
+
last_group = target.group;
|
|
308
|
+
items.push(
|
|
309
|
+
ListItem::new(format!(" {}", target.group))
|
|
310
|
+
.style(Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD)),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
272
314
|
let marker = if target.selected { "[x]" } else { "[ ]" };
|
|
273
315
|
let unavailable = if target.available { "" } else { " (not detected)" };
|
|
274
|
-
let line = format!("{} {}{}", marker, target.label, unavailable);
|
|
316
|
+
let line = format!("{} {} — {}{}", marker, target.label, target.description, unavailable);
|
|
275
317
|
|
|
276
|
-
if idx == cursor {
|
|
318
|
+
let item = if idx == cursor {
|
|
277
319
|
ListItem::new(format!("❯ {}", line)).style(
|
|
278
320
|
Style::default()
|
|
279
321
|
.fg(Color::White)
|
|
@@ -284,9 +326,9 @@ fn render_monitor_menu(
|
|
|
284
326
|
ListItem::new(format!(" {}", line)).style(Style::default().fg(Color::Cyan))
|
|
285
327
|
} else {
|
|
286
328
|
ListItem::new(format!(" {}", line)).style(Style::default().fg(Color::DarkGray))
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
329
|
+
};
|
|
330
|
+
items.push(item);
|
|
331
|
+
}
|
|
290
332
|
|
|
291
333
|
let list = List::new(items).block(
|
|
292
334
|
Block::default()
|
|
@@ -356,7 +398,7 @@ fn render_device_name_prompt(
|
|
|
356
398
|
);
|
|
357
399
|
f.render_widget(warning_widget, chunks[2]);
|
|
358
400
|
|
|
359
|
-
let help = Paragraph::new("Type name • Backspace edit • Enter continue •
|
|
401
|
+
let help = Paragraph::new("Type name • Backspace edit • Enter continue • Ctrl+C quit")
|
|
360
402
|
.style(Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD));
|
|
361
403
|
f.render_widget(help, chunks[3]);
|
|
362
404
|
})?;
|
|
@@ -421,7 +463,7 @@ fn render_api_key_prompt(
|
|
|
421
463
|
);
|
|
422
464
|
f.render_widget(warning_widget, chunks[2]);
|
|
423
465
|
|
|
424
|
-
let help = Paragraph::new("Paste key • Backspace edit • Enter continue •
|
|
466
|
+
let help = Paragraph::new("Paste key • Backspace edit • Enter continue • Ctrl+C quit")
|
|
425
467
|
.style(Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD));
|
|
426
468
|
f.render_widget(help, chunks[3]);
|
|
427
469
|
})?;
|
|
@@ -466,6 +508,11 @@ fn sanitize_device_id(raw: &str, fallback: &str) -> String {
|
|
|
466
508
|
out.trim_matches('-').to_string()
|
|
467
509
|
}
|
|
468
510
|
|
|
511
|
+
fn is_quit_key(key: &KeyEvent) -> bool {
|
|
512
|
+
matches!(key.code, KeyCode::Esc | KeyCode::Char('q'))
|
|
513
|
+
|| (matches!(key.code, KeyCode::Char('c')) && key.modifiers.contains(KeyModifiers::CONTROL))
|
|
514
|
+
}
|
|
515
|
+
|
|
469
516
|
fn main() -> Result<()> {
|
|
470
517
|
let config_dir = default_config_dir();
|
|
471
518
|
let env_file = std::env::var("IDLEWATCH_ENROLL_OUTPUT_ENV_FILE")
|
|
@@ -489,7 +536,7 @@ fn main() -> Result<()> {
|
|
|
489
536
|
KeyCode::Up => selected_mode = selected_mode.saturating_sub(1),
|
|
490
537
|
KeyCode::Down => selected_mode = (selected_mode + 1).min(1),
|
|
491
538
|
KeyCode::Enter => break,
|
|
492
|
-
|
|
539
|
+
_ if is_quit_key(&key) => {
|
|
493
540
|
disable_raw_mode()?;
|
|
494
541
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
495
542
|
return Err(anyhow!("setup_cancelled"));
|
|
@@ -528,7 +575,7 @@ fn main() -> Result<()> {
|
|
|
528
575
|
}
|
|
529
576
|
break;
|
|
530
577
|
}
|
|
531
|
-
|
|
578
|
+
_ if is_quit_key(&key) => {
|
|
532
579
|
disable_raw_mode()?;
|
|
533
580
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
534
581
|
return Err(anyhow!("setup_cancelled"));
|
|
@@ -570,7 +617,7 @@ fn main() -> Result<()> {
|
|
|
570
617
|
device_name_input.pop();
|
|
571
618
|
device_name_error = None;
|
|
572
619
|
}
|
|
573
|
-
|
|
620
|
+
_ if is_quit_key(&key) => {
|
|
574
621
|
disable_raw_mode()?;
|
|
575
622
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
576
623
|
return Err(anyhow!("setup_cancelled"));
|
|
@@ -617,7 +664,7 @@ fn main() -> Result<()> {
|
|
|
617
664
|
cloud_api_key_input.pop();
|
|
618
665
|
cloud_api_key_error = None;
|
|
619
666
|
}
|
|
620
|
-
|
|
667
|
+
_ if is_quit_key(&key) => {
|
|
621
668
|
disable_raw_mode()?;
|
|
622
669
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
623
670
|
return Err(anyhow!("setup_cancelled"));
|
|
@@ -653,7 +700,9 @@ fn main() -> Result<()> {
|
|
|
653
700
|
selected_keys.join(",")
|
|
654
701
|
};
|
|
655
702
|
|
|
656
|
-
let
|
|
703
|
+
let monitor_openclaw_usage = monitor_targets_csv
|
|
704
|
+
.split(',')
|
|
705
|
+
.any(|item| matches!(item, "token_usage" | "runtime_state"));
|
|
657
706
|
let device_name = normalize_device_name(&device_name_input, &host);
|
|
658
707
|
let safe_device_id = {
|
|
659
708
|
let candidate = sanitize_device_id(
|
|
@@ -668,7 +717,7 @@ fn main() -> Result<()> {
|
|
|
668
717
|
format!("IDLEWATCH_DEVICE_NAME={}", device_name),
|
|
669
718
|
format!("IDLEWATCH_DEVICE_ID={}", safe_device_id),
|
|
670
719
|
format!("IDLEWATCH_MONITOR_TARGETS={}", monitor_targets_csv),
|
|
671
|
-
format!("IDLEWATCH_OPENCLAW_USAGE={}", if
|
|
720
|
+
format!("IDLEWATCH_OPENCLAW_USAGE={}", if monitor_openclaw_usage { "auto" } else { "off" }),
|
|
672
721
|
format!(
|
|
673
722
|
"IDLEWATCH_LOCAL_LOG_PATH={}",
|
|
674
723
|
config_dir.join("logs").join(format!("{}-metrics.ndjson", safe_device_id)).display()
|