@gajae-code/coding-agent 0.6.5 → 0.7.0

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.
Files changed (127) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/types/async/job-manager.d.ts +3 -1
  3. package/dist/types/cli/daemon-cli.d.ts +25 -0
  4. package/dist/types/cli/notify-cli.d.ts +23 -0
  5. package/dist/types/cli/setup-cli.d.ts +20 -1
  6. package/dist/types/commands/daemon.d.ts +41 -0
  7. package/dist/types/commands/notify.d.ts +41 -0
  8. package/dist/types/config/model-profile-activation.d.ts +12 -0
  9. package/dist/types/config/model-profiles.d.ts +2 -1
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +38 -0
  13. package/dist/types/coordinator/contract.d.ts +1 -1
  14. package/dist/types/daemon/builtin.d.ts +20 -0
  15. package/dist/types/daemon/control-types.d.ts +57 -0
  16. package/dist/types/daemon/runtime.d.ts +25 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  18. package/dist/types/gjc-runtime/state-writer.d.ts +2 -0
  19. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
  20. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +14 -0
  21. package/dist/types/modes/components/oauth-selector.d.ts +2 -0
  22. package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
  23. package/dist/types/modes/interactive-mode.d.ts +1 -1
  24. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  25. package/dist/types/modes/types.d.ts +7 -1
  26. package/dist/types/notifications/config-commands.d.ts +26 -0
  27. package/dist/types/notifications/config.d.ts +61 -0
  28. package/dist/types/notifications/helpers.d.ts +55 -0
  29. package/dist/types/notifications/html-format.d.ts +62 -0
  30. package/dist/types/notifications/index.d.ts +28 -0
  31. package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
  32. package/dist/types/notifications/telegram-cli.d.ts +19 -0
  33. package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
  34. package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
  35. package/dist/types/notifications/telegram-daemon.d.ts +276 -0
  36. package/dist/types/notifications/telegram-reference.d.ts +111 -0
  37. package/dist/types/notifications/threaded-inbound.d.ts +58 -0
  38. package/dist/types/notifications/threaded-render.d.ts +66 -0
  39. package/dist/types/notifications/topic-registry.d.ts +67 -0
  40. package/dist/types/rlm/index.d.ts +12 -0
  41. package/dist/types/session/agent-session.d.ts +39 -2
  42. package/dist/types/session/auth-storage.d.ts +1 -1
  43. package/dist/types/setup/credential-auto-import.d.ts +63 -0
  44. package/dist/types/setup/credential-import.d.ts +3 -0
  45. package/dist/types/setup/host-plugin-setup.d.ts +39 -0
  46. package/dist/types/tools/ask-answer-registry.d.ts +13 -0
  47. package/dist/types/tools/index.d.ts +18 -0
  48. package/dist/types/tools/subagent.d.ts +3 -0
  49. package/package.json +7 -7
  50. package/scripts/build-binary.ts +3 -0
  51. package/src/async/job-manager.ts +5 -1
  52. package/src/cli/daemon-cli.ts +122 -0
  53. package/src/cli/notify-cli.ts +274 -0
  54. package/src/cli/setup-cli.ts +173 -84
  55. package/src/cli.ts +2 -0
  56. package/src/commands/daemon.ts +47 -0
  57. package/src/commands/notify.ts +61 -0
  58. package/src/commands/setup.ts +11 -1
  59. package/src/config/model-profile-activation.ts +74 -5
  60. package/src/config/model-profiles.ts +7 -4
  61. package/src/config/model-registry.ts +6 -3
  62. package/src/config/models-config-schema.ts +1 -1
  63. package/src/config/settings-schema.ts +29 -0
  64. package/src/coordinator/contract.ts +3 -0
  65. package/src/coordinator-mcp/server.ts +270 -1
  66. package/src/daemon/builtin.ts +46 -0
  67. package/src/daemon/control-types.ts +65 -0
  68. package/src/daemon/runtime.ts +51 -0
  69. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  70. package/src/extensibility/extensions/runner.ts +4 -0
  71. package/src/extensibility/extensions/types.ts +8 -0
  72. package/src/gjc-runtime/deep-interview-recorder.ts +12 -4
  73. package/src/gjc-runtime/state-runtime.ts +18 -4
  74. package/src/gjc-runtime/state-writer.ts +8 -8
  75. package/src/gjc-runtime/ultragoal-guard.ts +57 -2
  76. package/src/gjc-runtime/ultragoal-runtime.ts +105 -19
  77. package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
  78. package/src/gjc-runtime/workflow-manifest.ts +11 -1
  79. package/src/goals/tools/goal-tool.ts +11 -2
  80. package/src/internal-urls/docs-index.generated.ts +6 -5
  81. package/src/main.ts +30 -0
  82. package/src/modes/acp/acp-event-mapper.ts +1 -0
  83. package/src/modes/components/hook-editor.ts +7 -2
  84. package/src/modes/components/oauth-selector.ts +19 -0
  85. package/src/modes/controllers/event-controller.ts +20 -0
  86. package/src/modes/controllers/selector-controller.ts +80 -17
  87. package/src/modes/interactive-mode.ts +6 -2
  88. package/src/modes/runtime-init.ts +1 -0
  89. package/src/modes/shared/agent-wire/event-contract.ts +1 -0
  90. package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
  91. package/src/modes/shared/agent-wire/event-observation.ts +16 -0
  92. package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
  93. package/src/modes/types.ts +7 -1
  94. package/src/modes/utils/ui-helpers.ts +23 -0
  95. package/src/notifications/config-commands.ts +50 -0
  96. package/src/notifications/config.ts +107 -0
  97. package/src/notifications/helpers.ts +135 -0
  98. package/src/notifications/html-format.ts +389 -0
  99. package/src/notifications/index.ts +663 -0
  100. package/src/notifications/rate-limit-pool.ts +179 -0
  101. package/src/notifications/telegram-cli.ts +194 -0
  102. package/src/notifications/telegram-daemon-cli.ts +74 -0
  103. package/src/notifications/telegram-daemon-control.ts +370 -0
  104. package/src/notifications/telegram-daemon.ts +1370 -0
  105. package/src/notifications/telegram-reference.ts +335 -0
  106. package/src/notifications/threaded-inbound.ts +80 -0
  107. package/src/notifications/threaded-render.ts +155 -0
  108. package/src/notifications/topic-registry.ts +133 -0
  109. package/src/rlm/index.ts +19 -0
  110. package/src/sdk.ts +16 -0
  111. package/src/session/agent-session.ts +113 -3
  112. package/src/session/auth-storage.ts +3 -0
  113. package/src/session/session-dump-format.ts +43 -2
  114. package/src/session/session-manager.ts +39 -5
  115. package/src/setup/credential-auto-import.ts +258 -0
  116. package/src/setup/credential-import.ts +17 -0
  117. package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
  118. package/src/setup/host-plugin-setup.ts +142 -0
  119. package/src/slash-commands/builtin-registry.ts +4 -1
  120. package/src/task/executor.ts +5 -1
  121. package/src/tools/ask-answer-registry.ts +25 -0
  122. package/src/tools/ask.ts +74 -4
  123. package/src/tools/image-gen.ts +5 -8
  124. package/src/tools/index.ts +19 -0
  125. package/src/tools/inspect-image.ts +16 -11
  126. package/src/tools/subagent-render.ts +7 -0
  127. package/src/tools/subagent.ts +38 -7
@@ -1,5 +1,5 @@
1
1
  import type { Component } from "@gajae-code/tui";
2
- import type { InteractiveModeContext } from "../../modes/types";
2
+ import type { InteractiveModeContext, OAuthSelectorOptions } from "../../modes/types";
3
3
  import type { JobsObserver } from "../jobs-observer";
4
4
  import type { SessionObserverRegistry } from "../session-observer-registry";
5
5
  export declare class SelectorController {
@@ -44,7 +44,7 @@ export declare class SelectorController {
44
44
  showSessionSelector(): Promise<void>;
45
45
  handleResumeSession(sessionPath: string): Promise<void>;
46
46
  handleSessionDeleteCommand(): Promise<void>;
47
- showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void>;
47
+ showOAuthSelector(mode: "login" | "logout", providerId?: string, options?: OAuthSelectorOptions): Promise<void>;
48
48
  showDebugSelector(): void;
49
49
  showSessionObserver(registry: SessionObserverRegistry): void;
50
50
  /**
@@ -221,7 +221,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
221
221
  showSessionSelector(): void;
222
222
  handleResumeSession(sessionPath: string): Promise<void>;
223
223
  handleSessionDeleteCommand(): Promise<void>;
224
- showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void>;
224
+ showOAuthSelector(mode: "login" | "logout", providerId?: string, options?: import("./types").OAuthSelectorOptions): Promise<void>;
225
225
  showHookConfirm(title: string, message: string): Promise<boolean>;
226
226
  handleCtrlC(): void;
227
227
  handleCtrlD(): void;
@@ -34,6 +34,14 @@ export interface WorkflowGateEmitter {
34
34
  isUnattended(): boolean;
35
35
  /** Open + emit a gate; resolves with the agent's answer (from workflow_gate_response). */
36
36
  emitGate(input: OpenGateInput): Promise<unknown>;
37
+ /**
38
+ * Optional bridge surface (present on {@link UnattendedSessionControlPlane}) that
39
+ * lets an in-process extension observe emitted gates and answer them — used by
40
+ * the notifications SDK to resolve a real ask gate from a remote reply.
41
+ */
42
+ onGateEmitted?(listener: (gate: RpcWorkflowGate) => void): () => void;
43
+ resolveGate?(response: RpcWorkflowGateResponse): Promise<RpcWorkflowGateResolution>;
44
+ listPendingGates?(): RpcWorkflowGate[];
37
45
  }
38
46
  export interface UnattendedSessionOptions {
39
47
  runId: string;
@@ -61,6 +69,8 @@ export declare class UnattendedSessionControlPlane implements RpcUnattendedContr
61
69
  private readonly opts;
62
70
  constructor(opts: UnattendedSessionOptions);
63
71
  isUnattended(): boolean;
72
+ /** Observe every emitted gate (e.g. so an extension can map an ask to its gate_id). */
73
+ onGateEmitted(listener: (gate: RpcWorkflowGate) => void): () => void;
64
74
  get controller(): UnattendedRunController | undefined;
65
75
  negotiate(declaration: RpcUnattendedDeclaration): RpcUnattendedAccepted;
66
76
  preflightCommand(command: RpcCommand): void;
@@ -12,6 +12,7 @@ import type { MCPManager } from "../runtime-mcp";
12
12
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
13
13
  import type { HistoryStorage } from "../session/history-storage";
14
14
  import type { SessionContext, SessionManager } from "../session/session-manager";
15
+ import type { CredentialAutoImportOptions } from "../setup/credential-auto-import";
15
16
  import type { LspStartupServerInfo } from "../tools";
16
17
  import type { AssistantMessageComponent } from "./components/assistant-message";
17
18
  import type { BashExecutionComponent } from "./components/bash-execution";
@@ -226,7 +227,7 @@ export interface InteractiveModeContext {
226
227
  showSessionSelector(): void;
227
228
  handleResumeSession(sessionPath: string): Promise<void>;
228
229
  handleSessionDeleteCommand(): Promise<void>;
229
- showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void>;
230
+ showOAuthSelector(mode: "login" | "logout", providerId?: string, options?: OAuthSelectorOptions): Promise<void>;
230
231
  showHookConfirm(title: string, message: string): Promise<boolean>;
231
232
  showDebugSelector(): void;
232
233
  showSessionObserver(): void;
@@ -276,3 +277,8 @@ export interface InteractiveModeContext {
276
277
  showExtensionError(extensionPath: string, error: string): void;
277
278
  showToolError(toolName: string, error: string): void;
278
279
  }
280
+ export interface OAuthSelectorOptions {
281
+ allowExternalCredentialDiscovery?: boolean;
282
+ trigger?: "bare-login";
283
+ externalCredentialDiscover?: CredentialAutoImportOptions["discover"];
284
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * In-thread configuration slash commands for the threaded session surface.
3
+ *
4
+ * Replies are thread-native now (the old `/answer <sessionId> …` command is
5
+ * removed), but the user can still adjust per-surface behaviour from inside a
6
+ * session thread with small slash commands:
7
+ *
8
+ * - `/verbose` switch the mirror to verbose (full tool output + reasoning)
9
+ * - `/lean` switch back to lean (assistant text + tool names)
10
+ * - `/verbosity lean|verbose`
11
+ * - `/redact on|off` toggle redaction of streamed content
12
+ *
13
+ * This parser is pure so the command grammar is unit-testable; the daemon maps
14
+ * the returned change onto a `config_command` frame / settings update.
15
+ */
16
+ /** A parsed in-thread configuration change. */
17
+ export interface ConfigCommandChange {
18
+ verbosity?: "lean" | "verbose";
19
+ redact?: boolean;
20
+ }
21
+ /**
22
+ * Parse an in-thread config command. Returns the requested change, or
23
+ * `undefined` when the text is not a recognised config command (so the daemon
24
+ * can fall through to treating it as a free-text injection).
25
+ */
26
+ export declare function parseInThreadConfigCommand(text: string): ConfigCommandChange | undefined;
@@ -0,0 +1,61 @@
1
+ import type { Settings } from "../config/settings";
2
+ export interface NotificationConfig {
3
+ enabled: boolean;
4
+ botToken?: string;
5
+ chatId?: string;
6
+ redact: boolean;
7
+ verbosity: "lean" | "verbose";
8
+ idleTimeoutMs: number;
9
+ }
10
+ /** Read typed config from Settings. */
11
+ export declare function getNotificationConfig(settings: Settings): NotificationConfig;
12
+ /** Is global config sufficient for auto-on (enabled + botToken + chatId all present)? */
13
+ export declare function isGloballyConfigured(cfg: NotificationConfig): boolean;
14
+ /** Resolve whether the notifications extension should be registered at SDK startup. */
15
+ export declare function shouldRegisterNotificationsExtension(input: {
16
+ env: NodeJS.ProcessEnv;
17
+ cfg?: NotificationConfig;
18
+ }): boolean;
19
+ /**
20
+ * Resolve whether THIS session should run notifications.
21
+ * Precedence (highest first):
22
+ * 1) env.GJC_NOTIFICATIONS === "0" -> false (hard opt-out)
23
+ * 2) sessionDisabled === true -> false (local /notify off)
24
+ * 3) env.GJC_NOTIFICATIONS === "1" || env.GJC_NOTIFICATIONS_TOKEN present -> true (legacy explicit)
25
+ * 4) isGloballyConfigured(cfg) -> true (global auto-on)
26
+ * 5) otherwise false
27
+ */
28
+ export declare function isSessionNotificationsEnabled(input: {
29
+ cfg: NotificationConfig;
30
+ env: NodeJS.ProcessEnv;
31
+ sessionDisabled: boolean;
32
+ }): boolean;
33
+ /** Mask a bot token for display: first 4 chars + "…" + "(len N)"; "(unset)" when undefined/empty. Never reveal full token. */
34
+ export declare function maskToken(token: string | undefined): string;
35
+ /** Stable non-reversible fingerprint of a token: sha256 hex, first 12 chars. */
36
+ export declare function tokenFingerprint(token: string): string;
37
+ /** Short session tag for display, e.g. last 6 chars of sessionId. */
38
+ export declare function sessionTag(sessionId: string): string;
39
+ export interface RedactableAction {
40
+ id: string;
41
+ kind: string;
42
+ sessionId: string;
43
+ question?: string;
44
+ options?: string[];
45
+ summary?: string;
46
+ }
47
+ /**
48
+ * When redact is true, strip sensitive content for remote delivery:
49
+ * - ask: NOT redacted. An ask is an interactive prompt the human must read and
50
+ * answer on the remote surface; redacting its question/options would make it
51
+ * unanswerable, defeating remote answering. Asks are returned unchanged.
52
+ * - idle: summary removed, (no question/options).
53
+ * When redact is false, return the action unchanged.
54
+ *
55
+ * Redaction still applies to streamed content frames (turn_stream, context_update,
56
+ * image_attachment) which are suppressed at their emit sites, not here.
57
+ */
58
+ export declare function buildRedactedAction(action: RedactableAction, opts: {
59
+ redact: boolean;
60
+ sessionTag: string;
61
+ }): RedactableAction;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Pure helpers for the notifications extension.
3
+ *
4
+ * Kept side-effect-free so the mapping logic (ask extraction, idle summary,
5
+ * dedupe keys) is unit-testable without a live session or the native server.
6
+ */
7
+ import { type RedactableAction } from "./config";
8
+ /** A pending ask derived from an `ask` tool call. */
9
+ export interface PendingAsk {
10
+ /** Action id: `${toolCallId}:${questionId}`. */
11
+ id: string;
12
+ /** Question text. */
13
+ question: string;
14
+ /** Option labels (may be empty for free-text questions). */
15
+ options: string[];
16
+ }
17
+ /** Truncate text to `max` chars, appending an ellipsis when cut. */
18
+ export declare function truncate(text: string, max?: number): string;
19
+ /** Stable per-turn idle dedupe key so exactly one idle action fires per turn. */
20
+ export declare function idleDedupeKey(sessionId: string, turnIndex: number): string;
21
+ /**
22
+ * Extract pending asks from an `ask` tool call input.
23
+ *
24
+ * Defensive: tolerates partial/unknown shapes and always returns an array.
25
+ */
26
+ export declare function asksFromAskInput(toolCallId: string, input: unknown): PendingAsk[];
27
+ /** Prepare an action JSON payload for remote notification delivery. */
28
+ export declare function notificationActionPayload<T extends RedactableAction>(action: T, opts: {
29
+ redact: boolean;
30
+ sessionTag: string;
31
+ }): RedactableAction;
32
+ /** Extract a plain-text summary from an agent message's content, if any. */
33
+ export declare function summaryFromMessage(message: unknown, max?: number): string | undefined;
34
+ /**
35
+ * Extract an idle summary from an `agent_end` event's settled message list: the
36
+ * last message that yields text (i.e. the final assistant message; tool-result
37
+ * messages have no text and are skipped).
38
+ *
39
+ * `agent_end` fires exactly once when the agent loop settles to await the user,
40
+ * so emitting idle from this — instead of per-`turn_end` — produces exactly one
41
+ * idle notification per genuine idle, eliminating the multi-turn flood.
42
+ */
43
+ export declare function summaryFromMessages(messages: unknown, max?: number): string | undefined;
44
+ /** An agent-produced image extracted from a message's content. */
45
+ export interface ExtractedImage {
46
+ source: string;
47
+ mime: string;
48
+ data: string;
49
+ }
50
+ /**
51
+ * Extract agent-produced images (`{ type: "image", data, mimeType }` blocks)
52
+ * from a message's content — e.g. computer-use/browser screenshots or tool
53
+ * image outputs — for `image_attachment` delivery.
54
+ */
55
+ export declare function imageAttachmentsFromMessage(message: unknown, source?: string): ExtractedImage[];
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Telegram HTML formatting helpers for the notifications SDK.
3
+ *
4
+ * All notifications-SDK Telegram output is sent with `parse_mode: "HTML"`. This
5
+ * module is the single source of truth for: escaping dynamic text, converting a
6
+ * bounded markdown subset into Telegram HTML, safely truncating a finished
7
+ * message to Telegram's 4096-char limit without breaking tags/entities, and
8
+ * laying out inline-keyboard buttons as a numbered grid.
9
+ *
10
+ * Discipline: escape first, tag second. Telegram only parses a small tag set
11
+ * (b, i, u, s, code, pre, a, blockquote, tg-spoiler); a stray `<` or unbalanced
12
+ * tag can make Telegram reject the whole message, so dynamic text is always
13
+ * escaped before any tag is emitted.
14
+ */
15
+ export declare const TELEGRAM_PARSE_MODE: "HTML";
16
+ export declare const TELEGRAM_MESSAGE_LIMIT = 4096;
17
+ /** Escape text for Telegram HTML body content (`& < >`). */
18
+ export declare function escapeHtml(value: string): string;
19
+ /** Bold the given raw text (escaped internally). */
20
+ export declare function bold(raw: string): string;
21
+ /** Italicize the given raw text (escaped internally). */
22
+ export declare function italic(raw: string): string;
23
+ /** Render the given raw text as inline code (escaped internally). */
24
+ export declare function code(raw: string): string;
25
+ /** Render the given raw text as a preformatted block (escaped internally). */
26
+ export declare function pre(raw: string): string;
27
+ /**
28
+ * Convert a bounded markdown subset into Telegram HTML. Supported: fenced code,
29
+ * inline code, `**bold**`, `*italic*`, `[text](url)` (safe schemes only),
30
+ * `#` headers, `>` blockquotes, and GFM tables (rendered as a monospace block).
31
+ * Unsupported or malformed markdown is left as escaped literal text — never
32
+ * emitted as unbalanced tags.
33
+ */
34
+ export declare function markdownToTelegramHtml(markdown: string): string;
35
+ /**
36
+ * Truncate a finished Telegram HTML message to at most `max` chars without
37
+ * splitting a tag or entity, closing any still-open allowed tags and appending
38
+ * `marker`. The final string is guaranteed to be <= `max`.
39
+ */
40
+ export declare function truncateTelegramHtml(message: string, max?: number, marker?: string): string;
41
+ /** Finalize an optional message: undefined passthrough, else safe-truncate. */
42
+ export declare function finalizeTelegramHtml(message?: string): string | undefined;
43
+ /**
44
+ * One-based, plain-text button label (Telegram does not parse HTML in labels).
45
+ *
46
+ * Strips any leading `N.`/`N)` index already embedded in the label (e.g.
47
+ * deep-interview options pre-numbered by the ask tool) and applies the canonical
48
+ * one-based button index instead. This avoids duplicated numbering like
49
+ * `1. 1. …` and keeps the displayed number aligned with the button's real index.
50
+ */
51
+ export declare function buttonLabel(label: string, index: number): string;
52
+ export interface InlineButton {
53
+ text: string;
54
+ callback_data: string;
55
+ }
56
+ /**
57
+ * Lay out option labels as a numbered button grid. Long buttons take a
58
+ * full-width row; runs of short buttons are packed into rows of up to 3. The
59
+ * callback value comes from `callbackForIndex(i)` using the original zero-based
60
+ * option index — layout never changes callback semantics.
61
+ */
62
+ export declare function buildButtonGrid(labels: string[], callbackForIndex: (index: number) => string): InlineButton[][];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Notifications extension.
3
+ *
4
+ * Hosts a per-session loopback WebSocket notification server (the Rust core via
5
+ * N-API) and bridges GJC session events + the `ask` tool to it so a remote client
6
+ * (e.g. a Telegram bot) can both see action-needed signals and ANSWER them —
7
+ * without requiring RPC/unattended mode:
8
+ *
9
+ * - `ask` (interactive): registers an {@link AskAnswerSource}; the ask tool races
10
+ * the local UI against a remote reply. First valid answer wins; a local answer
11
+ * aborts the remote wait (and broadcasts `action_resolved` resolvedBy=local).
12
+ * - `ask` (unattended/RPC): observes emitted workflow gates and resolves the real
13
+ * gate on a remote reply via `ctx.workflowGate`.
14
+ * - `turn_end` -> `action_needed` (kind `idle`, deduped per turn).
15
+ * - `session_shutdown` -> stop the server + deregister the answer source.
16
+ *
17
+ * Enable with Settings notifications config, `GJC_NOTIFICATIONS=1` (a token is
18
+ * generated), or `GJC_NOTIFICATIONS_TOKEN`.
19
+ */
20
+ import type { ExtensionFactory } from "../extensibility/extensions";
21
+ /**
22
+ * Best-effort real repository name (no git spawn): resolves the main worktree
23
+ * root directory so linked worktrees report the repo (e.g. `gajae-code`)
24
+ * instead of the worktree directory (e.g. `feat-foo-01047f11`).
25
+ */
26
+ export declare function readGitRepoName(cwd: string): string | undefined;
27
+ export declare function notificationsEnabled(): boolean;
28
+ export declare const createNotificationsExtension: ExtensionFactory;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Host-wide shared Telegram rate-limit pool for the threaded session surface.
3
+ *
4
+ * Multiple GJC sessions on one host share a single bot token and paired chat.
5
+ * Telegram enforces per-bot/per-chat limits (~1 message/sec, bursts up to ~20),
6
+ * so the singleton notifications daemon owns ONE pool that all per-session
7
+ * threads draw from. The pool provides:
8
+ *
9
+ * - a token bucket (burst capacity + steady refill) modelling the chat limit;
10
+ * - priority lanes (`ask` > `finalized` > `live` > `idle`) so urgent frames
11
+ * win scarce tokens;
12
+ * - per-session round-robin fairness within a lane so one session's live-edit
13
+ * stream cannot starve other sessions;
14
+ * - coalescing of live edits that share a `coalesceKey` (the latest rendered
15
+ * text replaces the queued one) so throttled edit storms collapse.
16
+ *
17
+ * The core is a pull-based scheduler with an injectable clock so fairness,
18
+ * starvation, and burst behaviour are deterministically unit-testable without
19
+ * real time or a live Bot API.
20
+ */
21
+ /** Delivery lanes in descending priority. */
22
+ export type RateLimitLane = "ask" | "finalized" | "live" | "idle";
23
+ /** Lanes ordered from highest to lowest priority. */
24
+ export declare const LANE_PRIORITY: readonly RateLimitLane[];
25
+ /** A unit of work competing for a send slot. */
26
+ export interface RateLimitItem<T = unknown> {
27
+ /** Owning session id (used for per-session fairness). */
28
+ sessionId: string;
29
+ /** Priority lane. */
30
+ lane: RateLimitLane;
31
+ /**
32
+ * Optional coalesce key. Submitting another item with the same
33
+ * `(sessionId, lane, coalesceKey)` replaces the queued payload with the
34
+ * newer one instead of enqueuing a duplicate (used for live edits).
35
+ */
36
+ coalesceKey?: string;
37
+ /** Opaque payload the caller maps to an actual Telegram send. */
38
+ payload: T;
39
+ }
40
+ /** Options for {@link RateLimitPool}. */
41
+ export interface RateLimitPoolOptions {
42
+ /** Burst capacity (max tokens). Default 20 (Telegram per-chat burst). */
43
+ capacity?: number;
44
+ /** Steady refill rate in tokens per second. Default 1 (~1 msg/sec/chat). */
45
+ refillPerSec?: number;
46
+ /** Injectable clock in ms. Default `Date.now`. */
47
+ now?: () => number;
48
+ }
49
+ /**
50
+ * A deterministic, pull-based shared rate-limit scheduler.
51
+ *
52
+ * Callers {@link submit} work and periodically {@link drain} (e.g. on a timer
53
+ * or after each submit); `drain` returns the items granted a send slot, in the
54
+ * order they should be sent.
55
+ */
56
+ export declare class RateLimitPool<T = unknown> {
57
+ private readonly capacity;
58
+ private readonly refillPerSec;
59
+ private readonly now;
60
+ /** Per-lane FIFO queues; each lane holds items across sessions. */
61
+ private readonly lanes;
62
+ /** Rotating session cursor per lane for round-robin fairness. */
63
+ private readonly laneCursor;
64
+ private tokens;
65
+ private lastRefill;
66
+ private seqCounter;
67
+ constructor(options?: RateLimitPoolOptions);
68
+ /** Number of items currently queued across all lanes. */
69
+ get pending(): number;
70
+ /** Current available token count (after refill at `now`). */
71
+ availableTokens(nowMs?: number): number;
72
+ /**
73
+ * Submit an item. If it carries a `coalesceKey` matching a queued item in
74
+ * the same `(sessionId, lane)`, the queued payload is replaced (latest
75
+ * wins) and FIFO position is preserved; otherwise it is appended.
76
+ */
77
+ submit(item: RateLimitItem<T>): void;
78
+ /**
79
+ * Grant as many queued items as tokens allow at `nowMs`. Items are selected
80
+ * by lane priority, then round-robin across sessions within a lane (so no
81
+ * single session monopolises a lane), consuming one token each.
82
+ */
83
+ drain(nowMs?: number): RateLimitItem<T>[];
84
+ private refill;
85
+ /** Pop the next item by lane priority + per-session round-robin fairness. */
86
+ private takeNext;
87
+ /**
88
+ * Choose the index to serve from a lane queue using round-robin over the
89
+ * distinct session ids present, starting just after the last-served
90
+ * session. Falls back to FIFO (index 0) when only one session is queued.
91
+ */
92
+ private pickFairIndex;
93
+ }
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Reference CLI for the notifications SDK Telegram client.
4
+ *
5
+ * Bridges a running GJC session's notification endpoint to a Telegram bot so you
6
+ * can answer asks / see idle pings from your phone — no RPC mode required. This
7
+ * is an EXAMPLE/template (the SDK contract is in `docs/notifications-sdk.md`);
8
+ * Discord/Slack clients are written the same way.
9
+ *
10
+ * Usage:
11
+ * bun run packages/coding-agent/src/notifications/telegram-cli.ts \
12
+ * --bot-token <token> [--chat-id <id>] [--endpoint-file <path> | --session-id <id>] [--repo <dir>]
13
+ *
14
+ * Env fallbacks: GJC_TG_BOT_TOKEN, GJC_TG_CHAT_ID.
15
+ * If --chat-id is omitted it is auto-resolved from getUpdates (message the bot once).
16
+ * If neither --endpoint-file nor --session-id is given, the newest endpoint file
17
+ * under <repo>/.gjc/state/notifications/ is used.
18
+ */
19
+ export {};
@@ -0,0 +1,11 @@
1
+ import { Settings } from "../config/settings";
2
+ import { TelegramNotificationDaemon } from "./telegram-daemon";
3
+ export interface RunDaemonInternalDeps {
4
+ SettingsImpl?: Pick<typeof Settings, "init">;
5
+ DaemonImpl?: typeof TelegramNotificationDaemon;
6
+ processPid?: number;
7
+ }
8
+ export declare function runDaemonSmoke(opts?: {
9
+ agentDir?: string;
10
+ }): Promise<void>;
11
+ export declare function runDaemonInternal(argv: string[], deps?: RunDaemonInternalDeps): Promise<void>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Telegram daemon controller + owner-scoped control-request helpers.
3
+ *
4
+ * Reload is a hybrid: an owner-scoped control-request file records auditable
5
+ * intent, SIGTERM is the wakeup that aborts the in-flight long poll, and a
6
+ * fresh daemon is spawned only after the old pid is dead / has exited. This
7
+ * keeps the single-poller invariant (no Telegram getUpdates 409 overlap) and
8
+ * never steals a still-live owner.
9
+ */
10
+ import type { Settings } from "../config/settings";
11
+ import type { BuiltInDaemonController, DaemonOperationOptions, DaemonOperationResult, DaemonStatus } from "../daemon/control-types";
12
+ import { type TelegramDaemonDeps, type TelegramDaemonFs } from "./telegram-daemon";
13
+ export interface TelegramDaemonControlRequest {
14
+ version: 1;
15
+ requestId: string;
16
+ action: "reload" | "stop";
17
+ ownerId: string;
18
+ pid: number;
19
+ createdAt: number;
20
+ }
21
+ export declare function telegramControlRequestPath(agentDir: string): string;
22
+ export declare function readTelegramControlRequest(settings: Settings, fsImpl?: TelegramDaemonFs): Promise<TelegramDaemonControlRequest | undefined>;
23
+ export declare function writeTelegramControlRequest(settings: Settings, request: TelegramDaemonControlRequest, fsImpl?: TelegramDaemonFs): Promise<void>;
24
+ export declare function clearTelegramControlRequest(settings: Settings, requestId?: string, fsImpl?: TelegramDaemonFs): Promise<void>;
25
+ export interface TelegramDaemonControlDeps {
26
+ fs?: TelegramDaemonFs;
27
+ now?: () => number;
28
+ pidAlive?: (pid: number) => boolean;
29
+ sendSignal?: (pid: number, signal: NodeJS.Signals) => void;
30
+ spawn?: TelegramDaemonDeps["spawn"];
31
+ execPath?: string;
32
+ randomId?: () => string;
33
+ sleep?: (ms: number) => Promise<void>;
34
+ waitStepMs?: number;
35
+ }
36
+ export declare class TelegramDaemonController implements BuiltInDaemonController {
37
+ private readonly settings;
38
+ private readonly deps;
39
+ readonly kind: "telegram";
40
+ private readonly fsImpl;
41
+ private readonly now;
42
+ private readonly pidAlive;
43
+ private readonly sendSignal;
44
+ private readonly waitStepMs;
45
+ constructor(settings: Settings, deps?: TelegramDaemonControlDeps);
46
+ private runtimeInfo;
47
+ status(): Promise<DaemonStatus>;
48
+ private spawnDeps;
49
+ private sleep;
50
+ private waitForPidDeath;
51
+ private result;
52
+ reload(opts?: DaemonOperationOptions): Promise<DaemonOperationResult>;
53
+ stop(opts?: DaemonOperationOptions): Promise<DaemonOperationResult>;
54
+ private stopOrReload;
55
+ private clearOwnRequest;
56
+ }