@gajae-code/coding-agent 0.7.2 → 0.7.3

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 (52) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/types/cli/mcp-cli.d.ts +25 -0
  3. package/dist/types/cli.d.ts +6 -0
  4. package/dist/types/commands/mcp.d.ts +70 -0
  5. package/dist/types/config/keybindings.d.ts +2 -2
  6. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  7. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  8. package/dist/types/modes/components/model-selector.d.ts +2 -0
  9. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  10. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  11. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  12. package/dist/types/notifications/telegram-daemon.d.ts +54 -16
  13. package/dist/types/notifications/topic-registry.d.ts +2 -0
  14. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  15. package/dist/types/web/insane/url-guard.d.ts +6 -3
  16. package/dist/types/web/scrapers/types.d.ts +5 -0
  17. package/dist/types/web/scrapers/utils.d.ts +7 -1
  18. package/package.json +7 -7
  19. package/src/cli/mcp-cli.ts +272 -0
  20. package/src/cli.ts +6 -2
  21. package/src/commands/mcp.ts +117 -0
  22. package/src/config/keybindings.ts +2 -2
  23. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  24. package/src/defaults/gjc/skills/deep-interview/SKILL.md +4 -3
  25. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  26. package/src/extensibility/extensions/runner.ts +1 -0
  27. package/src/gjc-runtime/tmux-common.ts +3 -1
  28. package/src/gjc-runtime/ultragoal-guard.ts +25 -8
  29. package/src/hooks/skill-state.ts +57 -0
  30. package/src/internal-urls/docs-index.generated.ts +10 -7
  31. package/src/modes/bridge/bridge-mode.ts +11 -0
  32. package/src/modes/components/custom-editor.ts +2 -0
  33. package/src/modes/components/footer.ts +2 -3
  34. package/src/modes/components/model-selector.ts +12 -0
  35. package/src/modes/components/status-line/git-utils.ts +25 -0
  36. package/src/modes/components/status-line.ts +10 -11
  37. package/src/modes/components/welcome.ts +2 -3
  38. package/src/modes/controllers/selector-controller.ts +3 -0
  39. package/src/modes/interactive-mode.ts +2 -1
  40. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  41. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  42. package/src/modes/theme/defaults/index.ts +2 -0
  43. package/src/notifications/operator-runtime.ts +171 -0
  44. package/src/notifications/telegram-daemon.ts +347 -251
  45. package/src/notifications/topic-registry.ts +5 -0
  46. package/src/slash-commands/helpers/parse.ts +2 -1
  47. package/src/tools/bash.ts +9 -0
  48. package/src/tools/composer-bash-policy.ts +96 -0
  49. package/src/tools/fetch.ts +18 -2
  50. package/src/web/insane/url-guard.ts +18 -14
  51. package/src/web/scrapers/types.ts +143 -45
  52. package/src/web/scrapers/utils.ts +70 -19
package/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.7.3] - 2026-06-25
6
+
7
+ ### Added
8
+
9
+ - Added the `gruvbox-dark` built-in theme: the canonical Gruvbox dark palette mapped across every GJC theme token, selectable via `/theme`.
10
+ - Added a standalone MCP registration command: `gjc mcp add|list|remove` writes explicit user-provided MCP server definitions (stdio/http/sse) into GJC config without importing or inheriting other tools' live MCP configs, with env/header/auth values redacted in output (#1095).
11
+
12
+ ### Changed
13
+
14
+ - Refined the interactive composer chrome so the input box, status rail, and welcome banner share one visual language: the composer now uses a rounded border (matching the rounded welcome banner) instead of a sharp rectangle, and the status rail uses the subtle elevated `userMessageBg` surface tone instead of the heavy `statusLineBg` block, so it reads as a quiet layered zone rather than a solid bar. Both resolve through existing semantic theme slots, so every bundled theme tracks automatically.
15
+ - When a Composer harness model is active, the `bash` tool now hard-blocks repository file I/O — pipes, process/heredoc/command substitution, redirection, `tee`, file read/discovery (`cat`/`head`/`tail`/`grep`/`find`/`ls`), file mutation (`cp`/`mv`/`rm`/`touch`/`mkdir`/`chmod`/`ln`), `sed`/`awk`, git file-read subcommands, and script file I/O — unless the command is on a strict allowlist (`bun test`/`run check|test|build`, `cargo test|check|build`, `git status`/`rev-parse`, package version queries), forcing Composer models to use the dedicated find/search/read/edit tools for file discovery and mutation (#1027).
16
+ ### Documentation
17
+
18
+ - Documented the docs-only Aside evaluation boundary as an opt-in search/context retrieval sidecar using explicit user-provided MCP configuration, with browser actions, login flows, payments, internal tools, secrets, and raw browser/session payload logging out of scope by default (#1097).
19
+ - Added a UI design and visual QA contract governing future TUI/dashboard/terminal visual work (#1101).
20
+ - Added a CodeGraph custom-tool integration guide (#1073).
21
+ - Documented the Windows psmux namespace boundary for `gjc --tmux`, `gjc session`, and `gjc team`: cwd/`-c` is now called out as a start directory rather than server isolation, `-L <namespace>` is identified as the psmux namespace primitive, and tmux command overrides are documented as executable names rather than shell command lines (#1118).
22
+ - Clarified the Telegram Threaded Mode fallback documentation (#1122).
23
+
24
+ ### Fixed
25
+
26
+ - Expanded the initial GJC forge welcome box to the live terminal viewport width and pinned the status/composer area to the bottom when the startup layout is shorter than the screen (#1120).
27
+
28
+ - Deep Interview Restate/option gates now recover through the ask selector path instead of waiting on plaintext `Options:` output.
29
+ - Widened the forge splash on wide terminals so it no longer clips (#1110).
30
+ - Parse quoted SSH remote host names in the slash-command host parser (#1104).
31
+ - Tolerate an unreadable git HEAD in the status chrome instead of throwing (#1072).
32
+ - Registered the `plugin` command in the CLI command registry so `gjc plugin …` (install/uninstall/list/marketplace/enable/disable/doctor) resolves instead of silently falling through to the default launch/chat command — the command was implemented and tested but was never registered (#1071).
33
+ - Keybinding/Ctrl+Enter newline-handling sweep across the editor and input controller (#1111).
34
+ - Fixed model-profile default badge precedence in the `/model` selector so the correct default-profile badge wins (#1117).
35
+ - Prevented duplicate Telegram topics being created for transient sessions (#1125).
36
+
37
+ ### Security
38
+
39
+ - User-supplied URL reads now share the public HTTP(S) network guard that was previously insane-fallback-only: the initial target, the redirect chain, and binary-conversion redirects are all revalidated against private-network blocking before any request is opened or followed, closing an SSRF path through the normal read-tool fetch pipeline (#1114).
40
+ - Bridge workflow-gate responses now require the claimed controller token before the unattended control plane may resolve a gate, and the `workflow_gate_response` RPC command was raised from prompt scope to control scope, so prompt-only clients can no longer answer lifecycle workflow gates (#1116).
41
+
5
42
  ## [0.7.2] - 2026-06-24
6
43
  ### Added
7
44
 
@@ -19,6 +56,7 @@
19
56
  - Preserve GJC-managed tmux sessions on attach/disconnect instead of tearing them down, and stop implicitly attaching on launch (#1063).
20
57
  - Corrected the auto-compaction output reserve so post-compaction responses keep adequate headroom (#1021).
21
58
  - Improved active-input shortcut hints and the busy-input queueing hint for clearer in-session guidance (#1022, #1024).
59
+ - Fixed the Ultragoal ask guard blocking the `ask` tool when no GJC session can be resolved. `ultragoalReadPaths` falls back to the legacy/global `.gjc/ultragoal` directory when neither `GJC_SESSION_ID` nor an auto-detectable active session is present, but the follow-up `readUltragoalPlan`/`readUltragoalLedger` reads ignored that resolution and re-ran session detection, throwing `no active GJC session found` and surfacing `durable_state_unreadable` — which blocked `ask` for every agent even with no active Ultragoal run. `ultragoalReadPaths` now returns the resolved session id (or `null`); the ask guard treats a null session as inactive and falls open, and threads the resolved id into the plan/ledger reads so they no longer re-resolve. An inconsistent state (state dir present but `goals.json` missing/empty) still fails closed so the pause guard keeps blocking give-ups.
22
60
 
23
61
  ## [0.7.1] - 2026-06-23
24
62
  ### Fixed
@@ -0,0 +1,25 @@
1
+ import type { MCPServerConfig } from "../runtime-mcp/types";
2
+ export type MCPAction = "add" | "list" | "remove";
3
+ export interface MCPCommandArgs {
4
+ action: MCPAction;
5
+ name?: string;
6
+ commandArgs?: string[];
7
+ flags: {
8
+ project?: boolean;
9
+ force?: boolean;
10
+ json?: boolean;
11
+ type?: "stdio" | "http" | "sse";
12
+ command?: string;
13
+ url?: string;
14
+ arg?: string[];
15
+ env?: string[];
16
+ header?: string[];
17
+ cwd?: string;
18
+ timeout?: number;
19
+ };
20
+ cwd?: string;
21
+ }
22
+ export declare class MCPArgsError extends Error {
23
+ }
24
+ export declare function redactMCPServerConfig(config: MCPServerConfig): MCPServerConfig;
25
+ export declare function runMCPCommand(args: MCPCommandArgs): Promise<void>;
@@ -1,3 +1,9 @@
1
1
  #!/usr/bin/env bun
2
+ /**
3
+ * CLI entry point — registers all commands explicitly and delegates to the
4
+ * lightweight CLI runner from pi-utils.
5
+ */
6
+ import { type CommandEntry } from "@gajae-code/utils/cli";
7
+ export declare const commands: CommandEntry[];
2
8
  /** Run the CLI with the given argv (no `process.argv` prefix). */
3
9
  export declare function runCli(argv: string[]): Promise<void>;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Direct MCP server registration for standalone GJC.
3
+ */
4
+ import { Command } from "@gajae-code/utils/cli";
5
+ import { type MCPAction } from "../cli/mcp-cli";
6
+ export default class MCP extends Command {
7
+ static description: string;
8
+ static delegateHelp: boolean;
9
+ static examples: string[];
10
+ static args: {
11
+ action: import("@gajae-code/utils/cli").ArgDescriptor & {
12
+ description: string;
13
+ required: false;
14
+ options: MCPAction[];
15
+ };
16
+ name: import("@gajae-code/utils/cli").ArgDescriptor & {
17
+ description: string;
18
+ required: false;
19
+ };
20
+ commandArgs: import("@gajae-code/utils/cli").ArgDescriptor & {
21
+ description: string;
22
+ required: false;
23
+ multiple: true;
24
+ };
25
+ };
26
+ static flags: {
27
+ project: import("@gajae-code/utils/cli").FlagDescriptor<"boolean"> & {
28
+ description: string;
29
+ };
30
+ force: import("@gajae-code/utils/cli").FlagDescriptor<"boolean"> & {
31
+ description: string;
32
+ default: boolean;
33
+ };
34
+ json: import("@gajae-code/utils/cli").FlagDescriptor<"boolean"> & {
35
+ char: string;
36
+ description: string;
37
+ default: boolean;
38
+ };
39
+ type: import("@gajae-code/utils/cli").FlagDescriptor<"string"> & {
40
+ description: string;
41
+ options: string[];
42
+ };
43
+ command: import("@gajae-code/utils/cli").FlagDescriptor<"string"> & {
44
+ description: string;
45
+ };
46
+ url: import("@gajae-code/utils/cli").FlagDescriptor<"string"> & {
47
+ description: string;
48
+ };
49
+ arg: import("@gajae-code/utils/cli").FlagDescriptor<"string"> & {
50
+ description: string;
51
+ multiple: true;
52
+ };
53
+ env: import("@gajae-code/utils/cli").FlagDescriptor<"string"> & {
54
+ description: string;
55
+ multiple: true;
56
+ };
57
+ header: import("@gajae-code/utils/cli").FlagDescriptor<"string"> & {
58
+ description: string;
59
+ multiple: true;
60
+ };
61
+ cwd: import("@gajae-code/utils/cli").FlagDescriptor<"string"> & {
62
+ description: string;
63
+ };
64
+ timeout: import("@gajae-code/utils/cli").FlagDescriptor<"integer"> & {
65
+ description: string;
66
+ };
67
+ };
68
+ run(): Promise<void>;
69
+ private printHelp;
70
+ }
@@ -230,8 +230,8 @@ export declare const KEYBINDINGS: {
230
230
  readonly description: "Open external editor";
231
231
  };
232
232
  readonly "app.message.followUp": {
233
- readonly defaultKeys: "ctrl+enter";
234
- readonly description: "Send follow-up message";
233
+ readonly defaultKeys: [];
234
+ readonly description: "Send follow-up message (no default; Ctrl+Enter inserts a newline)";
235
235
  };
236
236
  readonly "app.message.queue": {
237
237
  readonly defaultKeys: "alt+enter";
@@ -0,0 +1,11 @@
1
+ export type DeepInterviewPlaintextAskLeakOption = "Yes, crystallize" | "Adjust wording" | "Missing scope";
2
+ export type DeepInterviewPlaintextAskLeakResult = {
3
+ kind: "deep_interview_plaintext_ask_leak";
4
+ matchedOptions: DeepInterviewPlaintextAskLeakOption[];
5
+ signals: {
6
+ optionsHeading: true;
7
+ restateIntent: true;
8
+ deepInterviewContext: boolean;
9
+ };
10
+ };
11
+ export declare function detectDeepInterviewPlaintextAskLeak(text: string): DeepInterviewPlaintextAskLeakResult | null;
@@ -1,6 +1,6 @@
1
1
  import { Editor, type KeyId } from "@gajae-code/tui";
2
2
  import { type AppKeybinding } from "../../config/keybindings";
3
- type ConfigurableEditorAction = Extract<AppKeybinding, "app.interrupt" | "app.clear" | "app.exit" | "app.suspend" | "app.thinking.cycle" | "app.model.cycleForward" | "app.model.cycleBackward" | "app.model.select" | "app.model.selectTemporary" | "app.tools.expand" | "app.thinking.toggle" | "app.editor.external" | "app.history.search" | "app.message.dequeue" | "app.message.queue" | "app.clipboard.pasteImage" | "app.clipboard.copyPrompt">;
3
+ type ConfigurableEditorAction = Extract<AppKeybinding, "app.interrupt" | "app.clear" | "app.exit" | "app.suspend" | "app.thinking.cycle" | "app.model.cycleForward" | "app.model.cycleBackward" | "app.model.select" | "app.model.selectTemporary" | "app.tools.expand" | "app.thinking.toggle" | "app.editor.external" | "app.history.search" | "app.message.dequeue" | "app.message.followUp" | "app.message.queue" | "app.clipboard.pasteImage" | "app.clipboard.copyPrompt">;
4
4
  type PastePendingClearReason = "timeout" | "queue-limit";
5
5
  /**
6
6
  * Custom editor that handles configurable app-level shortcuts for coding-agent.
@@ -35,6 +35,8 @@ export declare class ModelSelectorComponent extends Container {
35
35
  sessionId?: string;
36
36
  isFastForProvider?: (provider?: string) => boolean;
37
37
  isFastForSubagentProvider?: (provider?: string) => boolean;
38
+ currentThinkingLevel?: ThinkingLevel;
39
+ activeModelProfile?: string;
38
40
  });
39
41
  handleInput(keyData: string): void;
40
42
  getSearchInput(): Input;
@@ -1,3 +1,4 @@
1
+ import type { GitHeadState } from "../../../utils/git";
1
2
  /**
2
3
  * Extract "owner/repo" from a GitHub remote URL.
3
4
  * Handles HTTPS, SSH (scp-style), and git:// protocols.
@@ -20,3 +21,8 @@ export declare function canReuseCachedPr(cachedPr: {
20
21
  number: number;
21
22
  url: string;
22
23
  } | null | undefined, cachedContext: PrCacheContext | undefined, currentContext: PrCacheContext | null): boolean;
24
+ export interface CurrentBranchState {
25
+ readonly branch: string | null;
26
+ readonly repoId: string | null;
27
+ }
28
+ export declare function resolveCurrentBranch(cwd: string, resolveHead?: (cwd: string) => GitHeadState | null): CurrentBranchState;
@@ -325,6 +325,105 @@ export declare const defaultThemes: {
325
325
  "preset": string;
326
326
  };
327
327
  };
328
+ "gruvbox-dark": {
329
+ $schema: string;
330
+ name: string;
331
+ vars: {
332
+ "bgHard": string;
333
+ "surface": string;
334
+ "surfaceBright": string;
335
+ "borderNeutral": string;
336
+ "borderSubtle": string;
337
+ "fg": string;
338
+ "muted": string;
339
+ "dim": string;
340
+ "gray": string;
341
+ "red": string;
342
+ "green": string;
343
+ "yellow": string;
344
+ "blue": string;
345
+ "purple": string;
346
+ "aqua": string;
347
+ "orange": string;
348
+ "diffRemovalRed": string;
349
+ };
350
+ colors: {
351
+ "accent": string;
352
+ "border": string;
353
+ "borderAccent": string;
354
+ "borderMuted": string;
355
+ "success": string;
356
+ "error": string;
357
+ "warning": string;
358
+ "muted": string;
359
+ "dim": string;
360
+ "text": string;
361
+ "thinkingText": string;
362
+ "selectedBg": string;
363
+ "userMessageBg": string;
364
+ "userMessageText": string;
365
+ "customMessageBg": string;
366
+ "customMessageText": string;
367
+ "customMessageLabel": string;
368
+ "toolPendingBg": string;
369
+ "toolSuccessBg": string;
370
+ "toolErrorBg": string;
371
+ "toolTitle": string;
372
+ "toolOutput": string;
373
+ "mdHeading": string;
374
+ "mdLink": string;
375
+ "mdLinkUrl": string;
376
+ "mdCode": string;
377
+ "mdCodeBlock": string;
378
+ "mdCodeBlockBorder": string;
379
+ "mdQuote": string;
380
+ "mdQuoteBorder": string;
381
+ "mdHr": string;
382
+ "mdListBullet": string;
383
+ "toolDiffAdded": string;
384
+ "toolDiffRemoved": string;
385
+ "toolDiffContext": string;
386
+ "syntaxComment": string;
387
+ "syntaxKeyword": string;
388
+ "syntaxFunction": string;
389
+ "syntaxVariable": string;
390
+ "syntaxString": string;
391
+ "syntaxNumber": string;
392
+ "syntaxType": string;
393
+ "syntaxOperator": string;
394
+ "syntaxPunctuation": string;
395
+ "thinkingOff": string;
396
+ "thinkingMinimal": string;
397
+ "thinkingLow": string;
398
+ "thinkingMedium": string;
399
+ "thinkingHigh": string;
400
+ "thinkingXhigh": string;
401
+ "bashMode": string;
402
+ "pythonMode": string;
403
+ "statusLineBg": string;
404
+ "statusLineSep": string;
405
+ "statusLineModel": string;
406
+ "statusLinePath": string;
407
+ "statusLineGitClean": string;
408
+ "statusLineGitDirty": string;
409
+ "statusLineContext": string;
410
+ "statusLineSpend": string;
411
+ "statusLineStaged": string;
412
+ "statusLineDirty": string;
413
+ "statusLineUntracked": string;
414
+ "statusLineOutput": string;
415
+ "statusLineCost": string;
416
+ "statusLineSubagents": string;
417
+ };
418
+ export: {
419
+ "pageBg": string;
420
+ "cardBg": string;
421
+ "infoBg": string;
422
+ };
423
+ symbols: {
424
+ "preset": string;
425
+ };
426
+ };
328
427
  opencode: {
329
428
  $schema: string;
330
429
  name: string;
@@ -0,0 +1,52 @@
1
+ export interface NotificationOperatorTimerDeps {
2
+ now?: () => number;
3
+ setTimeoutImpl?: typeof setTimeout;
4
+ clearTimeoutImpl?: typeof clearTimeout;
5
+ setIntervalImpl?: typeof setInterval;
6
+ clearIntervalImpl?: typeof clearInterval;
7
+ }
8
+ export interface NotificationOperatorRuntimeState {
9
+ running: boolean;
10
+ stopRequested: boolean;
11
+ activeAbort: boolean;
12
+ }
13
+ export interface OperatorBackoffOptions {
14
+ initialMs: number;
15
+ maxMs: number;
16
+ factor?: number;
17
+ }
18
+ export declare class OperatorBackoffPolicy {
19
+ #private;
20
+ constructor(opts: OperatorBackoffOptions);
21
+ next(): number;
22
+ reset(): void;
23
+ get currentMs(): number;
24
+ }
25
+ export interface OperatorRoute<TContext> {
26
+ name: string;
27
+ matches(event: Record<string, unknown>): boolean;
28
+ handle(context: TContext, event: Record<string, unknown>): Promise<void> | void;
29
+ }
30
+ export declare class OperatorEventRouter<TContext> {
31
+ readonly routes: OperatorRoute<TContext>[];
32
+ add(input: OperatorRoute<TContext>): this;
33
+ dispatch(context: TContext, event: Record<string, unknown>): Promise<boolean>;
34
+ }
35
+ export declare class NotificationOperatorRuntime {
36
+ #private;
37
+ constructor(deps?: NotificationOperatorTimerDeps);
38
+ get state(): NotificationOperatorRuntimeState;
39
+ start(): void;
40
+ stop(): void;
41
+ requestStop(): void;
42
+ get running(): boolean;
43
+ get stopRequested(): boolean;
44
+ createAbortController(): AbortController;
45
+ clearAbortController(controller: AbortController): void;
46
+ startInterval(name: string, intervalMs: number, tick: () => void): void;
47
+ stopInterval(name: string): void;
48
+ stopAllIntervals(): void;
49
+ runExclusive(name: string, fn: () => Promise<void>): Promise<void>;
50
+ sleep(ms: number, signal?: AbortSignal): Promise<void>;
51
+ now(): number;
52
+ }
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import type { Settings } from "../config/settings";
3
3
  import type { DaemonRuntimeInfo } from "../daemon/control-types";
4
+ import { NotificationOperatorRuntime, OperatorBackoffPolicy } from "./operator-runtime";
4
5
  import { type AliasTable, type CallbackRoute, type PendingAsk } from "./telegram-reference";
5
6
  export type EnsureDaemonResult = "owner_spawned" | "attached" | "disabled";
6
7
  export interface DaemonState {
@@ -146,6 +147,40 @@ export interface BotApi {
146
147
  signal?: AbortSignal;
147
148
  }): Promise<unknown>;
148
149
  }
150
+ export interface TelegramTransportOptions {
151
+ botToken: string;
152
+ apiBase?: string;
153
+ fetchImpl?: typeof fetch;
154
+ setTimeoutImpl?: typeof setTimeout;
155
+ }
156
+ /** Telegram Bot API transport: HTTP JSON/multipart details stay out of daemon orchestration. */
157
+ export declare class TelegramBotTransport implements BotApi {
158
+ #private;
159
+ constructor(opts: TelegramTransportOptions);
160
+ call(method: string, body: unknown, opts?: {
161
+ signal?: AbortSignal;
162
+ }): Promise<unknown>;
163
+ }
164
+ export interface TelegramUpdatePollerOptions {
165
+ botApi: BotApi;
166
+ runtime: NotificationOperatorRuntime;
167
+ backoff: OperatorBackoffPolicy;
168
+ processUpdate: (update: unknown) => Promise<void>;
169
+ }
170
+ /** Owns getUpdates offset, conflict backoff, and per-update error isolation. */
171
+ export declare class TelegramUpdatePoller {
172
+ #private;
173
+ constructor(opts: TelegramUpdatePollerOptions);
174
+ pollOnce(signal?: AbortSignal): Promise<number>;
175
+ }
176
+ /** Mutable dispatch state shared by session frames and inbound Telegram updates. */
177
+ export declare class TelegramEventDispatchState {
178
+ readonly busy: Set<string>;
179
+ readonly inboundReactions: Map<number, {
180
+ messageId: number;
181
+ }>;
182
+ readonly seenUpdateIds: Set<number>;
183
+ }
149
184
  /**
150
185
  * Cooperative control seam for the daemon run loop. Implemented by the
151
186
  * daemon-internal CLI / controller against the owner-scoped control-request
@@ -199,33 +234,31 @@ export declare class TelegramNotificationDaemon {
199
234
  readonly aliasTable: AliasTable;
200
235
  readonly messageRoutes: Map<string | number, CallbackRoute | Omit<CallbackRoute, "answer">>;
201
236
  readonly sessions: Map<string, SessionSocket>;
237
+ private readonly runtime;
238
+ private readonly sessionRouter;
239
+ private readonly pollConflictBackoff;
240
+ private readonly loopBackoff;
202
241
  private running;
203
- private offset;
204
242
  private readonly fsImpl;
205
243
  private readonly botApi;
206
244
  private readonly topics;
207
245
  private readonly pool;
208
- private readonly seenUpdateIds;
246
+ private readonly poller;
247
+ private readonly dispatchState;
248
+ /** Identity-bearing sessions by repo/branch surface, used to avoid transient duplicate topics. */
249
+ private readonly topicOwnerByIdentity;
250
+ /** Non-identity frames held until identity creates the correct thread. */
251
+ private readonly pendingThreadedFrames;
209
252
  /** True once the daemon has nudged the user to enable Threaded Mode. */
210
253
  private threadedFallbackNoticeSent;
211
254
  /** Sessions whose identity header was already sent flat (Threaded Mode off). */
212
255
  private readonly flatIdentitySent;
213
256
  /** Cached result of whether the paired chat is a private chat (flat-fallback gate). */
214
257
  private pairedChatPrivate;
215
- private flushTimer;
216
- private scanTimer;
217
- private scanning;
218
- private typingTimer;
219
258
  /** Sessions whose agent loop is currently busy (drives the typing indicator). */
220
- private readonly busy;
259
+ private get busy();
221
260
  /** Inbound update id → originating Telegram message, for delivery reactions. */
222
- private readonly inboundReactions;
223
- /** AbortController for the in-flight long poll; aborted by requestStop() to wake the loop. */
224
- private activePoll;
225
- /** Set when a cooperative stop has been requested (signal or control request). */
226
- private stopRequested;
227
- /** Current bounded backoff after a Telegram getUpdates 409 conflict (0 when healthy). */
228
- private pollConflictBackoffMs;
261
+ private get inboundReactions();
229
262
  /**
230
263
  * Cooperatively stop the daemon: set the stop flag and abort the in-flight
231
264
  * long poll so the run loop wakes immediately instead of waiting out the
@@ -233,6 +266,7 @@ export declare class TelegramNotificationDaemon {
233
266
  */
234
267
  requestStop(_reason?: "reload" | "stop" | "signal"): void;
235
268
  constructor(opts: TelegramDaemonOptions);
269
+ private createSessionRouter;
236
270
  loadAliases(): Promise<void>;
237
271
  persistAliases(): Promise<void>;
238
272
  scanRoots(): Promise<void>;
@@ -254,6 +288,12 @@ export declare class TelegramNotificationDaemon {
254
288
  private dropSession;
255
289
  private static readonly THREADED_FRAMES;
256
290
  private topicNameFor;
291
+ private topicIdentityKey;
292
+ private topicIdentityBase;
293
+ private topicOwnerForIdentity;
294
+ private submitThreadedFrame;
295
+ private rememberPendingThreadedFrame;
296
+ private flushPendingThreadedFrames;
257
297
  private ensureTopic;
258
298
  private persistTopics;
259
299
  loadTopics(): Promise<void>;
@@ -285,8 +325,6 @@ export declare class TelegramNotificationDaemon {
285
325
  private sendStaleGuidance;
286
326
  handleTelegramUpdate(update: unknown): Promise<void>;
287
327
  pollOnce(signal?: AbortSignal): Promise<number>;
288
- /** Abortable sleep honoring the injected timer; resolves early on abort. */
289
- private sleep;
290
328
  /** Sync the bot's Telegram command menu to what the daemon actually handles. */
291
329
  registerBotCommands(): Promise<void>;
292
330
  run(): Promise<void>;
@@ -45,6 +45,8 @@ export declare class TopicRegistry {
45
45
  load(state: TopicRegistryState): void;
46
46
  /** Resolve the owning session for a topic id (for fail-closed inbound routing). */
47
47
  sessionForTopic(topicId: string): string | undefined;
48
+ /** All session ids with a persisted topic record. */
49
+ sessionIds(): string[];
48
50
  /** The existing topic record for a session, if any. */
49
51
  get(sessionId: string): TopicRecord | undefined;
50
52
  /**
@@ -0,0 +1,14 @@
1
+ export declare const COMPOSER_BASH_POLICY_ERROR = "Composer bash policy blocked repository file I/O. Use find, search, read, and edit tools for file discovery, file inspection, and file mutation.";
2
+ type ComposerBashPolicyResult = {
3
+ allowed: true;
4
+ } | {
5
+ allowed: false;
6
+ reason: string;
7
+ message: string;
8
+ };
9
+ export declare function isComposerBashPolicyModel(modelId: string | undefined): boolean;
10
+ export declare function checkComposerBashPolicy(input: {
11
+ modelId?: string;
12
+ commands: readonly string[];
13
+ }): ComposerBashPolicyResult;
14
+ export {};
@@ -13,10 +13,13 @@ export type AddressResolver = (hostname: string) => Promise<string[]>;
13
13
  /** True for any address that is not a routable public unicast address. */
14
14
  export declare function isPrivateOrSpecialAddress(address: string): boolean;
15
15
  /**
16
- * Validate that `rawUrl` is a public http/https target safe to hand to the
17
- * insane-search engine. Resolves DNS names and rejects any that map to a
18
- * private/special address. Never throws; returns a discriminated result.
16
+ * Validate that `rawUrl` is a public http/https target. Resolves DNS names and
17
+ * rejects any that map to a private/special address. Never throws; returns a
18
+ * discriminated result.
19
19
  */
20
+ export declare function validatePublicHttpUrl(rawUrl: string, options?: {
21
+ resolver?: AddressResolver;
22
+ }): Promise<PublicUrlResult>;
20
23
  export declare function validatePublicHttpUrlForInsane(rawUrl: string, options?: {
21
24
  resolver?: AddressResolver;
22
25
  }): Promise<PublicUrlResult>;
@@ -1,4 +1,5 @@
1
1
  import type { AgentStorage } from "../../session/agent-storage";
2
+ import type { AddressResolver } from "../insane/url-guard";
2
3
  export { formatNumber } from "@gajae-code/utils";
3
4
  export interface RenderResult {
4
5
  url: string;
@@ -27,6 +28,9 @@ export interface LoadPageOptions {
27
28
  body?: string;
28
29
  maxBytes?: number;
29
30
  signal?: AbortSignal;
31
+ publicUrlGuard?: boolean;
32
+ resolver?: AddressResolver;
33
+ maxRedirects?: number;
30
34
  }
31
35
  export interface LoadPageResult {
32
36
  content: string;
@@ -34,6 +38,7 @@ export interface LoadPageResult {
34
38
  finalUrl: string;
35
39
  ok: boolean;
36
40
  status?: number;
41
+ error?: string;
37
42
  }
38
43
  /**
39
44
  * Fetch a page with timeout and size limit
@@ -1,5 +1,6 @@
1
1
  import { isRecord } from "@gajae-code/utils";
2
2
  export { isRecord };
3
+ import type { AddressResolver } from "../insane/url-guard";
3
4
  export declare function asRecord(value: unknown): Record<string, unknown> | null;
4
5
  export declare function asString(value: unknown): string | null;
5
6
  export declare function asNumber(value: unknown): number | null;
@@ -12,10 +13,15 @@ export type BinaryFetchResult = BinaryFetchSuccess | {
12
13
  ok: false;
13
14
  error?: string;
14
15
  };
16
+ export interface FetchBinaryOptions {
17
+ publicUrlGuard?: boolean;
18
+ resolver?: AddressResolver;
19
+ maxRedirects?: number;
20
+ }
15
21
  /**
16
22
  * Fetch binary content from a URL
17
23
  */
18
- export declare function fetchBinary(url: string, timeout?: number, signal?: AbortSignal): Promise<BinaryFetchResult>;
24
+ export declare function fetchBinary(url: string, timeout?: number, signal?: AbortSignal, options?: FetchBinaryOptions): Promise<BinaryFetchResult>;
19
25
  /**
20
26
  * Convert binary content to markdown using markit.
21
27
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.7.2",
4
+ "version": "0.7.3",
5
5
  "description": "Gajae Code CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -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
- "@gajae-code/stats": "0.7.2",
56
- "@gajae-code/agent-core": "0.7.2",
57
- "@gajae-code/ai": "0.7.2",
58
- "@gajae-code/natives": "0.7.2",
59
- "@gajae-code/tui": "0.7.2",
60
- "@gajae-code/utils": "0.7.2",
55
+ "@gajae-code/stats": "0.7.3",
56
+ "@gajae-code/agent-core": "0.7.3",
57
+ "@gajae-code/ai": "0.7.3",
58
+ "@gajae-code/natives": "0.7.3",
59
+ "@gajae-code/tui": "0.7.3",
60
+ "@gajae-code/utils": "0.7.3",
61
61
  "@puppeteer/browsers": "^2.13.0",
62
62
  "@types/turndown": "5.0.6",
63
63
  "@xterm/headless": "^6.0.0",