@sayknow-cli/coding-agent 0.2.7 → 0.3.1
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 +20 -0
- package/dist/types/async/job-manager.d.ts +3 -1
- package/dist/types/cli/daemon-cli.d.ts +25 -0
- package/dist/types/cli/notify-cli.d.ts +25 -0
- package/dist/types/cli/setup-cli.d.ts +20 -1
- package/dist/types/commands/daemon.d.ts +41 -0
- package/dist/types/commands/notify.d.ts +41 -0
- package/dist/types/config/model-profile-activation.d.ts +12 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +136 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/daemon/builtin.d.ts +20 -0
- package/dist/types/daemon/control-types.d.ts +57 -0
- package/dist/types/daemon/runtime.d.ts +25 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/i18n/messages/en.d.ts +1 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +2 -0
- package/dist/types/modes/components/settings-defs.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/notifications/attachment-registry.d.ts +17 -0
- package/dist/types/notifications/chat-adapters.d.ts +9 -0
- package/dist/types/notifications/config-commands.d.ts +26 -0
- package/dist/types/notifications/config.d.ts +69 -0
- package/dist/types/notifications/engine.d.ts +59 -0
- package/dist/types/notifications/helpers.d.ts +55 -0
- package/dist/types/notifications/html-format.d.ts +62 -0
- package/dist/types/notifications/index.d.ts +28 -0
- package/dist/types/notifications/managed-daemon.d.ts +48 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
- package/dist/types/notifications/telegram-cli.d.ts +19 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
- package/dist/types/notifications/telegram-daemon.d.ts +295 -0
- package/dist/types/notifications/telegram-reference.d.ts +111 -0
- package/dist/types/notifications/threaded-inbound.d.ts +77 -0
- package/dist/types/notifications/threaded-render.d.ts +71 -0
- package/dist/types/notifications/topic-registry.d.ts +67 -0
- package/dist/types/rlm/index.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +41 -2
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/setup/credential-auto-import.d.ts +63 -0
- package/dist/types/setup/credential-import.d.ts +3 -0
- package/dist/types/setup/host-plugin-setup.d.ts +39 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/state-writer.d.ts +2 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +14 -0
- package/dist/types/tools/ask-answer-registry.d.ts +13 -0
- package/dist/types/tools/fetch.d.ts +23 -0
- package/dist/types/tools/index.d.ts +19 -0
- package/dist/types/tools/subagent.d.ts +3 -0
- package/dist/types/tools/telegram-send.d.ts +32 -0
- package/dist/types/web/insane/bridge.d.ts +103 -0
- package/dist/types/web/insane/url-guard.d.ts +22 -0
- package/dist/types/web/search/provider.d.ts +18 -1
- package/dist/types/web/search/providers/insane.d.ts +53 -0
- package/dist/types/web/search/providers/text-citations.d.ts +23 -0
- package/dist/types/web/search/types.d.ts +12 -4
- package/package.json +10 -8
- package/scripts/build-binary.ts +3 -0
- package/scripts/verify-insane-vendor.ts +132 -0
- package/src/async/job-manager.ts +5 -1
- package/src/cli/args.ts +1 -1
- package/src/cli/daemon-cli.ts +122 -0
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/notify-cli.ts +421 -0
- package/src/cli/setup-cli.ts +173 -84
- package/src/cli.ts +3 -3
- package/src/commands/daemon.ts +47 -0
- package/src/commands/notify.ts +61 -0
- package/src/commands/setup.ts +11 -1
- package/src/commands/team.ts +1 -1
- package/src/config/model-profile-activation.ts +74 -5
- package/src/config/model-profiles.ts +7 -4
- package/src/config/model-registry.ts +6 -3
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +143 -0
- package/src/coordinator/contract.ts +3 -0
- package/src/coordinator-mcp/server.ts +270 -1
- package/src/daemon/builtin.ts +46 -0
- package/src/daemon/control-types.ts +65 -0
- package/src/daemon/runtime.ts +51 -0
- package/src/defaults/skc/rules/ponytail.md +68 -0
- package/src/defaults/skc/skills/ralplan/SKILL.md +11 -4
- package/src/defaults/skc/skills/ultragoal/SKILL.md +16 -0
- package/src/edit/modes/replace.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/extensibility/shared-events.ts +1 -0
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/hashline/hash.ts +1 -1
- package/src/i18n/messages/de.ts +1 -0
- package/src/i18n/messages/en.ts +1 -0
- package/src/i18n/messages/es.ts +1 -0
- package/src/i18n/messages/fr.ts +1 -0
- package/src/i18n/messages/ja.ts +1 -0
- package/src/i18n/messages/ko.ts +1 -0
- package/src/i18n/messages/zh.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +12 -10
- package/src/lsp/config.ts +16 -3
- package/src/lsp/defaults.json +7 -0
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +30 -0
- package/src/modes/acp/acp-event-mapper.ts +1 -0
- package/src/modes/components/hook-editor.ts +7 -2
- package/src/modes/components/oauth-selector.ts +19 -0
- package/src/modes/components/settings-defs.ts +2 -1
- package/src/modes/components/settings-selector.ts +7 -2
- package/src/modes/controllers/event-controller.ts +35 -0
- package/src/modes/controllers/selector-controller.ts +80 -17
- package/src/modes/interactive-mode.ts +52 -4
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/event-contract.ts +1 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
- package/src/modes/shared/agent-wire/event-observation.ts +16 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
- package/src/modes/theme/theme.ts +4 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +23 -0
- package/src/notifications/attachment-registry.ts +23 -0
- package/src/notifications/chat-adapters.ts +147 -0
- package/src/notifications/config-commands.ts +50 -0
- package/src/notifications/config.ts +128 -0
- package/src/notifications/engine.ts +100 -0
- package/src/notifications/helpers.ts +135 -0
- package/src/notifications/html-format.ts +389 -0
- package/src/notifications/index.ts +842 -0
- package/src/notifications/managed-daemon.ts +163 -0
- package/src/notifications/rate-limit-pool.ts +179 -0
- package/src/notifications/telegram-cli.ts +194 -0
- package/src/notifications/telegram-daemon-cli.ts +74 -0
- package/src/notifications/telegram-daemon-control.ts +370 -0
- package/src/notifications/telegram-daemon.ts +1591 -0
- package/src/notifications/telegram-reference.ts +335 -0
- package/src/notifications/threaded-inbound.ts +136 -0
- package/src/notifications/threaded-render.ts +173 -0
- package/src/notifications/topic-registry.ts +133 -0
- package/src/rlm/index.ts +19 -0
- package/src/sdk.ts +16 -0
- package/src/session/agent-session.ts +195 -54
- package/src/session/auth-storage.ts +3 -0
- package/src/session/session-dump-format.ts +43 -2
- package/src/session/session-manager.ts +39 -5
- package/src/setup/credential-auto-import.ts +258 -0
- package/src/setup/credential-import.ts +17 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
- package/src/setup/host-plugin-setup.ts +142 -0
- package/src/skc-runtime/deep-interview-recorder.ts +2 -2
- package/src/skc-runtime/launch-tmux.ts +27 -5
- package/src/skc-runtime/ledger-event-renderer.ts +1 -0
- package/src/skc-runtime/ralplan-runtime.ts +2 -2
- package/src/skc-runtime/state-runtime.ts +18 -10
- package/src/skc-runtime/state-writer.ts +8 -8
- package/src/skc-runtime/tmux-common.ts +8 -0
- package/src/skc-runtime/tmux-sessions.ts +8 -1
- package/src/skc-runtime/ultragoal-guard.ts +57 -2
- package/src/skc-runtime/ultragoal-runtime.ts +105 -19
- package/src/skc-runtime/workflow-manifest.generated.json +56 -2
- package/src/skc-runtime/workflow-manifest.ts +18 -3
- package/src/slash-commands/builtin-registry.ts +4 -1
- package/src/task/executor.ts +5 -1
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +77 -6
- package/src/tools/fetch.ts +78 -1
- package/src/tools/image-gen.ts +5 -8
- package/src/tools/index.ts +22 -0
- package/src/tools/inspect-image.ts +16 -11
- package/src/tools/subagent-render.ts +7 -0
- package/src/tools/subagent.ts +38 -7
- package/src/tools/telegram-send.ts +137 -0
- package/src/web/insane/bridge.ts +350 -0
- package/src/web/insane/url-guard.ts +155 -0
- package/src/web/search/provider.ts +77 -18
- package/src/web/search/providers/anthropic.ts +70 -3
- package/src/web/search/providers/codex.ts +1 -119
- package/src/web/search/providers/gemini.ts +99 -0
- package/src/web/search/providers/insane.ts +551 -0
- package/src/web/search/providers/openai-compatible.ts +66 -32
- package/src/web/search/providers/text-citations.ts +111 -0
- package/src/web/search/types.ts +13 -2
- package/vendor/insane-search/LICENSE +21 -0
- package/vendor/insane-search/MANIFEST.json +24 -0
- package/vendor/insane-search/engine/__init__.py +23 -0
- package/vendor/insane-search/engine/__main__.py +128 -0
- package/vendor/insane-search/engine/bias_check.py +183 -0
- package/vendor/insane-search/engine/executor.py +254 -0
- package/vendor/insane-search/engine/fetch_chain.py +725 -0
- package/vendor/insane-search/engine/learning.py +175 -0
- package/vendor/insane-search/engine/phase0.py +214 -0
- package/vendor/insane-search/engine/safety.py +91 -0
- package/vendor/insane-search/engine/templates/package.json +11 -0
- package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
- package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
- package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
- package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
- package/vendor/insane-search/engine/tests/test_u1.py +200 -0
- package/vendor/insane-search/engine/tests/test_u4.py +131 -0
- package/vendor/insane-search/engine/tests/test_u5.py +163 -0
- package/vendor/insane-search/engine/tests/test_u7.py +124 -0
- package/vendor/insane-search/engine/transport.py +211 -0
- package/vendor/insane-search/engine/url_transforms.py +98 -0
- package/vendor/insane-search/engine/validators.py +331 -0
- package/vendor/insane-search/engine/waf_detector.py +214 -0
- package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
|
@@ -59,6 +59,14 @@ export interface WorkflowGateEmitter {
|
|
|
59
59
|
isUnattended(): boolean;
|
|
60
60
|
/** Open + emit a gate; resolves with the agent's answer (from workflow_gate_response). */
|
|
61
61
|
emitGate(input: OpenGateInput): Promise<unknown>;
|
|
62
|
+
/**
|
|
63
|
+
* Optional bridge surface (present on {@link UnattendedSessionControlPlane}) that
|
|
64
|
+
* lets an in-process extension observe emitted gates and answer them — used by
|
|
65
|
+
* the notifications SDK to resolve a real ask gate from a remote reply.
|
|
66
|
+
*/
|
|
67
|
+
onGateEmitted?(listener: (gate: RpcWorkflowGate) => void): () => void;
|
|
68
|
+
resolveGate?(response: RpcWorkflowGateResponse): Promise<RpcWorkflowGateResolution>;
|
|
69
|
+
listPendingGates?(): RpcWorkflowGate[];
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
export interface UnattendedSessionOptions {
|
|
@@ -82,6 +90,7 @@ export class UnattendedSessionControlPlane implements RpcUnattendedControlPlane,
|
|
|
82
90
|
#broker: WorkflowGateBroker | undefined;
|
|
83
91
|
readonly #pending = new Map<string, { resolve: (answer: unknown) => void; reject: (err: Error) => void }>();
|
|
84
92
|
readonly #earlyAnswers = new Map<string, unknown>();
|
|
93
|
+
readonly #gateListeners = new Set<(gate: RpcWorkflowGate) => void>();
|
|
85
94
|
|
|
86
95
|
constructor(private readonly opts: UnattendedSessionOptions) {}
|
|
87
96
|
|
|
@@ -89,6 +98,12 @@ export class UnattendedSessionControlPlane implements RpcUnattendedControlPlane,
|
|
|
89
98
|
return this.#controller !== undefined;
|
|
90
99
|
}
|
|
91
100
|
|
|
101
|
+
/** Observe every emitted gate (e.g. so an extension can map an ask to its gate_id). */
|
|
102
|
+
onGateEmitted(listener: (gate: RpcWorkflowGate) => void): () => void {
|
|
103
|
+
this.#gateListeners.add(listener);
|
|
104
|
+
return () => this.#gateListeners.delete(listener);
|
|
105
|
+
}
|
|
106
|
+
|
|
92
107
|
get controller(): UnattendedRunController | undefined {
|
|
93
108
|
return this.#controller;
|
|
94
109
|
}
|
|
@@ -195,6 +210,13 @@ export class UnattendedSessionControlPlane implements RpcUnattendedControlPlane,
|
|
|
195
210
|
return Promise.reject(new Error("cannot emit a workflow gate before unattended mode is negotiated"));
|
|
196
211
|
}
|
|
197
212
|
const gate = this.#broker.openGate(input);
|
|
213
|
+
for (const listener of this.#gateListeners) {
|
|
214
|
+
try {
|
|
215
|
+
listener(gate);
|
|
216
|
+
} catch {
|
|
217
|
+
// A misbehaving observer must never break gate emission.
|
|
218
|
+
}
|
|
219
|
+
}
|
|
198
220
|
if (this.#earlyAnswers.has(gate.gate_id)) {
|
|
199
221
|
const answer = this.#earlyAnswers.get(gate.gate_id);
|
|
200
222
|
this.#earlyAnswers.delete(gate.gate_id);
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -183,6 +183,7 @@ export type SymbolKey =
|
|
|
183
183
|
| "tab.appearance"
|
|
184
184
|
| "tab.model"
|
|
185
185
|
| "tab.interaction"
|
|
186
|
+
| "tab.notifications"
|
|
186
187
|
| "tab.context"
|
|
187
188
|
| "tab.editing"
|
|
188
189
|
| "tab.tools"
|
|
@@ -347,6 +348,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
347
348
|
"tab.appearance": "🎨",
|
|
348
349
|
"tab.model": "🤖",
|
|
349
350
|
"tab.interaction": "⌨",
|
|
351
|
+
"tab.notifications": "🔔",
|
|
350
352
|
"tab.context": "📋",
|
|
351
353
|
"tab.editing": "💻",
|
|
352
354
|
"tab.tools": "🔧",
|
|
@@ -606,6 +608,7 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
606
608
|
"tab.appearance": "",
|
|
607
609
|
"tab.model": "",
|
|
608
610
|
"tab.interaction": "",
|
|
611
|
+
"tab.notifications": "",
|
|
609
612
|
"tab.context": "",
|
|
610
613
|
"tab.editing": "",
|
|
611
614
|
"tab.tools": "",
|
|
@@ -768,6 +771,7 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
768
771
|
"tab.appearance": "[A]",
|
|
769
772
|
"tab.model": "[M]",
|
|
770
773
|
"tab.interaction": "[I]",
|
|
774
|
+
"tab.notifications": "[N]",
|
|
771
775
|
"tab.context": "[X]",
|
|
772
776
|
"tab.editing": "[E]",
|
|
773
777
|
"tab.tools": "[T]",
|
package/src/modes/types.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type { MCPManager } from "../runtime-mcp";
|
|
|
17
17
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
18
18
|
import type { HistoryStorage } from "../session/history-storage";
|
|
19
19
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
20
|
+
import type { CredentialAutoImportOptions } from "../setup/credential-auto-import";
|
|
20
21
|
import type { LspStartupServerInfo } from "../tools";
|
|
21
22
|
import type { AssistantMessageComponent } from "./components/assistant-message";
|
|
22
23
|
import type { BashExecutionComponent } from "./components/bash-execution";
|
|
@@ -247,7 +248,7 @@ export interface InteractiveModeContext {
|
|
|
247
248
|
showSessionSelector(): void;
|
|
248
249
|
handleResumeSession(sessionPath: string): Promise<void>;
|
|
249
250
|
handleSessionDeleteCommand(): Promise<void>;
|
|
250
|
-
showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void>;
|
|
251
|
+
showOAuthSelector(mode: "login" | "logout", providerId?: string, options?: OAuthSelectorOptions): Promise<void>;
|
|
251
252
|
showHookConfirm(title: string, message: string): Promise<boolean>;
|
|
252
253
|
showDebugSelector(): void;
|
|
253
254
|
showSessionObserver(): void;
|
|
@@ -311,3 +312,8 @@ export interface InteractiveModeContext {
|
|
|
311
312
|
showExtensionError(extensionPath: string, error: string): void;
|
|
312
313
|
showToolError(toolName: string, error: string): void;
|
|
313
314
|
}
|
|
315
|
+
export interface OAuthSelectorOptions {
|
|
316
|
+
allowExternalCredentialDiscovery?: boolean;
|
|
317
|
+
trigger?: "bare-login";
|
|
318
|
+
externalCredentialDiscover?: CredentialAutoImportOptions["discover"];
|
|
319
|
+
}
|
|
@@ -204,14 +204,14 @@ export function computeContextBreakdown(
|
|
|
204
204
|
if (contextWindow > 0) {
|
|
205
205
|
const compactionSettings = session.settings.getGroup("compaction") as CompactionSettings;
|
|
206
206
|
if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
|
|
207
|
-
const threshold = resolveThresholdTokens(contextWindow, compactionSettings
|
|
207
|
+
const threshold = resolveThresholdTokens(contextWindow, compactionSettings);
|
|
208
208
|
autoCompactBufferTokens = Math.max(0, contextWindow - threshold);
|
|
209
209
|
} else {
|
|
210
210
|
autoCompactBufferTokens = 0;
|
|
211
211
|
}
|
|
212
212
|
// Even when fully disabled, fall back to a sensible reserve floor for display.
|
|
213
213
|
if (autoCompactBufferTokens === 0 && compactionSettings.enabled) {
|
|
214
|
-
autoCompactBufferTokens = effectiveReserveTokens(contextWindow, compactionSettings
|
|
214
|
+
autoCompactBufferTokens = effectiveReserveTokens(contextWindow, compactionSettings);
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
autoCompactBufferTokens = Math.min(autoCompactBufferTokens, Math.max(0, contextWindow - usedTokens));
|
|
@@ -205,6 +205,29 @@ export class UiHelpers {
|
|
|
205
205
|
}
|
|
206
206
|
return components;
|
|
207
207
|
}
|
|
208
|
+
if (message.customType === "subagent:steer" || message.customType === "subagent:steer:relay") {
|
|
209
|
+
const details = (
|
|
210
|
+
message as CustomMessage<{
|
|
211
|
+
from?: string;
|
|
212
|
+
to?: string;
|
|
213
|
+
body?: string;
|
|
214
|
+
state?: string;
|
|
215
|
+
}>
|
|
216
|
+
).details;
|
|
217
|
+
const components: Component[] = [];
|
|
218
|
+
const header = `${theme.fg("accent", `[Steer ${details?.state ?? "queued"}] ${details?.from ?? "?"} ⇨ ${details?.to ?? "?"}`)}`;
|
|
219
|
+
const headerComponent = new Text(header, 1, 0);
|
|
220
|
+
this.ctx.chatContainer.addChild(headerComponent);
|
|
221
|
+
components.push(headerComponent);
|
|
222
|
+
if (details?.body) {
|
|
223
|
+
for (const line of details.body.split("\n")) {
|
|
224
|
+
const lineComponent = new Text(theme.fg("muted", ` ${line}`), 0, 0);
|
|
225
|
+
this.ctx.chatContainer.addChild(lineComponent);
|
|
226
|
+
components.push(lineComponent);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return components;
|
|
230
|
+
}
|
|
208
231
|
const renderer = this.ctx.session.extensionRunner?.getMessageRenderer(message.customType);
|
|
209
232
|
// Both HookMessage and CustomMessage have the same structure, cast for compatibility
|
|
210
233
|
const component = new CustomMessageComponent(message as CustomMessage<unknown>, renderer);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide registry mapping a session id to a sink that delivers a local
|
|
3
|
+
* file to the session's connected Telegram chat as a document. Registered by
|
|
4
|
+
* the notifications extension; consumed by the telegram_send tool.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Delivers a local file to the session's Telegram chat. */
|
|
8
|
+
export type TelegramFileSink = (file: { path: string; caption?: string }) => Promise<{ ok: boolean; error?: string }>;
|
|
9
|
+
|
|
10
|
+
const sinks = new Map<string, TelegramFileSink>();
|
|
11
|
+
|
|
12
|
+
/** Register `sink` for `sessionId`. Returns a disposer that clears it. */
|
|
13
|
+
export function registerTelegramFileSink(sessionId: string, sink: TelegramFileSink): () => void {
|
|
14
|
+
sinks.set(sessionId, sink);
|
|
15
|
+
return () => {
|
|
16
|
+
if (sinks.get(sessionId) === sink) sinks.delete(sessionId);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The Telegram file sink for `sessionId`, if one is registered. */
|
|
21
|
+
export function getTelegramFileSink(sessionId: string): TelegramFileSink | undefined {
|
|
22
|
+
return sinks.get(sessionId);
|
|
23
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NotificationAdapterPayload,
|
|
3
|
+
NotificationEvent,
|
|
4
|
+
NotificationPresentationAdapter,
|
|
5
|
+
NotificationReplyRoute,
|
|
6
|
+
} from "./engine";
|
|
7
|
+
import { truncate } from "./helpers";
|
|
8
|
+
|
|
9
|
+
type AdapterKind = "discord" | "slack";
|
|
10
|
+
|
|
11
|
+
interface ChatAdapterOptions {
|
|
12
|
+
kind: AdapterKind;
|
|
13
|
+
channelId?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface InboundShape {
|
|
17
|
+
sessionId?: unknown;
|
|
18
|
+
actionId?: unknown;
|
|
19
|
+
answer?: unknown;
|
|
20
|
+
text?: unknown;
|
|
21
|
+
value?: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function text(value: unknown): string | undefined {
|
|
25
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function publicLine(label: string, value: unknown): string | undefined {
|
|
29
|
+
const v = text(value);
|
|
30
|
+
return v ? `${label}: ${truncate(v, 280)}` : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function actionText(event: Extract<NotificationEvent, { type: "action_needed" }>, format: "discord" | "slack"): string {
|
|
34
|
+
if (event.kind === "idle") {
|
|
35
|
+
const summary = text(event.summary);
|
|
36
|
+
return summary ? `Agent idle\n${truncate(summary, 1200)}` : "Agent idle";
|
|
37
|
+
}
|
|
38
|
+
const question = truncate(text(event.question) ?? "Question", 1200);
|
|
39
|
+
const options = Array.isArray(event.options) ? event.options.map(option => String(option)) : [];
|
|
40
|
+
const lines = [`Question: ${question}`];
|
|
41
|
+
if (options.length > 0) {
|
|
42
|
+
lines.push(
|
|
43
|
+
...options.map((option, index) => {
|
|
44
|
+
const label = truncate(option, 180);
|
|
45
|
+
return format === "slack" ? `${index + 1}. ${label}` : `**${index + 1}.** ${label}`;
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
} else {
|
|
49
|
+
lines.push("Reply with text.");
|
|
50
|
+
}
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function frameText(event: Extract<NotificationEvent, { type: "frame" }>): string | undefined {
|
|
55
|
+
const frame = event.frame;
|
|
56
|
+
const kind = text(frame.type);
|
|
57
|
+
if (!kind) return undefined;
|
|
58
|
+
const lines = [`SKC ${kind.replace(/_/g, " ")}`];
|
|
59
|
+
for (const line of [
|
|
60
|
+
publicLine("title", frame.title),
|
|
61
|
+
publicLine("repo", frame.repo),
|
|
62
|
+
publicLine("branch", frame.branch),
|
|
63
|
+
publicLine("task", frame.task),
|
|
64
|
+
publicLine("goal", frame.goal),
|
|
65
|
+
publicLine("model", frame.model),
|
|
66
|
+
]) {
|
|
67
|
+
if (line) lines.push(line);
|
|
68
|
+
}
|
|
69
|
+
const body = text(frame.text) ?? text(frame.lastMessage) ?? text(frame.caption);
|
|
70
|
+
if (body) lines.push(truncate(body, 1800));
|
|
71
|
+
return lines.join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function routeFromInbound(input: unknown): NotificationReplyRoute | undefined {
|
|
75
|
+
const raw = input as InboundShape;
|
|
76
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
77
|
+
const sessionId = text(raw.sessionId);
|
|
78
|
+
const actionId = text(raw.actionId);
|
|
79
|
+
if (!sessionId || !actionId) return undefined;
|
|
80
|
+
const answer = raw.answer ?? raw.value ?? raw.text;
|
|
81
|
+
if (typeof answer !== "string" && typeof answer !== "number" && typeof answer !== "object") return undefined;
|
|
82
|
+
return { sessionId, actionId, answer: answer as NotificationReplyRoute["answer"] };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class DiscordNotificationAdapter implements NotificationPresentationAdapter {
|
|
86
|
+
readonly kind = "discord" as const;
|
|
87
|
+
constructor(private readonly opts: ChatAdapterOptions) {}
|
|
88
|
+
|
|
89
|
+
render(event: NotificationEvent): NotificationAdapterPayload[] {
|
|
90
|
+
if (event.type === "action_resolved") return [];
|
|
91
|
+
const content = event.type === "action_needed" ? actionText(event, "discord") : frameText(event);
|
|
92
|
+
if (!content) return [];
|
|
93
|
+
const payload: Record<string, unknown> = {
|
|
94
|
+
content,
|
|
95
|
+
allowed_mentions: { parse: [] },
|
|
96
|
+
};
|
|
97
|
+
if (this.opts.channelId) payload.channel_id = this.opts.channelId;
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
adapter: this.kind,
|
|
101
|
+
channelKey: this.opts.channelId,
|
|
102
|
+
body: payload,
|
|
103
|
+
route: event.type === "action_needed" ? { sessionId: event.sessionId, actionId: event.id } : undefined,
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
mapInbound(input: unknown): NotificationReplyRoute | undefined {
|
|
109
|
+
return routeFromInbound(input);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
class SlackNotificationAdapter implements NotificationPresentationAdapter {
|
|
114
|
+
readonly kind = "slack" as const;
|
|
115
|
+
constructor(private readonly opts: ChatAdapterOptions) {}
|
|
116
|
+
|
|
117
|
+
render(event: NotificationEvent): NotificationAdapterPayload[] {
|
|
118
|
+
if (event.type === "action_resolved") return [];
|
|
119
|
+
const textValue = event.type === "action_needed" ? actionText(event, "slack") : frameText(event);
|
|
120
|
+
if (!textValue) return [];
|
|
121
|
+
const payload: Record<string, unknown> = {
|
|
122
|
+
text: textValue,
|
|
123
|
+
mrkdwn: true,
|
|
124
|
+
};
|
|
125
|
+
if (this.opts.channelId) payload.channel = this.opts.channelId;
|
|
126
|
+
return [
|
|
127
|
+
{
|
|
128
|
+
adapter: this.kind,
|
|
129
|
+
channelKey: this.opts.channelId,
|
|
130
|
+
body: payload,
|
|
131
|
+
route: event.type === "action_needed" ? { sessionId: event.sessionId, actionId: event.id } : undefined,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
mapInbound(input: unknown): NotificationReplyRoute | undefined {
|
|
137
|
+
return routeFromInbound(input);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function createDiscordAdapter(opts: Omit<ChatAdapterOptions, "kind"> = {}): NotificationPresentationAdapter {
|
|
142
|
+
return new DiscordNotificationAdapter({ ...opts, kind: "discord" });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function createSlackAdapter(opts: Omit<ChatAdapterOptions, "kind"> = {}): NotificationPresentationAdapter {
|
|
146
|
+
return new SlackNotificationAdapter({ ...opts, kind: "slack" });
|
|
147
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-thread configuration slash commands for the threaded session surface.
|
|
3
|
+
*
|
|
4
|
+
* Replies are thread-native now (the old `/answer <sessionId> …` command is
|
|
5
|
+
* removed), but the user can still adjust per-surface behaviour from inside a
|
|
6
|
+
* session thread with small slash commands:
|
|
7
|
+
*
|
|
8
|
+
* - `/verbose` switch the mirror to verbose (full tool output + reasoning)
|
|
9
|
+
* - `/lean` switch back to lean (assistant text + tool names)
|
|
10
|
+
* - `/verbosity lean|verbose`
|
|
11
|
+
* - `/redact on|off` toggle redaction of streamed content
|
|
12
|
+
*
|
|
13
|
+
* This parser is pure so the command grammar is unit-testable; the daemon maps
|
|
14
|
+
* the returned change onto a `config_command` frame / settings update.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** A parsed in-thread configuration change. */
|
|
18
|
+
export interface ConfigCommandChange {
|
|
19
|
+
verbosity?: "lean" | "verbose";
|
|
20
|
+
redact?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse an in-thread config command. Returns the requested change, or
|
|
25
|
+
* `undefined` when the text is not a recognised config command (so the daemon
|
|
26
|
+
* can fall through to treating it as a free-text injection).
|
|
27
|
+
*/
|
|
28
|
+
export function parseInThreadConfigCommand(text: string): ConfigCommandChange | undefined {
|
|
29
|
+
const trimmed = text.trim();
|
|
30
|
+
if (!trimmed.startsWith("/")) return undefined;
|
|
31
|
+
const [rawCommand, ...rest] = trimmed.slice(1).split(/\s+/);
|
|
32
|
+
const command = rawCommand?.toLowerCase();
|
|
33
|
+
const arg = rest[0]?.toLowerCase();
|
|
34
|
+
|
|
35
|
+
switch (command) {
|
|
36
|
+
case "verbose":
|
|
37
|
+
return { verbosity: "verbose" };
|
|
38
|
+
case "lean":
|
|
39
|
+
return { verbosity: "lean" };
|
|
40
|
+
case "verbosity":
|
|
41
|
+
if (arg === "lean" || arg === "verbose") return { verbosity: arg };
|
|
42
|
+
return undefined;
|
|
43
|
+
case "redact":
|
|
44
|
+
if (arg === "on" || arg === "true" || arg === "1") return { redact: true };
|
|
45
|
+
if (arg === "off" || arg === "false" || arg === "0") return { redact: false };
|
|
46
|
+
return undefined;
|
|
47
|
+
default:
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import type { Settings } from "../config/settings";
|
|
3
|
+
|
|
4
|
+
export interface NotificationConfig {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
botToken?: string;
|
|
7
|
+
chatId?: string;
|
|
8
|
+
discord: {
|
|
9
|
+
botToken?: string;
|
|
10
|
+
channelId?: string;
|
|
11
|
+
};
|
|
12
|
+
slack: {
|
|
13
|
+
botToken?: string;
|
|
14
|
+
channelId?: string;
|
|
15
|
+
};
|
|
16
|
+
redact: boolean;
|
|
17
|
+
verbosity: "lean" | "verbose";
|
|
18
|
+
idleTimeoutMs: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Read typed config from Settings. */
|
|
22
|
+
export function getNotificationConfig(settings: Settings): NotificationConfig {
|
|
23
|
+
return {
|
|
24
|
+
enabled: settings.get("notifications.enabled"),
|
|
25
|
+
botToken: settings.get("notifications.telegram.botToken"),
|
|
26
|
+
chatId: settings.get("notifications.telegram.chatId"),
|
|
27
|
+
discord: {
|
|
28
|
+
botToken: settings.get("notifications.discord.botToken"),
|
|
29
|
+
channelId: settings.get("notifications.discord.channelId"),
|
|
30
|
+
},
|
|
31
|
+
slack: {
|
|
32
|
+
botToken: settings.get("notifications.slack.botToken"),
|
|
33
|
+
channelId: settings.get("notifications.slack.channelId"),
|
|
34
|
+
},
|
|
35
|
+
redact: settings.get("notifications.redact"),
|
|
36
|
+
verbosity: settings.get("notifications.verbosity") === "verbose" ? "verbose" : "lean",
|
|
37
|
+
idleTimeoutMs: settings.get("notifications.daemon.idleTimeoutMs"),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Is global config sufficient for auto-on (enabled + at least one configured adapter)? */
|
|
42
|
+
export function isGloballyConfigured(cfg: NotificationConfig): boolean {
|
|
43
|
+
return (
|
|
44
|
+
cfg.enabled &&
|
|
45
|
+
((Boolean(cfg.botToken) && Boolean(cfg.chatId)) ||
|
|
46
|
+
(Boolean(cfg.discord.botToken) && Boolean(cfg.discord.channelId)) ||
|
|
47
|
+
(Boolean(cfg.slack.botToken) && Boolean(cfg.slack.channelId)))
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Resolve whether the notifications extension should be registered at SDK startup. */
|
|
52
|
+
export function shouldRegisterNotificationsExtension(input: {
|
|
53
|
+
env: NodeJS.ProcessEnv;
|
|
54
|
+
cfg?: NotificationConfig;
|
|
55
|
+
}): boolean {
|
|
56
|
+
if (input.env.SKC_NOTIFICATIONS === "0") return false;
|
|
57
|
+
if (input.env.SKC_NOTIFICATIONS === "1" || input.env.SKC_NOTIFICATIONS_TOKEN) return true;
|
|
58
|
+
return input.cfg ? isGloballyConfigured(input.cfg) : false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve whether THIS session should run notifications.
|
|
63
|
+
* Precedence (highest first):
|
|
64
|
+
* 1) env.SKC_NOTIFICATIONS === "0" -> false (hard opt-out)
|
|
65
|
+
* 2) sessionDisabled === true -> false (local /notify off)
|
|
66
|
+
* 3) env.SKC_NOTIFICATIONS === "1" || env.SKC_NOTIFICATIONS_TOKEN present -> true (legacy explicit)
|
|
67
|
+
* 4) isGloballyConfigured(cfg) -> true (global auto-on)
|
|
68
|
+
* 5) otherwise false
|
|
69
|
+
*/
|
|
70
|
+
export function isSessionNotificationsEnabled(input: {
|
|
71
|
+
cfg: NotificationConfig;
|
|
72
|
+
env: NodeJS.ProcessEnv;
|
|
73
|
+
sessionDisabled: boolean;
|
|
74
|
+
}): boolean {
|
|
75
|
+
if (input.env.SKC_NOTIFICATIONS === "0") return false;
|
|
76
|
+
if (input.sessionDisabled) return false;
|
|
77
|
+
if (input.env.SKC_NOTIFICATIONS === "1" || input.env.SKC_NOTIFICATIONS_TOKEN) return true;
|
|
78
|
+
return isGloballyConfigured(input.cfg);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Mask a bot token for display: first 4 chars + "…" + "(len N)"; "(unset)" when undefined/empty. Never reveal full token. */
|
|
82
|
+
export function maskToken(token: string | undefined): string {
|
|
83
|
+
if (!token) return "(unset)";
|
|
84
|
+
return `${token.slice(0, 4)}…(len ${token.length})`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Stable non-reversible fingerprint of a token: sha256 hex, first 12 chars. */
|
|
88
|
+
export function tokenFingerprint(token: string): string {
|
|
89
|
+
return crypto.createHash("sha256").update(token).digest("hex").slice(0, 12);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Short session tag for display, e.g. last 6 chars of sessionId. */
|
|
93
|
+
export function sessionTag(sessionId: string): string {
|
|
94
|
+
return sessionId.slice(-6);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface RedactableAction {
|
|
98
|
+
id: string;
|
|
99
|
+
kind: string;
|
|
100
|
+
sessionId: string;
|
|
101
|
+
question?: string;
|
|
102
|
+
options?: string[];
|
|
103
|
+
summary?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* When redact is true, strip sensitive content for remote delivery:
|
|
108
|
+
* - ask: NOT redacted. An ask is an interactive prompt the human must read and
|
|
109
|
+
* answer on the remote surface; redacting its question/options would make it
|
|
110
|
+
* unanswerable, defeating remote answering. Asks are returned unchanged.
|
|
111
|
+
* - idle: summary removed, (no question/options).
|
|
112
|
+
* When redact is false, return the action unchanged.
|
|
113
|
+
*
|
|
114
|
+
* Redaction still applies to streamed content frames (turn_stream, context_update,
|
|
115
|
+
* image_attachment) which are suppressed at their emit sites, not here.
|
|
116
|
+
*/
|
|
117
|
+
export function buildRedactedAction(
|
|
118
|
+
action: RedactableAction,
|
|
119
|
+
opts: { redact: boolean; sessionTag: string },
|
|
120
|
+
): RedactableAction {
|
|
121
|
+
if (!opts.redact) return action;
|
|
122
|
+
|
|
123
|
+
// Asks stay fully readable/answerable even under redaction.
|
|
124
|
+
if (action.kind === "ask") return action;
|
|
125
|
+
|
|
126
|
+
const { summary: _summary, question: _question, ...base } = action;
|
|
127
|
+
return base;
|
|
128
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { buildRedactedAction, type RedactableAction } from "./config";
|
|
2
|
+
|
|
3
|
+
export type NotificationEvent =
|
|
4
|
+
| ({ type: "action_needed" } & RedactableAction)
|
|
5
|
+
| { type: "action_resolved"; id: string; sessionId: string; resolvedBy?: string }
|
|
6
|
+
| { type: "frame"; sessionId: string; frame: Record<string, unknown> };
|
|
7
|
+
|
|
8
|
+
export interface NotificationReplyRoute {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
actionId: string;
|
|
11
|
+
answer: number | string | { selected?: Array<number | string>; custom?: string };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface NotificationAdapterPayload {
|
|
15
|
+
adapter: string;
|
|
16
|
+
channelKey?: string;
|
|
17
|
+
body: unknown;
|
|
18
|
+
route?: Omit<NotificationReplyRoute, "answer">;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface NotificationPresentationAdapter {
|
|
22
|
+
readonly kind: "telegram" | "discord" | "slack";
|
|
23
|
+
render(event: NotificationEvent): NotificationAdapterPayload[];
|
|
24
|
+
mapInbound(input: unknown): NotificationReplyRoute | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface EngineSessionSink {
|
|
28
|
+
sendReply(route: NotificationReplyRoute): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface NotificationEngineOptions {
|
|
32
|
+
redact: boolean;
|
|
33
|
+
sessionTag: (sessionId: string) => string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Shared presentation engine for managed notification clients.
|
|
38
|
+
*
|
|
39
|
+
* It owns fanout, redaction boundaries, pending-action routing, and reply
|
|
40
|
+
* delivery into session sinks. Transport adapters stay pure: render an internal
|
|
41
|
+
* event into a public-safe payload and map an inbound transport interaction
|
|
42
|
+
* back into a session/action answer.
|
|
43
|
+
*/
|
|
44
|
+
export class NotificationPresentationEngine {
|
|
45
|
+
readonly adapters: readonly NotificationPresentationAdapter[];
|
|
46
|
+
private readonly sessions = new Map<string, EngineSessionSink>();
|
|
47
|
+
private readonly pending = new Map<string, { sessionId: string; actionId: string }>();
|
|
48
|
+
|
|
49
|
+
constructor(
|
|
50
|
+
adapters: readonly NotificationPresentationAdapter[],
|
|
51
|
+
private readonly opts: NotificationEngineOptions,
|
|
52
|
+
) {
|
|
53
|
+
this.adapters = adapters;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
connectSession(sessionId: string, sink: EngineSessionSink): void {
|
|
57
|
+
this.sessions.set(sessionId, sink);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
dropSession(sessionId: string): void {
|
|
61
|
+
this.sessions.delete(sessionId);
|
|
62
|
+
for (const [key, route] of this.pending) {
|
|
63
|
+
if (route.sessionId === sessionId) this.pending.delete(key);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fanout(event: NotificationEvent): NotificationAdapterPayload[] {
|
|
68
|
+
const safeEvent = this.redactEvent(event);
|
|
69
|
+
if (safeEvent.type === "action_needed" && safeEvent.kind === "ask") {
|
|
70
|
+
this.pending.set(safeEvent.id, { sessionId: safeEvent.sessionId, actionId: safeEvent.id });
|
|
71
|
+
}
|
|
72
|
+
if (safeEvent.type === "action_resolved") {
|
|
73
|
+
this.pending.delete(safeEvent.id);
|
|
74
|
+
}
|
|
75
|
+
return this.adapters.flatMap(adapter => adapter.render(safeEvent));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
routeInbound(adapterKind: NotificationPresentationAdapter["kind"], input: unknown): boolean {
|
|
79
|
+
const adapter = this.adapters.find(candidate => candidate.kind === adapterKind);
|
|
80
|
+
const route = adapter?.mapInbound(input);
|
|
81
|
+
if (!route) return false;
|
|
82
|
+
const pending = this.pending.get(route.actionId);
|
|
83
|
+
if (!pending || pending.sessionId !== route.sessionId) return false;
|
|
84
|
+
const sink = this.sessions.get(route.sessionId);
|
|
85
|
+
if (!sink) return false;
|
|
86
|
+
sink.sendReply(route);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private redactEvent(event: NotificationEvent): NotificationEvent {
|
|
91
|
+
if (event.type !== "action_needed") return event;
|
|
92
|
+
return {
|
|
93
|
+
...buildRedactedAction(event, {
|
|
94
|
+
redact: this.opts.redact,
|
|
95
|
+
sessionTag: this.opts.sessionTag(event.sessionId),
|
|
96
|
+
}),
|
|
97
|
+
type: "action_needed",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|