@poolzin/pool-bot 2026.3.7 → 2026.3.10
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/CHANGELOG.md +40 -0
- package/README.md +147 -69
- package/dist/.buildstamp +1 -1
- package/dist/agents/error-classifier.js +251 -0
- package/dist/agents/skills/security.js +211 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/cron-cli/register.cron-dashboard.js +339 -0
- package/dist/cli/cron-cli/register.js +2 -0
- package/dist/cli/errors.js +187 -0
- package/dist/cli/lazy-commands.example.js +113 -0
- package/dist/cli/lazy-commands.js +329 -0
- package/dist/cli/program/command-registry.js +26 -0
- package/dist/cli/program/register.maintenance.js +21 -0
- package/dist/cli/program/register.skills.js +4 -0
- package/dist/cli/program/register.subclis.js +9 -0
- package/dist/cli/swarm-cli/register.js +8 -0
- package/dist/cli/swarm-cli/register.swarm-status.js +488 -0
- package/dist/cli/telemetry-cli/register.js +10 -0
- package/dist/cli/telemetry-cli/register.telemetry-alerts.js +176 -0
- package/dist/cli/telemetry-cli/register.telemetry-metrics.js +323 -0
- package/dist/cli/telemetry-cli/register.telemetry-status.js +179 -0
- package/dist/commands/doctor-checks.js +498 -0
- package/dist/config/config.js +1 -0
- package/dist/config/secrets-integration.js +88 -0
- package/dist/context-engine/index.js +33 -0
- package/dist/context-engine/legacy.js +179 -0
- package/dist/context-engine/registry.js +86 -0
- package/dist/context-engine/summarizing.js +290 -0
- package/dist/context-engine/types.js +7 -0
- package/dist/cron/service/timer.js +18 -0
- package/dist/gateway/protocol/index.js +5 -2
- package/dist/gateway/protocol/schema/error-codes.js +1 -0
- package/dist/gateway/protocol/schema/swarm.js +80 -0
- package/dist/gateway/protocol/schema.js +1 -0
- package/dist/gateway/server-close.js +4 -0
- package/dist/gateway/server-constants.js +1 -0
- package/dist/gateway/server-cron.js +29 -0
- package/dist/gateway/server-maintenance.js +35 -2
- package/dist/gateway/server-methods/swarm.js +58 -0
- package/dist/gateway/server-methods/telemetry.js +71 -0
- package/dist/gateway/server-methods-list.js +8 -0
- package/dist/gateway/server-methods.js +9 -2
- package/dist/gateway/server.impl.js +33 -16
- package/dist/infra/abort-pattern.js +106 -0
- package/dist/infra/retry.js +96 -0
- package/dist/secrets/index.js +28 -0
- package/dist/secrets/resolver.js +185 -0
- package/dist/secrets/runtime.js +142 -0
- package/dist/secrets/types.js +11 -0
- package/dist/security/dangerous-tools.js +80 -0
- package/dist/security/types.js +12 -0
- package/dist/skills/commands.js +333 -0
- package/dist/skills/index.js +164 -0
- package/dist/skills/loader.js +282 -0
- package/dist/skills/parser.js +446 -0
- package/dist/skills/registry.js +394 -0
- package/dist/skills/security.js +312 -0
- package/dist/skills/types.js +21 -0
- package/dist/swarm/service.js +247 -0
- package/dist/telemetry/alert-engine.js +258 -0
- package/dist/telemetry/cron-instrumentation.js +49 -0
- package/dist/telemetry/gateway-instrumentation.js +80 -0
- package/dist/telemetry/instrumentation.js +66 -0
- package/dist/telemetry/service.js +345 -0
- package/dist/test-utils/index.js +219 -0
- package/dist/tui/components/assistant-message.js +6 -2
- package/dist/tui/components/hyperlink-markdown.js +32 -0
- package/dist/tui/components/searchable-select-list.js +12 -1
- package/dist/tui/components/user-message.js +6 -2
- package/dist/tui/index.js +611 -0
- package/dist/tui/theme/theme-detection.js +226 -0
- package/dist/tui/tui-command-handlers.js +20 -0
- package/dist/tui/tui-formatters.js +4 -3
- package/dist/tui/utils/ctrl-c-handler.js +67 -0
- package/dist/tui/utils/osc8-hyperlinks.js +208 -0
- package/dist/tui/utils/safe-stop.js +180 -0
- package/dist/tui/utils/session-key-utils.js +81 -0
- package/dist/tui/utils/text-sanitization.js +284 -0
- package/dist/utils/lru-cache.js +116 -0
- package/dist/utils/performance.js +199 -0
- package/dist/utils/retry.js +240 -0
- package/docs/INTEGRATION_PLAN.md +475 -0
- package/docs/INTEGRATION_SUMMARY.md +215 -0
- package/docs/MELHORIAS_IMPLEMENTADAS.md +228 -0
- package/docs/MELHORIAS_PROFISSIONAIS.md +282 -0
- package/docs/PLANO_ACAO_TUI.md +357 -0
- package/docs/PROGRESSO_TUI.md +66 -0
- package/docs/RELATORIO_FINAL.md +217 -0
- package/docs/diagnostico-shell-completion.md +265 -0
- package/docs/features/advanced-memory.md +585 -0
- package/docs/features/discord-components-v2.md +277 -0
- package/docs/features/swarm.md +100 -0
- package/docs/features/telemetry.md +284 -0
- package/docs/integrations/HEXSTRIKE_PLAN.md +796 -0
- package/docs/integrations/INTEGRATION_PLAN.md +744 -0
- package/docs/integrations/PAGE_AGENT_PLAN.md +370 -0
- package/docs/integrations/XYOPS_PLAN.md +978 -0
- package/docs/models/provider-infrastructure.md +400 -0
- package/docs/security/exec-approvals.md +294 -0
- package/docs/skills/IMPLEMENTATION_SUMMARY.md +145 -0
- package/docs/skills/SKILL.md +524 -0
- package/docs/skills.md +405 -0
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/feishu/package.json +1 -1
- package/extensions/google-antigravity-auth/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/hexstrike-bridge/README.md +119 -0
- package/extensions/hexstrike-bridge/index.test.ts +247 -0
- package/extensions/hexstrike-bridge/index.ts +487 -0
- package/extensions/hexstrike-bridge/package.json +17 -0
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/matrix/CHANGELOG.md +5 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/mattermost/package.json +1 -1
- package/extensions/mcp-server/index.ts +14 -0
- package/extensions/mcp-server/package.json +11 -0
- package/extensions/mcp-server/src/service.ts +540 -0
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +5 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +5 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/openai-codex-auth/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +5 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +5 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +5 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +5 -0
- package/extensions/zalouser/package.json +1 -1
- package/package.json +8 -1
- package/skills/example-skill/SKILL.md +195 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Security Scanner
|
|
3
|
+
*
|
|
4
|
+
* Security scanning for PoolBot skills.
|
|
5
|
+
* Detects potential security issues in skill files.
|
|
6
|
+
*
|
|
7
|
+
* @module agents/skills/security
|
|
8
|
+
*/
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Constants
|
|
11
|
+
// ============================================================================
|
|
12
|
+
const SCANNER_VERSION = "1.0.0";
|
|
13
|
+
// Patterns that indicate potential security issues
|
|
14
|
+
const PATTERNS = [
|
|
15
|
+
// Prompt injection attempts
|
|
16
|
+
{
|
|
17
|
+
type: "prompt_injection",
|
|
18
|
+
severity: "critical",
|
|
19
|
+
pattern: /ignore\s+(?:previous|above|prior)|disregard\s+(?:instructions?|prompt)|system\s*:\s*you\s+are|new\s+instructions?\s*:/i,
|
|
20
|
+
description: "Potential prompt injection attempt detected",
|
|
21
|
+
remediation: "Review skill content for malicious instruction overrides",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
type: "prompt_injection",
|
|
25
|
+
severity: "high",
|
|
26
|
+
pattern: /\[\s*system\s*\]|\(\s*system\s*\)|\{\s*system\s*\}|\bDAN\b|do\s+anything\s+now/i,
|
|
27
|
+
description: "Suspicious system role reference",
|
|
28
|
+
remediation: "Verify skill doesn't attempt to override system behavior",
|
|
29
|
+
},
|
|
30
|
+
// Command injection
|
|
31
|
+
{
|
|
32
|
+
type: "command_injection",
|
|
33
|
+
severity: "critical",
|
|
34
|
+
pattern: /(?:bash|sh|zsh|cmd|powershell)\s+-c\s+["']|exec\s*\(|eval\s*\(|system\s*\(/i,
|
|
35
|
+
description: "Potential command injection pattern",
|
|
36
|
+
remediation: "Avoid executing arbitrary shell commands from skill content",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: "command_injection",
|
|
40
|
+
severity: "high",
|
|
41
|
+
pattern: /`[^`]*(?:rm|del|format|mkfs|dd|wget|curl|fetch)[^`]*`|\$\([^)]*(?:rm|del|wget|curl)[^)]*\)/i,
|
|
42
|
+
description: "Dangerous command in template literal",
|
|
43
|
+
remediation: "Review shell command usage for safety",
|
|
44
|
+
},
|
|
45
|
+
// Path traversal
|
|
46
|
+
{
|
|
47
|
+
type: "path_traversal",
|
|
48
|
+
severity: "high",
|
|
49
|
+
pattern: /\.\.[/\\]|\.\.%2f|\.\.%5c|%2e%2e[/\\]/i,
|
|
50
|
+
description: "Path traversal attempt detected",
|
|
51
|
+
remediation: "Validate and sanitize all file paths",
|
|
52
|
+
},
|
|
53
|
+
// Suspicious patterns
|
|
54
|
+
{
|
|
55
|
+
type: "suspicious_pattern",
|
|
56
|
+
severity: "medium",
|
|
57
|
+
pattern: /(?:password|secret|token|key|credential)\s*=\s*["'][^"']{8,}["']/i,
|
|
58
|
+
description: "Hardcoded credential-like pattern",
|
|
59
|
+
remediation: "Use environment variables or secure secret storage",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: "suspicious_pattern",
|
|
63
|
+
severity: "medium",
|
|
64
|
+
pattern: /base64\s*\(\s*["'][^"']{20,}["']\s*\)|atob\s*\(|btoa\s*\(/i,
|
|
65
|
+
description: "Suspicious encoding/decoding pattern",
|
|
66
|
+
remediation: "Verify encoding is not used to obfuscate malicious content",
|
|
67
|
+
},
|
|
68
|
+
// External dependencies
|
|
69
|
+
{
|
|
70
|
+
type: "external_dependency",
|
|
71
|
+
severity: "low",
|
|
72
|
+
pattern: /(?:npm|pip|gem|cargo|go\s+get)\s+install/i,
|
|
73
|
+
description: "External package installation mentioned",
|
|
74
|
+
remediation: "Verify all external dependencies are trustworthy",
|
|
75
|
+
},
|
|
76
|
+
// Data exfiltration
|
|
77
|
+
{
|
|
78
|
+
type: "data_exfiltration",
|
|
79
|
+
severity: "high",
|
|
80
|
+
pattern: /(?:https?:\/\/|ftp:\/\/)[^\s"']+(?:webhook|callback|exfil|collect|steal|send)/i,
|
|
81
|
+
description: "Potential data exfiltration endpoint",
|
|
82
|
+
remediation: "Verify all external URLs are legitimate",
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Scanner
|
|
87
|
+
// ============================================================================
|
|
88
|
+
/**
|
|
89
|
+
* Scan skill content for security issues
|
|
90
|
+
*/
|
|
91
|
+
export function scanSkill(skillName, content) {
|
|
92
|
+
const findings = [];
|
|
93
|
+
const lines = content.split("\n");
|
|
94
|
+
for (const { type, severity, pattern, description, remediation } of PATTERNS) {
|
|
95
|
+
for (let i = 0; i < lines.length; i++) {
|
|
96
|
+
const line = lines[i];
|
|
97
|
+
const matches = line.matchAll(pattern);
|
|
98
|
+
for (const match of matches) {
|
|
99
|
+
if (match.index !== undefined) {
|
|
100
|
+
findings.push({
|
|
101
|
+
type,
|
|
102
|
+
severity,
|
|
103
|
+
line: i + 1,
|
|
104
|
+
column: match.index + 1,
|
|
105
|
+
match: match[0].slice(0, 100), // Limit match length
|
|
106
|
+
description,
|
|
107
|
+
remediation,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Sort by severity
|
|
114
|
+
const severityOrder = ["critical", "high", "medium", "low", "info"];
|
|
115
|
+
findings.sort((a, b) => severityOrder.indexOf(a.severity) - severityOrder.indexOf(b.severity));
|
|
116
|
+
return {
|
|
117
|
+
skillName,
|
|
118
|
+
scannerVersion: SCANNER_VERSION,
|
|
119
|
+
scannedAt: new Date(),
|
|
120
|
+
findings,
|
|
121
|
+
passed: !findings.some((f) => f.severity === "critical" || f.severity === "high"),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Quick security check - returns true if skill passes basic security
|
|
126
|
+
*/
|
|
127
|
+
export function quickSecurityCheck(skillName, content) {
|
|
128
|
+
const report = scanSkill(skillName, content);
|
|
129
|
+
return report.passed;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get security summary for display
|
|
133
|
+
*/
|
|
134
|
+
export function getSecuritySummary(report) {
|
|
135
|
+
const counts = {
|
|
136
|
+
critical: 0,
|
|
137
|
+
high: 0,
|
|
138
|
+
medium: 0,
|
|
139
|
+
low: 0,
|
|
140
|
+
info: 0,
|
|
141
|
+
};
|
|
142
|
+
for (const finding of report.findings) {
|
|
143
|
+
counts[finding.severity]++;
|
|
144
|
+
}
|
|
145
|
+
const hasCritical = counts.critical > 0;
|
|
146
|
+
const hasHigh = counts.high > 0;
|
|
147
|
+
const hasMedium = counts.medium > 0;
|
|
148
|
+
const hasLow = counts.low > 0;
|
|
149
|
+
if (hasCritical) {
|
|
150
|
+
return {
|
|
151
|
+
status: "Failed",
|
|
152
|
+
color: "red",
|
|
153
|
+
summary: `${counts.critical} critical, ${counts.high} high severity issues`,
|
|
154
|
+
counts,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (hasHigh) {
|
|
158
|
+
return {
|
|
159
|
+
status: "Warning",
|
|
160
|
+
color: "yellow",
|
|
161
|
+
summary: `${counts.high} high severity issues`,
|
|
162
|
+
counts,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (hasMedium || hasLow) {
|
|
166
|
+
return {
|
|
167
|
+
status: "Passed",
|
|
168
|
+
color: "yellow",
|
|
169
|
+
summary: `${counts.medium} medium, ${counts.low} low severity issues`,
|
|
170
|
+
counts,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
status: "Passed",
|
|
175
|
+
color: "green",
|
|
176
|
+
summary: "No security issues found",
|
|
177
|
+
counts,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Format findings for display
|
|
182
|
+
*/
|
|
183
|
+
export function formatFindings(findings) {
|
|
184
|
+
if (findings.length === 0) {
|
|
185
|
+
return ["No security issues found."];
|
|
186
|
+
}
|
|
187
|
+
const lines = [];
|
|
188
|
+
const bySeverity = {
|
|
189
|
+
critical: [],
|
|
190
|
+
high: [],
|
|
191
|
+
medium: [],
|
|
192
|
+
low: [],
|
|
193
|
+
info: [],
|
|
194
|
+
};
|
|
195
|
+
for (const finding of findings) {
|
|
196
|
+
bySeverity[finding.severity].push(finding);
|
|
197
|
+
}
|
|
198
|
+
for (const severity of ["critical", "high", "medium", "low", "info"]) {
|
|
199
|
+
const items = bySeverity[severity];
|
|
200
|
+
if (items.length === 0)
|
|
201
|
+
continue;
|
|
202
|
+
lines.push(`\n${severity.toUpperCase()} (${items.length}):`);
|
|
203
|
+
for (const finding of items) {
|
|
204
|
+
lines.push(` [${finding.type}] Line ${finding.line}: ${finding.description}`);
|
|
205
|
+
if (finding.remediation) {
|
|
206
|
+
lines.push(` → ${finding.remediation}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return lines;
|
|
211
|
+
}
|
package/dist/build-info.json
CHANGED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { danger } from "../../globals.js";
|
|
2
|
+
import { defaultRuntime } from "../../runtime.js";
|
|
3
|
+
import { colorize, isRich, theme } from "../../terminal/theme.js";
|
|
4
|
+
import { renderTable } from "../../terminal/table.js";
|
|
5
|
+
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
|
6
|
+
import { formatDurationHuman } from "../../infra/format-time/format-duration.js";
|
|
7
|
+
import { resolveCronStaggerMs } from "../../cron/stagger.js";
|
|
8
|
+
// Sparkline characters for visual history
|
|
9
|
+
const SPARK_CHARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
10
|
+
// Status indicators
|
|
11
|
+
const STATUS_ICONS = {
|
|
12
|
+
ok: "●",
|
|
13
|
+
error: "✖",
|
|
14
|
+
running: "▶",
|
|
15
|
+
skipped: "○",
|
|
16
|
+
idle: "◌",
|
|
17
|
+
disabled: "⊘",
|
|
18
|
+
};
|
|
19
|
+
// Format relative time with color coding
|
|
20
|
+
function formatRelativeTime(ms, nowMs) {
|
|
21
|
+
if (!ms)
|
|
22
|
+
return "-";
|
|
23
|
+
const delta = ms - nowMs;
|
|
24
|
+
const absMs = Math.abs(delta);
|
|
25
|
+
if (absMs < 60_000)
|
|
26
|
+
return delta >= 0 ? "<1m" : "<1m ago";
|
|
27
|
+
if (absMs < 3_600_000) {
|
|
28
|
+
const mins = Math.round(absMs / 60_000);
|
|
29
|
+
return delta >= 0 ? `${mins}m` : `${mins}m ago`;
|
|
30
|
+
}
|
|
31
|
+
if (absMs < 86_400_000) {
|
|
32
|
+
const hours = Math.round(absMs / 3_600_000);
|
|
33
|
+
return delta >= 0 ? `${hours}h` : `${hours}h ago`;
|
|
34
|
+
}
|
|
35
|
+
const days = Math.round(absMs / 86_400_000);
|
|
36
|
+
return delta >= 0 ? `${days}d` : `${days}d ago`;
|
|
37
|
+
}
|
|
38
|
+
// Format schedule compactly
|
|
39
|
+
function formatScheduleCompact(job) {
|
|
40
|
+
const { schedule } = job;
|
|
41
|
+
if (schedule.kind === "at") {
|
|
42
|
+
const date = new Date(schedule.at);
|
|
43
|
+
return `once ${date.toLocaleDateString()}`;
|
|
44
|
+
}
|
|
45
|
+
if (schedule.kind === "every") {
|
|
46
|
+
return `every ${formatDurationHuman(schedule.everyMs)}`;
|
|
47
|
+
}
|
|
48
|
+
// cron
|
|
49
|
+
const staggerMs = resolveCronStaggerMs(schedule);
|
|
50
|
+
const stagger = staggerMs > 0 ? `~${formatDurationHuman(staggerMs)}` : "exact";
|
|
51
|
+
return `${schedule.expr} ${stagger}`;
|
|
52
|
+
}
|
|
53
|
+
// Get status with icon
|
|
54
|
+
function getStatusDisplay(job, rich) {
|
|
55
|
+
if (!job.enabled) {
|
|
56
|
+
return rich ? `${colorize(rich, theme.muted, STATUS_ICONS.disabled)} disabled` : "disabled";
|
|
57
|
+
}
|
|
58
|
+
if (job.state.runningAtMs) {
|
|
59
|
+
return rich ? `${colorize(rich, theme.warn, STATUS_ICONS.running)} running` : "running";
|
|
60
|
+
}
|
|
61
|
+
const status = job.state.lastStatus ?? "idle";
|
|
62
|
+
const icon = STATUS_ICONS[status] ?? STATUS_ICONS.idle;
|
|
63
|
+
if (!rich)
|
|
64
|
+
return status;
|
|
65
|
+
switch (status) {
|
|
66
|
+
case "ok":
|
|
67
|
+
return `${colorize(rich, theme.success, icon)} ${status}`;
|
|
68
|
+
case "error":
|
|
69
|
+
return `${colorize(rich, theme.error, icon)} ${status}`;
|
|
70
|
+
case "skipped":
|
|
71
|
+
return `${colorize(rich, theme.muted, icon)} ${status}`;
|
|
72
|
+
default:
|
|
73
|
+
return `${colorize(rich, theme.muted, icon)} ${status}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Generate sparkline from execution history (simulated from state)
|
|
77
|
+
function generateSparkline(job, rich) {
|
|
78
|
+
// In a real implementation, this would use actual history data
|
|
79
|
+
// For now, we create a visual indicator based on recent state
|
|
80
|
+
const { consecutiveErrors = 0, lastStatus } = job.state;
|
|
81
|
+
if (!rich) {
|
|
82
|
+
return consecutiveErrors > 0 ? `!${consecutiveErrors}` : "-";
|
|
83
|
+
}
|
|
84
|
+
if (consecutiveErrors > 0) {
|
|
85
|
+
const level = Math.min(consecutiveErrors, SPARK_CHARS.length - 1);
|
|
86
|
+
return colorize(rich, theme.error, SPARK_CHARS[level].repeat(3));
|
|
87
|
+
}
|
|
88
|
+
if (lastStatus === "ok") {
|
|
89
|
+
return colorize(rich, theme.success, "▁▃▆");
|
|
90
|
+
}
|
|
91
|
+
return colorize(rich, theme.muted, "···");
|
|
92
|
+
}
|
|
93
|
+
// Format job target
|
|
94
|
+
function formatTarget(job, rich) {
|
|
95
|
+
const { sessionTarget, agentId } = job;
|
|
96
|
+
if (!rich)
|
|
97
|
+
return agentId ?? sessionTarget ?? "-";
|
|
98
|
+
const target = sessionTarget === "isolated"
|
|
99
|
+
? colorize(rich, theme.accentBright, "iso")
|
|
100
|
+
: colorize(rich, theme.accent, "main");
|
|
101
|
+
if (agentId) {
|
|
102
|
+
return `${target}:${colorize(rich, theme.info, agentId.slice(0, 8))}`;
|
|
103
|
+
}
|
|
104
|
+
return target;
|
|
105
|
+
}
|
|
106
|
+
// Calculate health score (0-100)
|
|
107
|
+
function calculateHealthScore(job) {
|
|
108
|
+
if (!job.enabled)
|
|
109
|
+
return 100; // Disabled jobs are "healthy" by not running
|
|
110
|
+
if (job.state.runningAtMs)
|
|
111
|
+
return 100; // Currently running is fine
|
|
112
|
+
const { consecutiveErrors = 0, lastStatus, scheduleErrorCount = 0 } = job.state;
|
|
113
|
+
if (consecutiveErrors >= 5 || scheduleErrorCount >= 10)
|
|
114
|
+
return 0;
|
|
115
|
+
if (consecutiveErrors > 0)
|
|
116
|
+
return Math.max(0, 100 - consecutiveErrors * 20);
|
|
117
|
+
if (lastStatus === "error")
|
|
118
|
+
return 50;
|
|
119
|
+
if (lastStatus === "skipped")
|
|
120
|
+
return 75;
|
|
121
|
+
return 100;
|
|
122
|
+
}
|
|
123
|
+
// Get health indicator
|
|
124
|
+
function getHealthIndicator(score, rich) {
|
|
125
|
+
if (!rich)
|
|
126
|
+
return `${score}%`;
|
|
127
|
+
if (score >= 90)
|
|
128
|
+
return colorize(rich, theme.success, "●");
|
|
129
|
+
if (score >= 70)
|
|
130
|
+
return colorize(rich, theme.warn, "●");
|
|
131
|
+
return colorize(rich, theme.error, "●");
|
|
132
|
+
}
|
|
133
|
+
// Print dashboard header
|
|
134
|
+
function printDashboardHeader(runtime = defaultRuntime) {
|
|
135
|
+
const rich = isRich();
|
|
136
|
+
const now = new Date();
|
|
137
|
+
const timestamp = now.toLocaleString();
|
|
138
|
+
runtime.log("");
|
|
139
|
+
if (rich) {
|
|
140
|
+
runtime.log(colorize(rich, theme.heading, "╔═══════════════════════════════════════════════════════════╗"));
|
|
141
|
+
runtime.log(colorize(rich, theme.heading, `║ 🕐 CRON DASHBOARD ${timestamp.padStart(26)} ║`));
|
|
142
|
+
runtime.log(colorize(rich, theme.heading, "╚═══════════════════════════════════════════════════════════╝"));
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
runtime.log("═══════════════════════════════════════════════════════════");
|
|
146
|
+
runtime.log(`CRON DASHBOARD - ${timestamp}`);
|
|
147
|
+
runtime.log("═══════════════════════════════════════════════════════════");
|
|
148
|
+
}
|
|
149
|
+
runtime.log("");
|
|
150
|
+
}
|
|
151
|
+
// Print summary stats
|
|
152
|
+
function printSummaryStats(jobs, runtime = defaultRuntime) {
|
|
153
|
+
const rich = isRich();
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
const enabled = jobs.filter((j) => j.enabled).length;
|
|
156
|
+
const running = jobs.filter((j) => j.state.runningAtMs).length;
|
|
157
|
+
const errors = jobs.filter((j) => j.enabled && j.state.lastStatus === "error" && !j.state.runningAtMs).length;
|
|
158
|
+
const upcoming = jobs.filter((j) => j.enabled && j.state.nextRunAtMs && j.state.nextRunAtMs > now).length;
|
|
159
|
+
if (rich) {
|
|
160
|
+
const stats = [
|
|
161
|
+
`${colorize(rich, theme.info, "Total:")} ${jobs.length}`,
|
|
162
|
+
`${colorize(rich, theme.success, "Enabled:")} ${enabled}`,
|
|
163
|
+
`${colorize(rich, theme.warn, "Running:")} ${running}`,
|
|
164
|
+
`${colorize(rich, theme.error, "Errors:")} ${errors}`,
|
|
165
|
+
`${colorize(rich, theme.accent, "Upcoming:")} ${upcoming}`,
|
|
166
|
+
].join(" │ ");
|
|
167
|
+
runtime.log(` ${stats}`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
runtime.log(` Total: ${jobs.length} | Enabled: ${enabled} | Running: ${running} | Errors: ${errors} | Upcoming: ${upcoming}`);
|
|
171
|
+
}
|
|
172
|
+
runtime.log("");
|
|
173
|
+
}
|
|
174
|
+
// Print jobs table
|
|
175
|
+
function printJobsTable(jobs, runtime = defaultRuntime) {
|
|
176
|
+
const rich = isRich();
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
if (jobs.length === 0) {
|
|
179
|
+
runtime.log(rich
|
|
180
|
+
? colorize(rich, theme.muted, " No cron jobs configured.")
|
|
181
|
+
: " No cron jobs configured.");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Sort: running first, then by next run time
|
|
185
|
+
const sortedJobs = [...jobs].sort((a, b) => {
|
|
186
|
+
if (a.state.runningAtMs && !b.state.runningAtMs)
|
|
187
|
+
return -1;
|
|
188
|
+
if (!a.state.runningAtMs && b.state.runningAtMs)
|
|
189
|
+
return 1;
|
|
190
|
+
if (!a.enabled && b.enabled)
|
|
191
|
+
return 1;
|
|
192
|
+
if (a.enabled && !b.enabled)
|
|
193
|
+
return -1;
|
|
194
|
+
const aNext = a.state.nextRunAtMs ?? Infinity;
|
|
195
|
+
const bNext = b.state.nextRunAtMs ?? Infinity;
|
|
196
|
+
return aNext - bNext;
|
|
197
|
+
});
|
|
198
|
+
const columns = [
|
|
199
|
+
{ key: "health", header: "", align: "center", minWidth: 2, maxWidth: 3 },
|
|
200
|
+
{
|
|
201
|
+
key: "status",
|
|
202
|
+
header: rich ? colorize(rich, theme.heading, "Status") : "Status",
|
|
203
|
+
align: "left",
|
|
204
|
+
minWidth: 10,
|
|
205
|
+
maxWidth: 12,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
key: "name",
|
|
209
|
+
header: rich ? colorize(rich, theme.heading, "Name") : "Name",
|
|
210
|
+
align: "left",
|
|
211
|
+
minWidth: 20,
|
|
212
|
+
flex: true,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
key: "schedule",
|
|
216
|
+
header: rich ? colorize(rich, theme.heading, "Schedule") : "Schedule",
|
|
217
|
+
align: "left",
|
|
218
|
+
minWidth: 18,
|
|
219
|
+
maxWidth: 25,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
key: "next",
|
|
223
|
+
header: rich ? colorize(rich, theme.heading, "Next") : "Next",
|
|
224
|
+
align: "right",
|
|
225
|
+
minWidth: 10,
|
|
226
|
+
maxWidth: 12,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
key: "last",
|
|
230
|
+
header: rich ? colorize(rich, theme.heading, "Last") : "Last",
|
|
231
|
+
align: "right",
|
|
232
|
+
minWidth: 10,
|
|
233
|
+
maxWidth: 12,
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
key: "target",
|
|
237
|
+
header: rich ? colorize(rich, theme.heading, "Target") : "Target",
|
|
238
|
+
align: "left",
|
|
239
|
+
minWidth: 12,
|
|
240
|
+
maxWidth: 15,
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
key: "trend",
|
|
244
|
+
header: rich ? colorize(rich, theme.heading, "Trend") : "Trend",
|
|
245
|
+
align: "center",
|
|
246
|
+
minWidth: 4,
|
|
247
|
+
maxWidth: 6,
|
|
248
|
+
},
|
|
249
|
+
];
|
|
250
|
+
const rows = sortedJobs.map((job) => {
|
|
251
|
+
const healthScore = calculateHealthScore(job);
|
|
252
|
+
const nextRun = formatRelativeTime(job.state.nextRunAtMs, now);
|
|
253
|
+
const lastRun = formatRelativeTime(job.state.lastRunAtMs, now);
|
|
254
|
+
// Color-code the times
|
|
255
|
+
const coloredNext = rich && job.enabled && job.state.nextRunAtMs
|
|
256
|
+
? job.state.nextRunAtMs - now < 300_000
|
|
257
|
+
? colorize(rich, theme.warn, nextRun) // < 5 min
|
|
258
|
+
: colorize(rich, theme.muted, nextRun)
|
|
259
|
+
: nextRun;
|
|
260
|
+
const coloredLast = rich && job.state.lastRunAtMs
|
|
261
|
+
? now - job.state.lastRunAtMs < 300_000
|
|
262
|
+
? colorize(rich, theme.accentBright, lastRun) // < 5 min ago
|
|
263
|
+
: colorize(rich, theme.muted, lastRun)
|
|
264
|
+
: lastRun;
|
|
265
|
+
return {
|
|
266
|
+
health: getHealthIndicator(healthScore, rich),
|
|
267
|
+
status: getStatusDisplay(job, rich),
|
|
268
|
+
name: rich ? colorize(rich, theme.info, job.name) : job.name,
|
|
269
|
+
schedule: formatScheduleCompact(job),
|
|
270
|
+
next: coloredNext,
|
|
271
|
+
last: coloredLast,
|
|
272
|
+
target: formatTarget(job, rich),
|
|
273
|
+
trend: generateSparkline(job, rich),
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
const table = renderTable({
|
|
277
|
+
columns,
|
|
278
|
+
rows,
|
|
279
|
+
border: rich ? "unicode" : "ascii",
|
|
280
|
+
padding: 1,
|
|
281
|
+
});
|
|
282
|
+
runtime.log(table);
|
|
283
|
+
}
|
|
284
|
+
// Print legend
|
|
285
|
+
function printLegend(runtime = defaultRuntime) {
|
|
286
|
+
const rich = isRich();
|
|
287
|
+
runtime.log("");
|
|
288
|
+
if (rich) {
|
|
289
|
+
runtime.log(colorize(rich, theme.muted, " Legend:"));
|
|
290
|
+
runtime.log(` ${colorize(rich, theme.success, STATUS_ICONS.ok)} ok ${colorize(rich, theme.error, STATUS_ICONS.error)} error ${colorize(rich, theme.warn, STATUS_ICONS.running)} running ${colorize(rich, theme.muted, STATUS_ICONS.skipped)} skipped ${colorize(rich, theme.muted, STATUS_ICONS.disabled)} disabled`);
|
|
291
|
+
runtime.log(` ${colorize(rich, theme.success, "●")} healthy ${colorize(rich, theme.warn, "●")} warning ${colorize(rich, theme.error, "●")} critical`);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
runtime.log(" Legend:");
|
|
295
|
+
runtime.log(" ● ok ✖ error ▶ running ○ skipped ⊘ disabled");
|
|
296
|
+
runtime.log(" ● healthy ● warning ● critical");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Print footer with tips
|
|
300
|
+
function printFooter(runtime = defaultRuntime) {
|
|
301
|
+
const rich = isRich();
|
|
302
|
+
runtime.log("");
|
|
303
|
+
if (rich) {
|
|
304
|
+
runtime.log(colorize(rich, theme.muted, " Commands: poolbot cron list | poolbot cron add --help | poolbot cron status"));
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
runtime.log(" Commands: poolbot cron list | poolbot cron add --help | poolbot cron status");
|
|
308
|
+
}
|
|
309
|
+
runtime.log("");
|
|
310
|
+
}
|
|
311
|
+
export function registerCronDashboardCommand(cron) {
|
|
312
|
+
addGatewayClientOptions(cron
|
|
313
|
+
.command("dashboard")
|
|
314
|
+
.alias("dash")
|
|
315
|
+
.description("Visual dashboard for cron jobs")
|
|
316
|
+
.option("--all", "Include disabled jobs", false)
|
|
317
|
+
.option("--json", "Output JSON (disables visual dashboard)", false)
|
|
318
|
+
.action(async (opts) => {
|
|
319
|
+
try {
|
|
320
|
+
const res = await callGatewayFromCli("cron.list", opts, {
|
|
321
|
+
includeDisabled: Boolean(opts.all),
|
|
322
|
+
});
|
|
323
|
+
if (opts.json) {
|
|
324
|
+
defaultRuntime.log(JSON.stringify(res, null, 2));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const jobs = res?.jobs ?? [];
|
|
328
|
+
printDashboardHeader();
|
|
329
|
+
printSummaryStats(jobs);
|
|
330
|
+
printJobsTable(jobs);
|
|
331
|
+
printLegend();
|
|
332
|
+
printFooter();
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
defaultRuntime.error(danger(String(err)));
|
|
336
|
+
defaultRuntime.exit(1);
|
|
337
|
+
}
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { formatDocsLink } from "../../terminal/links.js";
|
|
2
2
|
import { theme } from "../../terminal/theme.js";
|
|
3
3
|
import { registerCronAddCommand, registerCronListCommand, registerCronStatusCommand, } from "./register.cron-add.js";
|
|
4
|
+
import { registerCronDashboardCommand } from "./register.cron-dashboard.js";
|
|
4
5
|
import { registerCronEditCommand } from "./register.cron-edit.js";
|
|
5
6
|
import { registerCronSimpleCommands } from "./register.cron-simple.js";
|
|
6
7
|
export function registerCronCli(program) {
|
|
@@ -10,6 +11,7 @@ export function registerCronCli(program) {
|
|
|
10
11
|
.addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/cron", "docs.molt.bot/cli/cron")}\n`);
|
|
11
12
|
registerCronStatusCommand(cron);
|
|
12
13
|
registerCronListCommand(cron);
|
|
14
|
+
registerCronDashboardCommand(cron);
|
|
13
15
|
registerCronAddCommand(cron);
|
|
14
16
|
registerCronSimpleCommands(cron);
|
|
15
17
|
registerCronEditCommand(cron);
|