@gajae-code/coding-agent 0.6.5 → 0.7.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 +38 -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 +23 -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 +38 -0
- 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/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +2 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +14 -0
- package/dist/types/modes/components/oauth-selector.d.ts +2 -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/types.d.ts +7 -1
- package/dist/types/notifications/config-commands.d.ts +26 -0
- package/dist/types/notifications/config.d.ts +61 -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/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 +276 -0
- package/dist/types/notifications/telegram-reference.d.ts +111 -0
- package/dist/types/notifications/threaded-inbound.d.ts +58 -0
- package/dist/types/notifications/threaded-render.d.ts +66 -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 +39 -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/tools/ask-answer-registry.d.ts +13 -0
- package/dist/types/tools/index.d.ts +18 -0
- package/dist/types/tools/subagent.d.ts +3 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +3 -0
- package/src/async/job-manager.ts +5 -1
- package/src/cli/daemon-cli.ts +122 -0
- package/src/cli/notify-cli.ts +274 -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/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 +29 -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/gjc/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/gjc-runtime/deep-interview-recorder.ts +12 -4
- package/src/gjc-runtime/launch-tmux.ts +10 -2
- package/src/gjc-runtime/state-runtime.ts +18 -4
- package/src/gjc-runtime/state-writer.ts +8 -8
- package/src/gjc-runtime/tmux-common.ts +8 -0
- package/src/gjc-runtime/tmux-sessions.ts +8 -1
- package/src/gjc-runtime/ultragoal-guard.ts +57 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +105 -19
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
- package/src/gjc-runtime/workflow-manifest.ts +11 -1
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/hashline/hash.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +9 -7
- 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/controllers/event-controller.ts +20 -0
- package/src/modes/controllers/selector-controller.ts +80 -17
- package/src/modes/interactive-mode.ts +6 -2
- 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/types.ts +7 -1
- package/src/modes/utils/ui-helpers.ts +23 -0
- package/src/notifications/config-commands.ts +50 -0
- package/src/notifications/config.ts +107 -0
- package/src/notifications/helpers.ts +135 -0
- package/src/notifications/html-format.ts +389 -0
- package/src/notifications/index.ts +700 -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 +1370 -0
- package/src/notifications/telegram-reference.ts +335 -0
- package/src/notifications/threaded-inbound.ts +80 -0
- package/src/notifications/threaded-render.ts +155 -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 +113 -3
- 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/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/image-gen.ts +5 -8
- package/src/tools/index.ts +19 -0
- package/src/tools/inspect-image.ts +16 -11
- package/src/tools/subagent-render.ts +7 -0
- package/src/tools/subagent.ts +38 -7
|
@@ -27,10 +27,25 @@ export declare function validateCompletionReceipt(input: {
|
|
|
27
27
|
export declare function readUltragoalVerificationState(input: {
|
|
28
28
|
cwd: string;
|
|
29
29
|
currentGoal?: CurrentGoalLike | null;
|
|
30
|
+
sessionId?: string | null;
|
|
30
31
|
}): Promise<UltragoalGuardDiagnostic>;
|
|
31
32
|
export declare function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBlockDiagnostic>;
|
|
32
33
|
export declare function assertCanCompleteCurrentGoal(input: {
|
|
33
34
|
cwd: string;
|
|
34
35
|
currentGoal?: CurrentGoalLike | null;
|
|
36
|
+
sessionId?: string | null;
|
|
35
37
|
}): Promise<void>;
|
|
36
38
|
export declare function isUltragoalBypassPrompt(prompt: string): boolean;
|
|
39
|
+
export interface UltragoalPauseBlockDiagnostic {
|
|
40
|
+
blocked: boolean;
|
|
41
|
+
reason: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* While an Ultragoal run is active, `goal({"op":"pause"})` is only allowed when the
|
|
45
|
+
* current durable Ultragoal state is readable and the latest durable ledger event
|
|
46
|
+
* classifies the current blocker as `human_blocked`. Resolvable blockers must be
|
|
47
|
+
* worked, not parked. Reads fail closed so unreadable durable state or ledger data
|
|
48
|
+
* blocks pause rather than silently allowing a give-up.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isUltragoalPauseBlocked(cwd: string): Promise<UltragoalPauseBlockDiagnostic>;
|
|
51
|
+
export declare function assertUltragoalPauseAllowed(cwd: string): Promise<void>;
|
|
@@ -193,6 +193,19 @@ export declare function recordUltragoalReviewBlockers(input: {
|
|
|
193
193
|
evidence: string;
|
|
194
194
|
gjcGoalJson?: string;
|
|
195
195
|
}): Promise<UltragoalPlan>;
|
|
196
|
+
export type UltragoalBlockerClassification = "human_blocked" | "resolvable";
|
|
197
|
+
/**
|
|
198
|
+
* Record an audited blocker triage classification in the durable ledger. A
|
|
199
|
+
* `human_blocked` classification is the only thing that authorizes
|
|
200
|
+
* `goal({"op":"pause"})` while an Ultragoal run is active; `resolvable` is an
|
|
201
|
+
* audit note and never unblocks pause.
|
|
202
|
+
*/
|
|
203
|
+
export declare function recordUltragoalBlockerClassification(input: {
|
|
204
|
+
cwd: string;
|
|
205
|
+
classification: UltragoalBlockerClassification;
|
|
206
|
+
evidence: string;
|
|
207
|
+
goalId?: string;
|
|
208
|
+
}): Promise<UltragoalLedgerEvent>;
|
|
196
209
|
type UltragoalReviewContractStrength = "strong" | "thin-derived";
|
|
197
210
|
interface UltragoalReviewFinding extends JsonObject {
|
|
198
211
|
severity: "blocker";
|
|
@@ -208,6 +221,7 @@ interface UltragoalReviewResult extends JsonObject {
|
|
|
208
221
|
weakContractCapApplied: boolean;
|
|
209
222
|
blockerGoalIds?: string[];
|
|
210
223
|
}
|
|
224
|
+
export declare function resolveGitBase(cwd: string, branch?: string): Promise<string>;
|
|
211
225
|
export declare function runUltragoalReview(cwd: string, args: readonly string[]): Promise<UltragoalReviewResult>;
|
|
212
226
|
export declare function runNativeUltragoalCommand(args: string[], cwd?: string): Promise<UltragoalCommandResult>;
|
|
213
227
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Container } from "@gajae-code/tui";
|
|
2
2
|
import type { AuthStorage } from "../../session/auth-storage";
|
|
3
|
+
import type { ImportableCredential } from "../../setup/credential-import";
|
|
3
4
|
/**
|
|
4
5
|
* Component that renders an OAuth provider selector.
|
|
5
6
|
*/
|
|
@@ -8,6 +9,7 @@ export declare class OAuthSelectorComponent extends Container {
|
|
|
8
9
|
constructor(mode: "login" | "logout", authStorage: AuthStorage, onSelect: (providerId: string) => void, onCancel: () => void, options?: {
|
|
9
10
|
validateAuth?: (providerId: string) => Promise<boolean>;
|
|
10
11
|
requestRender?: () => void;
|
|
12
|
+
externalCredentialCandidates?: ImportableCredential[];
|
|
11
13
|
});
|
|
12
14
|
stopValidation(): void;
|
|
13
15
|
dispose(): void;
|
|
@@ -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
|
+
}
|