@sayknow-cli/coding-agent 0.3.0 → 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/dist/types/cli/notify-cli.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +98 -3
- package/dist/types/defaults/skc-defaults.d.ts +2 -13
- 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/settings-defs.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -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.d.ts +9 -1
- package/dist/types/notifications/engine.d.ts +59 -0
- package/dist/types/notifications/managed-daemon.d.ts +48 -0
- package/dist/types/notifications/telegram-daemon.d.ts +19 -0
- package/dist/types/notifications/threaded-inbound.d.ts +19 -0
- package/dist/types/notifications/threaded-render.d.ts +6 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +23 -0
- package/dist/types/tools/index.d.ts +1 -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/verify-insane-vendor.ts +132 -0
- package/src/cli/args.ts +1 -1
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/notify-cli.ts +152 -5
- package/src/commands/team.ts +1 -1
- package/src/config/settings-schema.ts +119 -5
- package/src/defaults/skc/skills/ralplan/SKILL.md +11 -4
- package/src/defaults/skc-defaults.ts +2 -27
- package/src/extensibility/shared-events.ts +1 -0
- 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 +9 -9
- package/src/lsp/config.ts +16 -3
- package/src/lsp/defaults.json +7 -0
- package/src/lsp/types.ts +2 -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 +15 -0
- package/src/modes/interactive-mode.ts +46 -2
- package/src/modes/theme/theme.ts +4 -0
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/notifications/attachment-registry.ts +23 -0
- package/src/notifications/chat-adapters.ts +147 -0
- package/src/notifications/config.ts +23 -2
- package/src/notifications/engine.ts +100 -0
- package/src/notifications/index.ts +180 -38
- package/src/notifications/managed-daemon.ts +163 -0
- package/src/notifications/telegram-daemon.ts +235 -14
- package/src/notifications/threaded-inbound.ts +60 -4
- package/src/notifications/threaded-render.ts +20 -2
- package/src/session/agent-session.ts +82 -51
- package/src/skc-runtime/launch-tmux.ts +17 -3
- package/src/skc-runtime/ledger-event-renderer.ts +1 -0
- package/src/skc-runtime/ralplan-runtime.ts +2 -2
- package/src/skc-runtime/workflow-manifest.generated.json +29 -0
- package/src/skc-runtime/workflow-manifest.ts +7 -2
- package/src/tools/fetch.ts +78 -1
- package/src/tools/index.ts +3 -0
- 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
package/src/lsp/config.ts
CHANGED
|
@@ -88,6 +88,7 @@ function normalizeServerConfig(name: string, config: RawServerConfig): ServerCon
|
|
|
88
88
|
: isRecord(config.initializationOptions)
|
|
89
89
|
? config.initializationOptions
|
|
90
90
|
: undefined;
|
|
91
|
+
const supersedes = normalizeStringArray(config.supersedes);
|
|
91
92
|
|
|
92
93
|
return {
|
|
93
94
|
...config,
|
|
@@ -96,6 +97,7 @@ function normalizeServerConfig(name: string, config: RawServerConfig): ServerCon
|
|
|
96
97
|
fileTypes,
|
|
97
98
|
rootMarkers,
|
|
98
99
|
...(initOptions ? { initOptions } : {}),
|
|
100
|
+
...(supersedes ? { supersedes } : {}),
|
|
99
101
|
};
|
|
100
102
|
}
|
|
101
103
|
|
|
@@ -144,6 +146,18 @@ function mergeServers(
|
|
|
144
146
|
return merged;
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
function applyServerPrecedence(servers: Record<string, ServerConfig>): Record<string, ServerConfig> {
|
|
150
|
+
const suppressed = new Set<string>();
|
|
151
|
+
for (const config of Object.values(servers)) {
|
|
152
|
+
for (const serverName of config.supersedes ?? []) {
|
|
153
|
+
suppressed.add(serverName);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (suppressed.size === 0) return servers;
|
|
157
|
+
|
|
158
|
+
return Object.fromEntries(Object.entries(servers).filter(([name]) => !suppressed.has(name)));
|
|
159
|
+
}
|
|
160
|
+
|
|
147
161
|
function applyRuntimeDefaults(servers: Record<string, ServerConfig>): Record<string, ServerConfig> {
|
|
148
162
|
const updated: Record<string, ServerConfig> = { ...servers };
|
|
149
163
|
|
|
@@ -422,9 +436,8 @@ export function loadConfig(cwd: string): LspConfig {
|
|
|
422
436
|
detected[name] = { ...config, resolvedCommand: resolved };
|
|
423
437
|
}
|
|
424
438
|
|
|
425
|
-
return { servers: detected, idleTimeoutMs };
|
|
439
|
+
return { servers: applyServerPrecedence(detected), idleTimeoutMs };
|
|
426
440
|
}
|
|
427
|
-
|
|
428
441
|
// Merge overrides with defaults and filter to available servers
|
|
429
442
|
const mergedWithRuntime = applyRuntimeDefaults(mergedServers);
|
|
430
443
|
const available: Record<string, ServerConfig> = {};
|
|
@@ -437,7 +450,7 @@ export function loadConfig(cwd: string): LspConfig {
|
|
|
437
450
|
available[name] = { ...config, resolvedCommand: resolved };
|
|
438
451
|
}
|
|
439
452
|
|
|
440
|
-
return { servers: available, idleTimeoutMs };
|
|
453
|
+
return { servers: applyServerPrecedence(available), idleTimeoutMs };
|
|
441
454
|
}
|
|
442
455
|
|
|
443
456
|
// =============================================================================
|
package/src/lsp/defaults.json
CHANGED
|
@@ -342,6 +342,13 @@
|
|
|
342
342
|
"fileTypes": [".php"],
|
|
343
343
|
"rootMarkers": ["composer.json", ".phpactor.json", ".phpactor.yml"]
|
|
344
344
|
},
|
|
345
|
+
"csharp-ls": {
|
|
346
|
+
"command": "csharp-ls",
|
|
347
|
+
"args": [],
|
|
348
|
+
"fileTypes": [".cs", ".csx"],
|
|
349
|
+
"rootMarkers": ["*.sln", "*.slnx", "*.csproj", "global.json"],
|
|
350
|
+
"supersedes": ["omnisharp"]
|
|
351
|
+
},
|
|
345
352
|
"omnisharp": {
|
|
346
353
|
"command": "omnisharp",
|
|
347
354
|
"args": ["-z", "--hostPID", "$PID", "--encoding", "utf-8", "--languageserver"],
|
package/src/lsp/types.ts
CHANGED
|
@@ -358,6 +358,8 @@ export interface ServerConfig {
|
|
|
358
358
|
/** Per-server warmup timeout in milliseconds. Overrides the global WARMUP_TIMEOUT_MS for this server during startup. */
|
|
359
359
|
warmupTimeoutMs?: number;
|
|
360
360
|
capabilities?: ServerCapabilities;
|
|
361
|
+
/** Names of lower-precedence servers to drop when this server is available. */
|
|
362
|
+
supersedes?: string[];
|
|
361
363
|
/** If true, this is a linter/formatter server (e.g., Biome) - used only for diagnostics/actions, not type intelligence */
|
|
362
364
|
isLinter?: boolean;
|
|
363
365
|
/** Resolved absolute path to the command binary (set during config loading) */
|
|
@@ -62,6 +62,7 @@ export interface SubmenuSettingDef extends BaseSettingDef {
|
|
|
62
62
|
|
|
63
63
|
export interface TextInputSettingDef extends BaseSettingDef {
|
|
64
64
|
type: "text";
|
|
65
|
+
secret?: boolean;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
export type SettingDef = BooleanSettingDef | EnumSettingDef | SubmenuSettingDef | TextInputSettingDef;
|
|
@@ -128,7 +129,7 @@ function pathToSettingDef(path: SettingPath): SettingDef | null {
|
|
|
128
129
|
if (options) {
|
|
129
130
|
return { ...base, type: "submenu", options };
|
|
130
131
|
}
|
|
131
|
-
return { ...base, type: "text" };
|
|
132
|
+
return { ...base, type: "text", secret: (ui as { secret?: boolean }).secret };
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
return null;
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
Text,
|
|
15
15
|
} from "@sayknow-cli/tui";
|
|
16
16
|
import { type SettingPath, settings } from "../../config/settings";
|
|
17
|
+
import { maskToken } from "../../notifications/config";
|
|
17
18
|
import type {
|
|
18
19
|
SettingTab,
|
|
19
20
|
StatusLinePreset,
|
|
@@ -352,7 +353,9 @@ export class SettingsSelectorComponent extends Container {
|
|
|
352
353
|
id: def.path,
|
|
353
354
|
label,
|
|
354
355
|
description,
|
|
355
|
-
currentValue:
|
|
356
|
+
currentValue: def.secret
|
|
357
|
+
? maskToken(currentValue as string | undefined)
|
|
358
|
+
: ((currentValue as string) ?? ""),
|
|
356
359
|
submenu: (cv, done) => this.#createTextInput(def, cv, done),
|
|
357
360
|
};
|
|
358
361
|
}
|
|
@@ -480,10 +483,12 @@ export class SettingsSelectorComponent extends Container {
|
|
|
480
483
|
this.#textInputActive = false;
|
|
481
484
|
done(value);
|
|
482
485
|
};
|
|
486
|
+
// For secret fields, pre-fill with the real stored value rather than the masked display value.
|
|
487
|
+
const prefillValue = def.secret ? ((settings.get(def.path) as string) ?? "") : currentValue;
|
|
483
488
|
return new TextInputSubmenu(
|
|
484
489
|
tSettingLabel(def),
|
|
485
490
|
tSettingDesc(def),
|
|
486
|
-
|
|
491
|
+
prefillValue,
|
|
487
492
|
value => {
|
|
488
493
|
// Empty string clears the setting; undefined-typed string settings
|
|
489
494
|
// store "" which the browser.ts expandPath ignores (no-op fallback).
|
|
@@ -201,6 +201,7 @@ export class EventController {
|
|
|
201
201
|
this.ctx.statusContainer.clear();
|
|
202
202
|
}
|
|
203
203
|
this.#cancelIdleCompaction();
|
|
204
|
+
this.ctx.updateEditorBorderColor();
|
|
204
205
|
this.ctx.ensureLoadingAnimation();
|
|
205
206
|
this.ctx.ui.requestRender();
|
|
206
207
|
}
|
|
@@ -633,6 +634,7 @@ export class EventController {
|
|
|
633
634
|
this.#readToolCallArgs.clear();
|
|
634
635
|
this.#readToolCallAssistantComponents.clear();
|
|
635
636
|
this.#lastAssistantComponent = undefined;
|
|
637
|
+
this.ctx.updateEditorBorderColor();
|
|
636
638
|
this.ctx.ui.requestRender();
|
|
637
639
|
this.#scheduleIdleCompaction();
|
|
638
640
|
this.sendCompletionNotification();
|
|
@@ -642,6 +644,7 @@ export class EventController {
|
|
|
642
644
|
event: Extract<AgentSessionEvent, { type: "auto_compaction_start" }>,
|
|
643
645
|
): Promise<void> {
|
|
644
646
|
this.#cancelIdleCompaction();
|
|
647
|
+
this.ctx.updateEditorBorderColor();
|
|
645
648
|
this.ctx.autoCompactionEscapeHandler = this.ctx.editor.onEscape;
|
|
646
649
|
this.ctx.editor.onEscape = () => {
|
|
647
650
|
this.ctx.session.abortCompaction();
|
|
@@ -672,13 +675,20 @@ export class EventController {
|
|
|
672
675
|
this.ctx.autoCompactionLoader = undefined;
|
|
673
676
|
this.ctx.statusContainer.clear();
|
|
674
677
|
}
|
|
678
|
+
this.ctx.updateEditorBorderColor();
|
|
675
679
|
const isHandoffAction = event.action === "handoff";
|
|
680
|
+
const continuationDisabled = event.continuationSkipReason === "auto_continue_disabled_non_resumable_tail";
|
|
676
681
|
if (event.aborted) {
|
|
677
682
|
this.ctx.showStatus(isHandoffAction ? "Auto-handoff cancelled" : "Auto context-full maintenance cancelled");
|
|
678
683
|
} else if (event.result) {
|
|
679
684
|
this.ctx.rebuildChatFromMessages();
|
|
680
685
|
this.ctx.statusLine.invalidate();
|
|
681
686
|
this.ctx.updateEditorTopBorder();
|
|
687
|
+
if (continuationDisabled && !isHandoffAction) {
|
|
688
|
+
this.ctx.showStatus("Context overflow recovery skipped: auto_continue_disabled_non_resumable_tail");
|
|
689
|
+
} else if (event.willRetry && !isHandoffAction) {
|
|
690
|
+
this.ctx.showStatus("Context overflow maintenance completed");
|
|
691
|
+
}
|
|
682
692
|
} else if (event.errorMessage) {
|
|
683
693
|
this.ctx.showWarning(event.errorMessage);
|
|
684
694
|
} else if (isHandoffAction) {
|
|
@@ -691,6 +701,11 @@ export class EventController {
|
|
|
691
701
|
} else if (event.skipped) {
|
|
692
702
|
// Benign skip: no model selected, no candidate models available, or nothing
|
|
693
703
|
// to compact yet. Not a failure — suppress the warning.
|
|
704
|
+
if (continuationDisabled) {
|
|
705
|
+
this.ctx.showStatus("Context overflow recovery skipped: auto_continue_disabled_non_resumable_tail");
|
|
706
|
+
} else if (event.willRetry && !isHandoffAction) {
|
|
707
|
+
this.ctx.showStatus("Context overflow maintenance skipped");
|
|
708
|
+
}
|
|
694
709
|
} else {
|
|
695
710
|
this.ctx.showWarning("Auto context-full maintenance failed; continuing without maintenance");
|
|
696
711
|
}
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
import { APP_NAME, adjustHsv, getProjectDir, hsvToRgb, isEnoent, logger, postmortem, prompt } from "@sayknow-cli/utils";
|
|
30
30
|
import chalk from "chalk";
|
|
31
31
|
import { AsyncJobManager } from "../async";
|
|
32
|
-
import { KeybindingsManager } from "../config/keybindings";
|
|
32
|
+
import { type AppKeybinding, KeybindingsManager } from "../config/keybindings";
|
|
33
33
|
import { isSettingsInitialized, type Settings, settings } from "../config/settings";
|
|
34
34
|
import { DEFAULT_SKC_DEFINITION_NAMES } from "../defaults/skc-defaults";
|
|
35
35
|
import type {
|
|
@@ -118,6 +118,24 @@ import type { CompactionQueuedMessage, InteractiveModeContext, SubmittedUserInpu
|
|
|
118
118
|
import { UiHelpers } from "./utils/ui-helpers";
|
|
119
119
|
|
|
120
120
|
const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
|
|
121
|
+
const DEFAULT_COMPOSER_PLACEHOLDER = "Type your message...";
|
|
122
|
+
const FRIENDLY_KEY_PARTS: Record<string, string> = {
|
|
123
|
+
alt: "Alt",
|
|
124
|
+
cmd: "Command",
|
|
125
|
+
command: "Command",
|
|
126
|
+
ctrl: "Control",
|
|
127
|
+
enter: "Enter",
|
|
128
|
+
meta: process.platform === "darwin" ? "Command" : "Meta",
|
|
129
|
+
option: "Option",
|
|
130
|
+
shift: "Shift",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
function formatShortcutForPlaceholder(key: string): string {
|
|
134
|
+
return key
|
|
135
|
+
.split("+")
|
|
136
|
+
.map(part => FRIENDLY_KEY_PARTS[part.toLowerCase()] ?? part)
|
|
137
|
+
.join("+");
|
|
138
|
+
}
|
|
121
139
|
|
|
122
140
|
const HINT_SHIMMER_PALETTE: ShimmerPalette = {
|
|
123
141
|
low: "dim",
|
|
@@ -142,7 +160,7 @@ function configureDefaultComposerChrome(editor: CustomEditor): void {
|
|
|
142
160
|
editor.setClosedBorderBox(true);
|
|
143
161
|
editor.setPromptGutter(undefined);
|
|
144
162
|
editor.setInputPrefix(getDefaultInputPrefix());
|
|
145
|
-
editor.setPlaceholder(
|
|
163
|
+
editor.setPlaceholder(DEFAULT_COMPOSER_PLACEHOLDER);
|
|
146
164
|
editor.setPaddingX(1);
|
|
147
165
|
editor.setTopBorder(undefined);
|
|
148
166
|
}
|
|
@@ -904,6 +922,31 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
904
922
|
this.editor.setMaxHeight(this.#computeEditorMaxHeight());
|
|
905
923
|
}
|
|
906
924
|
|
|
925
|
+
#isPromptDeliveryBusy(): boolean {
|
|
926
|
+
return this.session.isStreaming || this.session.isCompacting;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
#getFirstKeyForAction(action: AppKeybinding): string | undefined {
|
|
930
|
+
return this.keybindings.getKeys(action)[0];
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
#getMessageQueueShortcut(): string | undefined {
|
|
934
|
+
const preferredAction: AppKeybinding =
|
|
935
|
+
process.platform === "darwin" ? "app.message.followUp" : "app.message.queue";
|
|
936
|
+
const fallbackAction: AppKeybinding =
|
|
937
|
+
process.platform === "darwin" ? "app.message.queue" : "app.message.followUp";
|
|
938
|
+
return this.#getFirstKeyForAction(preferredAction) ?? this.#getFirstKeyForAction(fallbackAction);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
#getComposerPlaceholder(): string {
|
|
942
|
+
if (!this.#isPromptDeliveryBusy()) return DEFAULT_COMPOSER_PLACEHOLDER;
|
|
943
|
+
const enterAction = this.settings.get("busyPromptMode") === "steer" ? "Steering" : "Message Queueing";
|
|
944
|
+
const parts = [`Enter: ${enterAction}`];
|
|
945
|
+
const queueKey = this.#getMessageQueueShortcut();
|
|
946
|
+
if (queueKey) parts.push(`${formatShortcutForPlaceholder(queueKey)}: Message Queueing`);
|
|
947
|
+
return `${DEFAULT_COMPOSER_PLACEHOLDER} ${parts.join(" · ")}`;
|
|
948
|
+
}
|
|
949
|
+
|
|
907
950
|
updateEditorChrome(): void {
|
|
908
951
|
if (this.isBashMode) {
|
|
909
952
|
this.editor.borderColor = this.isBashNoContext
|
|
@@ -927,6 +970,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
927
970
|
if (!this.isBashMode) {
|
|
928
971
|
this.editor.setInputPrefix(getDefaultInputPrefix());
|
|
929
972
|
}
|
|
973
|
+
this.editor.setPlaceholder(this.#getComposerPlaceholder());
|
|
930
974
|
this.#setComposerTopBorder();
|
|
931
975
|
this.ui.requestRender();
|
|
932
976
|
}
|
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]",
|
|
@@ -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));
|
|
@@ -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
|
+
}
|
|
@@ -5,6 +5,14 @@ export interface NotificationConfig {
|
|
|
5
5
|
enabled: boolean;
|
|
6
6
|
botToken?: string;
|
|
7
7
|
chatId?: string;
|
|
8
|
+
discord: {
|
|
9
|
+
botToken?: string;
|
|
10
|
+
channelId?: string;
|
|
11
|
+
};
|
|
12
|
+
slack: {
|
|
13
|
+
botToken?: string;
|
|
14
|
+
channelId?: string;
|
|
15
|
+
};
|
|
8
16
|
redact: boolean;
|
|
9
17
|
verbosity: "lean" | "verbose";
|
|
10
18
|
idleTimeoutMs: number;
|
|
@@ -16,15 +24,28 @@ export function getNotificationConfig(settings: Settings): NotificationConfig {
|
|
|
16
24
|
enabled: settings.get("notifications.enabled"),
|
|
17
25
|
botToken: settings.get("notifications.telegram.botToken"),
|
|
18
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
|
+
},
|
|
19
35
|
redact: settings.get("notifications.redact"),
|
|
20
36
|
verbosity: settings.get("notifications.verbosity") === "verbose" ? "verbose" : "lean",
|
|
21
37
|
idleTimeoutMs: settings.get("notifications.daemon.idleTimeoutMs"),
|
|
22
38
|
};
|
|
23
39
|
}
|
|
24
40
|
|
|
25
|
-
/** Is global config sufficient for auto-on (enabled +
|
|
41
|
+
/** Is global config sufficient for auto-on (enabled + at least one configured adapter)? */
|
|
26
42
|
export function isGloballyConfigured(cfg: NotificationConfig): boolean {
|
|
27
|
-
return
|
|
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
|
+
);
|
|
28
49
|
}
|
|
29
50
|
|
|
30
51
|
/** Resolve whether the notifications extension should be registered at SDK startup. */
|
|
@@ -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
|
+
}
|