@poolzin/pool-bot 2026.2.0 → 2026.2.2
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 +118 -0
- package/README-header.png +0 -0
- package/dist/agents/bash-tools.exec.js +76 -25
- package/dist/agents/cli-runner/helpers.js +9 -11
- package/dist/agents/context.js +1 -1
- package/dist/agents/identity.js +47 -7
- package/dist/agents/memory-search.js +25 -8
- package/dist/agents/model-catalog.js +1 -1
- package/dist/agents/model-selection.js +21 -0
- package/dist/agents/pi-embedded-block-chunker.js +117 -42
- package/dist/agents/pi-embedded-helpers/errors.js +183 -78
- package/dist/agents/pi-embedded-helpers.js +1 -1
- package/dist/agents/pi-embedded-runner/compact.js +8 -10
- package/dist/agents/pi-embedded-runner/model.js +62 -3
- package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
- package/dist/agents/pi-embedded-runner/run.js +199 -46
- package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
- package/dist/agents/pi-embedded-subscribe.js +118 -29
- package/dist/agents/pi-tools.js +10 -5
- package/dist/agents/poolbot-tools.js +15 -10
- package/dist/agents/sandbox-paths.js +31 -0
- package/dist/agents/session-tool-result-guard.js +94 -15
- package/dist/agents/shell-utils.js +51 -0
- package/dist/agents/skills/bundled-context.js +23 -0
- package/dist/agents/skills/bundled-dir.js +41 -7
- package/dist/agents/skills-install.js +60 -23
- package/dist/agents/subagent-announce.js +79 -34
- package/dist/agents/tool-policy.conformance.js +14 -0
- package/dist/agents/tool-policy.js +24 -0
- package/dist/agents/tools/cron-tool.js +166 -19
- package/dist/agents/tools/discord-actions-presence.js +78 -0
- package/dist/agents/tools/image-tool.js +1 -1
- package/dist/agents/tools/message-tool.js +56 -2
- package/dist/agents/tools/sessions-history-tool.js +69 -1
- package/dist/agents/tools/web-search.js +211 -42
- package/dist/agents/usage.js +23 -1
- package/dist/agents/workspace-run.js +67 -0
- package/dist/agents/workspace-templates.js +44 -0
- package/dist/auto-reply/command-auth.js +121 -6
- package/dist/auto-reply/envelope.js +74 -82
- package/dist/auto-reply/reply/commands-compact.js +1 -0
- package/dist/auto-reply/reply/commands-context-report.js +1 -0
- package/dist/auto-reply/reply/commands-context.js +1 -0
- package/dist/auto-reply/reply/commands-models.js +107 -60
- package/dist/auto-reply/reply/commands-ptt.js +171 -0
- package/dist/auto-reply/reply/get-reply-run.js +2 -1
- package/dist/auto-reply/reply/inbound-context.js +5 -1
- package/dist/auto-reply/reply/mentions.js +1 -1
- package/dist/auto-reply/reply/model-selection.js +3 -3
- package/dist/auto-reply/thinking.js +88 -43
- package/dist/browser/bridge-server.js +13 -0
- package/dist/browser/cdp.helpers.js +38 -24
- package/dist/browser/client-fetch.js +50 -7
- package/dist/browser/config.js +1 -10
- package/dist/browser/extension-relay.js +101 -40
- package/dist/browser/pw-ai.js +1 -1
- package/dist/browser/pw-session.js +143 -8
- package/dist/browser/pw-tools-core.interactions.js +125 -27
- package/dist/browser/pw-tools-core.responses.js +1 -1
- package/dist/browser/pw-tools-core.state.js +1 -1
- package/dist/browser/routes/agent.act.js +86 -41
- package/dist/browser/routes/dispatcher.js +4 -4
- package/dist/browser/screenshot.js +1 -1
- package/dist/browser/server.js +13 -0
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/index.html +28 -28
- package/dist/channels/reply-prefix.js +8 -1
- package/dist/cli/cron-cli/register.cron-add.js +61 -40
- package/dist/cli/cron-cli/register.cron-edit.js +60 -34
- package/dist/cli/cron-cli/shared.js +56 -41
- package/dist/cli/dns-cli.js +26 -14
- package/dist/cli/gateway-cli/register.js +37 -19
- package/dist/cli/memory-cli.js +5 -5
- package/dist/cli/parse-bytes.js +37 -0
- package/dist/cli/update-cli.js +173 -52
- package/dist/commands/agent.js +1 -0
- package/dist/commands/auth-choice.apply.oauth.js +1 -1
- package/dist/commands/doctor-config-flow.js +61 -5
- package/dist/commands/doctor-state-migrations.js +1 -1
- package/dist/commands/health.js +1 -1
- package/dist/commands/model-allowlist.js +29 -0
- package/dist/commands/model-picker.js +2 -1
- package/dist/commands/models/list.registry.js +1 -1
- package/dist/commands/models/list.status-command.js +43 -23
- package/dist/commands/models/shared.js +15 -0
- package/dist/commands/onboard-custom.js +384 -0
- package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
- package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
- package/dist/commands/onboard-skills.js +63 -38
- package/dist/commands/openai-model-default.js +41 -0
- package/dist/compat/legacy-names.js +2 -0
- package/dist/config/defaults.js +3 -2
- package/dist/config/paths.js +136 -35
- package/dist/config/plugin-auto-enable.js +21 -5
- package/dist/config/redact-snapshot.js +153 -0
- package/dist/config/schema.field-metadata.js +590 -0
- package/dist/config/schema.js +2 -2
- package/dist/config/sessions/store.js +291 -23
- package/dist/config/zod-schema.agent-defaults.js +3 -0
- package/dist/config/zod-schema.agent-runtime.js +13 -2
- package/dist/config/zod-schema.providers-core.js +142 -0
- package/dist/config/zod-schema.session.js +3 -0
- package/dist/control-ui/assets/{index-CIRDm-Lu.css → index-CSfXd2LO.css} +1 -1
- package/dist/control-ui/assets/{index-CmNMuoem.js → index-HRr1grwl.js} +446 -413
- package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -0
- package/dist/control-ui/index.html +4 -4
- package/dist/cron/delivery.js +57 -0
- package/dist/cron/isolated-agent/delivery-target.js +18 -3
- package/dist/cron/isolated-agent/helpers.js +22 -5
- package/dist/cron/isolated-agent/run.js +172 -63
- package/dist/cron/isolated-agent/session.js +2 -0
- package/dist/cron/normalize.js +356 -28
- package/dist/cron/parse.js +10 -5
- package/dist/cron/run-log.js +35 -10
- package/dist/cron/schedule.js +41 -6
- package/dist/cron/service/jobs.js +208 -35
- package/dist/cron/service/ops.js +72 -16
- package/dist/cron/service/state.js +2 -0
- package/dist/cron/service/store.js +386 -14
- package/dist/cron/service/timer.js +390 -147
- package/dist/cron/session-reaper.js +86 -0
- package/dist/cron/store.js +23 -8
- package/dist/cron/validate-timestamp.js +43 -0
- package/dist/discord/monitor/agent-components.js +438 -0
- package/dist/discord/monitor/allow-list.js +28 -5
- package/dist/discord/monitor/gateway-registry.js +29 -0
- package/dist/discord/monitor/native-command.js +44 -23
- package/dist/discord/monitor/sender-identity.js +45 -0
- package/dist/discord/pluralkit.js +27 -0
- package/dist/discord/send.outbound.js +92 -5
- package/dist/discord/send.shared.js +60 -23
- package/dist/discord/targets.js +84 -1
- package/dist/entry.js +15 -9
- package/dist/extensionAPI.js +8 -0
- package/dist/gateway/control-ui.js +8 -1
- package/dist/gateway/hooks-mapping.js +3 -0
- package/dist/gateway/hooks.js +65 -0
- package/dist/gateway/net.js +96 -31
- package/dist/gateway/node-command-policy.js +50 -15
- package/dist/gateway/origin-check.js +56 -0
- package/dist/gateway/protocol/client-info.js +9 -0
- package/dist/gateway/protocol/index.js +9 -2
- package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
- package/dist/gateway/protocol/schema/cron.js +22 -10
- package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
- package/dist/gateway/protocol/schema/sessions.js +12 -0
- package/dist/gateway/server/hooks.js +1 -1
- package/dist/gateway/server-broadcast.js +26 -9
- package/dist/gateway/server-chat.js +112 -23
- package/dist/gateway/server-discovery-runtime.js +10 -2
- package/dist/gateway/server-http.js +109 -11
- package/dist/gateway/server-methods/agent-timestamp.js +60 -0
- package/dist/gateway/server-methods/agents.js +321 -2
- package/dist/gateway/server-methods/usage.js +559 -16
- package/dist/gateway/server-runtime-state.js +22 -8
- package/dist/gateway/server-startup-memory.js +16 -0
- package/dist/gateway/server.impl.js +5 -1
- package/dist/gateway/session-utils.fs.js +23 -25
- package/dist/gateway/session-utils.js +20 -10
- package/dist/gateway/sessions-patch.js +7 -22
- package/dist/gateway/test-helpers.mocks.js +11 -7
- package/dist/gateway/test-helpers.server.js +35 -2
- package/dist/imessage/constants.js +2 -0
- package/dist/imessage/monitor/deliver.js +4 -1
- package/dist/imessage/monitor/monitor-provider.js +51 -1
- package/dist/infra/bonjour-discovery.js +131 -70
- package/dist/infra/control-ui-assets.js +134 -12
- package/dist/infra/errors.js +12 -0
- package/dist/infra/exec-approvals.js +266 -57
- package/dist/infra/format-time/format-datetime.js +79 -0
- package/dist/infra/format-time/format-duration.js +81 -0
- package/dist/infra/format-time/format-relative.js +80 -0
- package/dist/infra/heartbeat-runner.js +140 -49
- package/dist/infra/home-dir.js +54 -0
- package/dist/infra/net/fetch-guard.js +122 -0
- package/dist/infra/net/ssrf.js +65 -29
- package/dist/infra/outbound/abort.js +14 -0
- package/dist/infra/outbound/message-action-runner.js +77 -13
- package/dist/infra/outbound/outbound-session.js +143 -37
- package/dist/infra/poolbot-root.js +43 -1
- package/dist/infra/session-cost-usage.js +631 -41
- package/dist/infra/state-migrations.js +317 -47
- package/dist/infra/update-global.js +35 -0
- package/dist/infra/update-runner.js +149 -43
- package/dist/infra/warning-filter.js +65 -0
- package/dist/infra/widearea-dns.js +30 -9
- package/dist/logging/redact-identifier.js +12 -0
- package/dist/media/fetch.js +81 -58
- package/dist/media/store.js +2 -0
- package/dist/media-understanding/apply.js +403 -3
- package/dist/media-understanding/attachments.js +38 -27
- package/dist/media-understanding/defaults.js +16 -0
- package/dist/media-understanding/providers/deepgram/audio.js +22 -14
- package/dist/media-understanding/providers/google/audio.js +24 -17
- package/dist/media-understanding/providers/google/video.js +24 -17
- package/dist/media-understanding/providers/image.js +3 -3
- package/dist/media-understanding/providers/index.js +4 -1
- package/dist/media-understanding/providers/openai/audio.js +22 -14
- package/dist/media-understanding/providers/shared.js +16 -11
- package/dist/media-understanding/providers/zai/index.js +6 -0
- package/dist/media-understanding/runner.js +158 -90
- package/dist/memory/batch-voyage.js +277 -0
- package/dist/memory/embeddings-voyage.js +75 -0
- package/dist/memory/embeddings.js +28 -16
- package/dist/memory/internal.js +101 -18
- package/dist/memory/manager.js +154 -48
- package/dist/memory/search-manager.js +173 -0
- package/dist/memory/session-files.js +9 -3
- package/dist/node-host/runner.js +34 -24
- package/dist/node-host/with-timeout.js +27 -0
- package/dist/plugins/commands.js +5 -1
- package/dist/plugins/config-state.js +86 -7
- package/dist/plugins/source-display.js +51 -0
- package/dist/process/exec.js +20 -2
- package/dist/routing/resolve-route.js +12 -0
- package/dist/routing/session-key.js +15 -0
- package/dist/runtime.js +2 -0
- package/dist/security/audit-extra.async.js +601 -0
- package/dist/security/audit-extra.js +2 -830
- package/dist/security/audit-extra.sync.js +505 -0
- package/dist/security/channel-metadata.js +34 -0
- package/dist/security/external-content.js +88 -6
- package/dist/security/skill-scanner.js +330 -0
- package/dist/sessions/session-key-utils.js +7 -0
- package/dist/signal/monitor/event-handler.js +80 -1
- package/dist/slack/monitor/media.js +85 -15
- package/dist/tailscale/detect.js +1 -2
- package/dist/telegram/bot/helpers.js +109 -28
- package/dist/telegram/bot-handlers.js +144 -3
- package/dist/telegram/bot-message-context.js +37 -10
- package/dist/telegram/bot-message-dispatch.js +54 -17
- package/dist/telegram/bot-native-commands.js +86 -29
- package/dist/telegram/bot.js +30 -29
- package/dist/telegram/model-buttons.js +163 -0
- package/dist/telegram/monitor.js +110 -85
- package/dist/telegram/send.js +129 -47
- package/dist/terminal/restore.js +45 -0
- package/dist/test-helpers/state-dir-env.js +16 -0
- package/dist/tts/tts.js +12 -6
- package/dist/tui/tui-session-actions.js +166 -54
- package/dist/utils/fetch-timeout.js +20 -0
- package/dist/utils/normalize-secret-input.js +19 -0
- package/dist/utils/transcript-tools.js +58 -0
- package/dist/utils.js +45 -14
- package/dist/version.js +42 -5
- package/dist/wizard/clack-prompter.js +9 -6
- package/extensions/googlechat/node_modules/.bin/poolbot +21 -0
- package/extensions/googlechat/package.json +2 -2
- package/extensions/line/node_modules/.bin/poolbot +21 -0
- package/extensions/line/package.json +1 -1
- package/extensions/matrix/node_modules/.bin/poolbot +21 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/memory-core/node_modules/.bin/poolbot +21 -0
- package/extensions/memory-core/package.json +4 -1
- package/extensions/twitch/node_modules/.bin/poolbot +21 -0
- package/extensions/twitch/package.json +1 -1
- package/package.json +183 -24
- package/dist/control-ui/assets/index-CmNMuoem.js.map +0 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { hasErrnoCode } from "../infra/errors.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Scannable extensions
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
const SCANNABLE_EXTENSIONS = new Set([
|
|
8
|
+
".js",
|
|
9
|
+
".ts",
|
|
10
|
+
".mjs",
|
|
11
|
+
".cjs",
|
|
12
|
+
".mts",
|
|
13
|
+
".cts",
|
|
14
|
+
".jsx",
|
|
15
|
+
".tsx",
|
|
16
|
+
]);
|
|
17
|
+
const DEFAULT_MAX_SCAN_FILES = 500;
|
|
18
|
+
const DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
|
|
19
|
+
export function isScannable(filePath) {
|
|
20
|
+
return SCANNABLE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
21
|
+
}
|
|
22
|
+
const LINE_RULES = [
|
|
23
|
+
{
|
|
24
|
+
ruleId: "dangerous-exec",
|
|
25
|
+
severity: "critical",
|
|
26
|
+
message: "Shell command execution detected (child_process)",
|
|
27
|
+
pattern: /\b(exec|execSync|spawn|spawnSync|execFile|execFileSync)\s*\(/,
|
|
28
|
+
requiresContext: /child_process/,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
ruleId: "dynamic-code-execution",
|
|
32
|
+
severity: "critical",
|
|
33
|
+
message: "Dynamic code execution detected",
|
|
34
|
+
pattern: /\beval\s*\(|new\s+Function\s*\(/,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
ruleId: "crypto-mining",
|
|
38
|
+
severity: "critical",
|
|
39
|
+
message: "Possible crypto-mining reference detected",
|
|
40
|
+
pattern: /stratum\+tcp|stratum\+ssl|coinhive|cryptonight|xmrig/i,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
ruleId: "suspicious-network",
|
|
44
|
+
severity: "warn",
|
|
45
|
+
message: "WebSocket connection to non-standard port",
|
|
46
|
+
pattern: /new\s+WebSocket\s*\(\s*["']wss?:\/\/[^"']*:(\d+)/,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
const STANDARD_PORTS = new Set([80, 443, 8080, 8443, 3000]);
|
|
50
|
+
const SOURCE_RULES = [
|
|
51
|
+
{
|
|
52
|
+
ruleId: "potential-exfiltration",
|
|
53
|
+
severity: "warn",
|
|
54
|
+
message: "File read combined with network send — possible data exfiltration",
|
|
55
|
+
pattern: /readFileSync|readFile/,
|
|
56
|
+
requiresContext: /\bfetch\b|\bpost\b|http\.request/i,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
ruleId: "obfuscated-code",
|
|
60
|
+
severity: "warn",
|
|
61
|
+
message: "Hex-encoded string sequence detected (possible obfuscation)",
|
|
62
|
+
pattern: /(\\x[0-9a-fA-F]{2}){6,}/,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
ruleId: "obfuscated-code",
|
|
66
|
+
severity: "warn",
|
|
67
|
+
message: "Large base64 payload with decode call detected (possible obfuscation)",
|
|
68
|
+
pattern: /(?:atob|Buffer\.from)\s*\(\s*["'][A-Za-z0-9+/=]{200,}["']/,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
ruleId: "env-harvesting",
|
|
72
|
+
severity: "critical",
|
|
73
|
+
message: "Environment variable access combined with network send — possible credential harvesting",
|
|
74
|
+
pattern: /process\.env/,
|
|
75
|
+
requiresContext: /\bfetch\b|\bpost\b|http\.request/i,
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Core scanner
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
function truncateEvidence(evidence, maxLen = 120) {
|
|
82
|
+
if (evidence.length <= maxLen) {
|
|
83
|
+
return evidence;
|
|
84
|
+
}
|
|
85
|
+
return `${evidence.slice(0, maxLen)}…`;
|
|
86
|
+
}
|
|
87
|
+
export function scanSource(source, filePath) {
|
|
88
|
+
const findings = [];
|
|
89
|
+
const lines = source.split("\n");
|
|
90
|
+
const matchedLineRules = new Set();
|
|
91
|
+
// --- Line rules ---
|
|
92
|
+
for (const rule of LINE_RULES) {
|
|
93
|
+
if (matchedLineRules.has(rule.ruleId)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// Skip rule entirely if context requirement not met
|
|
97
|
+
if (rule.requiresContext && !rule.requiresContext.test(source)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
101
|
+
const line = lines[i];
|
|
102
|
+
const match = rule.pattern.exec(line);
|
|
103
|
+
if (!match) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// Special handling for suspicious-network: check port
|
|
107
|
+
if (rule.ruleId === "suspicious-network") {
|
|
108
|
+
const port = parseInt(match[1], 10);
|
|
109
|
+
if (STANDARD_PORTS.has(port)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
findings.push({
|
|
114
|
+
ruleId: rule.ruleId,
|
|
115
|
+
severity: rule.severity,
|
|
116
|
+
file: filePath,
|
|
117
|
+
line: i + 1,
|
|
118
|
+
message: rule.message,
|
|
119
|
+
evidence: truncateEvidence(line.trim()),
|
|
120
|
+
});
|
|
121
|
+
matchedLineRules.add(rule.ruleId);
|
|
122
|
+
break; // one finding per line-rule per file
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// --- Source rules ---
|
|
126
|
+
const matchedSourceRules = new Set();
|
|
127
|
+
for (const rule of SOURCE_RULES) {
|
|
128
|
+
// Allow multiple findings for different messages with the same ruleId
|
|
129
|
+
// but deduplicate exact (ruleId+message) combos
|
|
130
|
+
const ruleKey = `${rule.ruleId}::${rule.message}`;
|
|
131
|
+
if (matchedSourceRules.has(ruleKey)) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!rule.pattern.test(source)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (rule.requiresContext && !rule.requiresContext.test(source)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// Find the first matching line for evidence + line number
|
|
141
|
+
let matchLine = 0;
|
|
142
|
+
let matchEvidence = "";
|
|
143
|
+
for (let i = 0; i < lines.length; i++) {
|
|
144
|
+
if (rule.pattern.test(lines[i])) {
|
|
145
|
+
matchLine = i + 1;
|
|
146
|
+
matchEvidence = lines[i].trim();
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// For source rules, if we can't find a line match the pattern might span
|
|
151
|
+
// lines. Report line 0 with truncated source as evidence.
|
|
152
|
+
if (matchLine === 0) {
|
|
153
|
+
matchLine = 1;
|
|
154
|
+
matchEvidence = source.slice(0, 120);
|
|
155
|
+
}
|
|
156
|
+
findings.push({
|
|
157
|
+
ruleId: rule.ruleId,
|
|
158
|
+
severity: rule.severity,
|
|
159
|
+
file: filePath,
|
|
160
|
+
line: matchLine,
|
|
161
|
+
message: rule.message,
|
|
162
|
+
evidence: truncateEvidence(matchEvidence),
|
|
163
|
+
});
|
|
164
|
+
matchedSourceRules.add(ruleKey);
|
|
165
|
+
}
|
|
166
|
+
return findings;
|
|
167
|
+
}
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Directory scanner
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
function normalizeScanOptions(opts) {
|
|
172
|
+
return {
|
|
173
|
+
includeFiles: opts?.includeFiles ?? [],
|
|
174
|
+
maxFiles: Math.max(1, opts?.maxFiles ?? DEFAULT_MAX_SCAN_FILES),
|
|
175
|
+
maxFileBytes: Math.max(1, opts?.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function isPathInside(basePath, candidatePath) {
|
|
179
|
+
const base = path.resolve(basePath);
|
|
180
|
+
const candidate = path.resolve(candidatePath);
|
|
181
|
+
const rel = path.relative(base, candidate);
|
|
182
|
+
return rel === "" || (!rel.startsWith(`..${path.sep}`) && rel !== ".." && !path.isAbsolute(rel));
|
|
183
|
+
}
|
|
184
|
+
async function walkDirWithLimit(dirPath, maxFiles) {
|
|
185
|
+
const files = [];
|
|
186
|
+
const stack = [dirPath];
|
|
187
|
+
while (stack.length > 0 && files.length < maxFiles) {
|
|
188
|
+
const currentDir = stack.pop();
|
|
189
|
+
if (!currentDir) {
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
if (files.length >= maxFiles) {
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
// Skip hidden dirs and node_modules
|
|
198
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
202
|
+
if (entry.isDirectory()) {
|
|
203
|
+
stack.push(fullPath);
|
|
204
|
+
}
|
|
205
|
+
else if (isScannable(entry.name)) {
|
|
206
|
+
files.push(fullPath);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return files;
|
|
211
|
+
}
|
|
212
|
+
async function resolveForcedFiles(params) {
|
|
213
|
+
if (params.includeFiles.length === 0) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
const seen = new Set();
|
|
217
|
+
const out = [];
|
|
218
|
+
for (const rawIncludePath of params.includeFiles) {
|
|
219
|
+
const includePath = path.resolve(params.rootDir, rawIncludePath);
|
|
220
|
+
if (!isPathInside(params.rootDir, includePath)) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (!isScannable(includePath)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (seen.has(includePath)) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
let st = null;
|
|
230
|
+
try {
|
|
231
|
+
st = await fs.stat(includePath);
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (hasErrnoCode(err, "ENOENT")) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
if (!st?.isFile()) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
out.push(includePath);
|
|
243
|
+
seen.add(includePath);
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
async function collectScannableFiles(dirPath, opts) {
|
|
248
|
+
const forcedFiles = await resolveForcedFiles({
|
|
249
|
+
rootDir: dirPath,
|
|
250
|
+
includeFiles: opts.includeFiles,
|
|
251
|
+
});
|
|
252
|
+
if (forcedFiles.length >= opts.maxFiles) {
|
|
253
|
+
return forcedFiles.slice(0, opts.maxFiles);
|
|
254
|
+
}
|
|
255
|
+
const walkedFiles = await walkDirWithLimit(dirPath, opts.maxFiles);
|
|
256
|
+
const seen = new Set(forcedFiles.map((f) => path.resolve(f)));
|
|
257
|
+
const out = [...forcedFiles];
|
|
258
|
+
for (const walkedFile of walkedFiles) {
|
|
259
|
+
if (out.length >= opts.maxFiles) {
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
const resolved = path.resolve(walkedFile);
|
|
263
|
+
if (seen.has(resolved)) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
out.push(walkedFile);
|
|
267
|
+
seen.add(resolved);
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
async function readScannableSource(filePath, maxFileBytes) {
|
|
272
|
+
let st = null;
|
|
273
|
+
try {
|
|
274
|
+
st = await fs.stat(filePath);
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
if (hasErrnoCode(err, "ENOENT")) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
if (!st?.isFile() || st.size > maxFileBytes) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
return await fs.readFile(filePath, "utf-8");
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
if (hasErrnoCode(err, "ENOENT")) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
throw err;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
export async function scanDirectory(dirPath, opts) {
|
|
296
|
+
const scanOptions = normalizeScanOptions(opts);
|
|
297
|
+
const files = await collectScannableFiles(dirPath, scanOptions);
|
|
298
|
+
const allFindings = [];
|
|
299
|
+
for (const file of files) {
|
|
300
|
+
const source = await readScannableSource(file, scanOptions.maxFileBytes);
|
|
301
|
+
if (source == null) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const findings = scanSource(source, file);
|
|
305
|
+
allFindings.push(...findings);
|
|
306
|
+
}
|
|
307
|
+
return allFindings;
|
|
308
|
+
}
|
|
309
|
+
export async function scanDirectoryWithSummary(dirPath, opts) {
|
|
310
|
+
const scanOptions = normalizeScanOptions(opts);
|
|
311
|
+
const files = await collectScannableFiles(dirPath, scanOptions);
|
|
312
|
+
const allFindings = [];
|
|
313
|
+
let scannedFiles = 0;
|
|
314
|
+
for (const file of files) {
|
|
315
|
+
const source = await readScannableSource(file, scanOptions.maxFileBytes);
|
|
316
|
+
if (source == null) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
scannedFiles += 1;
|
|
320
|
+
const findings = scanSource(source, file);
|
|
321
|
+
allFindings.push(...findings);
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
scannedFiles,
|
|
325
|
+
critical: allFindings.filter((f) => f.severity === "critical").length,
|
|
326
|
+
warn: allFindings.filter((f) => f.severity === "warn").length,
|
|
327
|
+
info: allFindings.filter((f) => f.severity === "info").length,
|
|
328
|
+
findings: allFindings,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
@@ -13,6 +13,13 @@ export function parseAgentSessionKey(sessionKey) {
|
|
|
13
13
|
return null;
|
|
14
14
|
return { agentId, rest };
|
|
15
15
|
}
|
|
16
|
+
export function isCronRunSessionKey(sessionKey) {
|
|
17
|
+
const parsed = parseAgentSessionKey(sessionKey);
|
|
18
|
+
if (!parsed) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return /^cron:[^:]+:run:[^:]+$/.test(parsed.rest);
|
|
22
|
+
}
|
|
16
23
|
export function isSubagentSessionKey(sessionKey) {
|
|
17
24
|
const raw = (sessionKey ?? "").trim();
|
|
18
25
|
if (!raw)
|
|
@@ -3,14 +3,17 @@ import { hasControlCommand } from "../../auto-reply/command-detection.js";
|
|
|
3
3
|
import { formatInboundEnvelope, formatInboundFromLabel, resolveEnvelopeFormatOptions, } from "../../auto-reply/envelope.js";
|
|
4
4
|
import { createInboundDebouncer, resolveInboundDebounceMs, } from "../../auto-reply/inbound-debounce.js";
|
|
5
5
|
import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
|
|
6
|
-
import { buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, } from "../../auto-reply/reply/history.js";
|
|
6
|
+
import { buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, recordPendingHistoryEntryIfEnabled, } from "../../auto-reply/reply/history.js";
|
|
7
|
+
import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js";
|
|
7
8
|
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
|
|
8
9
|
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
|
|
9
10
|
import { logInboundDrop, logTypingFailure } from "../../channels/logging.js";
|
|
11
|
+
import { resolveMentionGatingWithBypass } from "../../channels/mention-gating.js";
|
|
10
12
|
import { createReplyPrefixContext } from "../../channels/reply-prefix.js";
|
|
11
13
|
import { recordInboundSession } from "../../channels/session.js";
|
|
12
14
|
import { createTypingCallbacks } from "../../channels/typing.js";
|
|
13
15
|
import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js";
|
|
16
|
+
import { resolveChannelGroupRequireMention } from "../../config/group-policy.js";
|
|
14
17
|
import { danger, logVerbose, shouldLogVerbose } from "../../globals.js";
|
|
15
18
|
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
|
16
19
|
import { mediaKindFromMime } from "../../media/constants.js";
|
|
@@ -79,8 +82,17 @@ export function createSignalEventHandler(deps) {
|
|
|
79
82
|
});
|
|
80
83
|
}
|
|
81
84
|
const signalTo = entry.isGroup ? `group:${entry.groupId}` : `signal:${entry.senderRecipient}`;
|
|
85
|
+
const inboundHistory = entry.isGroup && historyKey && deps.historyLimit > 0
|
|
86
|
+
? (deps.groupHistories.get(historyKey) ?? []).map((historyEntry) => ({
|
|
87
|
+
sender: historyEntry.sender,
|
|
88
|
+
body: historyEntry.body,
|
|
89
|
+
timestamp: historyEntry.timestamp,
|
|
90
|
+
}))
|
|
91
|
+
: undefined;
|
|
82
92
|
const ctxPayload = finalizeInboundContext({
|
|
83
93
|
Body: combinedBody,
|
|
94
|
+
BodyForAgent: entry.bodyText,
|
|
95
|
+
InboundHistory: inboundHistory,
|
|
84
96
|
RawBody: entry.bodyText,
|
|
85
97
|
CommandBody: entry.bodyText,
|
|
86
98
|
From: entry.isGroup
|
|
@@ -101,6 +113,7 @@ export function createSignalEventHandler(deps) {
|
|
|
101
113
|
MediaPath: entry.mediaPath,
|
|
102
114
|
MediaType: entry.mediaType,
|
|
103
115
|
MediaUrl: entry.mediaPath,
|
|
116
|
+
WasMentioned: entry.isGroup ? entry.wasMentioned === true : undefined,
|
|
104
117
|
CommandAuthorized: entry.commandAuthorized,
|
|
105
118
|
OriginatingChannel: "signal",
|
|
106
119
|
OriginatingTo: signalTo,
|
|
@@ -417,6 +430,71 @@ export function createSignalEventHandler(deps) {
|
|
|
417
430
|
});
|
|
418
431
|
return;
|
|
419
432
|
}
|
|
433
|
+
const route = resolveAgentRoute({
|
|
434
|
+
cfg: deps.cfg,
|
|
435
|
+
channel: "signal",
|
|
436
|
+
accountId: deps.accountId,
|
|
437
|
+
peer: {
|
|
438
|
+
kind: isGroup ? "group" : "dm",
|
|
439
|
+
id: isGroup ? (groupId ?? "unknown") : senderPeerId,
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
const mentionRegexes = buildMentionRegexes(deps.cfg, route.agentId);
|
|
443
|
+
const wasMentioned = isGroup && matchesMentionPatterns(messageText, mentionRegexes);
|
|
444
|
+
const requireMention = isGroup &&
|
|
445
|
+
resolveChannelGroupRequireMention({
|
|
446
|
+
cfg: deps.cfg,
|
|
447
|
+
channel: "signal",
|
|
448
|
+
groupId,
|
|
449
|
+
accountId: deps.accountId,
|
|
450
|
+
});
|
|
451
|
+
const canDetectMention = mentionRegexes.length > 0;
|
|
452
|
+
const mentionGate = resolveMentionGatingWithBypass({
|
|
453
|
+
isGroup,
|
|
454
|
+
requireMention: Boolean(requireMention),
|
|
455
|
+
canDetectMention,
|
|
456
|
+
wasMentioned,
|
|
457
|
+
implicitMention: false,
|
|
458
|
+
hasAnyMention: false,
|
|
459
|
+
allowTextCommands: true,
|
|
460
|
+
hasControlCommand: hasControlCommandInMessage,
|
|
461
|
+
commandAuthorized,
|
|
462
|
+
});
|
|
463
|
+
const effectiveWasMentioned = mentionGate.effectiveWasMentioned;
|
|
464
|
+
if (isGroup && requireMention && canDetectMention && mentionGate.shouldSkip) {
|
|
465
|
+
logInboundDrop({
|
|
466
|
+
log: logVerbose,
|
|
467
|
+
channel: "signal",
|
|
468
|
+
reason: "no mention",
|
|
469
|
+
target: senderDisplay,
|
|
470
|
+
});
|
|
471
|
+
const quoteText = dataMessage.quote?.text?.trim() || "";
|
|
472
|
+
const pendingPlaceholder = (() => {
|
|
473
|
+
if (!dataMessage.attachments?.length) {
|
|
474
|
+
return "";
|
|
475
|
+
}
|
|
476
|
+
if (deps.ignoreAttachments) {
|
|
477
|
+
return "<media:attachment>";
|
|
478
|
+
}
|
|
479
|
+
const firstContentType = dataMessage.attachments?.[0]?.contentType;
|
|
480
|
+
const pendingKind = mediaKindFromMime(firstContentType ?? undefined);
|
|
481
|
+
return pendingKind ? `<media:${pendingKind}>` : "<media:attachment>";
|
|
482
|
+
})();
|
|
483
|
+
const pendingBodyText = messageText || pendingPlaceholder || quoteText;
|
|
484
|
+
const historyKey = groupId ?? "unknown";
|
|
485
|
+
recordPendingHistoryEntryIfEnabled({
|
|
486
|
+
historyMap: deps.groupHistories,
|
|
487
|
+
historyKey,
|
|
488
|
+
limit: deps.historyLimit,
|
|
489
|
+
entry: {
|
|
490
|
+
sender: envelope.sourceName ?? senderDisplay,
|
|
491
|
+
body: pendingBodyText,
|
|
492
|
+
timestamp: envelope.timestamp ?? undefined,
|
|
493
|
+
messageId: typeof envelope.timestamp === "number" ? String(envelope.timestamp) : undefined,
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
420
498
|
let mediaPath;
|
|
421
499
|
let mediaType;
|
|
422
500
|
let placeholder = "";
|
|
@@ -487,6 +565,7 @@ export function createSignalEventHandler(deps) {
|
|
|
487
565
|
mediaPath,
|
|
488
566
|
mediaType,
|
|
489
567
|
commandAuthorized,
|
|
568
|
+
wasMentioned: effectiveWasMentioned,
|
|
490
569
|
});
|
|
491
570
|
};
|
|
492
571
|
}
|
|
@@ -1,14 +1,77 @@
|
|
|
1
1
|
import { fetchRemoteMedia } from "../../media/fetch.js";
|
|
2
2
|
import { saveMediaBuffer } from "../../media/store.js";
|
|
3
|
+
function normalizeHostname(hostname) {
|
|
4
|
+
const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
|
|
5
|
+
if (normalized.startsWith("[") && normalized.endsWith("]")) {
|
|
6
|
+
return normalized.slice(1, -1);
|
|
7
|
+
}
|
|
8
|
+
return normalized;
|
|
9
|
+
}
|
|
10
|
+
function isSlackHostname(hostname) {
|
|
11
|
+
const normalized = normalizeHostname(hostname);
|
|
12
|
+
if (!normalized) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
// Slack-hosted files typically come from *.slack.com and redirect to Slack CDN domains.
|
|
16
|
+
// Include a small allowlist of known Slack domains to avoid leaking tokens if a file URL
|
|
17
|
+
// is ever spoofed or mishandled.
|
|
18
|
+
const allowedSuffixes = ["slack.com", "slack-edge.com", "slack-files.com"];
|
|
19
|
+
return allowedSuffixes.some((suffix) => normalized === suffix || normalized.endsWith(`.${suffix}`));
|
|
20
|
+
}
|
|
21
|
+
function assertSlackFileUrl(rawUrl) {
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = new URL(rawUrl);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new Error(`Invalid Slack file URL: ${rawUrl}`);
|
|
28
|
+
}
|
|
29
|
+
if (parsed.protocol !== "https:") {
|
|
30
|
+
throw new Error(`Refusing Slack file URL with non-HTTPS protocol: ${parsed.protocol}`);
|
|
31
|
+
}
|
|
32
|
+
if (!isSlackHostname(parsed.hostname)) {
|
|
33
|
+
throw new Error(`Refusing to send Slack token to non-Slack host "${parsed.hostname}" (url: ${rawUrl})`);
|
|
34
|
+
}
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
function resolveRequestUrl(input) {
|
|
38
|
+
if (typeof input === "string") {
|
|
39
|
+
return input;
|
|
40
|
+
}
|
|
41
|
+
if (input instanceof URL) {
|
|
42
|
+
return input.toString();
|
|
43
|
+
}
|
|
44
|
+
if ("url" in input && typeof input.url === "string") {
|
|
45
|
+
return input.url;
|
|
46
|
+
}
|
|
47
|
+
throw new Error("Unsupported fetch input: expected string, URL, or Request");
|
|
48
|
+
}
|
|
49
|
+
function createSlackMediaFetch(token) {
|
|
50
|
+
let includeAuth = true;
|
|
51
|
+
return async (input, init) => {
|
|
52
|
+
const url = resolveRequestUrl(input);
|
|
53
|
+
const { headers: initHeaders, redirect: _redirect, ...rest } = init ?? {};
|
|
54
|
+
const headers = new Headers(initHeaders);
|
|
55
|
+
if (includeAuth) {
|
|
56
|
+
includeAuth = false;
|
|
57
|
+
const parsed = assertSlackFileUrl(url);
|
|
58
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
59
|
+
return fetch(parsed.href, { ...rest, headers, redirect: "manual" });
|
|
60
|
+
}
|
|
61
|
+
headers.delete("Authorization");
|
|
62
|
+
return fetch(url, { ...rest, headers, redirect: "manual" });
|
|
63
|
+
};
|
|
64
|
+
}
|
|
3
65
|
/**
|
|
4
66
|
* Fetches a URL with Authorization header, handling cross-origin redirects.
|
|
5
67
|
* Node.js fetch strips Authorization headers on cross-origin redirects for security.
|
|
6
|
-
* Slack's
|
|
7
|
-
*
|
|
68
|
+
* Slack's file URLs redirect to CDN domains with pre-signed URLs that don't need the
|
|
69
|
+
* Authorization header, so we handle the initial auth request manually.
|
|
8
70
|
*/
|
|
9
71
|
export async function fetchWithSlackAuth(url, token) {
|
|
72
|
+
const parsed = assertSlackFileUrl(url);
|
|
10
73
|
// Initial request with auth and manual redirect handling
|
|
11
|
-
const initialRes = await fetch(
|
|
74
|
+
const initialRes = await fetch(parsed.href, {
|
|
12
75
|
headers: { Authorization: `Bearer ${token}` },
|
|
13
76
|
redirect: "manual",
|
|
14
77
|
});
|
|
@@ -22,31 +85,36 @@ export async function fetchWithSlackAuth(url, token) {
|
|
|
22
85
|
return initialRes;
|
|
23
86
|
}
|
|
24
87
|
// Resolve relative URLs against the original
|
|
25
|
-
const resolvedUrl = new URL(redirectUrl,
|
|
88
|
+
const resolvedUrl = new URL(redirectUrl, parsed.href);
|
|
89
|
+
// Only follow safe protocols (we do NOT include Authorization on redirects).
|
|
90
|
+
if (resolvedUrl.protocol !== "https:") {
|
|
91
|
+
return initialRes;
|
|
92
|
+
}
|
|
26
93
|
// Follow the redirect without the Authorization header
|
|
27
94
|
// (Slack's CDN URLs are pre-signed and don't need it)
|
|
28
|
-
return fetch(resolvedUrl, { redirect: "follow" });
|
|
95
|
+
return fetch(resolvedUrl.toString(), { redirect: "follow" });
|
|
29
96
|
}
|
|
30
97
|
export async function resolveSlackMedia(params) {
|
|
31
98
|
const files = params.files ?? [];
|
|
32
99
|
for (const file of files) {
|
|
33
100
|
const url = file.url_private_download ?? file.url_private;
|
|
34
|
-
if (!url)
|
|
101
|
+
if (!url) {
|
|
35
102
|
continue;
|
|
103
|
+
}
|
|
36
104
|
try {
|
|
37
|
-
// Note:
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return fetchWithSlackAuth(inputUrl, params.token);
|
|
42
|
-
};
|
|
105
|
+
// Note: fetchRemoteMedia calls fetchImpl(url) with the URL string today and
|
|
106
|
+
// handles size limits internally. Provide a fetcher that uses auth once, then lets
|
|
107
|
+
// the redirect chain continue without credentials.
|
|
108
|
+
const fetchImpl = createSlackMediaFetch(params.token);
|
|
43
109
|
const fetched = await fetchRemoteMedia({
|
|
44
110
|
url,
|
|
45
111
|
fetchImpl,
|
|
46
112
|
filePathHint: file.name,
|
|
113
|
+
maxBytes: params.maxBytes,
|
|
47
114
|
});
|
|
48
|
-
if (fetched.buffer.byteLength > params.maxBytes)
|
|
115
|
+
if (fetched.buffer.byteLength > params.maxBytes) {
|
|
49
116
|
continue;
|
|
117
|
+
}
|
|
50
118
|
const saved = await saveMediaBuffer(fetched.buffer, fetched.contentType ?? file.mimetype, "inbound", params.maxBytes);
|
|
51
119
|
const label = fetched.fileName ?? file.name;
|
|
52
120
|
return {
|
|
@@ -65,8 +133,9 @@ const THREAD_STARTER_CACHE = new Map();
|
|
|
65
133
|
export async function resolveSlackThreadStarter(params) {
|
|
66
134
|
const cacheKey = `${params.channelId}:${params.threadTs}`;
|
|
67
135
|
const cached = THREAD_STARTER_CACHE.get(cacheKey);
|
|
68
|
-
if (cached)
|
|
136
|
+
if (cached) {
|
|
69
137
|
return cached;
|
|
138
|
+
}
|
|
70
139
|
try {
|
|
71
140
|
const response = (await params.client.conversations.replies({
|
|
72
141
|
channel: params.channelId,
|
|
@@ -76,8 +145,9 @@ export async function resolveSlackThreadStarter(params) {
|
|
|
76
145
|
}));
|
|
77
146
|
const message = response?.messages?.[0];
|
|
78
147
|
const text = (message?.text ?? "").trim();
|
|
79
|
-
if (!message || !text)
|
|
148
|
+
if (!message || !text) {
|
|
80
149
|
return null;
|
|
150
|
+
}
|
|
81
151
|
const starter = {
|
|
82
152
|
text,
|
|
83
153
|
userId: message.user,
|
package/dist/tailscale/detect.js
CHANGED
|
@@ -65,8 +65,7 @@ async function getTailscaleStatus() {
|
|
|
65
65
|
canFunnel: self.Capabilities?.map.includes("funnel") ?? false,
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
|
-
catch
|
|
69
|
-
// If command fails, return minimal info
|
|
68
|
+
catch {
|
|
70
69
|
return {
|
|
71
70
|
installed: await isTailscaleInstalled(),
|
|
72
71
|
running: false,
|