@gajae-code/coding-agent 0.7.1 → 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 (135) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/types/cli/mcp-cli.d.ts +25 -0
  3. package/dist/types/cli/notify-cli.d.ts +2 -0
  4. package/dist/types/cli.d.ts +6 -0
  5. package/dist/types/commands/mcp.d.ts +70 -0
  6. package/dist/types/config/keybindings.d.ts +2 -2
  7. package/dist/types/config/settings-schema.d.ts +39 -2
  8. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  9. package/dist/types/extensibility/shared-events.d.ts +1 -0
  10. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +1 -1
  11. package/dist/types/lsp/types.d.ts +2 -0
  12. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  13. package/dist/types/modes/components/model-selector.d.ts +2 -0
  14. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  15. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  16. package/dist/types/notifications/attachment-registry.d.ts +17 -0
  17. package/dist/types/notifications/chat-adapters.d.ts +9 -0
  18. package/dist/types/notifications/config.d.ts +9 -1
  19. package/dist/types/notifications/engine.d.ts +59 -0
  20. package/dist/types/notifications/managed-daemon.d.ts +48 -0
  21. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  22. package/dist/types/notifications/telegram-daemon.d.ts +73 -16
  23. package/dist/types/notifications/threaded-inbound.d.ts +19 -0
  24. package/dist/types/notifications/threaded-render.d.ts +6 -1
  25. package/dist/types/notifications/topic-registry.d.ts +2 -0
  26. package/dist/types/session/agent-session.d.ts +2 -0
  27. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  28. package/dist/types/tools/fetch.d.ts +23 -0
  29. package/dist/types/tools/index.d.ts +1 -0
  30. package/dist/types/tools/telegram-send.d.ts +32 -0
  31. package/dist/types/web/insane/bridge.d.ts +103 -0
  32. package/dist/types/web/insane/url-guard.d.ts +25 -0
  33. package/dist/types/web/scrapers/types.d.ts +5 -0
  34. package/dist/types/web/scrapers/utils.d.ts +7 -1
  35. package/dist/types/web/search/provider.d.ts +18 -1
  36. package/dist/types/web/search/providers/insane.d.ts +53 -0
  37. package/dist/types/web/search/providers/text-citations.d.ts +23 -0
  38. package/dist/types/web/search/types.d.ts +12 -4
  39. package/package.json +10 -8
  40. package/scripts/verify-insane-vendor.ts +132 -0
  41. package/src/cli/args.ts +1 -1
  42. package/src/cli/fast-help.ts +1 -1
  43. package/src/cli/mcp-cli.ts +272 -0
  44. package/src/cli/notify-cli.ts +152 -5
  45. package/src/cli.ts +6 -2
  46. package/src/commands/mcp.ts +117 -0
  47. package/src/commands/team.ts +1 -1
  48. package/src/config/keybindings.ts +2 -2
  49. package/src/config/settings-schema.ts +30 -1
  50. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  51. package/src/defaults/gjc/skills/deep-interview/SKILL.md +4 -3
  52. package/src/defaults/gjc/skills/ralplan/SKILL.md +11 -4
  53. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  54. package/src/extensibility/extensions/runner.ts +1 -0
  55. package/src/extensibility/shared-events.ts +1 -0
  56. package/src/gjc-runtime/launch-tmux.ts +17 -3
  57. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  58. package/src/gjc-runtime/ralplan-runtime.ts +2 -2
  59. package/src/gjc-runtime/tmux-common.ts +3 -1
  60. package/src/gjc-runtime/ultragoal-guard.ts +25 -8
  61. package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
  62. package/src/gjc-runtime/workflow-manifest.ts +7 -2
  63. package/src/hooks/skill-state.ts +57 -0
  64. package/src/internal-urls/docs-index.generated.ts +14 -11
  65. package/src/lsp/config.ts +16 -3
  66. package/src/lsp/defaults.json +7 -0
  67. package/src/lsp/types.ts +2 -0
  68. package/src/modes/bridge/bridge-mode.ts +11 -0
  69. package/src/modes/components/custom-editor.ts +2 -0
  70. package/src/modes/components/footer.ts +2 -3
  71. package/src/modes/components/model-selector.ts +12 -0
  72. package/src/modes/components/status-line/git-utils.ts +25 -0
  73. package/src/modes/components/status-line.ts +10 -11
  74. package/src/modes/components/welcome.ts +2 -3
  75. package/src/modes/controllers/event-controller.ts +15 -0
  76. package/src/modes/controllers/selector-controller.ts +3 -0
  77. package/src/modes/interactive-mode.ts +48 -3
  78. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  79. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  80. package/src/modes/theme/defaults/index.ts +2 -0
  81. package/src/modes/utils/context-usage.ts +2 -2
  82. package/src/notifications/attachment-registry.ts +23 -0
  83. package/src/notifications/chat-adapters.ts +147 -0
  84. package/src/notifications/config.ts +23 -2
  85. package/src/notifications/engine.ts +100 -0
  86. package/src/notifications/index.ts +180 -38
  87. package/src/notifications/managed-daemon.ts +163 -0
  88. package/src/notifications/operator-runtime.ts +171 -0
  89. package/src/notifications/telegram-daemon.ts +553 -236
  90. package/src/notifications/threaded-inbound.ts +60 -4
  91. package/src/notifications/threaded-render.ts +20 -2
  92. package/src/notifications/topic-registry.ts +5 -0
  93. package/src/session/agent-session.ts +82 -51
  94. package/src/slash-commands/helpers/parse.ts +2 -1
  95. package/src/tools/bash.ts +9 -0
  96. package/src/tools/composer-bash-policy.ts +96 -0
  97. package/src/tools/fetch.ts +94 -1
  98. package/src/tools/index.ts +3 -0
  99. package/src/tools/telegram-send.ts +137 -0
  100. package/src/web/insane/bridge.ts +350 -0
  101. package/src/web/insane/url-guard.ts +159 -0
  102. package/src/web/scrapers/types.ts +143 -45
  103. package/src/web/scrapers/utils.ts +70 -19
  104. package/src/web/search/provider.ts +77 -18
  105. package/src/web/search/providers/anthropic.ts +70 -3
  106. package/src/web/search/providers/codex.ts +1 -119
  107. package/src/web/search/providers/gemini.ts +99 -0
  108. package/src/web/search/providers/insane.ts +551 -0
  109. package/src/web/search/providers/openai-compatible.ts +66 -32
  110. package/src/web/search/providers/text-citations.ts +111 -0
  111. package/src/web/search/types.ts +13 -2
  112. package/vendor/insane-search/LICENSE +21 -0
  113. package/vendor/insane-search/MANIFEST.json +24 -0
  114. package/vendor/insane-search/engine/__init__.py +23 -0
  115. package/vendor/insane-search/engine/__main__.py +128 -0
  116. package/vendor/insane-search/engine/bias_check.py +183 -0
  117. package/vendor/insane-search/engine/executor.py +254 -0
  118. package/vendor/insane-search/engine/fetch_chain.py +725 -0
  119. package/vendor/insane-search/engine/learning.py +175 -0
  120. package/vendor/insane-search/engine/phase0.py +214 -0
  121. package/vendor/insane-search/engine/safety.py +91 -0
  122. package/vendor/insane-search/engine/templates/package.json +11 -0
  123. package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
  124. package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
  125. package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
  126. package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
  127. package/vendor/insane-search/engine/tests/test_u1.py +200 -0
  128. package/vendor/insane-search/engine/tests/test_u4.py +131 -0
  129. package/vendor/insane-search/engine/tests/test_u5.py +163 -0
  130. package/vendor/insane-search/engine/tests/test_u7.py +124 -0
  131. package/vendor/insane-search/engine/transport.py +211 -0
  132. package/vendor/insane-search/engine/url_transforms.py +98 -0
  133. package/vendor/insane-search/engine/validators.py +331 -0
  134. package/vendor/insane-search/engine/waf_detector.py +214 -0
  135. package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
@@ -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,27 +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;
209
- private flushTimer;
210
- private scanTimer;
211
- private scanning;
212
- private typingTimer;
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;
252
+ /** True once the daemon has nudged the user to enable Threaded Mode. */
253
+ private threadedFallbackNoticeSent;
254
+ /** Sessions whose identity header was already sent flat (Threaded Mode off). */
255
+ private readonly flatIdentitySent;
256
+ /** Cached result of whether the paired chat is a private chat (flat-fallback gate). */
257
+ private pairedChatPrivate;
213
258
  /** Sessions whose agent loop is currently busy (drives the typing indicator). */
214
- private readonly busy;
259
+ private get busy();
215
260
  /** Inbound update id → originating Telegram message, for delivery reactions. */
216
- private readonly inboundReactions;
217
- /** AbortController for the in-flight long poll; aborted by requestStop() to wake the loop. */
218
- private activePoll;
219
- /** Set when a cooperative stop has been requested (signal or control request). */
220
- private stopRequested;
221
- /** Current bounded backoff after a Telegram getUpdates 409 conflict (0 when healthy). */
222
- private pollConflictBackoffMs;
261
+ private get inboundReactions();
223
262
  /**
224
263
  * Cooperatively stop the daemon: set the stop flag and abort the in-flight
225
264
  * long poll so the run loop wakes immediately instead of waiting out the
@@ -227,6 +266,7 @@ export declare class TelegramNotificationDaemon {
227
266
  */
228
267
  requestStop(_reason?: "reload" | "stop" | "signal"): void;
229
268
  constructor(opts: TelegramDaemonOptions);
269
+ private createSessionRouter;
230
270
  loadAliases(): Promise<void>;
231
271
  persistAliases(): Promise<void>;
232
272
  scanRoots(): Promise<void>;
@@ -248,10 +288,29 @@ export declare class TelegramNotificationDaemon {
248
288
  private dropSession;
249
289
  private static readonly THREADED_FRAMES;
250
290
  private topicNameFor;
291
+ private topicIdentityKey;
292
+ private topicIdentityBase;
293
+ private topicOwnerForIdentity;
294
+ private submitThreadedFrame;
295
+ private rememberPendingThreadedFrame;
296
+ private flushPendingThreadedFrames;
251
297
  private ensureTopic;
252
298
  private persistTopics;
253
299
  loadTopics(): Promise<void>;
300
+ private downloadTelegramFile;
301
+ /**
302
+ * Per-session private temp directories (mode 0700) holding inbound non-image
303
+ * attachments. Keyed by session id and reused across transient reconnects;
304
+ * removed when the daemon stops (see {@link cleanupAllAttachmentDirs}).
305
+ */
306
+ private readonly attachmentDirs;
307
+ private ensureAttachmentDir;
308
+ private cleanupAllAttachmentDirs;
309
+ private resolveInboundAttachment;
254
310
  private flushPool;
311
+ private deliverFlatFallback;
312
+ private pairedChatIsPrivate;
313
+ private notifyThreadedFallback;
255
314
  private startFlushTimer;
256
315
  private stopFlushTimer;
257
316
  private runScan;
@@ -266,8 +325,6 @@ export declare class TelegramNotificationDaemon {
266
325
  private sendStaleGuidance;
267
326
  handleTelegramUpdate(update: unknown): Promise<void>;
268
327
  pollOnce(signal?: AbortSignal): Promise<number>;
269
- /** Abortable sleep honoring the injected timer; resolves early on abort. */
270
- private sleep;
271
328
  /** Sync the bot's Telegram command menu to what the daemon actually handles. */
272
329
  registerBotCommands(): Promise<void>;
273
330
  run(): Promise<void>;
@@ -20,12 +20,30 @@ export interface InboundUpdate {
20
20
  message?: {
21
21
  message_id?: unknown;
22
22
  text?: unknown;
23
+ caption?: unknown;
24
+ photo?: unknown;
25
+ document?: unknown;
26
+ video?: unknown;
27
+ audio?: unknown;
28
+ voice?: unknown;
29
+ animation?: unknown;
23
30
  chat?: {
24
31
  id?: unknown;
25
32
  };
26
33
  message_thread_id?: unknown;
27
34
  };
28
35
  }
36
+ /** A downloadable media attachment referenced by an inbound message. */
37
+ export interface InboundAttachment {
38
+ /** Telegram file_id to resolve via getFile. */
39
+ fileId: string;
40
+ /** Source media kind; "photo" is always an image. */
41
+ kind: "photo" | "document" | "video" | "audio" | "voice" | "animation";
42
+ /** MIME type when Telegram provides one. */
43
+ mime?: string;
44
+ /** Original file name when provided. */
45
+ fileName?: string;
46
+ }
29
47
  /** Context for {@link decideThreadedInbound}. All lookups are injected. */
30
48
  export interface ThreadedInboundCtx {
31
49
  /** The single paired chat id (string-compared). */
@@ -43,6 +61,7 @@ export type ThreadedInboundDecision = {
43
61
  updateId: number;
44
62
  threadId: string;
45
63
  messageId?: number;
64
+ attachment?: InboundAttachment;
46
65
  } | {
47
66
  kind: "duplicate";
48
67
  updateId: number;
@@ -11,15 +11,19 @@
11
11
  import type { RateLimitLane } from "./rate-limit-pool";
12
12
  /** A Telegram send derived from a threaded frame (topic id is applied by the daemon). */
13
13
  export interface ThreadedSend {
14
- method: "sendMessage" | "sendPhoto";
14
+ method: "sendMessage" | "sendPhoto" | "sendDocument";
15
15
  /** Rate-limit lane for prioritisation/fairness. */
16
16
  lane: RateLimitLane;
17
17
  /** Message text (sendMessage) or photo caption (sendPhoto). */
18
18
  text?: string;
19
19
  /** Base64 image bytes for sendPhoto. */
20
20
  photoBase64?: string;
21
+ /** Base64 file bytes for sendDocument. */
22
+ documentBase64?: string;
21
23
  /** Image MIME type for sendPhoto. */
22
24
  mime?: string;
25
+ /** Suggested document filename. */
26
+ fileName?: string;
23
27
  /** Coalesce key for live edits (same key collapses to the latest). */
24
28
  coalesceKey?: string;
25
29
  /** True for the one-time identity header (the daemon pins it once). */
@@ -45,6 +49,7 @@ interface ThreadedFrame {
45
49
  data?: unknown;
46
50
  mime?: unknown;
47
51
  caption?: unknown;
52
+ name?: unknown;
48
53
  verbosity?: unknown;
49
54
  redact?: unknown;
50
55
  }
@@ -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
  /**
@@ -81,6 +81,7 @@ import type { BranchSummaryEntry, NewSessionOptions, SessionContext, SessionMana
81
81
  import { ToolChoiceQueue } from "./tool-choice-queue";
82
82
  import { YieldQueue } from "./yield-queue";
83
83
  /** Session-specific events that extend the core AgentEvent */
84
+ export type AutoCompactionContinuationSkipReason = "auto_continue_disabled_non_resumable_tail";
84
85
  export type AgentSessionEvent = AgentEvent | {
85
86
  type: "auto_compaction_start";
86
87
  reason: "threshold" | "overflow" | "idle";
@@ -94,6 +95,7 @@ export type AgentSessionEvent = AgentEvent | {
94
95
  errorMessage?: string;
95
96
  /** True when compaction was skipped for a benign reason (no model, no candidates, nothing to compact). */
96
97
  skipped?: boolean;
98
+ continuationSkipReason?: AutoCompactionContinuationSkipReason;
97
99
  } | {
98
100
  type: "auto_retry_start";
99
101
  attempt: number;
@@ -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 {};
@@ -1,8 +1,10 @@
1
1
  import type { AgentToolResult } from "@gajae-code/agent-core";
2
2
  import { type Component } from "@gajae-code/tui";
3
+ import type { Settings } from "../config/settings";
3
4
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
4
5
  import { type Theme } from "../modes/theme/theme";
5
6
  import type { ToolSession } from "../sdk";
7
+ import type { RenderResult } from "../web/scrapers/types";
6
8
  import { type OutputMeta } from "./output-meta";
7
9
  export declare function isReadableUrlPath(value: string): boolean;
8
10
  export interface ParsedReadUrlTarget {
@@ -16,6 +18,27 @@ interface FetchImagePayload {
16
18
  data: string;
17
19
  mimeType: string;
18
20
  }
21
+ type FetchRenderResult = RenderResult & {
22
+ image?: FetchImagePayload;
23
+ };
24
+ /**
25
+ * Opt-in insane-search fallback for blocked / degraded public URL reads.
26
+ *
27
+ * Returns a finalized `method: "insane"` result on success, or null (so the
28
+ * caller continues with its normal degraded behavior). Fail-closed: no note,
29
+ * guard DNS, dependency probe, or subprocess when raw mode or the opt-in
30
+ * setting is off. The public-URL guard runs BEFORE any probe/spawn.
31
+ */
32
+ export declare function tryInsaneFallback(args: {
33
+ url: string;
34
+ finalUrl: string;
35
+ timeout: number;
36
+ raw: boolean;
37
+ settings: Settings;
38
+ signal: AbortSignal | undefined;
39
+ fetchedAt: string;
40
+ notes: string[];
41
+ }): Promise<FetchRenderResult | null>;
19
42
  export interface ReadUrlToolDetails {
20
43
  kind: "url";
21
44
  url: string;
@@ -55,6 +55,7 @@ export * from "./search-tool-bm25";
55
55
  export * from "./skill";
56
56
  export * from "./ssh";
57
57
  export * from "./subagent";
58
+ export * from "./telegram-send";
58
59
  export * from "./todo-write";
59
60
  export * from "./vim";
60
61
  export * from "./write";
@@ -0,0 +1,32 @@
1
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
2
+ import { z } from "zod/v4";
3
+ import type { ToolSession } from "./index";
4
+ declare const telegramSendSchema: z.ZodObject<{
5
+ path: z.ZodString;
6
+ caption: z.ZodOptional<z.ZodString>;
7
+ }, z.core.$strip>;
8
+ type TelegramSendParams = z.infer<typeof telegramSendSchema>;
9
+ interface TelegramSendDetails {
10
+ path: string;
11
+ caption?: string;
12
+ ok: boolean;
13
+ error?: string;
14
+ }
15
+ export declare class TelegramSendTool implements AgentTool<typeof telegramSendSchema, TelegramSendDetails> {
16
+ private readonly session;
17
+ readonly name = "telegram_send";
18
+ readonly label = "TelegramSend";
19
+ readonly summary = "Send a workspace file to Telegram";
20
+ readonly loadMode = "discoverable";
21
+ readonly description: string;
22
+ readonly parameters: z.ZodObject<{
23
+ path: z.ZodString;
24
+ caption: z.ZodOptional<z.ZodString>;
25
+ }, z.core.$strip>;
26
+ readonly strict = true;
27
+ constructor(session: ToolSession);
28
+ static createIf(session: ToolSession): TelegramSendTool | null;
29
+ private resolveContainedFile;
30
+ execute(_toolCallId: string, params: TelegramSendParams, _signal?: AbortSignal, _onUpdate?: AgentToolUpdateCallback<TelegramSendDetails>, _context?: AgentToolContext): Promise<AgentToolResult<TelegramSendDetails>>;
31
+ }
32
+ export {};
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Bridge from TypeScript to the vendored insane-search Python engine.
3
+ *
4
+ * Invokes `python3 -m engine "<url>" --json` per fallback attempt (cwd + PYTHONPATH
5
+ * pointed at the vendored engine), validates the JSON envelope, and maps it onto a
6
+ * discriminated result. Hardened: clamped timeout, AbortSignal propagation that
7
+ * kills+reaps the child, bounded stdout/stderr capture, and a per-process
8
+ * concurrency cap so blocked reads cannot fork-storm.
9
+ *
10
+ * Fail-closed: missing dependencies / bad output / auth-required never throw past
11
+ * the caller and never auto-install anything; they return ok:false with a stable,
12
+ * bounded note so `read` can continue with its normal degraded result.
13
+ */
14
+ import { spawn as nodeSpawn } from "node:child_process";
15
+ /** packages/coding-agent/vendor/insane-search */
16
+ export declare const INSANE_VENDOR_DIR: string;
17
+ /** Stable note prefixes — tests assert on these without depending on full stderr. */
18
+ export declare const INSANE_NOTES: {
19
+ readonly guardBlocked: (reason: string) => string;
20
+ readonly vendorMissing: `insane fallback unavailable: vendor engine missing at packages/coding-agent/vendor/insane-search`;
21
+ readonly noPython: `insane fallback unavailable: python3 not found; install python3 and curl_cffi, then retry with web.insaneFallback=true`;
22
+ readonly noCurlCffi: `insane fallback unavailable: python3 cannot import curl_cffi; install curl_cffi for Phase 0-2`;
23
+ readonly noBrowser: `insane fallback unavailable: node/playwright/stealth dependencies missing for Phase 3; install dependencies under packages/coding-agent/vendor/insane-search/engine/templates`;
24
+ readonly timeout: (seconds: number) => string;
25
+ readonly invalidJson: `insane fallback failed: engine returned invalid JSON`;
26
+ readonly authRequired: `insane fallback stopped: authentication required`;
27
+ readonly verdict: (verdict: string) => string;
28
+ readonly untried: (routes: string) => string;
29
+ readonly mustBrowserMcp: `insane fallback requires browser MCP/manual phase: must_invoke_playwright_mcp=true`;
30
+ readonly concurrency: `insane fallback skipped: max concurrent engine attempts reached`;
31
+ readonly emptyContent: `insane fallback failed: engine reported ok but returned no content`;
32
+ };
33
+ /** Raw JSON envelope produced by `python3 -m engine --json`. */
34
+ export interface InsaneFetchResultRaw {
35
+ ok?: boolean;
36
+ verdict?: string;
37
+ content?: string;
38
+ profile_used?: string;
39
+ trace?: unknown;
40
+ untried_routes?: string[];
41
+ must_invoke_playwright_mcp?: boolean;
42
+ }
43
+ export interface InsaneSuccess {
44
+ ok: true;
45
+ content: string;
46
+ profileUsed?: string;
47
+ notes: string[];
48
+ }
49
+ export interface InsaneFailure {
50
+ ok: false;
51
+ reason: string;
52
+ verdict?: string;
53
+ notes: string[];
54
+ }
55
+ export type InsaneBridgeResult = InsaneSuccess | InsaneFailure;
56
+ export interface EngineInvocation {
57
+ url: string;
58
+ timeoutMs: number;
59
+ signal?: AbortSignal;
60
+ }
61
+ export interface EngineRawOutput {
62
+ code: number | null;
63
+ stdout: string;
64
+ stderr: string;
65
+ timedOut: boolean;
66
+ aborted: boolean;
67
+ }
68
+ /** Seam: run the engine subprocess. Default spawns python3. */
69
+ export type EngineRunner = (inv: EngineInvocation) => Promise<EngineRawOutput>;
70
+ export interface InsaneDependencyStatus {
71
+ vendorPresent: boolean;
72
+ python: boolean;
73
+ curlCffi: boolean;
74
+ browser: boolean;
75
+ }
76
+ /** Seam: probe dependencies. Default probes the real environment (cached). */
77
+ export type DependencyProber = () => Promise<InsaneDependencyStatus>;
78
+ type SpawnImpl = typeof nodeSpawn;
79
+ /** Real engine runner: `python3 -m engine "<url>" --json`. */
80
+ export declare function runEngineSubprocess(inv: EngineInvocation, options?: {
81
+ spawnImpl?: SpawnImpl;
82
+ }): Promise<EngineRawOutput>;
83
+ /** Reset the probe cache between tests so probe state never leaks. */
84
+ export declare function resetInsaneProbeCacheForTest(): void;
85
+ /** Probe (and cache) the insane-search runtime dependencies. */
86
+ export declare function probeInsaneDependencies(): Promise<InsaneDependencyStatus>;
87
+ export declare function resetInsaneConcurrencyForTest(): void;
88
+ export interface TryInsaneFetchOptions {
89
+ timeoutMs?: number;
90
+ signal?: AbortSignal;
91
+ concurrencyLimit?: number;
92
+ /** Seam: dependency prober (default real, cached). */
93
+ prober?: DependencyProber;
94
+ /** Seam: engine runner (default real subprocess). */
95
+ runner?: EngineRunner;
96
+ }
97
+ /**
98
+ * Attempt to read `url` through the insane-search engine. The caller is
99
+ * responsible for the opt-in gate, raw-mode skip, and the public-URL guard
100
+ * (which MUST run before this is called). Never throws; always returns a result.
101
+ */
102
+ export declare function tryInsaneFetch(url: string, options?: TryInsaneFetchOptions): Promise<InsaneBridgeResult>;
103
+ export {};
@@ -0,0 +1,25 @@
1
+ export interface PublicUrlAccepted {
2
+ ok: true;
3
+ url: URL;
4
+ addresses: string[];
5
+ }
6
+ export interface PublicUrlRejected {
7
+ ok: false;
8
+ reason: string;
9
+ }
10
+ export type PublicUrlResult = PublicUrlAccepted | PublicUrlRejected;
11
+ /** Resolver seam so tests can inject DNS results without real lookups. */
12
+ export type AddressResolver = (hostname: string) => Promise<string[]>;
13
+ /** True for any address that is not a routable public unicast address. */
14
+ export declare function isPrivateOrSpecialAddress(address: string): boolean;
15
+ /**
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
+ */
20
+ export declare function validatePublicHttpUrl(rawUrl: string, options?: {
21
+ resolver?: AddressResolver;
22
+ }): Promise<PublicUrlResult>;
23
+ export declare function validatePublicHttpUrlForInsane(rawUrl: string, options?: {
24
+ resolver?: AddressResolver;
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
  */
@@ -20,5 +20,22 @@ export declare function looksHostedModelId(modelId: string | undefined): boolean
20
20
  export declare function isLocalBaseUrl(baseUrl: string | undefined): boolean;
21
21
  export declare function inferNativeProviderFromModel(ctx: ActiveSearchModelContext | undefined): SearchProviderId | undefined;
22
22
  export declare function canUseGenericCredentials(authStorage: AuthStorage, ctx: ActiveSearchModelContext | undefined, sessionId?: string, signal?: AbortSignal): Promise<boolean>;
23
- export declare function shouldTryGenericOpenAICompat(authStorage: AuthStorage, ctx: ActiveSearchModelContext | undefined, sessionId?: string, signal?: AbortSignal): Promise<boolean>;
23
+ /**
24
+ * Native web-search provider to attempt by reusing the ACTIVE model's own
25
+ * credentials + baseUrl, dispatched by the model's wire protocol.
26
+ *
27
+ * This is the "native search over a proxy" path: when a model is served through
28
+ * a proxy/custom endpoint, its canonical search credentials (e.g. a dedicated
29
+ * `anthropic` key, or ChatGPT OAuth for `codex`) are usually absent, but the
30
+ * credential that authenticates the model itself — stored under the active
31
+ * provider id and aimed at `ctx.baseUrl` — can drive native web search just as
32
+ * well. Each provider's `search()` falls back to those active credentials when
33
+ * its canonical ones are missing.
34
+ *
35
+ * Returned ids are matched purely from the wire `api` (+ model-id family where a
36
+ * native tool only makes sense for that family); the providers themselves fail
37
+ * closed (and the chain falls through to DuckDuckGo) if the endpoint does not
38
+ * actually support web search.
39
+ */
40
+ export declare function activeContextNativeId(ctx: ActiveSearchModelContext | undefined): SearchProviderId | undefined;
24
41
  export declare function resolveProviderChain(options: ResolveProviderChainOptions): Promise<SearchProvider[]>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Insane Search Provider
3
+ *
4
+ * Native TypeScript, fail-closed adaptation of the MIT-licensed upstream
5
+ * fivetaku/insane-search public-route strategy. This ports only safe Phase 0
6
+ * concepts: deterministic no-auth public endpoints plus route-attempt tracing.
7
+ *
8
+ * Deliberately excluded from upstream: TLS impersonation, browser/cookie warming,
9
+ * CAPTCHA/paywall/login bypasses, credential storage, Playwright automation, and
10
+ * auto dependency installation. Unsupported or terminal auth/paywall/block states
11
+ * throw instead of pretending a shallow fetch succeeded.
12
+ */
13
+ import type { AuthStorage } from "@gajae-code/ai";
14
+ import type { SearchResponse, SearchSource } from "../../../web/search/types";
15
+ import type { SearchParams } from "./base";
16
+ import { SearchProvider } from "./base";
17
+ export interface InsaneRouteAttempt {
18
+ platform: InsanePlatform;
19
+ route: string;
20
+ ok: boolean;
21
+ status: number;
22
+ bytes: number;
23
+ note?: string;
24
+ }
25
+ type InsanePlatform = "reddit" | "x" | "youtube" | "hackernews";
26
+ interface RouteSuccess {
27
+ platform: InsanePlatform;
28
+ route: string;
29
+ finalUrl: string;
30
+ sources: SearchSource[];
31
+ attempts: InsaneRouteAttempt[];
32
+ }
33
+ interface RouteFailure {
34
+ platform: InsanePlatform;
35
+ attempts: InsaneRouteAttempt[];
36
+ }
37
+ type RouteResult = RouteSuccess | RouteFailure | null;
38
+ export declare function routeInsanePublicUrl(rawUrl: string, signal?: AbortSignal): Promise<RouteResult>;
39
+ /** Execute safe Insane Search public-route discovery. */
40
+ export declare function searchInsane(params: {
41
+ query: string;
42
+ num_results?: number;
43
+ recency?: "day" | "week" | "month" | "year";
44
+ signal?: AbortSignal;
45
+ }): Promise<SearchResponse>;
46
+ /** Keyless provider that ports safe upstream public-route fallbacks only. */
47
+ export declare class InsaneProvider extends SearchProvider {
48
+ readonly id = "insane";
49
+ readonly label = "Insane";
50
+ isAvailable(_authStorage: AuthStorage): boolean;
51
+ search(params: SearchParams): Promise<SearchResponse>;
52
+ }
53
+ export {};
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Inline citation extraction shared by native web-search providers.
3
+ *
4
+ * Web-search-capable models sometimes return a genuinely grounded answer whose
5
+ * sources are written inline (markdown links or bare URLs) instead of as
6
+ * structured citation annotations. When a provider has independent proof that a
7
+ * web search actually ran, these helpers recover sources from the answer text so
8
+ * the result is not discarded.
9
+ */
10
+ import type { SearchSource } from "../types";
11
+ /** Append a source, de-duplicating by URL. */
12
+ export declare function addSource(sources: SearchSource[], source: SearchSource): void;
13
+ /**
14
+ * Strips prose punctuation and unmatched closing delimiters from extracted URLs.
15
+ * Models often return links embedded in markdown or sentence text.
16
+ */
17
+ export declare function normalizeExtractedUrl(candidate: string): string | null;
18
+ /**
19
+ * Extracts citation sources from markdown links and bare URLs in answer text.
20
+ * Used only as a fallback when a provider confirms a search ran but omits
21
+ * structured citation annotations.
22
+ */
23
+ export declare function extractTextSources(text: string): SearchSource[];