@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
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notify CLI command handlers.
|
|
3
|
+
*
|
|
4
|
+
* Handles `skc notify` setup/status and the hidden daemon entrypoint.
|
|
5
|
+
*/
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { APP_NAME } from "@sayknow-cli/utils";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { Settings } from "../config/settings";
|
|
10
|
+
import { getNotificationConfig, maskToken } from "../notifications/config";
|
|
11
|
+
|
|
12
|
+
export type NotifyAction = "setup" | "status" | "daemon-internal";
|
|
13
|
+
|
|
14
|
+
export interface NotifyCommandArgs {
|
|
15
|
+
action: NotifyAction;
|
|
16
|
+
smoke?: boolean;
|
|
17
|
+
rawArgs: string[];
|
|
18
|
+
token?: string;
|
|
19
|
+
chatId?: string;
|
|
20
|
+
redact?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface NotifyCommandDeps {
|
|
24
|
+
fetchImpl?: typeof fetch;
|
|
25
|
+
apiBase?: string;
|
|
26
|
+
settings?: Settings;
|
|
27
|
+
setupToken?: string;
|
|
28
|
+
pollTimeoutMs?: number;
|
|
29
|
+
pollIntervalMs?: number;
|
|
30
|
+
setupChatId?: string;
|
|
31
|
+
setupRedact?: boolean;
|
|
32
|
+
setupInteractive?: boolean;
|
|
33
|
+
threadedModePrompt?: (message: string) => Promise<string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface TelegramApiResponse<T> {
|
|
37
|
+
ok: boolean;
|
|
38
|
+
result?: T;
|
|
39
|
+
description?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface TelegramUpdate {
|
|
43
|
+
update_id: number;
|
|
44
|
+
message?: {
|
|
45
|
+
chat?: {
|
|
46
|
+
id?: number | string;
|
|
47
|
+
type?: string;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface TelegramUser {
|
|
53
|
+
id: number;
|
|
54
|
+
is_bot?: boolean;
|
|
55
|
+
first_name?: string;
|
|
56
|
+
username?: string;
|
|
57
|
+
has_topics_enabled?: boolean;
|
|
58
|
+
allows_users_to_create_topics?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type ThreadedModeState = "enabled" | "disabled" | "unknown";
|
|
62
|
+
type ThreadedModeFinalLabel = "verified" | "unverified" | "unknown";
|
|
63
|
+
|
|
64
|
+
const DEFAULT_API_BASE = "https://api.telegram.org";
|
|
65
|
+
const DEFAULT_POLL_TIMEOUT_MS = 60_000;
|
|
66
|
+
const DEFAULT_POLL_INTERVAL_MS = 1_000;
|
|
67
|
+
|
|
68
|
+
export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
|
|
69
|
+
if (args.length === 0 || args[0] !== "notify") {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const action = args[1];
|
|
74
|
+
if (action === "setup" || action === "status") {
|
|
75
|
+
const rest = args.slice(2);
|
|
76
|
+
const flag = (name: string): string | undefined => {
|
|
77
|
+
const i = rest.indexOf(name);
|
|
78
|
+
return i >= 0 ? rest[i + 1] : undefined;
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
action,
|
|
82
|
+
rawArgs: rest,
|
|
83
|
+
token: flag("--token"),
|
|
84
|
+
chatId: flag("--chat-id"),
|
|
85
|
+
redact: rest.includes("--redact"),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (action === "daemon-internal") {
|
|
89
|
+
return {
|
|
90
|
+
action,
|
|
91
|
+
smoke: args.slice(2).includes("--smoke"),
|
|
92
|
+
rawArgs: args.slice(2),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { action: "status", rawArgs: args.slice(1) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function runNotifyCommand(cmd: NotifyCommandArgs, deps: NotifyCommandDeps = {}): Promise<void> {
|
|
100
|
+
switch (cmd.action) {
|
|
101
|
+
case "setup":
|
|
102
|
+
await runSetup({
|
|
103
|
+
...deps,
|
|
104
|
+
setupToken: deps.setupToken ?? cmd.token,
|
|
105
|
+
setupChatId: deps.setupChatId ?? cmd.chatId,
|
|
106
|
+
setupRedact: deps.setupRedact ?? cmd.redact,
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
case "status":
|
|
110
|
+
await runStatus(deps);
|
|
111
|
+
return;
|
|
112
|
+
case "daemon-internal": {
|
|
113
|
+
const m = await import("../notifications/telegram-daemon-cli");
|
|
114
|
+
if (cmd.smoke) {
|
|
115
|
+
await m.runDaemonSmoke();
|
|
116
|
+
} else {
|
|
117
|
+
await m.runDaemonInternal(cmd.rawArgs);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function getSettings(deps: NotifyCommandDeps): Promise<Settings> {
|
|
125
|
+
return deps.settings ?? (await Settings.init());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function runSetup(deps: NotifyCommandDeps): Promise<void> {
|
|
129
|
+
const settings = await getSettings(deps);
|
|
130
|
+
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
131
|
+
const apiBase = deps.apiBase ?? DEFAULT_API_BASE;
|
|
132
|
+
const token = deps.setupToken ?? (await promptForToken());
|
|
133
|
+
if (!token.trim()) {
|
|
134
|
+
throw new Error("Telegram bot token is required.");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const user = await getMe(fetchImpl, apiBase, token);
|
|
138
|
+
const threadedState = await verifyThreadedMode(fetchImpl, apiBase, token, user, {
|
|
139
|
+
interactive: resolveSetupInteractive(deps),
|
|
140
|
+
prompt: deps.threadedModePrompt ?? promptForThreadedMode,
|
|
141
|
+
});
|
|
142
|
+
process.stdout.write(
|
|
143
|
+
"Token validated. Message your bot now from the private Telegram chat to pair notifications.\n",
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
let chatId: string;
|
|
147
|
+
if (deps.setupChatId?.trim()) {
|
|
148
|
+
chatId = deps.setupChatId.trim();
|
|
149
|
+
process.stdout.write(`Using provided chat id ${chatId} (non-interactive).\n`);
|
|
150
|
+
} else {
|
|
151
|
+
const stale = await getUpdates(fetchImpl, apiBase, token, { timeout: 0, allowed_updates: ["message"] });
|
|
152
|
+
const offset = nextOffset(stale);
|
|
153
|
+
chatId = await waitForPrivateChat(fetchImpl, apiBase, token, {
|
|
154
|
+
offset,
|
|
155
|
+
pollTimeoutMs: deps.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,
|
|
156
|
+
pollIntervalMs: deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
settings.set("notifications.telegram.botToken", token);
|
|
161
|
+
settings.set("notifications.telegram.chatId", chatId);
|
|
162
|
+
settings.set("notifications.enabled", true);
|
|
163
|
+
if (deps.setupRedact) settings.set("notifications.redact", true);
|
|
164
|
+
await settings.flush();
|
|
165
|
+
|
|
166
|
+
process.stdout.write(
|
|
167
|
+
`Notifications enabled. botToken=${maskToken(token)} chatId=${chatId} threaded=${threadedLabel(threadedState)}\n`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function promptForToken(): Promise<string> {
|
|
172
|
+
if (!process.stdin.isTTY) {
|
|
173
|
+
throw new Error("notify setup requires an interactive TTY unless setupToken is injected.");
|
|
174
|
+
}
|
|
175
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
176
|
+
try {
|
|
177
|
+
return (await rl.question("Telegram BotFather token: ")).trim();
|
|
178
|
+
} finally {
|
|
179
|
+
rl.close();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const THREADED_ENABLED_SUCCESS =
|
|
184
|
+
"Telegram Threaded Mode capability verified for this bot. SKC will request a private-chat topic per session; if Telegram ever refuses topic creation, notifications fall back to this flat chat with a one-time nudge.\n";
|
|
185
|
+
|
|
186
|
+
const THREADED_MISSING_WARNING =
|
|
187
|
+
"Warning: Telegram getMe did not include has_topics_enabled, so SKC cannot verify private-chat Threaded Mode capability for this bot. Setup will continue; update Telegram/Bot API support or re-run setup if per-session topics fail.\n";
|
|
188
|
+
|
|
189
|
+
const THREADED_NONINTERACTIVE_WARNING =
|
|
190
|
+
"Warning: Telegram Threaded Mode capability is OFF for this bot. Setup will be saved because this run is non-interactive, but per-session Telegram delivery may fail closed until the bot owner enables Threaded Mode in @BotFather. SKC cannot enable it through the Bot API.\n";
|
|
191
|
+
|
|
192
|
+
const THREADED_DISABLED_GUIDANCE =
|
|
193
|
+
"Telegram Threaded Mode is OFF for this bot. SKC needs Telegram private-chat topics so each session can use its own thread.\n" +
|
|
194
|
+
"SKC cannot enable this through the Bot API. Open @BotFather, select this bot, enable Threaded Mode / forum topics for private chats, then return here.\n" +
|
|
195
|
+
"Telegram may require an additional Stars purchase fee for private-chat topics.\n";
|
|
196
|
+
|
|
197
|
+
const THREADED_DISABLED_PROMPT =
|
|
198
|
+
"Press Enter after enabling Threaded Mode, or type skip to finish setup with a warning: ";
|
|
199
|
+
|
|
200
|
+
const THREADED_STILL_OFF = "Telegram still reports Threaded Mode OFF for this bot.\n";
|
|
201
|
+
|
|
202
|
+
const THREADED_RETRY_PROMPT = "Press Enter to check again, or type skip to finish setup with a warning: ";
|
|
203
|
+
|
|
204
|
+
const THREADED_SKIP_WARNING =
|
|
205
|
+
"Warning: continuing without verified Telegram Threaded Mode capability. Setup will be saved, but per-session Telegram delivery may fail closed until Threaded Mode is enabled in BotFather.\n";
|
|
206
|
+
|
|
207
|
+
const THREADED_INVALID_INPUT = "Type Enter to retry or skip to continue with a warning.\n";
|
|
208
|
+
|
|
209
|
+
const THREADED_RETRY_INPUTS = new Set(["", "y", "yes", "r", "retry"]);
|
|
210
|
+
const THREADED_SKIP_INPUTS = new Set(["s", "skip", "n", "no"]);
|
|
211
|
+
|
|
212
|
+
function isTelegramUser(value: unknown): value is TelegramUser {
|
|
213
|
+
return Boolean(value) && typeof value === "object" && typeof (value as { id?: unknown }).id === "number";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function getMe(fetchImpl: typeof fetch, apiBase: string, token: string): Promise<TelegramUser> {
|
|
217
|
+
const user = await callTelegram<unknown>(fetchImpl, apiBase, token, "getMe", {});
|
|
218
|
+
if (!isTelegramUser(user)) {
|
|
219
|
+
throw new Error("Telegram getMe returned invalid Telegram response: missing valid User result.");
|
|
220
|
+
}
|
|
221
|
+
return user;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function threadedModeState(user: TelegramUser): ThreadedModeState {
|
|
225
|
+
if (user.has_topics_enabled === true) return "enabled";
|
|
226
|
+
if (user.has_topics_enabled === false) return "disabled";
|
|
227
|
+
return "unknown";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function threadedLabel(state: ThreadedModeState): ThreadedModeFinalLabel {
|
|
231
|
+
if (state === "enabled") return "verified";
|
|
232
|
+
if (state === "disabled") return "unverified";
|
|
233
|
+
return "unknown";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function resolveSetupInteractive(deps: NotifyCommandDeps): boolean {
|
|
237
|
+
if (deps.setupInteractive !== undefined) return deps.setupInteractive;
|
|
238
|
+
return Boolean(process.stdin.isTTY) && !deps.setupChatId?.trim();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function promptForThreadedMode(message: string): Promise<string> {
|
|
242
|
+
if (!process.stdin.isTTY) return "skip";
|
|
243
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
244
|
+
try {
|
|
245
|
+
return (await rl.question(message)).trim();
|
|
246
|
+
} finally {
|
|
247
|
+
rl.close();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function verifyThreadedMode(
|
|
252
|
+
fetchImpl: typeof fetch,
|
|
253
|
+
apiBase: string,
|
|
254
|
+
token: string,
|
|
255
|
+
initialUser: TelegramUser,
|
|
256
|
+
opts: { interactive: boolean; prompt: (message: string) => Promise<string> },
|
|
257
|
+
): Promise<ThreadedModeState> {
|
|
258
|
+
const classify = (user: TelegramUser): ThreadedModeState | undefined => {
|
|
259
|
+
const state = threadedModeState(user);
|
|
260
|
+
if (state === "enabled") {
|
|
261
|
+
process.stdout.write(THREADED_ENABLED_SUCCESS);
|
|
262
|
+
return "enabled";
|
|
263
|
+
}
|
|
264
|
+
if (state === "unknown") {
|
|
265
|
+
process.stdout.write(THREADED_MISSING_WARNING);
|
|
266
|
+
return "unknown";
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const initial = classify(initialUser);
|
|
272
|
+
if (initial) return initial;
|
|
273
|
+
|
|
274
|
+
if (!opts.interactive) {
|
|
275
|
+
process.stdout.write(THREADED_NONINTERACTIVE_WARNING);
|
|
276
|
+
return "disabled";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
process.stdout.write(THREADED_DISABLED_GUIDANCE);
|
|
280
|
+
let firstPrompt = true;
|
|
281
|
+
for (;;) {
|
|
282
|
+
const answer = (await opts.prompt(firstPrompt ? THREADED_DISABLED_PROMPT : THREADED_RETRY_PROMPT))
|
|
283
|
+
.trim()
|
|
284
|
+
.toLowerCase();
|
|
285
|
+
firstPrompt = false;
|
|
286
|
+
if (THREADED_SKIP_INPUTS.has(answer)) {
|
|
287
|
+
process.stdout.write(THREADED_SKIP_WARNING);
|
|
288
|
+
return "disabled";
|
|
289
|
+
}
|
|
290
|
+
if (!THREADED_RETRY_INPUTS.has(answer)) {
|
|
291
|
+
process.stdout.write(THREADED_INVALID_INPUT);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const resolved = classify(await getMe(fetchImpl, apiBase, token));
|
|
295
|
+
if (resolved) return resolved;
|
|
296
|
+
process.stdout.write(THREADED_STILL_OFF);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function runStatus(deps: NotifyCommandDeps): Promise<void> {
|
|
301
|
+
const settings = await getSettings(deps);
|
|
302
|
+
const cfg = getNotificationConfig(settings);
|
|
303
|
+
process.stdout.write(
|
|
304
|
+
`${chalk.bold("Notifications")}\n` +
|
|
305
|
+
` enabled: ${cfg.enabled}\n` +
|
|
306
|
+
` telegram.botToken: ${maskToken(cfg.botToken)}\n` +
|
|
307
|
+
` telegram.chatId: ${cfg.chatId ?? "(unset)"}\n` +
|
|
308
|
+
` discord.botToken: ${maskToken(cfg.discord.botToken)}\n` +
|
|
309
|
+
` discord.channelId: ${cfg.discord.channelId ?? "(unset)"}\n` +
|
|
310
|
+
` slack.botToken: ${maskToken(cfg.slack.botToken)}\n` +
|
|
311
|
+
` slack.channelId: ${cfg.slack.channelId ?? "(unset)"}\n` +
|
|
312
|
+
` redact: ${cfg.redact}\n`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function waitForPrivateChat(
|
|
317
|
+
fetchImpl: typeof fetch,
|
|
318
|
+
apiBase: string,
|
|
319
|
+
token: string,
|
|
320
|
+
opts: { offset: number | undefined; pollTimeoutMs: number; pollIntervalMs: number },
|
|
321
|
+
): Promise<string> {
|
|
322
|
+
const deadline = Date.now() + opts.pollTimeoutMs;
|
|
323
|
+
let offset = opts.offset;
|
|
324
|
+
let sawRejectedChatType: string | undefined;
|
|
325
|
+
|
|
326
|
+
while (Date.now() <= deadline) {
|
|
327
|
+
const updates = await getUpdates(fetchImpl, apiBase, token, { offset, timeout: 0, allowed_updates: ["message"] });
|
|
328
|
+
offset = nextOffset(updates, offset);
|
|
329
|
+
for (const update of updates) {
|
|
330
|
+
const chat = update.message?.chat;
|
|
331
|
+
if (!chat) continue;
|
|
332
|
+
if (chat.type === "private" && chat.id !== undefined) {
|
|
333
|
+
return String(chat.id);
|
|
334
|
+
}
|
|
335
|
+
if (chat.type === "group" || chat.type === "supergroup" || chat.type === "channel") {
|
|
336
|
+
sawRejectedChatType = chat.type;
|
|
337
|
+
process.stderr.write(
|
|
338
|
+
`Rejected ${chat.type} chat. Pairing requires a private Telegram chat with the bot.\n`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (opts.pollIntervalMs > 0) {
|
|
343
|
+
await new Promise(resolve =>
|
|
344
|
+
setTimeout(resolve, Math.min(opts.pollIntervalMs, Math.max(0, deadline - Date.now()))),
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (sawRejectedChatType) {
|
|
350
|
+
throw new Error(`Pairing rejected ${sawRejectedChatType} chat; message the bot from a private chat.`);
|
|
351
|
+
}
|
|
352
|
+
throw new Error("Timed out waiting for a private Telegram message to pair notifications.");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function nextOffset(updates: TelegramUpdate[], fallback?: number): number | undefined {
|
|
356
|
+
let max = fallback === undefined ? undefined : fallback - 1;
|
|
357
|
+
for (const update of updates) {
|
|
358
|
+
if (typeof update.update_id === "number" && (max === undefined || update.update_id > max)) {
|
|
359
|
+
max = update.update_id;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return max === undefined ? fallback : max + 1;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function getUpdates(
|
|
366
|
+
fetchImpl: typeof fetch,
|
|
367
|
+
apiBase: string,
|
|
368
|
+
token: string,
|
|
369
|
+
params: Record<string, unknown>,
|
|
370
|
+
): Promise<TelegramUpdate[]> {
|
|
371
|
+
return await callTelegram<TelegramUpdate[]>(fetchImpl, apiBase, token, "getUpdates", params);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function callTelegram<T>(
|
|
375
|
+
fetchImpl: typeof fetch,
|
|
376
|
+
apiBase: string,
|
|
377
|
+
token: string,
|
|
378
|
+
method: string,
|
|
379
|
+
body: Record<string, unknown>,
|
|
380
|
+
): Promise<T> {
|
|
381
|
+
const response = await fetchImpl(`${apiBase.replace(/\/$/, "")}/bot${token}/${method}`, {
|
|
382
|
+
method: "POST",
|
|
383
|
+
headers: { "content-type": "application/json" },
|
|
384
|
+
body: JSON.stringify(body),
|
|
385
|
+
});
|
|
386
|
+
let payload: TelegramApiResponse<T>;
|
|
387
|
+
try {
|
|
388
|
+
payload = (await response.json()) as TelegramApiResponse<T>;
|
|
389
|
+
} catch {
|
|
390
|
+
throw new Error(`Telegram ${method} returned invalid JSON.`);
|
|
391
|
+
}
|
|
392
|
+
if (!response.ok || !payload.ok) {
|
|
393
|
+
throw new Error(`Telegram ${method} failed: ${payload.description ?? response.statusText}`);
|
|
394
|
+
}
|
|
395
|
+
return payload.result as T;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function printNotifyHelp(): void {
|
|
399
|
+
process.stdout.write(`${chalk.bold(`${APP_NAME} notify`)} - Configure Telegram notifications
|
|
400
|
+
|
|
401
|
+
${chalk.bold("Usage:")}
|
|
402
|
+
${APP_NAME} notify setup
|
|
403
|
+
${APP_NAME} notify setup --token <botToken> --chat-id <chatId> [--redact]
|
|
404
|
+
${APP_NAME} notify status
|
|
405
|
+
|
|
406
|
+
${chalk.bold("Subcommands:")}
|
|
407
|
+
setup Pair a Telegram bot token with a private chat and verify Threaded Mode capability
|
|
408
|
+
status Show notification configuration without secrets
|
|
409
|
+
|
|
410
|
+
${chalk.bold("Examples:")}
|
|
411
|
+
${APP_NAME} notify setup
|
|
412
|
+
${APP_NAME} notify setup --token <botToken> --chat-id <chatId> [--redact]
|
|
413
|
+
${APP_NAME} notify status
|
|
414
|
+
|
|
415
|
+
${chalk.bold("Threaded Mode:")}
|
|
416
|
+
SKC uses Telegram private-chat topics for per-session threads. Setup verifies the bot
|
|
417
|
+
capability via getMe.has_topics_enabled. If it is off, enable Threaded Mode in @BotFather;
|
|
418
|
+
bots cannot toggle it through the Bot API. If Telegram refuses topic creation at runtime,
|
|
419
|
+
SKC delivers flat to the paired private chat and nudges you to enable Threaded Mode.
|
|
420
|
+
`);
|
|
421
|
+
}
|