@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.
Files changed (150) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +147 -69
  3. package/dist/.buildstamp +1 -1
  4. package/dist/agents/error-classifier.js +251 -0
  5. package/dist/agents/skills/security.js +211 -0
  6. package/dist/build-info.json +3 -3
  7. package/dist/cli/cron-cli/register.cron-dashboard.js +339 -0
  8. package/dist/cli/cron-cli/register.js +2 -0
  9. package/dist/cli/errors.js +187 -0
  10. package/dist/cli/lazy-commands.example.js +113 -0
  11. package/dist/cli/lazy-commands.js +329 -0
  12. package/dist/cli/program/command-registry.js +26 -0
  13. package/dist/cli/program/register.maintenance.js +21 -0
  14. package/dist/cli/program/register.skills.js +4 -0
  15. package/dist/cli/program/register.subclis.js +9 -0
  16. package/dist/cli/swarm-cli/register.js +8 -0
  17. package/dist/cli/swarm-cli/register.swarm-status.js +488 -0
  18. package/dist/cli/telemetry-cli/register.js +10 -0
  19. package/dist/cli/telemetry-cli/register.telemetry-alerts.js +176 -0
  20. package/dist/cli/telemetry-cli/register.telemetry-metrics.js +323 -0
  21. package/dist/cli/telemetry-cli/register.telemetry-status.js +179 -0
  22. package/dist/commands/doctor-checks.js +498 -0
  23. package/dist/config/config.js +1 -0
  24. package/dist/config/secrets-integration.js +88 -0
  25. package/dist/context-engine/index.js +33 -0
  26. package/dist/context-engine/legacy.js +179 -0
  27. package/dist/context-engine/registry.js +86 -0
  28. package/dist/context-engine/summarizing.js +290 -0
  29. package/dist/context-engine/types.js +7 -0
  30. package/dist/cron/service/timer.js +18 -0
  31. package/dist/gateway/protocol/index.js +5 -2
  32. package/dist/gateway/protocol/schema/error-codes.js +1 -0
  33. package/dist/gateway/protocol/schema/swarm.js +80 -0
  34. package/dist/gateway/protocol/schema.js +1 -0
  35. package/dist/gateway/server-close.js +4 -0
  36. package/dist/gateway/server-constants.js +1 -0
  37. package/dist/gateway/server-cron.js +29 -0
  38. package/dist/gateway/server-maintenance.js +35 -2
  39. package/dist/gateway/server-methods/swarm.js +58 -0
  40. package/dist/gateway/server-methods/telemetry.js +71 -0
  41. package/dist/gateway/server-methods-list.js +8 -0
  42. package/dist/gateway/server-methods.js +9 -2
  43. package/dist/gateway/server.impl.js +33 -16
  44. package/dist/infra/abort-pattern.js +106 -0
  45. package/dist/infra/retry.js +96 -0
  46. package/dist/secrets/index.js +28 -0
  47. package/dist/secrets/resolver.js +185 -0
  48. package/dist/secrets/runtime.js +142 -0
  49. package/dist/secrets/types.js +11 -0
  50. package/dist/security/dangerous-tools.js +80 -0
  51. package/dist/security/types.js +12 -0
  52. package/dist/skills/commands.js +333 -0
  53. package/dist/skills/index.js +164 -0
  54. package/dist/skills/loader.js +282 -0
  55. package/dist/skills/parser.js +446 -0
  56. package/dist/skills/registry.js +394 -0
  57. package/dist/skills/security.js +312 -0
  58. package/dist/skills/types.js +21 -0
  59. package/dist/swarm/service.js +247 -0
  60. package/dist/telemetry/alert-engine.js +258 -0
  61. package/dist/telemetry/cron-instrumentation.js +49 -0
  62. package/dist/telemetry/gateway-instrumentation.js +80 -0
  63. package/dist/telemetry/instrumentation.js +66 -0
  64. package/dist/telemetry/service.js +345 -0
  65. package/dist/test-utils/index.js +219 -0
  66. package/dist/tui/components/assistant-message.js +6 -2
  67. package/dist/tui/components/hyperlink-markdown.js +32 -0
  68. package/dist/tui/components/searchable-select-list.js +12 -1
  69. package/dist/tui/components/user-message.js +6 -2
  70. package/dist/tui/index.js +611 -0
  71. package/dist/tui/theme/theme-detection.js +226 -0
  72. package/dist/tui/tui-command-handlers.js +20 -0
  73. package/dist/tui/tui-formatters.js +4 -3
  74. package/dist/tui/utils/ctrl-c-handler.js +67 -0
  75. package/dist/tui/utils/osc8-hyperlinks.js +208 -0
  76. package/dist/tui/utils/safe-stop.js +180 -0
  77. package/dist/tui/utils/session-key-utils.js +81 -0
  78. package/dist/tui/utils/text-sanitization.js +284 -0
  79. package/dist/utils/lru-cache.js +116 -0
  80. package/dist/utils/performance.js +199 -0
  81. package/dist/utils/retry.js +240 -0
  82. package/docs/INTEGRATION_PLAN.md +475 -0
  83. package/docs/INTEGRATION_SUMMARY.md +215 -0
  84. package/docs/MELHORIAS_IMPLEMENTADAS.md +228 -0
  85. package/docs/MELHORIAS_PROFISSIONAIS.md +282 -0
  86. package/docs/PLANO_ACAO_TUI.md +357 -0
  87. package/docs/PROGRESSO_TUI.md +66 -0
  88. package/docs/RELATORIO_FINAL.md +217 -0
  89. package/docs/diagnostico-shell-completion.md +265 -0
  90. package/docs/features/advanced-memory.md +585 -0
  91. package/docs/features/discord-components-v2.md +277 -0
  92. package/docs/features/swarm.md +100 -0
  93. package/docs/features/telemetry.md +284 -0
  94. package/docs/integrations/HEXSTRIKE_PLAN.md +796 -0
  95. package/docs/integrations/INTEGRATION_PLAN.md +744 -0
  96. package/docs/integrations/PAGE_AGENT_PLAN.md +370 -0
  97. package/docs/integrations/XYOPS_PLAN.md +978 -0
  98. package/docs/models/provider-infrastructure.md +400 -0
  99. package/docs/security/exec-approvals.md +294 -0
  100. package/docs/skills/IMPLEMENTATION_SUMMARY.md +145 -0
  101. package/docs/skills/SKILL.md +524 -0
  102. package/docs/skills.md +405 -0
  103. package/extensions/bluebubbles/package.json +1 -1
  104. package/extensions/copilot-proxy/package.json +1 -1
  105. package/extensions/diagnostics-otel/package.json +1 -1
  106. package/extensions/discord/package.json +1 -1
  107. package/extensions/feishu/package.json +1 -1
  108. package/extensions/google-antigravity-auth/package.json +1 -1
  109. package/extensions/google-gemini-cli-auth/package.json +1 -1
  110. package/extensions/googlechat/package.json +1 -1
  111. package/extensions/hexstrike-bridge/README.md +119 -0
  112. package/extensions/hexstrike-bridge/index.test.ts +247 -0
  113. package/extensions/hexstrike-bridge/index.ts +487 -0
  114. package/extensions/hexstrike-bridge/package.json +17 -0
  115. package/extensions/imessage/package.json +1 -1
  116. package/extensions/irc/package.json +1 -1
  117. package/extensions/line/package.json +1 -1
  118. package/extensions/llm-task/package.json +1 -1
  119. package/extensions/lobster/package.json +1 -1
  120. package/extensions/matrix/CHANGELOG.md +5 -0
  121. package/extensions/matrix/package.json +1 -1
  122. package/extensions/mattermost/package.json +1 -1
  123. package/extensions/mcp-server/index.ts +14 -0
  124. package/extensions/mcp-server/package.json +11 -0
  125. package/extensions/mcp-server/src/service.ts +540 -0
  126. package/extensions/memory-core/package.json +1 -1
  127. package/extensions/memory-lancedb/package.json +1 -1
  128. package/extensions/minimax-portal-auth/package.json +1 -1
  129. package/extensions/msteams/CHANGELOG.md +5 -0
  130. package/extensions/msteams/package.json +1 -1
  131. package/extensions/nextcloud-talk/package.json +1 -1
  132. package/extensions/nostr/CHANGELOG.md +5 -0
  133. package/extensions/nostr/package.json +1 -1
  134. package/extensions/open-prose/package.json +1 -1
  135. package/extensions/openai-codex-auth/package.json +1 -1
  136. package/extensions/signal/package.json +1 -1
  137. package/extensions/slack/package.json +1 -1
  138. package/extensions/telegram/package.json +1 -1
  139. package/extensions/tlon/package.json +1 -1
  140. package/extensions/twitch/CHANGELOG.md +5 -0
  141. package/extensions/twitch/package.json +1 -1
  142. package/extensions/voice-call/CHANGELOG.md +5 -0
  143. package/extensions/voice-call/package.json +1 -1
  144. package/extensions/whatsapp/package.json +1 -1
  145. package/extensions/zalo/CHANGELOG.md +5 -0
  146. package/extensions/zalo/package.json +1 -1
  147. package/extensions/zalouser/CHANGELOG.md +5 -0
  148. package/extensions/zalouser/package.json +1 -1
  149. package/package.json +8 -1
  150. 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
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2026.3.7",
3
- "commit": "2bb47cd934fe9cb9d58ee7210ee3997714a07f09",
4
- "builtAt": "2026-03-07T19:13:06.611Z"
2
+ "version": "2026.3.10",
3
+ "commit": "2ec0a7b00dbe2a248658209e1a66d72b4a17ff41",
4
+ "builtAt": "2026-03-10T19:42:26.277Z"
5
5
  }
@@ -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);