@geekbeer/minion 2.33.4 → 2.42.5
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 +0 -3
- package/README.md +0 -1
- package/core/api.js +13 -0
- package/core/config.js +46 -1
- package/core/lib/log-manager.js +4 -1
- package/core/lib/platform.js +8 -13
- package/core/lib/revision-watcher.js +252 -0
- package/core/lib/step-poller.js +222 -0
- package/core/lib/strip-ansi.js +18 -0
- package/core/lib/workflow-orchestrator.js +382 -0
- package/core/routes/diagnose.js +296 -0
- package/core/routes/health.js +27 -0
- package/core/routes/routines.js +15 -10
- package/core/routes/skills.js +4 -1
- package/core/routes/workflows.js +49 -2
- package/core/stores/chat-store.js +8 -1
- package/core/stores/routine-store.js +2 -2
- package/linux/lib/process-manager.js +14 -0
- package/linux/minion-cli.sh +57 -16
- package/linux/routes/chat.js +182 -20
- package/linux/routes/config.js +8 -12
- package/linux/routine-runner.js +5 -4
- package/linux/server.js +53 -1
- package/linux/workflow-runner.js +25 -61
- package/package.json +1 -1
- package/roles/pm.md +11 -12
- package/win/lib/process-manager.js +15 -0
- package/win/minion-cli.ps1 +122 -27
- package/win/routes/chat.js +178 -14
- package/win/routes/config.js +6 -2
- package/win/routine-runner.js +4 -2
- package/win/server.js +53 -0
- package/win/workflow-runner.js +31 -43
- package/skills/execution-report/SKILL.md +0 -106
|
@@ -63,6 +63,11 @@ function buildAllowedCommands(procMgr) {
|
|
|
63
63
|
command: `npm install -g @geekbeer/minion@latest && powershell -Command "$pid = Get-Content '${pidFile}' -ErrorAction SilentlyContinue; if ($pid) { Get-CimInstance Win32_Process -Filter \\"ParentProcessId = $pid\\" -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }; Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue }; Start-Sleep -Seconds 2; Start-Process powershell -ArgumentList '-ExecutionPolicy Bypass -WindowStyle Hidden -File \\"${startScript}\\"' -WindowStyle Hidden"`,
|
|
64
64
|
deferred: true,
|
|
65
65
|
}
|
|
66
|
+
commands['update-agent-dev'] = {
|
|
67
|
+
description: 'Update @geekbeer/minion from Verdaccio (dev) and restart',
|
|
68
|
+
command: `npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873 && powershell -Command "$pid = Get-Content '${pidFile}' -ErrorAction SilentlyContinue; if ($pid) { Get-CimInstance Win32_Process -Filter \\"ParentProcessId = $pid\\" -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }; Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue }; Start-Sleep -Seconds 2; Start-Process powershell -ArgumentList '-ExecutionPolicy Bypass -WindowStyle Hidden -File \\"${startScript}\\"' -WindowStyle Hidden"`,
|
|
69
|
+
deferred: true,
|
|
70
|
+
}
|
|
66
71
|
commands['status-services'] = {
|
|
67
72
|
description: 'Show agent process info',
|
|
68
73
|
command: `powershell -Command "$pidFile = '${pidFile}'; if (Test-Path $pidFile) { $pid = (Get-Content $pidFile -Raw).Trim(); $proc = Get-Process -Id $pid -ErrorAction SilentlyContinue; if ($proc) { Write-Host 'minion-agent: running (PID:' $pid ')' } else { Write-Host 'minion-agent: not running (stale PID)' } } else { Write-Host 'minion-agent: not running' }"`,
|
|
@@ -78,6 +83,11 @@ function buildAllowedCommands(procMgr) {
|
|
|
78
83
|
command: 'npm install -g @geekbeer/minion@latest && nssm restart minion-agent',
|
|
79
84
|
deferred: true,
|
|
80
85
|
}
|
|
86
|
+
commands['update-agent-dev'] = {
|
|
87
|
+
description: 'Update @geekbeer/minion from Verdaccio (dev) and restart',
|
|
88
|
+
command: 'npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873 && nssm restart minion-agent',
|
|
89
|
+
deferred: true,
|
|
90
|
+
}
|
|
81
91
|
commands['restart-display'] = {
|
|
82
92
|
description: 'Restart TightVNC and websockify services',
|
|
83
93
|
command: 'nssm restart minion-websockify',
|
|
@@ -97,6 +107,11 @@ function buildAllowedCommands(procMgr) {
|
|
|
97
107
|
command: 'npm install -g @geekbeer/minion@latest && net stop minion-agent & net start minion-agent',
|
|
98
108
|
deferred: true,
|
|
99
109
|
}
|
|
110
|
+
commands['update-agent-dev'] = {
|
|
111
|
+
description: 'Update @geekbeer/minion from Verdaccio (dev) and restart',
|
|
112
|
+
command: 'npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873 && net stop minion-agent & net start minion-agent',
|
|
113
|
+
deferred: true,
|
|
114
|
+
}
|
|
100
115
|
commands['status-services'] = {
|
|
101
116
|
description: 'Check status of minion agent service',
|
|
102
117
|
command: 'sc query minion-agent',
|
package/win/minion-cli.ps1
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Usage:
|
|
4
4
|
# minion-cli-win setup --hq-url https://... --minion-id <UUID> --api-token <TOKEN>
|
|
5
5
|
# minion-cli-win setup --setup-tunnel
|
|
6
|
-
# minion-cli-win start | stop | restart | status | health | version | help
|
|
6
|
+
# minion-cli-win start | stop | restart | status | health | diagnose | version | help
|
|
7
7
|
|
|
8
8
|
# Parse arguments manually to avoid issues with npm wrapper passing $args as array
|
|
9
9
|
$Command = 'help'
|
|
@@ -16,7 +16,7 @@ $i = 0
|
|
|
16
16
|
while ($i -lt $args.Count) {
|
|
17
17
|
$arg = [string]$args[$i]
|
|
18
18
|
switch -Regex ($arg) {
|
|
19
|
-
'^(setup|reconfigure|start|stop|restart|status|health|version|help)$' { $Command = $arg }
|
|
19
|
+
'^(setup|reconfigure|start|stop|restart|status|health|diagnose|version|help)$' { $Command = $arg }
|
|
20
20
|
'^(-v|--version)$' { $Command = 'version' }
|
|
21
21
|
'^--hq-url$' { $i++; if ($i -lt $args.Count) { $HqUrl = [string]$args[$i] } }
|
|
22
22
|
'^--minion-id$' { $i++; if ($i -lt $args.Count) { $MinionId = [string]$args[$i] } }
|
|
@@ -68,6 +68,28 @@ function Test-CommandExists {
|
|
|
68
68
|
$null -ne (Get-Command $Name -ErrorAction SilentlyContinue)
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function Get-WebsockifyCommand {
|
|
72
|
+
# Returns @(executable, args-prefix) for launching websockify.
|
|
73
|
+
# 1) websockify.exe on PATH
|
|
74
|
+
if (Get-Command websockify -ErrorAction SilentlyContinue) {
|
|
75
|
+
return @((Get-Command websockify).Source)
|
|
76
|
+
}
|
|
77
|
+
# 2) Look in Python Scripts directories
|
|
78
|
+
if (Test-CommandExists 'python') {
|
|
79
|
+
$scriptsDir = & python -c "import sysconfig; print(sysconfig.get_path('scripts'))" 2>$null
|
|
80
|
+
if ($scriptsDir) {
|
|
81
|
+
$wsExe = Join-Path $scriptsDir 'websockify.exe'
|
|
82
|
+
if (Test-Path $wsExe) { return @($wsExe) }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
# 3) Fallback: python -m websockify
|
|
86
|
+
if (Test-CommandExists 'python') {
|
|
87
|
+
$check = & python -c "import websockify" 2>&1
|
|
88
|
+
if ($LASTEXITCODE -eq 0) { return @('python', '-m', 'websockify') }
|
|
89
|
+
}
|
|
90
|
+
return $null
|
|
91
|
+
}
|
|
92
|
+
|
|
71
93
|
function Get-LanIPAddress {
|
|
72
94
|
try {
|
|
73
95
|
$ip = (Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
|
|
@@ -193,8 +215,8 @@ function Restart-MinionProcess {
|
|
|
193
215
|
# ============================================================
|
|
194
216
|
|
|
195
217
|
function Invoke-Setup {
|
|
196
|
-
$totalSteps =
|
|
197
|
-
if ($SetupTunnel) { $totalSteps =
|
|
218
|
+
$totalSteps = 9
|
|
219
|
+
if ($SetupTunnel) { $totalSteps = 10 }
|
|
198
220
|
|
|
199
221
|
# Minionization warning
|
|
200
222
|
Write-Host ""
|
|
@@ -311,7 +333,6 @@ function Invoke-Setup {
|
|
|
311
333
|
New-Item -Path $LogDir -ItemType Directory -Force | Out-Null
|
|
312
334
|
$envValues = @{
|
|
313
335
|
'AGENT_PORT' = '8080'
|
|
314
|
-
'HEARTBEAT_INTERVAL' = '30'
|
|
315
336
|
'MINION_USER' = $env:USERNAME
|
|
316
337
|
}
|
|
317
338
|
if ($HqUrl) { $envValues['HQ_URL'] = $HqUrl }
|
|
@@ -330,17 +351,21 @@ function Invoke-Setup {
|
|
|
330
351
|
|
|
331
352
|
# Try prebuilt version first (no Build Tools required)
|
|
332
353
|
try {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
354
|
+
$npmResult = cmd /c "npm install node-pty-prebuilt-multiarch 2>&1"
|
|
355
|
+
if ($LASTEXITCODE -eq 0) {
|
|
356
|
+
Write-Detail "node-pty-prebuilt-multiarch installed (no Build Tools needed)"
|
|
357
|
+
$ptyInstalled = $true
|
|
358
|
+
}
|
|
336
359
|
} catch {}
|
|
337
360
|
|
|
338
361
|
# Fallback: source-compiled version (requires Visual Studio Build Tools)
|
|
339
362
|
if (-not $ptyInstalled) {
|
|
340
363
|
try {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
364
|
+
$npmResult = cmd /c "npm install node-pty 2>&1"
|
|
365
|
+
if ($LASTEXITCODE -eq 0) {
|
|
366
|
+
Write-Detail "node-pty installed (compiled from source)"
|
|
367
|
+
$ptyInstalled = $true
|
|
368
|
+
}
|
|
344
369
|
} catch {}
|
|
345
370
|
}
|
|
346
371
|
|
|
@@ -413,10 +438,17 @@ if (`$vncExe) {
|
|
|
413
438
|
}
|
|
414
439
|
# Reload registry config (ensures no-auth settings are applied)
|
|
415
440
|
& `$vncExe -controlapp -reload 2>`$null
|
|
416
|
-
|
|
441
|
+
`$wsCmd = Get-WebsockifyCommand
|
|
442
|
+
if (`$wsCmd) {
|
|
417
443
|
`$wsProc = Get-Process -Name websockify -ErrorAction SilentlyContinue
|
|
418
444
|
if (-not `$wsProc) {
|
|
419
|
-
|
|
445
|
+
if (`$wsCmd.Count -eq 1) {
|
|
446
|
+
Start-Process -FilePath `$wsCmd[0] -ArgumentList '6080', 'localhost:5900' -WindowStyle Hidden
|
|
447
|
+
} else {
|
|
448
|
+
# python -m websockify 6080 localhost:5900
|
|
449
|
+
`$wsArgs = (`$wsCmd[1..(`$wsCmd.Count-1)] + @('6080', 'localhost:5900')) -join ' '
|
|
450
|
+
Start-Process -FilePath `$wsCmd[0] -ArgumentList `$wsArgs -WindowStyle Hidden
|
|
451
|
+
}
|
|
420
452
|
}
|
|
421
453
|
}
|
|
422
454
|
}
|
|
@@ -469,8 +501,25 @@ Remove-Item `$PidFile -Force -ErrorAction SilentlyContinue
|
|
|
469
501
|
$shortcut.Save()
|
|
470
502
|
Write-Detail "Startup shortcut created: $shortcutPath"
|
|
471
503
|
|
|
472
|
-
# Step 6:
|
|
473
|
-
Write-Step 6 $totalSteps "
|
|
504
|
+
# Step 6: Disable screensaver, lock screen, and sleep
|
|
505
|
+
Write-Step 6 $totalSteps "Disabling screensaver, lock screen, and sleep..."
|
|
506
|
+
# Screensaver off
|
|
507
|
+
Set-ItemProperty -Path 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive -Value '0'
|
|
508
|
+
Set-ItemProperty -Path 'HKCU:\Control Panel\Desktop' -Name ScreenSaveTimeOut -Value '0'
|
|
509
|
+
Set-ItemProperty -Path 'HKCU:\Control Panel\Desktop' -Name SCRNSAVE.EXE -Value ''
|
|
510
|
+
Write-Detail "Screensaver disabled"
|
|
511
|
+
# Lock screen off (user-level: disable lock on resume)
|
|
512
|
+
Set-ItemProperty -Path 'HKCU:\Control Panel\Desktop' -Name ScreenSaverIsSecure -Value '0'
|
|
513
|
+
Write-Detail "Lock on resume disabled"
|
|
514
|
+
# Power settings: never sleep, never turn off display
|
|
515
|
+
& powercfg -change -standby-timeout-ac 0 2>$null
|
|
516
|
+
& powercfg -change -standby-timeout-dc 0 2>$null
|
|
517
|
+
& powercfg -change -monitor-timeout-ac 0 2>$null
|
|
518
|
+
& powercfg -change -monitor-timeout-dc 0 2>$null
|
|
519
|
+
Write-Detail "Sleep and monitor timeout disabled"
|
|
520
|
+
|
|
521
|
+
# Step 7: Install TightVNC Server
|
|
522
|
+
Write-Step 7 $totalSteps "Setting up TightVNC Server..."
|
|
474
523
|
$vncSystemPath = 'C:\Program Files\TightVNC\tvnserver.exe'
|
|
475
524
|
$vncPortableDir = Join-Path $DataDir 'tightvnc'
|
|
476
525
|
$vncPortablePath = Join-Path $vncPortableDir 'PFiles\TightVNC\tvnserver.exe'
|
|
@@ -529,9 +578,9 @@ Remove-Item `$PidFile -Force -ErrorAction SilentlyContinue
|
|
|
529
578
|
Write-Detail "TightVNC configured: localhost-only, no VNC password (auth via HQ proxy)"
|
|
530
579
|
}
|
|
531
580
|
|
|
532
|
-
# Step
|
|
533
|
-
Write-Step
|
|
534
|
-
if (
|
|
581
|
+
# Step 8: Setup websockify (WebSocket proxy for VNC)
|
|
582
|
+
Write-Step 8 $totalSteps "Setting up websockify..."
|
|
583
|
+
if (Get-WebsockifyCommand) {
|
|
535
584
|
Write-Detail "websockify already installed"
|
|
536
585
|
}
|
|
537
586
|
else {
|
|
@@ -545,9 +594,10 @@ Remove-Item `$PidFile -Force -ErrorAction SilentlyContinue
|
|
|
545
594
|
if (-not $pythonUsable) {
|
|
546
595
|
Write-Host " Python not found. Installing via winget..."
|
|
547
596
|
try {
|
|
548
|
-
|
|
597
|
+
$wingetResult = cmd /c "winget install --id Python.Python.3.12 --accept-package-agreements --accept-source-agreements 2>&1"
|
|
549
598
|
$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH', 'User')
|
|
550
|
-
Write-Detail "Python installed"
|
|
599
|
+
if ($LASTEXITCODE -eq 0) { Write-Detail "Python installed" }
|
|
600
|
+
else { Write-Warn "winget install may have failed (exit code $LASTEXITCODE): $wingetResult" }
|
|
551
601
|
}
|
|
552
602
|
catch {
|
|
553
603
|
Write-Warn "Failed to install Python: $_"
|
|
@@ -557,28 +607,35 @@ Remove-Item `$PidFile -Force -ErrorAction SilentlyContinue
|
|
|
557
607
|
|
|
558
608
|
Write-Host " Installing websockify via pip..."
|
|
559
609
|
try {
|
|
610
|
+
# Use cmd /c to prevent pip's stderr (progress bars, warnings) from
|
|
611
|
+
# becoming RemoteException errors in PowerShell remoting sessions.
|
|
560
612
|
if (Test-CommandExists 'pip') {
|
|
561
|
-
|
|
562
|
-
Write-Detail "websockify installed"
|
|
613
|
+
$pipResult = cmd /c "pip install websockify 2>&1"
|
|
614
|
+
if ($LASTEXITCODE -eq 0) { Write-Detail "websockify installed" }
|
|
615
|
+
else { Write-Warn "pip install failed (exit code $LASTEXITCODE): $pipResult" }
|
|
563
616
|
}
|
|
564
617
|
elseif (Test-CommandExists 'pip3') {
|
|
565
|
-
|
|
566
|
-
Write-Detail "websockify installed"
|
|
618
|
+
$pipResult = cmd /c "pip3 install websockify 2>&1"
|
|
619
|
+
if ($LASTEXITCODE -eq 0) { Write-Detail "websockify installed" }
|
|
620
|
+
else { Write-Warn "pip3 install failed (exit code $LASTEXITCODE): $pipResult" }
|
|
567
621
|
}
|
|
568
622
|
elseif (Test-CommandExists 'python') {
|
|
569
|
-
|
|
570
|
-
Write-Detail "websockify installed (via python -m pip)"
|
|
623
|
+
$pipResult = cmd /c "python -m pip install websockify 2>&1"
|
|
624
|
+
if ($LASTEXITCODE -eq 0) { Write-Detail "websockify installed (via python -m pip)" }
|
|
625
|
+
else { Write-Warn "python -m pip install failed (exit code $LASTEXITCODE): $pipResult" }
|
|
571
626
|
}
|
|
572
627
|
else {
|
|
573
628
|
Write-Warn "pip not found. Install Python first, then: pip install websockify"
|
|
574
629
|
}
|
|
630
|
+
# Refresh PATH so websockify.exe in Python Scripts dir is discoverable
|
|
631
|
+
$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH', 'User')
|
|
575
632
|
}
|
|
576
633
|
catch {
|
|
577
634
|
Write-Warn "Failed to install websockify: $_"
|
|
578
635
|
}
|
|
579
636
|
}
|
|
580
637
|
|
|
581
|
-
# Step
|
|
638
|
+
# Step 9 (optional): Cloudflare Tunnel
|
|
582
639
|
if ($SetupTunnel) {
|
|
583
640
|
$currentStep = $totalSteps - 1
|
|
584
641
|
Write-Step $currentStep $totalSteps "Setting up Cloudflare Tunnel..."
|
|
@@ -842,6 +899,43 @@ switch ($Command) {
|
|
|
842
899
|
Write-Error "Health check failed. Is the agent running?"
|
|
843
900
|
}
|
|
844
901
|
}
|
|
902
|
+
'diagnose' {
|
|
903
|
+
Write-Host "Running diagnostics..."
|
|
904
|
+
Write-Host ""
|
|
905
|
+
try {
|
|
906
|
+
$result = Invoke-RestMethod -Uri "$AgentUrl/api/diagnose" -TimeoutSec 15
|
|
907
|
+
}
|
|
908
|
+
catch {
|
|
909
|
+
Write-Host "FAIL: Cannot reach minion agent at $AgentUrl" -ForegroundColor Red
|
|
910
|
+
Write-Host " Is the agent running? Try: minion-cli-win start"
|
|
911
|
+
exit 1
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
$ver = if ($result.version) { $result.version } else { '?' }
|
|
915
|
+
$plat = if ($result.platform) { $result.platform } else { '?' }
|
|
916
|
+
Write-Host "=== Minion Diagnostics (v$ver, $plat) ==="
|
|
917
|
+
Write-Host ""
|
|
918
|
+
|
|
919
|
+
$checkNames = @('agent', 'hq', 'tunnel', 'vnc', 'terminal', 'llm', 'env')
|
|
920
|
+
foreach ($check in $checkNames) {
|
|
921
|
+
$c = $result.checks.$check
|
|
922
|
+
$label = $check.ToUpper().PadRight(10)
|
|
923
|
+
if ($c.ok) {
|
|
924
|
+
Write-Host " [PASS] $label $($c.details)" -ForegroundColor Green
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
Write-Host " [FAIL] $label $($c.details)" -ForegroundColor Red
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
Write-Host ""
|
|
932
|
+
if ($result.summary -eq 'ALL OK') {
|
|
933
|
+
Write-Host $result.summary -ForegroundColor Green
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
Write-Host $result.summary -ForegroundColor Yellow
|
|
937
|
+
}
|
|
938
|
+
}
|
|
845
939
|
'version' {
|
|
846
940
|
Write-Host "@geekbeer/minion v$CliVersion (Windows)"
|
|
847
941
|
}
|
|
@@ -856,6 +950,7 @@ switch ($Command) {
|
|
|
856
950
|
Write-Host " minion-cli-win restart # Restart agent process"
|
|
857
951
|
Write-Host " minion-cli-win status # Get current status"
|
|
858
952
|
Write-Host " minion-cli-win health # Health check"
|
|
953
|
+
Write-Host " minion-cli-win diagnose # Run full service diagnostics"
|
|
859
954
|
Write-Host " minion-cli-win version # Show version"
|
|
860
955
|
Write-Host ""
|
|
861
956
|
Write-Host "Setup options:"
|
package/win/routes/chat.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Windows Chat endpoints
|
|
3
3
|
*
|
|
4
|
-
* Same as routes/chat.js but with Windows-compatible PATH construction.
|
|
4
|
+
* Same as linux/routes/chat.js but with Windows-compatible PATH and shell construction.
|
|
5
|
+
*
|
|
6
|
+
* Endpoints:
|
|
7
|
+
* POST /api/chat - Send message, get SSE stream
|
|
8
|
+
* GET /api/chat/session - Get active session (messages + session_id)
|
|
9
|
+
* POST /api/chat/clear - Clear session and start fresh
|
|
10
|
+
* POST /api/chat/abort - Kill the active LLM CLI process
|
|
11
|
+
* POST /api/chat/reset - Summarize conversation and start fresh session
|
|
5
12
|
*/
|
|
6
13
|
|
|
7
14
|
const { spawn } = require('child_process')
|
|
@@ -99,6 +106,42 @@ async function chatRoutes(fastify) {
|
|
|
99
106
|
}, 2000)
|
|
100
107
|
return { success: true }
|
|
101
108
|
})
|
|
109
|
+
|
|
110
|
+
// POST /api/chat/reset - Summarize conversation and start fresh session
|
|
111
|
+
fastify.post('/api/chat/reset', async (request, reply) => {
|
|
112
|
+
if (!verifyToken(request)) {
|
|
113
|
+
reply.code(401)
|
|
114
|
+
return { success: false, error: 'Unauthorized' }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const session = await chatStore.load()
|
|
118
|
+
if (!session || session.messages.length === 0) {
|
|
119
|
+
await chatStore.clear()
|
|
120
|
+
return { success: true, summary: null }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const recentMessages = session.messages.slice(-20)
|
|
124
|
+
const conversationText = recentMessages
|
|
125
|
+
.map(m => `${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content.substring(0, 500)}`)
|
|
126
|
+
.join('\n\n')
|
|
127
|
+
|
|
128
|
+
const summarizePrompt = `以下の会話を要約してください。重要なコンテキスト、決定事項、進行中のタスクを含めてください。200文字以内で。\n\n${conversationText}`
|
|
129
|
+
|
|
130
|
+
let summary = null
|
|
131
|
+
try {
|
|
132
|
+
summary = await runQuickLlmCall(summarizePrompt)
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error('[Chat] summarization failed:', err.message)
|
|
135
|
+
const fallback = recentMessages.slice(-4)
|
|
136
|
+
.map(m => `${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content.substring(0, 200)}`)
|
|
137
|
+
.join('\n')
|
|
138
|
+
summary = fallback
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await chatStore.clear()
|
|
142
|
+
console.log(`[Chat] session reset with summary (${summary?.length || 0} chars)`)
|
|
143
|
+
return { success: true, summary }
|
|
144
|
+
})
|
|
102
145
|
}
|
|
103
146
|
|
|
104
147
|
function buildContextPrefix(message, context) {
|
|
@@ -154,6 +197,12 @@ function getLlmBinary() {
|
|
|
154
197
|
return cmd.split(/\s+/)[0]
|
|
155
198
|
}
|
|
156
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Stream LLM CLI output as SSE events.
|
|
202
|
+
* Uses --resume to continue existing sessions.
|
|
203
|
+
* Tracks block types to correctly forward tool_use vs text events
|
|
204
|
+
* and counts turns for session management.
|
|
205
|
+
*/
|
|
157
206
|
function streamLlmResponse(res, prompt, sessionId) {
|
|
158
207
|
return new Promise((resolve, reject) => {
|
|
159
208
|
const binaryName = getLlmBinary()
|
|
@@ -177,6 +226,7 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
177
226
|
cwd: config.HOME_DIR,
|
|
178
227
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
179
228
|
timeout: 600000,
|
|
229
|
+
shell: true, // Required on Windows to resolve .cmd shims (e.g. claude.cmd)
|
|
180
230
|
env: {
|
|
181
231
|
...process.env,
|
|
182
232
|
HOME: config.HOME_DIR,
|
|
@@ -195,6 +245,12 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
195
245
|
let lineBuffer = ''
|
|
196
246
|
let resolvedSessionId = sessionId || null
|
|
197
247
|
|
|
248
|
+
// Block-type state tracking for correct event forwarding
|
|
249
|
+
let currentBlockType = null // 'text' | 'tool_use' | null
|
|
250
|
+
let currentToolName = null
|
|
251
|
+
let toolInputBuffer = ''
|
|
252
|
+
let turnCount = 0
|
|
253
|
+
|
|
198
254
|
child.stdout.on('data', (data) => {
|
|
199
255
|
lineBuffer += data.toString()
|
|
200
256
|
const parts = lineBuffer.split('\n')
|
|
@@ -204,35 +260,85 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
204
260
|
if (!line.trim()) continue
|
|
205
261
|
try {
|
|
206
262
|
const parsed = JSON.parse(line)
|
|
263
|
+
|
|
264
|
+
// system init event — capture session_id
|
|
207
265
|
if (parsed.type === 'system' && parsed.session_id) {
|
|
208
266
|
resolvedSessionId = parsed.session_id
|
|
209
267
|
console.log(`[Chat] session_id: ${resolvedSessionId}`)
|
|
210
268
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
269
|
+
|
|
270
|
+
// content_block_start — track block type
|
|
271
|
+
if (parsed.type === 'content_block_start') {
|
|
272
|
+
const blockType = parsed.content_block?.type
|
|
273
|
+
if (blockType === 'tool_use') {
|
|
274
|
+
currentBlockType = 'tool_use'
|
|
275
|
+
currentToolName = parsed.content_block.name || 'unknown'
|
|
276
|
+
toolInputBuffer = ''
|
|
277
|
+
const event = JSON.stringify({
|
|
278
|
+
type: 'tool_start',
|
|
279
|
+
tool: currentToolName,
|
|
280
|
+
})
|
|
281
|
+
res.write(`data: ${event}\n\n`)
|
|
282
|
+
} else if (blockType === 'text') {
|
|
283
|
+
currentBlockType = 'text'
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// content_block_delta — handle both text and tool input
|
|
288
|
+
if (parsed.type === 'content_block_delta') {
|
|
289
|
+
const deltaType = parsed.delta?.type
|
|
290
|
+
if (deltaType === 'input_json_delta' && currentBlockType === 'tool_use') {
|
|
291
|
+
const partial = parsed.delta.partial_json || ''
|
|
292
|
+
if (partial) {
|
|
293
|
+
toolInputBuffer += partial
|
|
294
|
+
const event = JSON.stringify({ type: 'tool_input_delta', partial_json: partial })
|
|
295
|
+
res.write(`data: ${event}\n\n`)
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
const delta = parsed.delta?.text || ''
|
|
299
|
+
if (delta) {
|
|
300
|
+
fullResponse += delta
|
|
301
|
+
const event = JSON.stringify({ type: 'delta', content: delta })
|
|
302
|
+
res.write(`data: ${event}\n\n`)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
214
305
|
}
|
|
306
|
+
|
|
307
|
+
// content_block_stop — only emit tool_end for tool_use blocks (bug fix)
|
|
215
308
|
if (parsed.type === 'content_block_stop') {
|
|
216
|
-
|
|
309
|
+
if (currentBlockType === 'tool_use') {
|
|
310
|
+
let parsedInput = null
|
|
311
|
+
try {
|
|
312
|
+
if (toolInputBuffer) parsedInput = JSON.parse(toolInputBuffer)
|
|
313
|
+
} catch { /* partial or invalid JSON */ }
|
|
314
|
+
const event = JSON.stringify({
|
|
315
|
+
type: 'tool_end',
|
|
316
|
+
tool: currentToolName,
|
|
317
|
+
input: parsedInput,
|
|
318
|
+
})
|
|
319
|
+
res.write(`data: ${event}\n\n`)
|
|
320
|
+
}
|
|
321
|
+
currentBlockType = null
|
|
322
|
+
currentToolName = null
|
|
323
|
+
toolInputBuffer = ''
|
|
217
324
|
}
|
|
325
|
+
|
|
326
|
+
// assistant message — count turns and forward text blocks
|
|
218
327
|
if (parsed.type === 'assistant' && parsed.message) {
|
|
328
|
+
turnCount++
|
|
219
329
|
for (const block of (parsed.message.content || [])) {
|
|
220
330
|
if (block.type === 'text') {
|
|
221
331
|
fullResponse += block.text
|
|
222
332
|
res.write(`data: ${JSON.stringify({ type: 'text', content: block.text })}\n\n`)
|
|
223
333
|
}
|
|
224
334
|
}
|
|
225
|
-
} else if (parsed.type === 'content_block_delta') {
|
|
226
|
-
const delta = parsed.delta?.text || ''
|
|
227
|
-
if (delta) {
|
|
228
|
-
fullResponse += delta
|
|
229
|
-
res.write(`data: ${JSON.stringify({ type: 'delta', content: delta })}\n\n`)
|
|
230
|
-
}
|
|
231
335
|
} else if (parsed.type === 'result') {
|
|
232
336
|
const resultText = parsed.result || ''
|
|
233
337
|
if (resultText) {
|
|
234
338
|
res.write(`data: ${JSON.stringify({ type: 'result', content: resultText })}\n\n`)
|
|
235
|
-
fullResponse
|
|
339
|
+
if (!fullResponse) {
|
|
340
|
+
fullResponse = resultText
|
|
341
|
+
}
|
|
236
342
|
}
|
|
237
343
|
}
|
|
238
344
|
} catch {
|
|
@@ -254,14 +360,18 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
254
360
|
await chatStore.addMessage(resolvedSessionId, { role: 'user', content: prompt })
|
|
255
361
|
}
|
|
256
362
|
if (fullResponse) {
|
|
257
|
-
await chatStore.addMessage(resolvedSessionId, { role: 'assistant', content: fullResponse })
|
|
363
|
+
await chatStore.addMessage(resolvedSessionId, { role: 'assistant', content: fullResponse }, turnCount)
|
|
258
364
|
}
|
|
259
365
|
}
|
|
260
366
|
if (code !== 0 && !fullResponse) {
|
|
261
367
|
const errorMsg = stderrBuffer.trim() || `Claude CLI exited with code ${code}`
|
|
262
368
|
res.write(`data: ${JSON.stringify({ type: 'error', error: errorMsg })}\n\n`)
|
|
263
369
|
}
|
|
264
|
-
|
|
370
|
+
|
|
371
|
+
const session = await chatStore.load()
|
|
372
|
+
const totalTurnCount = session?.turn_count || turnCount
|
|
373
|
+
|
|
374
|
+
res.write(`data: ${JSON.stringify({ type: 'done', session_id: resolvedSessionId, turn_count: totalTurnCount })}\n\n`)
|
|
265
375
|
resolve()
|
|
266
376
|
})
|
|
267
377
|
|
|
@@ -277,4 +387,58 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
277
387
|
})
|
|
278
388
|
}
|
|
279
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Run a quick non-streaming LLM call (for summarization etc.)
|
|
392
|
+
*/
|
|
393
|
+
function runQuickLlmCall(prompt) {
|
|
394
|
+
return new Promise((resolve, reject) => {
|
|
395
|
+
const binaryName = getLlmBinary()
|
|
396
|
+
if (!binaryName) {
|
|
397
|
+
reject(new Error('LLM_COMMAND is not configured'))
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
const binaryPath = path.join(config.HOME_DIR, '.local', 'bin', binaryName)
|
|
401
|
+
const binary = fs.existsSync(binaryPath) ? binaryPath : binaryName
|
|
402
|
+
const extendedPath = buildExtendedPath(config.HOME_DIR)
|
|
403
|
+
|
|
404
|
+
const args = ['-p', '--model', 'haiku', '--max-turns', '1', '--output-format', 'json', prompt]
|
|
405
|
+
|
|
406
|
+
const child = spawn(binary, args, {
|
|
407
|
+
cwd: config.HOME_DIR,
|
|
408
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
409
|
+
timeout: 30000,
|
|
410
|
+
shell: true,
|
|
411
|
+
env: {
|
|
412
|
+
...process.env,
|
|
413
|
+
HOME: config.HOME_DIR,
|
|
414
|
+
USERPROFILE: config.HOME_DIR,
|
|
415
|
+
PATH: extendedPath,
|
|
416
|
+
},
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
child.stdin.end()
|
|
420
|
+
|
|
421
|
+
let stdout = ''
|
|
422
|
+
let stderr = ''
|
|
423
|
+
|
|
424
|
+
child.stdout.on('data', (data) => { stdout += data.toString() })
|
|
425
|
+
child.stderr.on('data', (data) => { stderr += data.toString() })
|
|
426
|
+
|
|
427
|
+
child.on('close', (code) => {
|
|
428
|
+
if (code !== 0) {
|
|
429
|
+
reject(new Error(`LLM call failed (exit ${code}): ${stderr.substring(0, 200)}`))
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
const parsed = JSON.parse(stdout)
|
|
434
|
+
resolve(parsed.result || stdout.trim())
|
|
435
|
+
} catch {
|
|
436
|
+
resolve(stdout.trim())
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
child.on('error', (err) => reject(err))
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
280
444
|
module.exports = { chatRoutes }
|
package/win/routes/config.js
CHANGED
|
@@ -10,7 +10,7 @@ const path = require('path')
|
|
|
10
10
|
const zlib = require('zlib')
|
|
11
11
|
const { verifyToken } = require('../../core/lib/auth')
|
|
12
12
|
const { clearLlmCache } = require('../../core/lib/llm-checker')
|
|
13
|
-
const { config } = require('../../core/config')
|
|
13
|
+
const { config, updateConfig } = require('../../core/config')
|
|
14
14
|
const { resolveEnvFilePath } = require('../../core/lib/platform')
|
|
15
15
|
|
|
16
16
|
const ALLOWED_ENV_KEYS = ['LLM_COMMAND']
|
|
@@ -212,8 +212,12 @@ function configRoutes(fastify, _opts, done) {
|
|
|
212
212
|
try {
|
|
213
213
|
writeEnvKey(envPath, key, value)
|
|
214
214
|
console.log(`[Config] Updated ${key} in ${envPath}`)
|
|
215
|
+
|
|
216
|
+
// Sync in-memory config so the change takes effect without restart
|
|
217
|
+
updateConfig(key, value)
|
|
218
|
+
|
|
215
219
|
clearLlmCache()
|
|
216
|
-
return { success: true, restart_required:
|
|
220
|
+
return { success: true, restart_required: false }
|
|
217
221
|
} catch (err) {
|
|
218
222
|
console.error(`[Config] Failed to update ${key} in ${envPath}:`, err.message)
|
|
219
223
|
const detail = err.code === 'EACCES' ? ' (permission denied)' : ''
|
package/win/routine-runner.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
const { Cron } = require('croner')
|
|
9
9
|
const crypto = require('crypto')
|
|
10
10
|
const path = require('path')
|
|
11
|
+
const { stripAnsi } = require('../core/lib/strip-ansi')
|
|
11
12
|
const fs = require('fs').promises
|
|
12
13
|
const fsSync = require('fs')
|
|
13
14
|
|
|
@@ -150,11 +151,12 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
150
151
|
}, timeout)
|
|
151
152
|
|
|
152
153
|
ptyProcess.onData((data) => {
|
|
153
|
-
|
|
154
|
+
const cleaned = stripAnsi(data)
|
|
155
|
+
session.buffer += cleaned
|
|
154
156
|
if (session.buffer.length > 1024 * 1024) {
|
|
155
157
|
session.buffer = session.buffer.slice(-512 * 1024)
|
|
156
158
|
}
|
|
157
|
-
try { logStream.write(
|
|
159
|
+
try { logStream.write(cleaned) } catch { /* ignore */ }
|
|
158
160
|
})
|
|
159
161
|
|
|
160
162
|
ptyProcess.onExit(({ exitCode }) => {
|