@sayknow-cli/coding-agent 0.3.8 → 0.3.9
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/config/settings-schema.d.ts +14 -0
- package/dist/types/modes/components/thinking-selector.d.ts +2 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
- package/dist/types/notifications/config-commands.d.ts +9 -0
- package/dist/types/notifications/config.d.ts +16 -0
- package/dist/types/notifications/index.d.ts +2 -0
- package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
- package/dist/types/notifications/reply-sent-store.d.ts +53 -0
- package/dist/types/notifications/rich-draft.d.ts +68 -0
- package/dist/types/notifications/rich-render.d.ts +90 -0
- package/dist/types/notifications/telegram-daemon.d.ts +22 -0
- package/dist/types/notifications/telegram-reference.d.ts +7 -0
- package/dist/types/notifications/threaded-render.d.ts +8 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/utils/pasted-image-path.d.ts +30 -0
- package/package.json +7 -7
- package/src/cli/notify-cli.ts +11 -9
- package/src/cli/update-cli.ts +63 -24
- package/src/config/keybindings.ts +1 -1
- package/src/config/settings-schema.ts +8 -0
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/lsp/client.ts +1 -0
- package/src/modes/components/footer.ts +7 -2
- package/src/modes/components/thinking-selector.ts +10 -8
- package/src/modes/controllers/event-controller.ts +11 -2
- package/src/modes/controllers/extension-ui-controller.ts +8 -1
- package/src/modes/controllers/input-controller.ts +12 -17
- package/src/modes/controllers/selector-controller.ts +41 -0
- package/src/modes/interactive-mode.ts +5 -0
- package/src/modes/rpc/rpc-mode.ts +3 -9
- package/src/modes/rpc/rpc-socket-security.ts +13 -1
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/injected-user-submission.ts +94 -0
- package/src/notifications/config-commands.ts +20 -0
- package/src/notifications/config.ts +27 -0
- package/src/notifications/index.ts +28 -5
- package/src/notifications/lifecycle-commands.ts +37 -16
- package/src/notifications/lifecycle-control-runtime.ts +8 -3
- package/src/notifications/reply-sent-store.ts +134 -0
- package/src/notifications/rich-draft.ts +107 -0
- package/src/notifications/rich-render.ts +142 -0
- package/src/notifications/telegram-daemon.ts +346 -89
- package/src/notifications/telegram-reference.ts +17 -0
- package/src/notifications/threaded-render.ts +28 -1
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/sdk.ts +36 -8
- package/src/session/agent-session.ts +6 -4
- package/src/skc-runtime/tmux-common.ts +1 -0
- package/src/skc-runtime/ultragoal-runtime.ts +10 -1
- package/src/slash-commands/builtin-registry.ts +78 -1
- package/src/utils/pasted-image-path.ts +170 -0
|
@@ -130,6 +130,14 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
130
130
|
readonly type: "string";
|
|
131
131
|
readonly default: undefined;
|
|
132
132
|
};
|
|
133
|
+
readonly "notifications.telegram.rich.enabled": {
|
|
134
|
+
readonly type: "boolean";
|
|
135
|
+
readonly default: true;
|
|
136
|
+
};
|
|
137
|
+
readonly "notifications.telegram.richDraft.enabled": {
|
|
138
|
+
readonly type: "boolean";
|
|
139
|
+
readonly default: false;
|
|
140
|
+
};
|
|
133
141
|
readonly "notifications.discord.botToken": {
|
|
134
142
|
readonly type: "string";
|
|
135
143
|
readonly default: undefined;
|
|
@@ -4102,6 +4110,12 @@ export interface NotificationsSettings {
|
|
|
4102
4110
|
telegram: {
|
|
4103
4111
|
botToken: string | undefined;
|
|
4104
4112
|
chatId: string | undefined;
|
|
4113
|
+
rich: {
|
|
4114
|
+
enabled: boolean;
|
|
4115
|
+
};
|
|
4116
|
+
richDraft: {
|
|
4117
|
+
enabled: boolean;
|
|
4118
|
+
};
|
|
4105
4119
|
};
|
|
4106
4120
|
discord: {
|
|
4107
4121
|
botToken: string | undefined;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ThinkingLevel as ThinkingLevelValue } from "@sayknow-cli/agent-core";
|
|
2
2
|
import { Container, SelectList } from "@sayknow-cli/tui";
|
|
3
3
|
/**
|
|
4
4
|
* Component that renders a thinking level selector with borders
|
|
5
5
|
*/
|
|
6
6
|
export declare class ThinkingSelectorComponent extends Container {
|
|
7
7
|
#private;
|
|
8
|
-
constructor(currentLevel:
|
|
8
|
+
constructor(currentLevel: ThinkingLevelValue | undefined, availableLevels: ThinkingLevelValue[], onSelect: (level: ThinkingLevelValue) => void, onCancel: () => void);
|
|
9
9
|
getSelectList(): SelectList;
|
|
10
10
|
}
|
|
@@ -26,6 +26,7 @@ export declare class SelectorController {
|
|
|
26
26
|
showProviderOnboarding(): void;
|
|
27
27
|
showCustomModelPresetWizard(snapshot: ModelProfileConfig): void;
|
|
28
28
|
showCustomProviderWizard(): void;
|
|
29
|
+
showEffortSelector(): void;
|
|
29
30
|
showSettingsSelector(): void;
|
|
30
31
|
showThemeSelector(): void;
|
|
31
32
|
showHistorySearch(): void;
|
|
@@ -96,6 +96,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
|
|
|
96
96
|
onInputCallback?: (input: SubmittedUserInput) => void;
|
|
97
97
|
optimisticUserMessageSignature: string | undefined;
|
|
98
98
|
locallySubmittedUserSignatures: Set<string>;
|
|
99
|
+
optimisticInjectedSignatures: Map<string, number>;
|
|
99
100
|
lastSigintTime: number;
|
|
100
101
|
lastEscapeTime: number;
|
|
101
102
|
lastComposerClearEscapeTime: number;
|
|
@@ -218,6 +219,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
|
|
|
218
219
|
showModelSelector(options?: {
|
|
219
220
|
temporaryOnly?: boolean;
|
|
220
221
|
}): void;
|
|
222
|
+
showEffortSelector(): void;
|
|
221
223
|
showProviderOnboarding(): void;
|
|
222
224
|
showPluginSelector(mode?: "install" | "uninstall"): void;
|
|
223
225
|
showUserMessageSelector(): void;
|
|
@@ -64,9 +64,7 @@ export declare function isFastLaneRpcCommand(type: RpcCommand["type"]): boolean;
|
|
|
64
64
|
export declare function createRpcCommandScheduler(run: (command: RpcCommand) => Promise<void>, track: (task: Promise<void>) => void): {
|
|
65
65
|
dispatch: (command: RpcCommand) => void;
|
|
66
66
|
};
|
|
67
|
-
export
|
|
68
|
-
constructor(socketPath: string);
|
|
69
|
-
}
|
|
67
|
+
export { RpcListenRefusedError } from "./rpc-socket-security";
|
|
70
68
|
/**
|
|
71
69
|
* Probe whether a unix-domain socket path has a live server accepting
|
|
72
70
|
* connections. Returns `true` when a connection succeeds (a previous owner is
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export declare class RpcSocketSecurityError extends Error {
|
|
2
2
|
constructor(message: string);
|
|
3
3
|
}
|
|
4
|
+
export declare class RpcListenRefusedError extends Error {
|
|
5
|
+
constructor(socketPath: string);
|
|
6
|
+
}
|
|
4
7
|
export declare function prepareRpcSocketPath(socketPath: string): Promise<void>;
|
|
5
8
|
export declare function assertSafeClientSocket(socketPath: string): Promise<void>;
|
|
6
9
|
export declare function verifyRpcSocketAfterListen(socketPath: string): Promise<void>;
|
|
@@ -100,6 +100,7 @@ export interface InteractiveModeContext {
|
|
|
100
100
|
onInputCallback?: (input: SubmittedUserInput) => void;
|
|
101
101
|
optimisticUserMessageSignature: string | undefined;
|
|
102
102
|
locallySubmittedUserSignatures: Set<string>;
|
|
103
|
+
optimisticInjectedSignatures: Map<string, number>;
|
|
103
104
|
lastSigintTime: number;
|
|
104
105
|
lastEscapeTime: number;
|
|
105
106
|
lastComposerClearEscapeTime: number;
|
|
@@ -223,6 +224,7 @@ export interface InteractiveModeContext {
|
|
|
223
224
|
showModelSelector(options?: {
|
|
224
225
|
temporaryOnly?: boolean;
|
|
225
226
|
}): void;
|
|
227
|
+
showEffortSelector(): void;
|
|
226
228
|
showProviderOnboarding(): void;
|
|
227
229
|
showPluginSelector(mode?: "install" | "uninstall"): void;
|
|
228
230
|
showUserMessageSelector(): void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ImageContent, TextContent } from "@sayknow-cli/ai";
|
|
2
|
+
import type { InteractiveModeContext } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* Normalize the content passed to an extension `sendUserMessage` call into a
|
|
5
|
+
* plain text string plus its image attachments. Mirrors the normalization in
|
|
6
|
+
* `AgentSession.sendUserMessage` (text parts joined with "\n") so the resulting
|
|
7
|
+
* text matches the eventual user `message_start` payload.
|
|
8
|
+
*/
|
|
9
|
+
export declare function normalizeInjectedUserContent(content: string | (TextContent | ImageContent)[]): {
|
|
10
|
+
text: string;
|
|
11
|
+
images: ImageContent[];
|
|
12
|
+
imageCount: number;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Record a remotely/programmatically injected user message (e.g. Telegram
|
|
16
|
+
* inbound routed through the extension API) into the interactive TUI, so it is
|
|
17
|
+
* captured in prompt history and shown immediately instead of only appearing
|
|
18
|
+
* once the eventual `message_start` event lands.
|
|
19
|
+
*
|
|
20
|
+
* Local TUI submissions never reach this path (they go through
|
|
21
|
+
* `session.prompt(...)` / `startPendingSubmission`), so this cannot double-add
|
|
22
|
+
* local prompt history.
|
|
23
|
+
*
|
|
24
|
+
* - Always adds the injected text to editor prompt history.
|
|
25
|
+
* - Idle injections optimistically render the user message and record a pending
|
|
26
|
+
* injected optimistic signature (a counting Map, so multiple idle injections
|
|
27
|
+
* before the first `message_start` do not clobber each other); the later user
|
|
28
|
+
* `message_start` consumes one count and skips both the duplicate chat add and
|
|
29
|
+
* the defensive editor clear (so a locally typed draft is preserved).
|
|
30
|
+
* - Busy/queued injections refresh the pending-message display, which the
|
|
31
|
+
* caller has already populated by invoking `session.sendUserMessage(...)`
|
|
32
|
+
* before this helper.
|
|
33
|
+
*
|
|
34
|
+
* This helper never clears the editor text.
|
|
35
|
+
*/
|
|
36
|
+
export declare function applyInjectedUserSubmission(ctx: InteractiveModeContext, input: {
|
|
37
|
+
content: string | (TextContent | ImageContent)[];
|
|
38
|
+
queued: boolean;
|
|
39
|
+
}): void;
|
|
40
|
+
/**
|
|
41
|
+
* Record one pending optimistic render for an injected user message.
|
|
42
|
+
*
|
|
43
|
+
* Injected sends are fire-and-forget and multiple idle injections can be
|
|
44
|
+
* rendered before the first `message_start` arrives, so a counting Map (not a
|
|
45
|
+
* single slot) tracks how many optimistic renders are outstanding per signature.
|
|
46
|
+
*/
|
|
47
|
+
export declare function incrementInjectedOptimisticSignature(ctx: InteractiveModeContext, signature: string): void;
|
|
48
|
+
/**
|
|
49
|
+
* Consume one pending injected optimistic render for `signature`. Decrements the
|
|
50
|
+
* count (deleting the key at zero) and returns whether one was outstanding.
|
|
51
|
+
*/
|
|
52
|
+
export declare function consumeInjectedOptimisticSignature(ctx: InteractiveModeContext, signature: string): boolean;
|
|
@@ -24,3 +24,12 @@ export interface ConfigCommandChange {
|
|
|
24
24
|
* can fall through to treating it as a free-text injection).
|
|
25
25
|
*/
|
|
26
26
|
export declare function parseInThreadConfigCommand(text: string): ConfigCommandChange | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Parse a `/rich on|off` toggle. Returns `true`/`false` for a recognised
|
|
29
|
+
* on/off argument, or `undefined` otherwise (not a `/rich` command, or `/rich`
|
|
30
|
+
* with a missing/invalid argument). This is intentionally SEPARATE from
|
|
31
|
+
* `parseInThreadConfigCommand`: `/verbose`/`/redact` are producer/session config
|
|
32
|
+
* forwarded over the WS, whereas rich is Telegram-daemon delivery policy handled
|
|
33
|
+
* daemon-locally, so it never becomes a `config_command` frame or a user turn.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseRichToggleCommand(text: string): boolean | undefined;
|
|
@@ -14,6 +14,12 @@ export interface NotificationConfig {
|
|
|
14
14
|
redact: boolean;
|
|
15
15
|
verbosity: "lean" | "verbose";
|
|
16
16
|
idleTimeoutMs: number;
|
|
17
|
+
rich: {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
};
|
|
20
|
+
richDraft: {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
};
|
|
17
23
|
}
|
|
18
24
|
/** Read typed config from Settings. */
|
|
19
25
|
export declare function getNotificationConfig(settings: Settings): NotificationConfig;
|
|
@@ -25,6 +31,16 @@ export declare function isTelegramConfigured(cfg: NotificationConfig): cfg is No
|
|
|
25
31
|
};
|
|
26
32
|
/** Is global config sufficient for auto-on (enabled + at least one configured adapter)? */
|
|
27
33
|
export declare function isGloballyConfigured(cfg: NotificationConfig): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Per-run opt-out for completion notifications, honored before settings lookups.
|
|
36
|
+
*
|
|
37
|
+
* `SKC_NOTIFY=off` (also `0` / `false`, case-insensitive) suppresses the
|
|
38
|
+
* completion notification surface for this process only. `config.yml` is
|
|
39
|
+
* untouched and child processes inherit the env var, which lets non-interactive
|
|
40
|
+
* fleet runs (`skc -p --no-session`) stay silent even when a user-level/global
|
|
41
|
+
* completion notification configuration is enabled.
|
|
42
|
+
*/
|
|
43
|
+
export declare function completionNotifyDisabledByEnv(env: NodeJS.ProcessEnv): boolean;
|
|
28
44
|
/** Resolve whether the notifications extension should be registered at SDK startup. */
|
|
29
45
|
export declare function shouldRegisterNotificationsExtension(input: {
|
|
30
46
|
env: NodeJS.ProcessEnv;
|
|
@@ -61,6 +61,8 @@ export interface SessionCreateFrame {
|
|
|
61
61
|
target: SessionCreateTarget;
|
|
62
62
|
/** Reference to the daemon-written, once-consumed startup-prompt file. */
|
|
63
63
|
startupPromptRef?: string;
|
|
64
|
+
/** Model profile preset to activate for the spawned session (--mpreset). */
|
|
65
|
+
modelPreset?: string;
|
|
64
66
|
}
|
|
65
67
|
/** Close (hard-kill, history preserved) a session. */
|
|
66
68
|
export interface SessionCloseFrame {
|
|
@@ -35,7 +35,8 @@ export declare function createRateLimiter(maxPerWindow: number, windowMs: number
|
|
|
35
35
|
* The launched session id is carried via `SKC_SESSION_ID` in the child env (see
|
|
36
36
|
* {@link daemonSpawnCreate}); the root `skc` launcher has no `--session-id`
|
|
37
37
|
* flag, so it must never appear in argv. Only flags the launch parser actually
|
|
38
|
-
* supports are emitted (`--worktree <branch>` for worktree targets
|
|
38
|
+
* supports are emitted (`--worktree <branch>` for worktree targets,
|
|
39
|
+
* `--mpreset <profile>` for model presets). */
|
|
39
40
|
export declare function buildCreateArgv(frame: SessionCreateFrame, _ids: {
|
|
40
41
|
intendedSessionId: string;
|
|
41
42
|
startupPromptRef?: string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index of rich messages the daemon sent, mapping `${chat_id}:${message_id}` to
|
|
3
|
+
* the original markdown. Telegram does not echo a `sendRichMessage` message's
|
|
4
|
+
* text in a reply's `reply_to_message`, so a user replying to a rich final
|
|
5
|
+
* answer would otherwise strand the agent without the quoted context. The daemon
|
|
6
|
+
* records the markdown on every promoted send and looks it up on inbound replies
|
|
7
|
+
* to restore that context.
|
|
8
|
+
*
|
|
9
|
+
* Fail-closed and off-path-neutral: nothing is recorded unless a send was
|
|
10
|
+
* actually promoted to rich, `record` is a no-op on any error (the in-memory map
|
|
11
|
+
* is only mutated after the atomic persist succeeds), and `lookup` returns
|
|
12
|
+
* undefined on any miss or error, so a bad index write/read never kills the
|
|
13
|
+
* daemon.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
/**
|
|
17
|
+
* Minimal async filesystem surface the store needs. Structurally a subset of the
|
|
18
|
+
* daemon's `TelegramDaemonFs`, so the daemon can pass its own `fs` straight
|
|
19
|
+
* through and tests can inject a fake.
|
|
20
|
+
*/
|
|
21
|
+
export interface ReplySentStoreFs {
|
|
22
|
+
mkdir(path: string, opts?: fs.MakeDirectoryOptions): Promise<unknown>;
|
|
23
|
+
readFile(path: string, encoding: BufferEncoding): Promise<string>;
|
|
24
|
+
writeFile(path: string, data: string, opts?: fs.WriteFileOptions): Promise<void>;
|
|
25
|
+
rename(oldPath: string, newPath: string): Promise<void>;
|
|
26
|
+
chmod(path: string, mode: number): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export declare class ReplySentStore {
|
|
29
|
+
#private;
|
|
30
|
+
constructor(input: {
|
|
31
|
+
agentDir: string;
|
|
32
|
+
fs?: ReplySentStoreFs;
|
|
33
|
+
now?: () => number;
|
|
34
|
+
});
|
|
35
|
+
/** Restore the persisted index into memory. No-op on a missing or corrupt file. */
|
|
36
|
+
load(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Record the original markdown of a rich message the daemon just sent. The
|
|
39
|
+
* text is capped at {@link MAX_TEXT_LENGTH}; the index is capped at
|
|
40
|
+
* {@link MAX_ENTRIES} (oldest-by-timestamp evicted). No-op on any failure: the
|
|
41
|
+
* in-memory map is only replaced after the atomic persist succeeds.
|
|
42
|
+
*/
|
|
43
|
+
record(input: {
|
|
44
|
+
chatId: string | number;
|
|
45
|
+
messageId: number;
|
|
46
|
+
text: string;
|
|
47
|
+
}): Promise<void>;
|
|
48
|
+
/** Look up the original markdown for a message the daemon sent. undefined on miss or failure. */
|
|
49
|
+
lookup(input: {
|
|
50
|
+
chatId: string | number;
|
|
51
|
+
messageId: number;
|
|
52
|
+
}): string | undefined;
|
|
53
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in rich-draft streaming for a turn's in-progress preview.
|
|
3
|
+
*
|
|
4
|
+
* When the operator opts in (`richDraft.enabled`) the daemon streams LIVE turn
|
|
5
|
+
* markdown to the Bot API `sendRichMessageDraft` method as a debounced preview.
|
|
6
|
+
* Every failure is a harmless no-op (warn once, keep the daemon alive), and when
|
|
7
|
+
* off no draft call is ever made — so the off-state request bodies stay
|
|
8
|
+
* byte-identical.
|
|
9
|
+
*/
|
|
10
|
+
import type { BotApi } from "./telegram-daemon";
|
|
11
|
+
import type { ThreadedSend } from "./threaded-render";
|
|
12
|
+
/** Minimum gap between two draft sends for one session (debounce floor). */
|
|
13
|
+
export declare const DRAFT_DEBOUNCE_MS = 1500;
|
|
14
|
+
/**
|
|
15
|
+
* Wrap raw markdown + a monotonic draft id in the `sendRichMessageDraft` request
|
|
16
|
+
* payload shape: mirrors `buildRichMessage`'s proven `rich_message.markdown`
|
|
17
|
+
* content wrapper, with a top-level `draft_id` selecting the draft revision to
|
|
18
|
+
* update (a routing param, like `message_thread_id`).
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildRichDraft(draftId: number, raw: string): {
|
|
21
|
+
draft_id: number;
|
|
22
|
+
rich_message: {
|
|
23
|
+
markdown: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Whether a granted send should stream a rich draft. Fail-closed: every clause
|
|
28
|
+
* must hold, otherwise no draft is sent. Mirrors `shouldPromoteRich` but targets
|
|
29
|
+
* the LIVE lane and the live-only `richDraftMarkdown` marker (set by
|
|
30
|
+
* `renderThreadedFrame` for non-finalized turn frames).
|
|
31
|
+
*/
|
|
32
|
+
export declare function shouldStreamDraft(input: {
|
|
33
|
+
enabled?: boolean;
|
|
34
|
+
send: ThreadedSend;
|
|
35
|
+
}): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Per-session debounce + monotonic draft-id state for draft streaming. Skips a
|
|
38
|
+
* draft when less than `debounceMs` has elapsed since the session's last SENT
|
|
39
|
+
* draft (the rate-limit pool already coalesces live frames to the latest, so a
|
|
40
|
+
* skipped frame is naturally superseded by the next one). `reset` clears a
|
|
41
|
+
* session's window when its turn finalizes so the next turn starts fresh.
|
|
42
|
+
*/
|
|
43
|
+
export declare class DraftStreamState {
|
|
44
|
+
#private;
|
|
45
|
+
constructor(debounceMs?: number);
|
|
46
|
+
/**
|
|
47
|
+
* If enough time has elapsed since the session's last draft, record `now` as
|
|
48
|
+
* the new last-sent time and return the next monotonic draft id; otherwise
|
|
49
|
+
* return `undefined` (debounced — the caller skips this frame).
|
|
50
|
+
*/
|
|
51
|
+
tryClaim(sessionId: string, now: number): number | undefined;
|
|
52
|
+
/** Clear a session's debounce window (called when its turn finalizes). The
|
|
53
|
+
* global draft id keeps incrementing so ids are never reused across turns. */
|
|
54
|
+
reset(sessionId: string): void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Best-effort delivery of a rich draft. Never throws and has no HTML fallback: a
|
|
58
|
+
* draft is a purely additive preview, so on any failure (a thrown transport
|
|
59
|
+
* error or an `{ ok: false }` JSON response — the transport returns `res.json()`
|
|
60
|
+
* for JSON methods, so `ok:false` does not throw) it warns exactly once and
|
|
61
|
+
* returns; the unchanged live HTML send still carries the content.
|
|
62
|
+
*/
|
|
63
|
+
export declare function deliverDraft(botApi: BotApi, base: {
|
|
64
|
+
chat_id: string | number;
|
|
65
|
+
message_thread_id?: number;
|
|
66
|
+
}, draftId: number, raw: string, log?: {
|
|
67
|
+
warn(msg: string): void;
|
|
68
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rich-message promotion for stable non-editable Telegram text sends.
|
|
3
|
+
*
|
|
4
|
+
* When enabled, the daemon promotes eligible finalized `sendMessage` payloads
|
|
5
|
+
* carrying raw markdown to the Bot API `sendRichMessage` method. On any miss or
|
|
6
|
+
* failure the daemon keeps the unchanged HTML `sendMessage` path, so the
|
|
7
|
+
* off-state request bodies are byte-identical.
|
|
8
|
+
*/
|
|
9
|
+
import type { BotApi } from "./telegram-daemon";
|
|
10
|
+
import type { ThreadedSend } from "./threaded-render";
|
|
11
|
+
/**
|
|
12
|
+
* Telegram's hard per-message character ceiling (4096). Surfaced here purely as
|
|
13
|
+
* documentation and a marker for a future native rich-message splitter — it is
|
|
14
|
+
* intentionally NON-BEHAVIORAL and MUST stay that way: nothing in the rich path
|
|
15
|
+
* branches on this value.
|
|
16
|
+
*
|
|
17
|
+
* Overflow is already safe without it. The production final-answer text is capped
|
|
18
|
+
* at 3500 chars upstream (`summaryFromMessage(..., 3500)`), so a promoted
|
|
19
|
+
* `sendRichMessage` never approaches this ceiling; and if the Bot API ever rejects
|
|
20
|
+
* an oversized rich payload it returns `{ ok: false }`, which
|
|
21
|
+
* `deliverRichWithFallback` (below) turns into the chunked HTML `splitTelegramHtml`
|
|
22
|
+
* fallback (each chunk ≤ TELEGRAM_MESSAGE_LIMIT). This constant only marks where a
|
|
23
|
+
* future rich splitter would read its ceiling; wiring it into a branch would change
|
|
24
|
+
* byte-for-byte behavior and is out of scope.
|
|
25
|
+
*/
|
|
26
|
+
export declare const RICH_MESSAGE_LIMIT = 4096;
|
|
27
|
+
/** Wrap raw markdown in the `sendRichMessage` request payload shape. */
|
|
28
|
+
export declare function buildRichMessage(raw: string, extras?: {
|
|
29
|
+
reply_markup?: unknown;
|
|
30
|
+
}): {
|
|
31
|
+
rich_message: {
|
|
32
|
+
markdown: string;
|
|
33
|
+
};
|
|
34
|
+
reply_markup?: unknown;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Whether a granted send should be promoted to `sendRichMessage`. Fail-closed
|
|
38
|
+
* and class-aware: every clause must hold, otherwise the daemon keeps the HTML path.
|
|
39
|
+
*/
|
|
40
|
+
export declare function shouldPromoteRich(input: {
|
|
41
|
+
enabled?: boolean;
|
|
42
|
+
send: ThreadedSend;
|
|
43
|
+
}): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Deliver the promoted rich message, falling back to `fallbackDeliver` (the
|
|
46
|
+
* unchanged HTML `sendMessage` loop) on any failure. A failure is either a
|
|
47
|
+
* thrown transport error or a `{ ok: false }` JSON response (the transport
|
|
48
|
+
* returns `res.json()` for JSON methods, so `ok:false` does not throw). On
|
|
49
|
+
* failure exactly one diagnostic is logged before the fallback runs; on success
|
|
50
|
+
* the fallback never runs.
|
|
51
|
+
*
|
|
52
|
+
* Returns the sent message's `message_id` on success (when the response carries
|
|
53
|
+
* one), otherwise `undefined` — including every failure/fallback path and a
|
|
54
|
+
* success whose response omits `result.message_id`. Callers that ignore the
|
|
55
|
+
* return value are unaffected.
|
|
56
|
+
*/
|
|
57
|
+
export declare function deliverRichWithFallback(botApi: BotApi, base: {
|
|
58
|
+
chat_id: string | number;
|
|
59
|
+
message_thread_id?: number;
|
|
60
|
+
}, send: ThreadedSend, fallbackDeliver: () => Promise<void>, log?: {
|
|
61
|
+
warn(msg: string): void;
|
|
62
|
+
}): Promise<number | undefined>;
|
|
63
|
+
/**
|
|
64
|
+
* Deliver an action-needed (ask/idle) message via `sendRichMessage`, falling
|
|
65
|
+
* back to the unchanged HTML chunk loop on any failure. Mirrors
|
|
66
|
+
* {@link deliverRichWithFallback} but takes an explicit markdown body plus an
|
|
67
|
+
* optional top-level `reply_markup` (probe-confirmed: `sendRichMessage` accepts
|
|
68
|
+
* `reply_markup` alongside `rich_message`), and surfaces a structured outcome so
|
|
69
|
+
* the daemon can route inbound replies to the resulting message id.
|
|
70
|
+
*
|
|
71
|
+
* On rich success: returns `{ messageId, usedRich: true, usedFallback: false }`
|
|
72
|
+
* where `messageId` is `res.result.message_id` when present. On a `{ ok:false }`
|
|
73
|
+
* response or a thrown transport error: warns exactly once, runs `htmlFallback`,
|
|
74
|
+
* and returns `{ messageId, usedRich: false, usedFallback: true }` where
|
|
75
|
+
* `messageId` is the fallback's return value (the last HTML chunk's id).
|
|
76
|
+
*/
|
|
77
|
+
export declare function deliverRichActionWithFallback(botApi: BotApi, base: {
|
|
78
|
+
chat_id: string | number;
|
|
79
|
+
message_thread_id?: number;
|
|
80
|
+
}, opts: {
|
|
81
|
+
markdown: string;
|
|
82
|
+
replyMarkup?: unknown;
|
|
83
|
+
requireMessageId?: boolean;
|
|
84
|
+
}, htmlFallback: () => Promise<number | undefined>, log?: {
|
|
85
|
+
warn(msg: string): void;
|
|
86
|
+
}): Promise<{
|
|
87
|
+
messageId?: number;
|
|
88
|
+
usedRich: boolean;
|
|
89
|
+
usedFallback: boolean;
|
|
90
|
+
}>;
|
|
@@ -231,6 +231,14 @@ export interface TelegramDaemonOptions {
|
|
|
231
231
|
* default applies (e.g. lifecycle control disabled), no control server starts.
|
|
232
232
|
*/
|
|
233
233
|
createLifecycleControlServer?: LifecycleControlServerFactory | null;
|
|
234
|
+
/** Rich text promotion (enabled by default; see rich-render.ts). */
|
|
235
|
+
rich?: {
|
|
236
|
+
enabled: boolean;
|
|
237
|
+
};
|
|
238
|
+
/** Opt-in rich-draft streaming of live turn previews (off by default; see rich-draft.ts). */
|
|
239
|
+
richDraft?: {
|
|
240
|
+
enabled: boolean;
|
|
241
|
+
};
|
|
234
242
|
}
|
|
235
243
|
interface SessionSocket {
|
|
236
244
|
sessionId: string;
|
|
@@ -268,6 +276,10 @@ export declare class TelegramNotificationDaemon {
|
|
|
268
276
|
private readonly pool;
|
|
269
277
|
private readonly poller;
|
|
270
278
|
private readonly dispatchState;
|
|
279
|
+
/** Original markdown of rich messages we sent (chat+message_id), for restoring reply context on inbound replies. */
|
|
280
|
+
private readonly replyStore;
|
|
281
|
+
/** Per-session debounce + monotonic draft-id state for opt-in draft streaming. */
|
|
282
|
+
private readonly draftStream;
|
|
271
283
|
/** Identity-bearing sessions by repo/branch surface, used to avoid transient duplicate topics. */
|
|
272
284
|
private readonly topicOwnerByIdentity;
|
|
273
285
|
/** Non-identity frames held until identity creates the correct thread. */
|
|
@@ -376,7 +388,17 @@ export declare class TelegramNotificationDaemon {
|
|
|
376
388
|
private ensureAttachmentDir;
|
|
377
389
|
private cleanupAllAttachmentDirs;
|
|
378
390
|
private resolveInboundAttachment;
|
|
391
|
+
/**
|
|
392
|
+
* Serialize all pool flushes. Every caller (`submitThreadedFrame`, the flat
|
|
393
|
+
* fallback, the drain timer's `void this.flushPool()`, topic teardown) goes
|
|
394
|
+
* through one promise chain, so two flushes never interleave — a live send can
|
|
395
|
+
* never be in-flight while a finalized flush reads `liveMessages` and decides
|
|
396
|
+
* to post a fresh (duplicate) final. Errors are swallowed so one failed flush
|
|
397
|
+
* never poisons the queue (each flush is already best-effort internally).
|
|
398
|
+
*/
|
|
399
|
+
private flushChain;
|
|
379
400
|
private flushPool;
|
|
401
|
+
private flushPoolInner;
|
|
380
402
|
/**
|
|
381
403
|
* Track the Telegram message id backing a streamed `(sessionId, coalesceKey)`
|
|
382
404
|
* so later live/finalized frames edit it in place. Evicts this session's stale
|
|
@@ -59,6 +59,13 @@ export declare function buildActionMessage(action: {
|
|
|
59
59
|
options?: string[];
|
|
60
60
|
summary?: string;
|
|
61
61
|
}): RenderedMessage;
|
|
62
|
+
/** Render an `action_needed` body as raw markdown (rich-message source; the HTML fallback stays on buildActionMessage). */
|
|
63
|
+
export declare function buildActionMarkdown(action: {
|
|
64
|
+
kind: "ask" | "idle";
|
|
65
|
+
question?: string;
|
|
66
|
+
options?: string[];
|
|
67
|
+
summary?: string;
|
|
68
|
+
}): string;
|
|
62
69
|
/** Send Telegram HTML text chunks sequentially so long messages preserve order. */
|
|
63
70
|
export declare function sendTelegramHtmlChunks(send: TelegramSend, chatId: string, text: string, inlineKeyboard?: InlineButton[][]): Promise<void>;
|
|
64
71
|
/** A protocol `reply` frame the client should send to the server. */
|
|
@@ -34,6 +34,13 @@ export interface ThreadedSend {
|
|
|
34
34
|
* message. Set for streamed turn frames so live + finalized share one message.
|
|
35
35
|
*/
|
|
36
36
|
editable?: boolean;
|
|
37
|
+
/** Rich message class metadata. Only finalized final-answer sends are ever
|
|
38
|
+
* rich-promoted; the daemon gate (`shouldPromoteRich`) requires exactly this. */
|
|
39
|
+
richClass?: "final";
|
|
40
|
+
/** Rich final-answer markdown (raw). Delivery marker derived ONLY from a frame's `finalAnswer` bit; never inferred from `phase`. */
|
|
41
|
+
richMarkdown?: string;
|
|
42
|
+
/** Live-turn raw markdown for opt-in draft streaming (set ONLY on non-finalized turn frames; never triggers rich-final promotion, which requires `lane === "finalized"`). */
|
|
43
|
+
richDraftMarkdown?: string;
|
|
37
44
|
}
|
|
38
45
|
interface ThreadedFrame {
|
|
39
46
|
type?: unknown;
|
|
@@ -50,6 +57,7 @@ interface ThreadedFrame {
|
|
|
50
57
|
diff?: unknown;
|
|
51
58
|
cwd?: unknown;
|
|
52
59
|
phase?: unknown;
|
|
60
|
+
finalAnswer?: boolean;
|
|
53
61
|
text?: unknown;
|
|
54
62
|
messageRef?: unknown;
|
|
55
63
|
source?: unknown;
|
|
@@ -2,6 +2,7 @@ import type { ResolvedTmuxBinary } from "./psmux-detect";
|
|
|
2
2
|
export declare const SKC_DEFAULT_TMUX_SESSION = "sayknow_cli";
|
|
3
3
|
export declare const SKC_TMUX_SESSION_PREFIX = "sayknow_cli_";
|
|
4
4
|
export declare const SKC_TMUX_COMMAND_ENV = "SKC_TMUX_COMMAND";
|
|
5
|
+
export declare const SKC_TMUX_ACTIVE_SESSION_ENV = "SKC_TMUX_ACTIVE_SESSION";
|
|
5
6
|
export declare const SKC_TMUX_PROFILE_ENV = "SKC_TMUX_PROFILE";
|
|
6
7
|
export declare const SKC_TMUX_MOUSE_ENV = "SKC_MOUSE";
|
|
7
8
|
export declare const SKC_TMUX_PROFILE_OPTION = "@skc-profile";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface DecodePastedPathOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Platform whose path semantics apply when decoding (shell unescaping,
|
|
4
|
+
* `file://` drive letters / UNC hosts). Defaults to `process.platform`;
|
|
5
|
+
* injectable so tests can pin the win32 contract from any host.
|
|
6
|
+
*/
|
|
7
|
+
platform?: NodeJS.Platform;
|
|
8
|
+
/** Home directory for `~/` expansion. Defaults to `os.homedir()`. */
|
|
9
|
+
homedir?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ResolvePastedImagePathOptions extends DecodePastedPathOptions {
|
|
12
|
+
/** Base directory for relative paths. Defaults to `process.cwd()`. */
|
|
13
|
+
cwd?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Decode pasted text into a filesystem path candidate. No filesystem access.
|
|
17
|
+
*
|
|
18
|
+
* Handles terminal drag-drop shell escaping (`\ `, `\(`, ...; skipped on
|
|
19
|
+
* win32 where `\` is the path separator), quoted paths, `file://` URIs
|
|
20
|
+
* (drive-letter, `file://localhost`, and UNC forms on win32), and `~/`
|
|
21
|
+
* expansion. Returns `undefined` when the text is empty, spans multiple
|
|
22
|
+
* lines, or is an invalid `file://` URI.
|
|
23
|
+
*/
|
|
24
|
+
export declare function decodePastedPathCandidate(text: string, options?: DecodePastedPathOptions): string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Returns the resolved path when the whole pasted text is a single path to an
|
|
27
|
+
* existing image file (verified by extension AND content signature),
|
|
28
|
+
* otherwise `undefined` (the paste is inserted as text).
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolvePastedImagePath(text: string, options?: ResolvePastedImagePathOptions): string | undefined;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@sayknow-cli/coding-agent",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.9",
|
|
5
5
|
"description": "Sayknow-CLI CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://sayknow-cli.com",
|
|
7
7
|
"author": "jaybeyond",
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
53
53
|
"@babel/parser": "^7.29.3",
|
|
54
54
|
"@mozilla/readability": "^0.6.0",
|
|
55
|
-
"@sayknow-cli/stats": "0.3.
|
|
56
|
-
"@sayknow-cli/agent-core": "0.3.
|
|
57
|
-
"@sayknow-cli/ai": "0.3.
|
|
58
|
-
"@sayknow-cli/natives": "0.3.
|
|
59
|
-
"@sayknow-cli/tui": "0.3.
|
|
60
|
-
"@sayknow-cli/utils": "0.3.
|
|
55
|
+
"@sayknow-cli/stats": "0.3.9",
|
|
56
|
+
"@sayknow-cli/agent-core": "0.3.9",
|
|
57
|
+
"@sayknow-cli/ai": "0.3.9",
|
|
58
|
+
"@sayknow-cli/natives": "0.3.9",
|
|
59
|
+
"@sayknow-cli/tui": "0.3.9",
|
|
60
|
+
"@sayknow-cli/utils": "0.3.9",
|
|
61
61
|
"@puppeteer/browsers": "^2.13.0",
|
|
62
62
|
"@types/turndown": "5.0.6",
|
|
63
63
|
"@xterm/headless": "^6.0.0",
|
package/src/cli/notify-cli.ts
CHANGED
|
@@ -249,18 +249,18 @@ export async function promptForToken(
|
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
const THREADED_ENABLED_SUCCESS =
|
|
252
|
-
"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";
|
|
252
|
+
"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 inline ask buttons only and a one-time Threaded Mode nudge.\n";
|
|
253
253
|
|
|
254
254
|
const THREADED_MISSING_WARNING =
|
|
255
|
-
"Warning: Telegram getMe did not include has_topics_enabled, so SKC cannot verify private-chat Threaded Mode capability for this bot. Setup will continue;
|
|
255
|
+
"Warning: Telegram getMe did not include has_topics_enabled, so SKC cannot verify private-chat Threaded Mode capability for this bot. Setup will continue; flat private-chat fallback supports outbound notifications and inline ask buttons only. Free-text replies and session commands require Threaded Mode/topic routing.\n";
|
|
256
256
|
|
|
257
257
|
const THREADED_NONINTERACTIVE_WARNING =
|
|
258
|
-
"Warning: Telegram Threaded Mode capability is OFF for this bot. Setup will be saved because this run is non-interactive
|
|
258
|
+
"Warning: Telegram Threaded Mode capability is OFF for this bot. Setup will be saved because this run is non-interactive. Flat private-chat fallback supports outbound notifications and inline ask buttons only; free-text replies and session commands require enabling Threaded Mode in @BotFather > Bot Settings > Threads Settings.\n";
|
|
259
259
|
|
|
260
260
|
const THREADED_DISABLED_GUIDANCE =
|
|
261
261
|
"Telegram Threaded Mode is OFF for this bot. SKC needs Telegram private-chat topics so each session can use its own thread.\n" +
|
|
262
|
-
"SKC cannot enable this through the Bot API. Open @BotFather
|
|
263
|
-
"
|
|
262
|
+
"SKC cannot enable this through the Bot API. Open @BotFather > Bot Settings > Threads Settings for this bot, enable Threaded Mode / forum topics for private chats, then return here.\n" +
|
|
263
|
+
"Without Threaded Mode, flat private-chat fallback supports outbound notifications and inline ask buttons only; free-text replies and session commands require topic routing.\n";
|
|
264
264
|
|
|
265
265
|
const THREADED_DISABLED_PROMPT =
|
|
266
266
|
"Press Enter after enabling Threaded Mode, or type skip to finish setup with a warning: ";
|
|
@@ -270,7 +270,7 @@ const THREADED_STILL_OFF = "Telegram still reports Threaded Mode OFF for this bo
|
|
|
270
270
|
const THREADED_RETRY_PROMPT = "Press Enter to check again, or type skip to finish setup with a warning: ";
|
|
271
271
|
|
|
272
272
|
const THREADED_SKIP_WARNING =
|
|
273
|
-
"Warning: continuing without verified Telegram Threaded Mode capability. Setup will be saved
|
|
273
|
+
"Warning: continuing without verified Telegram Threaded Mode capability. Setup will be saved. Flat private-chat fallback supports outbound notifications and inline ask buttons only; free-text replies and session commands require enabling Threaded Mode in BotFather.\n";
|
|
274
274
|
|
|
275
275
|
const THREADED_INVALID_INPUT = "Type Enter to retry or skip to continue with a warning.\n";
|
|
276
276
|
|
|
@@ -500,8 +500,10 @@ ${chalk.bold("Examples:")}
|
|
|
500
500
|
|
|
501
501
|
${chalk.bold("Threaded Mode:")}
|
|
502
502
|
SKC uses Telegram private-chat topics for per-session threads. Setup verifies the bot
|
|
503
|
-
capability via getMe.has_topics_enabled.
|
|
504
|
-
bots cannot toggle it through the Bot API. If Telegram refuses topic
|
|
505
|
-
SKC delivers flat to the paired private chat
|
|
503
|
+
capability via getMe.has_topics_enabled. Enable Threaded Mode in @BotFather > Bot Settings
|
|
504
|
+
> Threads Settings; bots cannot toggle it through the Bot API. If Telegram refuses topic
|
|
505
|
+
creation at runtime, SKC delivers flat to the paired private chat with outbound notifications
|
|
506
|
+
and inline ask buttons only, then nudges you to enable Threaded Mode for free-text replies
|
|
507
|
+
and session commands.
|
|
506
508
|
`);
|
|
507
509
|
}
|