gitdone-agent 0.8.12 → 0.8.13
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/index.js +207 -2
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -32,7 +32,7 @@ import { randomUUID, createHash } from 'node:crypto'
|
|
|
32
32
|
// Reported to the server on every sync so the web UI can flag outdated agents.
|
|
33
33
|
// Keep in lockstep with packages/agent/package.json. The server's offline
|
|
34
34
|
// fallback is bumped only after this release has actually reached npm.
|
|
35
|
-
const AGENT_VERSION = '0.8.
|
|
35
|
+
const AGENT_VERSION = '0.8.13'
|
|
36
36
|
|
|
37
37
|
const AGENT_DIR = join(homedir(), '.gitdone-agent')
|
|
38
38
|
const CONFIG_PATH = join(AGENT_DIR, 'config.json')
|
|
@@ -41,6 +41,7 @@ const CONFIG_BAK_PATH = CONFIG_PATH + '.bak'
|
|
|
41
41
|
const STABLE_AGENT = join(AGENT_DIR, 'agent.mjs')
|
|
42
42
|
const LOG_PATH = join(AGENT_DIR, 'agent.log')
|
|
43
43
|
const GAME_MONITOR_PATH = join(AGENT_DIR, 'game-monitor.ps1')
|
|
44
|
+
const GAME_OVERLAY_STATE_PATH = join(AGENT_DIR, 'game-overlay-state.json')
|
|
44
45
|
|
|
45
46
|
function ensureAgentDir() {
|
|
46
47
|
if (!existsSync(AGENT_DIR)) mkdirSync(AGENT_DIR, { recursive: true })
|
|
@@ -1221,6 +1222,7 @@ async function pushWowProgress(cfg) {
|
|
|
1221
1222
|
const GAME_PROFILES = [
|
|
1222
1223
|
{
|
|
1223
1224
|
id: 'ac-black-flag-resynced',
|
|
1225
|
+
shortName: 'Black Flag Resynced',
|
|
1224
1226
|
processNames: ['ACBlackFlag', 'ACBlackFlag_Plus'],
|
|
1225
1227
|
sha256: 'EE40622D0F25126A11BF05DBEEC9C128B9B3A67373BC1FA5692E4C4C2D4665E3',
|
|
1226
1228
|
heartbeatRvas: [0x0CCF9590, 0x0CCF9098],
|
|
@@ -1231,11 +1233,91 @@ const GAME_PROFILES = [
|
|
|
1231
1233
|
const GAME_REPORT_HEARTBEAT_MS = 5_000
|
|
1232
1234
|
const gameLastReport = new Map()
|
|
1233
1235
|
let gameReportQueue = Promise.resolve()
|
|
1236
|
+
let gameDisplays = []
|
|
1237
|
+
let gameOverlaySettings = {
|
|
1238
|
+
enabled: false,
|
|
1239
|
+
monitor: 0,
|
|
1240
|
+
position: 'top-right',
|
|
1241
|
+
fontFamily: 'Segoe UI',
|
|
1242
|
+
fontSize: 24,
|
|
1243
|
+
transparent: true,
|
|
1244
|
+
}
|
|
1245
|
+
let activeGameOverlay = null
|
|
1246
|
+
let lastOverlayStateJson = ''
|
|
1247
|
+
|
|
1248
|
+
function applyGameOverlaySettings(value) {
|
|
1249
|
+
if (!value || typeof value !== 'object') return
|
|
1250
|
+
gameOverlaySettings = {
|
|
1251
|
+
enabled: value.enabled === true,
|
|
1252
|
+
monitor: Number.isInteger(value.monitor) ? Math.max(0, Math.min(15, value.monitor)) : 0,
|
|
1253
|
+
position: ['top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right'].includes(value.position) ? value.position : 'top-right',
|
|
1254
|
+
fontFamily: typeof value.fontFamily === 'string' ? value.fontFamily.slice(0, 64) : 'Segoe UI',
|
|
1255
|
+
fontSize: Number.isInteger(value.fontSize) ? Math.max(14, Math.min(72, value.fontSize)) : 24,
|
|
1256
|
+
transparent: value.transparent !== false,
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function localDayStartIso() {
|
|
1261
|
+
const day = new Date()
|
|
1262
|
+
day.setHours(0, 0, 0, 0)
|
|
1263
|
+
return day.toISOString()
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function formatGameClock(milliseconds) {
|
|
1267
|
+
const seconds = Math.max(0, Math.floor(milliseconds / 1000))
|
|
1268
|
+
const hh = String(Math.floor(seconds / 3600)).padStart(2, '0')
|
|
1269
|
+
const mm = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0')
|
|
1270
|
+
const ss = String(seconds % 60).padStart(2, '0')
|
|
1271
|
+
return `${hh}:${mm}:${ss}`
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function writeGameOverlayState() {
|
|
1275
|
+
const now = Date.now()
|
|
1276
|
+
const runtime = activeGameOverlay
|
|
1277
|
+
const todayMs = runtime
|
|
1278
|
+
? runtime.todayMs + (runtime.playing ? Math.max(0, now - runtime.asOf) : 0)
|
|
1279
|
+
: 0
|
|
1280
|
+
const profile = runtime ? GAME_PROFILES.find((item) => item.id === runtime.gameId) : null
|
|
1281
|
+
const state = {
|
|
1282
|
+
...gameOverlaySettings,
|
|
1283
|
+
visible: gameOverlaySettings.enabled && runtime?.playing === true,
|
|
1284
|
+
text: profile ? `${profile.shortName} · Днес ${formatGameClock(todayMs)}` : '',
|
|
1285
|
+
}
|
|
1286
|
+
const json = JSON.stringify(state)
|
|
1287
|
+
if (json === lastOverlayStateJson) return
|
|
1288
|
+
lastOverlayStateJson = json
|
|
1289
|
+
try {
|
|
1290
|
+
ensureAgentDir()
|
|
1291
|
+
const temp = GAME_OVERLAY_STATE_PATH + '.tmp'
|
|
1292
|
+
writeFileSync(temp, json, 'utf8')
|
|
1293
|
+
renameSync(temp, GAME_OVERLAY_STATE_PATH)
|
|
1294
|
+
} catch (err) {
|
|
1295
|
+
log(`✗ game overlay state write failed: ${err.message}`)
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function observeGameActivity(gameId, state, observedAt) {
|
|
1300
|
+
const at = Date.parse(observedAt) || Date.now()
|
|
1301
|
+
const playing = state === 'playing'
|
|
1302
|
+
if (!activeGameOverlay || activeGameOverlay.gameId !== gameId) {
|
|
1303
|
+
if (playing) activeGameOverlay = { gameId, playing: true, todayMs: 0, asOf: at }
|
|
1304
|
+
} else if (activeGameOverlay.playing !== playing) {
|
|
1305
|
+
if (activeGameOverlay.playing) {
|
|
1306
|
+
activeGameOverlay.todayMs += Math.max(0, at - activeGameOverlay.asOf)
|
|
1307
|
+
}
|
|
1308
|
+
activeGameOverlay.playing = playing
|
|
1309
|
+
activeGameOverlay.asOf = at
|
|
1310
|
+
}
|
|
1311
|
+
writeGameOverlayState()
|
|
1312
|
+
}
|
|
1234
1313
|
|
|
1235
1314
|
function gameMonitorScript() {
|
|
1236
1315
|
const profiles = Buffer.from(JSON.stringify(GAME_PROFILES), 'utf8').toString('base64')
|
|
1316
|
+
const overlayStatePath = Buffer.from(GAME_OVERLAY_STATE_PATH, 'utf8').toString('base64')
|
|
1237
1317
|
return String.raw`
|
|
1238
1318
|
$ErrorActionPreference = 'Stop'
|
|
1319
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
1320
|
+
Add-Type -AssemblyName System.Drawing
|
|
1239
1321
|
Add-Type -TypeDefinition @'
|
|
1240
1322
|
using System;
|
|
1241
1323
|
using System.Runtime.InteropServices;
|
|
@@ -1246,6 +1328,10 @@ public static class GitDoneGameMemory {
|
|
|
1246
1328
|
public static extern bool ReadProcessMemory(IntPtr process, IntPtr address, byte[] buffer, int size, out UIntPtr read);
|
|
1247
1329
|
[DllImport("kernel32.dll")]
|
|
1248
1330
|
public static extern bool CloseHandle(IntPtr handle);
|
|
1331
|
+
[DllImport("user32.dll", SetLastError=true)]
|
|
1332
|
+
public static extern int GetWindowLong(IntPtr window, int index);
|
|
1333
|
+
[DllImport("user32.dll", SetLastError=true)]
|
|
1334
|
+
public static extern int SetWindowLong(IntPtr window, int index, int value);
|
|
1249
1335
|
public static UInt32 ReadUInt32(IntPtr process, UInt64 address) {
|
|
1250
1336
|
var bytes = new byte[4]; UIntPtr read;
|
|
1251
1337
|
if (!ReadProcessMemory(process, new IntPtr(unchecked((long)address)), bytes, 4, out read) || read.ToUInt64() != 4)
|
|
@@ -1256,8 +1342,100 @@ public static class GitDoneGameMemory {
|
|
|
1256
1342
|
'@
|
|
1257
1343
|
$profilesJson = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${profiles}'))
|
|
1258
1344
|
$profiles = @($profilesJson | ConvertFrom-Json)
|
|
1345
|
+
$overlayStatePath = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${overlayStatePath}'))
|
|
1259
1346
|
$attached = @{}
|
|
1260
1347
|
|
|
1348
|
+
$form = New-Object System.Windows.Forms.Form
|
|
1349
|
+
$form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::None
|
|
1350
|
+
$form.ShowInTaskbar = $false
|
|
1351
|
+
$form.TopMost = $true
|
|
1352
|
+
$form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
|
|
1353
|
+
$form.BackColor = [System.Drawing.Color]::Black
|
|
1354
|
+
$form.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Dpi
|
|
1355
|
+
$label = New-Object System.Windows.Forms.Label
|
|
1356
|
+
$label.AutoSize = $true
|
|
1357
|
+
$label.ForeColor = [System.Drawing.Color]::White
|
|
1358
|
+
$label.BackColor = [System.Drawing.Color]::Transparent
|
|
1359
|
+
$label.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
|
|
1360
|
+
$form.Controls.Add($label)
|
|
1361
|
+
$null = $form.Handle
|
|
1362
|
+
$extendedStyle = [GitDoneGameMemory]::GetWindowLong($form.Handle, -20)
|
|
1363
|
+
# WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE: never steal a click,
|
|
1364
|
+
# never take keyboard focus from the game, and stay out of Alt+Tab.
|
|
1365
|
+
[GitDoneGameMemory]::SetWindowLong($form.Handle, -20, $extendedStyle -bor 0x20 -bor 0x80 -bor 0x08000000) | Out-Null
|
|
1366
|
+
$form.Hide()
|
|
1367
|
+
$lastOverlayWrite = [DateTime]::MinValue
|
|
1368
|
+
$overlayWasVisible = $false
|
|
1369
|
+
|
|
1370
|
+
$screens = @([System.Windows.Forms.Screen]::AllScreens)
|
|
1371
|
+
$displayRows = @()
|
|
1372
|
+
for ($index = 0; $index -lt $screens.Count; $index++) {
|
|
1373
|
+
$screen = $screens[$index]
|
|
1374
|
+
$displayRows += [pscustomobject]@{
|
|
1375
|
+
index = $index
|
|
1376
|
+
name = $screen.DeviceName
|
|
1377
|
+
primary = $screen.Primary
|
|
1378
|
+
width = $screen.Bounds.Width
|
|
1379
|
+
height = $screen.Bounds.Height
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
[pscustomobject]@{ type='displays'; displays=$displayRows } | ConvertTo-Json -Depth 4 -Compress
|
|
1383
|
+
[Console]::Out.Flush()
|
|
1384
|
+
|
|
1385
|
+
function Update-Overlay {
|
|
1386
|
+
try {
|
|
1387
|
+
if (!(Test-Path -LiteralPath $overlayStatePath)) { $form.Hide(); $script:overlayWasVisible = $false; return }
|
|
1388
|
+
$write = (Get-Item -LiteralPath $overlayStatePath).LastWriteTimeUtc
|
|
1389
|
+
if ($write -eq $lastOverlayWrite) { return }
|
|
1390
|
+
$script:lastOverlayWrite = $write
|
|
1391
|
+
$state = Get-Content -LiteralPath $overlayStatePath -Raw | ConvertFrom-Json
|
|
1392
|
+
if (!$state.enabled -or !$state.visible -or [string]::IsNullOrWhiteSpace($state.text)) {
|
|
1393
|
+
$form.Hide()
|
|
1394
|
+
$script:overlayWasVisible = $false
|
|
1395
|
+
return
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
$screenIndex = [Math]::Max(0, [Math]::Min($screens.Count - 1, [int]$state.monitor))
|
|
1399
|
+
$screen = $screens[$screenIndex]
|
|
1400
|
+
$label.Text = [string]$state.text
|
|
1401
|
+
$label.Font = [System.Drawing.Font]::new([string]$state.fontFamily, [single]$state.fontSize, [System.Drawing.FontStyle]::Bold, [System.Drawing.GraphicsUnit]::Pixel)
|
|
1402
|
+
$paddingX = if ($state.transparent) { 3 } else { 12 }
|
|
1403
|
+
$paddingY = if ($state.transparent) { 2 } else { 7 }
|
|
1404
|
+
$label.Location = [System.Drawing.Point]::new([int]$paddingX, [int]$paddingY)
|
|
1405
|
+
$form.ClientSize = [System.Drawing.Size]::new([int]($label.PreferredWidth + 2 * $paddingX), [int]($label.PreferredHeight + 2 * $paddingY))
|
|
1406
|
+
if ($state.transparent) {
|
|
1407
|
+
$form.BackColor = [System.Drawing.Color]::Magenta
|
|
1408
|
+
$form.TransparencyKey = [System.Drawing.Color]::Magenta
|
|
1409
|
+
} else {
|
|
1410
|
+
$form.TransparencyKey = [System.Drawing.Color]::Empty
|
|
1411
|
+
$form.BackColor = [System.Drawing.Color]::FromArgb(22, 27, 38)
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
$margin = 24
|
|
1415
|
+
$area = $screen.WorkingArea
|
|
1416
|
+
$x = switch ([string]$state.position) {
|
|
1417
|
+
'top-left' { $area.Left + $margin; break }
|
|
1418
|
+
'bottom-left' { $area.Left + $margin; break }
|
|
1419
|
+
'top-center' { $area.Left + [Math]::Floor(($area.Width - $form.Width) / 2); break }
|
|
1420
|
+
'bottom-center' { $area.Left + [Math]::Floor(($area.Width - $form.Width) / 2); break }
|
|
1421
|
+
default { $area.Right - $form.Width - $margin }
|
|
1422
|
+
}
|
|
1423
|
+
$y = if ([string]$state.position -like 'bottom-*') { $area.Bottom - $form.Height - $margin } else { $area.Top + $margin }
|
|
1424
|
+
$form.Location = [System.Drawing.Point]::new([int]$x, [int]$y)
|
|
1425
|
+
if (!$form.Visible) { $form.Show() }
|
|
1426
|
+
if (!$script:overlayWasVisible -and $form.Visible) {
|
|
1427
|
+
[pscustomobject]@{ type='overlay'; visible=$true; monitor=$screenIndex; position=[string]$state.position } | ConvertTo-Json -Compress
|
|
1428
|
+
[Console]::Out.Flush()
|
|
1429
|
+
$script:overlayWasVisible = $true
|
|
1430
|
+
}
|
|
1431
|
+
} catch {
|
|
1432
|
+
$form.Hide()
|
|
1433
|
+
$script:overlayWasVisible = $false
|
|
1434
|
+
[pscustomobject]@{ type='overlay'; visible=$false; error=$_.Exception.Message } | ConvertTo-Json -Compress
|
|
1435
|
+
[Console]::Out.Flush()
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1261
1439
|
function Emit-State($profile, $state, $detail) {
|
|
1262
1440
|
[pscustomobject]@{
|
|
1263
1441
|
gameId = $profile.id
|
|
@@ -1269,6 +1447,8 @@ function Emit-State($profile, $state, $detail) {
|
|
|
1269
1447
|
}
|
|
1270
1448
|
|
|
1271
1449
|
while ($true) {
|
|
1450
|
+
[System.Windows.Forms.Application]::DoEvents()
|
|
1451
|
+
Update-Overlay
|
|
1272
1452
|
foreach ($profile in $profiles) {
|
|
1273
1453
|
$process = $null
|
|
1274
1454
|
foreach ($processName in @($profile.processNames)) {
|
|
@@ -1352,6 +1532,7 @@ function queueGameReport(cfg, reading) {
|
|
|
1352
1532
|
if (!GAME_PROFILES.some((profile) => profile.id === reading?.gameId)) return
|
|
1353
1533
|
const state = reading.state === 'detecting' ? 'paused' : reading.state
|
|
1354
1534
|
if (!['playing', 'paused', 'not_running', 'unsupported', 'error'].includes(state)) return
|
|
1535
|
+
observeGameActivity(reading.gameId, state, reading.observedAt)
|
|
1355
1536
|
|
|
1356
1537
|
const now = Date.now()
|
|
1357
1538
|
const previous = gameLastReport.get(reading.gameId)
|
|
@@ -1360,12 +1541,21 @@ function queueGameReport(cfg, reading) {
|
|
|
1360
1541
|
|
|
1361
1542
|
gameReportQueue = gameReportQueue
|
|
1362
1543
|
.then(async () => {
|
|
1363
|
-
await api(cfg, '/api/v1/games/activity', {
|
|
1544
|
+
const result = await api(cfg, '/api/v1/games/activity', {
|
|
1364
1545
|
machineId: cfg.machineId,
|
|
1365
1546
|
gameId: reading.gameId,
|
|
1366
1547
|
state,
|
|
1367
1548
|
observedAt: reading.observedAt || new Date().toISOString(),
|
|
1549
|
+
dayStart: localDayStartIso(),
|
|
1368
1550
|
})
|
|
1551
|
+
applyGameOverlaySettings(result.gameOverlay)
|
|
1552
|
+
if (activeGameOverlay?.gameId === reading.gameId &&
|
|
1553
|
+
activeGameOverlay.playing === (state === 'playing') &&
|
|
1554
|
+
typeof result.todayMs === 'number') {
|
|
1555
|
+
activeGameOverlay.todayMs = Math.max(0, result.todayMs)
|
|
1556
|
+
activeGameOverlay.asOf = Date.parse(result.asOf) || Date.now()
|
|
1557
|
+
}
|
|
1558
|
+
writeGameOverlayState()
|
|
1369
1559
|
if (previous?.state !== state) log(`🎮 ${reading.gameId}: ${state}`)
|
|
1370
1560
|
})
|
|
1371
1561
|
.catch((err) => log(`✗ game activity sync failed: ${err.message}`))
|
|
@@ -1388,6 +1578,16 @@ async function startGameActivityMonitor(cfg, printOnly = false) {
|
|
|
1388
1578
|
lines.on('line', (line) => {
|
|
1389
1579
|
try {
|
|
1390
1580
|
const reading = JSON.parse(line)
|
|
1581
|
+
if (reading.type === 'displays' && Array.isArray(reading.displays)) {
|
|
1582
|
+
gameDisplays = reading.displays
|
|
1583
|
+
if (printOnly) console.log(`Displays: ${JSON.stringify(gameDisplays)}`)
|
|
1584
|
+
return
|
|
1585
|
+
}
|
|
1586
|
+
if (reading.type === 'overlay') {
|
|
1587
|
+
if (printOnly) console.log(reading.error ? `Overlay error: ${reading.error}` : `Overlay: ${reading.visible ? 'visible' : 'hidden'} on monitor ${Number(reading.monitor) + 1}`)
|
|
1588
|
+
else if (reading.error) log(`✗ game overlay error: ${reading.error}`)
|
|
1589
|
+
return
|
|
1590
|
+
}
|
|
1391
1591
|
if (printOnly) console.log(`${reading.observedAt} ${reading.gameId}: ${reading.state} — ${reading.detail}`)
|
|
1392
1592
|
else queueGameReport(cfg, reading)
|
|
1393
1593
|
} catch { /* ignore non-JSON PowerShell host noise */ }
|
|
@@ -3220,16 +3420,19 @@ async function sync(cfg, discovered) {
|
|
|
3220
3420
|
agentVersion: AGENT_VERSION,
|
|
3221
3421
|
roots: cfg.roots,
|
|
3222
3422
|
repos: full ? discovered : [],
|
|
3423
|
+
gameDisplays,
|
|
3223
3424
|
...(usage ? { usage } : {}),
|
|
3224
3425
|
...(codexUsage ? { codexUsage } : {}),
|
|
3225
3426
|
})
|
|
3226
3427
|
if (full) { syncRepoSig = sig; syncFullCountdown = SYNC_FULL_TICKS }
|
|
3227
3428
|
else syncFullCountdown--
|
|
3429
|
+
applyGameOverlaySettings(data.gameOverlay)
|
|
3228
3430
|
return {
|
|
3229
3431
|
tracked: data.tracked ?? [],
|
|
3230
3432
|
latestVersion: data.latestVersion,
|
|
3231
3433
|
downloadUrl: data.downloadUrl,
|
|
3232
3434
|
downloadSha256: data.downloadSha256,
|
|
3435
|
+
gameOverlay: data.gameOverlay,
|
|
3233
3436
|
}
|
|
3234
3437
|
}
|
|
3235
3438
|
|
|
@@ -3429,6 +3632,8 @@ async function runLoop(cfg) {
|
|
|
3429
3632
|
// Fast command path runs alongside the snapshot poll. Fire-and-forget: it owns
|
|
3430
3633
|
// its own reconnect loop and never rejects.
|
|
3431
3634
|
streamCommands(cfg)
|
|
3635
|
+
writeGameOverlayState()
|
|
3636
|
+
setInterval(writeGameOverlayState, 1000)
|
|
3432
3637
|
startGameActivityMonitor(cfg)
|
|
3433
3638
|
|
|
3434
3639
|
await tick()
|