@gajae-code/coding-agent 0.7.1 → 0.7.2
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 +19 -0
- package/dist/types/cli/notify-cli.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +39 -2
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +1 -1
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/notifications/attachment-registry.d.ts +17 -0
- package/dist/types/notifications/chat-adapters.d.ts +9 -0
- package/dist/types/notifications/config.d.ts +9 -1
- package/dist/types/notifications/engine.d.ts +59 -0
- package/dist/types/notifications/managed-daemon.d.ts +48 -0
- package/dist/types/notifications/telegram-daemon.d.ts +19 -0
- package/dist/types/notifications/threaded-inbound.d.ts +19 -0
- package/dist/types/notifications/threaded-render.d.ts +6 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +23 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/telegram-send.d.ts +32 -0
- package/dist/types/web/insane/bridge.d.ts +103 -0
- package/dist/types/web/insane/url-guard.d.ts +22 -0
- package/dist/types/web/search/provider.d.ts +18 -1
- package/dist/types/web/search/providers/insane.d.ts +53 -0
- package/dist/types/web/search/providers/text-citations.d.ts +23 -0
- package/dist/types/web/search/types.d.ts +12 -4
- package/package.json +10 -8
- package/scripts/verify-insane-vendor.ts +132 -0
- package/src/cli/args.ts +1 -1
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/notify-cli.ts +152 -5
- package/src/commands/team.ts +1 -1
- package/src/config/settings-schema.ts +30 -1
- package/src/defaults/gjc/skills/ralplan/SKILL.md +11 -4
- package/src/extensibility/shared-events.ts +1 -0
- package/src/gjc-runtime/launch-tmux.ts +17 -3
- package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
- package/src/gjc-runtime/ralplan-runtime.ts +2 -2
- package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
- package/src/gjc-runtime/workflow-manifest.ts +7 -2
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/lsp/config.ts +16 -3
- package/src/lsp/defaults.json +7 -0
- package/src/lsp/types.ts +2 -0
- package/src/modes/controllers/event-controller.ts +15 -0
- package/src/modes/interactive-mode.ts +46 -2
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/notifications/attachment-registry.ts +23 -0
- package/src/notifications/chat-adapters.ts +147 -0
- package/src/notifications/config.ts +23 -2
- package/src/notifications/engine.ts +100 -0
- package/src/notifications/index.ts +180 -38
- package/src/notifications/managed-daemon.ts +163 -0
- package/src/notifications/telegram-daemon.ts +235 -14
- package/src/notifications/threaded-inbound.ts +60 -4
- package/src/notifications/threaded-render.ts +20 -2
- package/src/session/agent-session.ts +82 -51
- package/src/tools/fetch.ts +78 -1
- package/src/tools/index.ts +3 -0
- package/src/tools/telegram-send.ts +137 -0
- package/src/web/insane/bridge.ts +350 -0
- package/src/web/insane/url-guard.ts +155 -0
- package/src/web/search/provider.ts +77 -18
- package/src/web/search/providers/anthropic.ts +70 -3
- package/src/web/search/providers/codex.ts +1 -119
- package/src/web/search/providers/gemini.ts +99 -0
- package/src/web/search/providers/insane.ts +551 -0
- package/src/web/search/providers/openai-compatible.ts +66 -32
- package/src/web/search/providers/text-citations.ts +111 -0
- package/src/web/search/types.ts +13 -2
- package/vendor/insane-search/LICENSE +21 -0
- package/vendor/insane-search/MANIFEST.json +24 -0
- package/vendor/insane-search/engine/__init__.py +23 -0
- package/vendor/insane-search/engine/__main__.py +128 -0
- package/vendor/insane-search/engine/bias_check.py +183 -0
- package/vendor/insane-search/engine/executor.py +254 -0
- package/vendor/insane-search/engine/fetch_chain.py +725 -0
- package/vendor/insane-search/engine/learning.py +175 -0
- package/vendor/insane-search/engine/phase0.py +214 -0
- package/vendor/insane-search/engine/safety.py +91 -0
- package/vendor/insane-search/engine/templates/package.json +11 -0
- package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
- package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
- package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
- package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
- package/vendor/insane-search/engine/tests/test_u1.py +200 -0
- package/vendor/insane-search/engine/tests/test_u4.py +131 -0
- package/vendor/insane-search/engine/tests/test_u5.py +163 -0
- package/vendor/insane-search/engine/tests/test_u7.py +124 -0
- package/vendor/insane-search/engine/transport.py +211 -0
- package/vendor/insane-search/engine/url_transforms.py +98 -0
- package/vendor/insane-search/engine/validators.py +331 -0
- package/vendor/insane-search/engine/waf_detector.py +214 -0
- package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
|
@@ -286,6 +286,8 @@ import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
|
286
286
|
import { YieldQueue } from "./yield-queue";
|
|
287
287
|
|
|
288
288
|
/** Session-specific events that extend the core AgentEvent */
|
|
289
|
+
export type AutoCompactionContinuationSkipReason = "auto_continue_disabled_non_resumable_tail";
|
|
290
|
+
|
|
289
291
|
export type AgentSessionEvent =
|
|
290
292
|
| AgentEvent
|
|
291
293
|
| { type: "auto_compaction_start"; reason: "threshold" | "overflow" | "idle"; action: "context-full" | "handoff" }
|
|
@@ -298,6 +300,7 @@ export type AgentSessionEvent =
|
|
|
298
300
|
errorMessage?: string;
|
|
299
301
|
/** True when compaction was skipped for a benign reason (no model, no candidates, nothing to compact). */
|
|
300
302
|
skipped?: boolean;
|
|
303
|
+
continuationSkipReason?: AutoCompactionContinuationSkipReason;
|
|
301
304
|
}
|
|
302
305
|
| {
|
|
303
306
|
type: "auto_retry_start";
|
|
@@ -1869,7 +1872,6 @@ export class AgentSession {
|
|
|
1869
1872
|
this.#resolveTtsrResume();
|
|
1870
1873
|
return;
|
|
1871
1874
|
}
|
|
1872
|
-
this.#ttsrAbortPending = false;
|
|
1873
1875
|
this.#perToolTtsrInjections.clear();
|
|
1874
1876
|
const ttsrSettings = this.#ttsrManager?.getSettings();
|
|
1875
1877
|
if (ttsrSettings?.contextMode === "discard" && targetAssistantIndex !== -1) {
|
|
@@ -1898,11 +1900,22 @@ export class AgentSession {
|
|
|
1898
1900
|
);
|
|
1899
1901
|
this.#markTtsrInjected(details.rules);
|
|
1900
1902
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1903
|
+
await this.#scheduleAgentContinue({
|
|
1904
|
+
delayMs: 0,
|
|
1905
|
+
generation,
|
|
1906
|
+
shouldContinue: () => {
|
|
1907
|
+
this.#ttsrAbortPending = false;
|
|
1908
|
+
return true;
|
|
1909
|
+
},
|
|
1910
|
+
onSkip: () => {
|
|
1911
|
+
this.#ttsrAbortPending = false;
|
|
1912
|
+
this.#resolveTtsrResume();
|
|
1913
|
+
},
|
|
1914
|
+
onError: () => {
|
|
1915
|
+
this.#ttsrAbortPending = false;
|
|
1916
|
+
this.#resolveTtsrResume();
|
|
1917
|
+
},
|
|
1918
|
+
});
|
|
1906
1919
|
},
|
|
1907
1920
|
{ delayMs: 50 },
|
|
1908
1921
|
);
|
|
@@ -2214,7 +2227,7 @@ export class AgentSession {
|
|
|
2214
2227
|
#schedulePostPromptTask(
|
|
2215
2228
|
task: (signal: AbortSignal) => Promise<void>,
|
|
2216
2229
|
options?: { delayMs?: number; generation?: number; onSkip?: () => void },
|
|
2217
|
-
): void {
|
|
2230
|
+
): Promise<void> {
|
|
2218
2231
|
const delayMs = options?.delayMs ?? 0;
|
|
2219
2232
|
const signal = this.#postPromptTasksAbortController.signal;
|
|
2220
2233
|
const scheduled = (async () => {
|
|
@@ -2236,18 +2249,20 @@ export class AgentSession {
|
|
|
2236
2249
|
await task(signal);
|
|
2237
2250
|
})();
|
|
2238
2251
|
this.#trackPostPromptTask(scheduled);
|
|
2252
|
+
return scheduled;
|
|
2239
2253
|
}
|
|
2240
2254
|
|
|
2241
2255
|
#scheduleAgentContinue(options?: {
|
|
2242
2256
|
delayMs?: number;
|
|
2243
2257
|
generation?: number;
|
|
2258
|
+
skipCompactionCheck?: boolean;
|
|
2244
2259
|
shouldContinue?: () => boolean;
|
|
2245
2260
|
onSkip?: (reason: "generation_changed" | "aborted_signal" | "queue_drained") => void;
|
|
2246
2261
|
onError?: (error: unknown) => void;
|
|
2247
|
-
}): void {
|
|
2262
|
+
}): Promise<void> {
|
|
2248
2263
|
const scheduledGeneration = options?.generation;
|
|
2249
2264
|
const signal = this.#postPromptTasksAbortController.signal;
|
|
2250
|
-
this.#schedulePostPromptTask(
|
|
2265
|
+
return this.#schedulePostPromptTask(
|
|
2251
2266
|
async () => {
|
|
2252
2267
|
if (signal.aborted) {
|
|
2253
2268
|
options?.onSkip?.("aborted_signal");
|
|
@@ -2263,6 +2278,9 @@ export class AgentSession {
|
|
|
2263
2278
|
}
|
|
2264
2279
|
try {
|
|
2265
2280
|
await this.#maybeRestoreRetryFallbackPrimary();
|
|
2281
|
+
if (!options?.skipCompactionCheck) {
|
|
2282
|
+
await this.#checkEstimatedContextBeforePrompt();
|
|
2283
|
+
}
|
|
2266
2284
|
await this.agent.continue();
|
|
2267
2285
|
} catch (error) {
|
|
2268
2286
|
logger.warn("agent.continue failed after scheduling", {
|
|
@@ -2307,6 +2325,34 @@ export class AgentSession {
|
|
|
2307
2325
|
}
|
|
2308
2326
|
}
|
|
2309
2327
|
|
|
2328
|
+
#detectOverflowRetryContinuationSkip(): AutoCompactionContinuationSkipReason | undefined {
|
|
2329
|
+
this.#stripOverflowFailedTurnForRetry();
|
|
2330
|
+
if (this.#isResumableAgentTail()) return undefined;
|
|
2331
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
2332
|
+
return compactionSettings.autoContinue === false ? "auto_continue_disabled_non_resumable_tail" : undefined;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
#scheduleOverflowRetryContinuation(generation: number): void {
|
|
2336
|
+
this.#stripOverflowFailedTurnForRetry();
|
|
2337
|
+
if (this.#isResumableAgentTail()) {
|
|
2338
|
+
this.#scheduleAgentContinue({
|
|
2339
|
+
delayMs: 100,
|
|
2340
|
+
generation,
|
|
2341
|
+
onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
|
|
2342
|
+
onError: error => this.#logCompactionContinuationError("overflow_retry", error),
|
|
2343
|
+
});
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
2348
|
+
if (compactionSettings.autoContinue !== false) {
|
|
2349
|
+
this.#scheduleAutoContinuePrompt(generation);
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
this.#logCompactionContinuationSkipped("overflow_retry", "auto_continue_disabled_non_resumable_tail");
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2310
2356
|
#scheduleAutoContinuePrompt(generation: number): void {
|
|
2311
2357
|
const continuePrompt = async () => {
|
|
2312
2358
|
await this.#promptWithMessage(
|
|
@@ -3052,6 +3098,7 @@ export class AgentSession {
|
|
|
3052
3098
|
willRetry: event.willRetry,
|
|
3053
3099
|
errorMessage: event.errorMessage,
|
|
3054
3100
|
skipped: event.skipped,
|
|
3101
|
+
continuationSkipReason: event.continuationSkipReason,
|
|
3055
3102
|
});
|
|
3056
3103
|
} else if (event.type === "auto_retry_start") {
|
|
3057
3104
|
await this.#extensionRunner.emit({
|
|
@@ -6722,6 +6769,8 @@ export class AgentSession {
|
|
|
6722
6769
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6723
6770
|
if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
|
|
6724
6771
|
await this.#runAutoCompaction("overflow", true);
|
|
6772
|
+
} else {
|
|
6773
|
+
this.#scheduleOverflowRetryContinuation(generation);
|
|
6725
6774
|
}
|
|
6726
6775
|
return;
|
|
6727
6776
|
}
|
|
@@ -6732,17 +6781,19 @@ export class AgentSession {
|
|
|
6732
6781
|
// Skip if this was an error (non-overflow errors don't have usage data)
|
|
6733
6782
|
if (assistantMessage.stopReason === "error") return;
|
|
6734
6783
|
let contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
6735
|
-
|
|
6784
|
+
// Model maxTokens is a capability ceiling, not a per-turn reservation.
|
|
6785
|
+
// Auto maintenance should track actual context fullness.
|
|
6786
|
+
const autoCompactionOutputReserveTokens = 0;
|
|
6736
6787
|
// Cache-epoch invariant: pruning rewrites already-sent toolResult history,
|
|
6737
6788
|
// which breaks the provider prompt-cache prefix mid-epoch. Only prune at a
|
|
6738
6789
|
// sanctioned maintenance boundary, i.e. when the un-pruned context already
|
|
6739
6790
|
// crosses the compaction threshold. Pruning may then avert full compaction.
|
|
6740
|
-
if (!shouldCompact(contextTokens, contextWindow, compactionSettings,
|
|
6791
|
+
if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) return;
|
|
6741
6792
|
const pruneResult = await this.#pruneToolOutputs();
|
|
6742
6793
|
if (pruneResult) {
|
|
6743
6794
|
contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
6744
6795
|
}
|
|
6745
|
-
if (shouldCompact(contextTokens, contextWindow, compactionSettings,
|
|
6796
|
+
if (shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
|
|
6746
6797
|
// Try promotion first — if a larger model is available, switch instead of compacting
|
|
6747
6798
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
6748
6799
|
if (!promoted) {
|
|
@@ -6820,14 +6871,16 @@ export class AgentSession {
|
|
|
6820
6871
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
|
|
6821
6872
|
|
|
6822
6873
|
let contextTokens = this.#estimateContextTokensForCompaction(pendingMessages).tokens;
|
|
6823
|
-
|
|
6824
|
-
|
|
6874
|
+
// Model maxTokens is a capability ceiling, not a per-turn reservation.
|
|
6875
|
+
// Auto maintenance should track actual context fullness.
|
|
6876
|
+
const autoCompactionOutputReserveTokens = 0;
|
|
6877
|
+
if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) return;
|
|
6825
6878
|
|
|
6826
6879
|
const pruneResult = await this.#pruneToolOutputs();
|
|
6827
6880
|
if (pruneResult) {
|
|
6828
6881
|
contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
6829
6882
|
}
|
|
6830
|
-
if (shouldCompact(contextTokens, contextWindow, compactionSettings,
|
|
6883
|
+
if (shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
|
|
6831
6884
|
await this.#runAutoCompaction("threshold", false, false, {
|
|
6832
6885
|
continueAfterMaintenance: false,
|
|
6833
6886
|
deferHandoffMaintenance: false,
|
|
@@ -7730,32 +7783,18 @@ export class AgentSession {
|
|
|
7730
7783
|
|
|
7731
7784
|
const preparation = prepareCompaction(pathEntries, compactionSettings);
|
|
7732
7785
|
if (!preparation) {
|
|
7786
|
+
const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
|
|
7733
7787
|
await this.#emitSessionEvent({
|
|
7734
7788
|
type: "auto_compaction_end",
|
|
7735
7789
|
action,
|
|
7736
7790
|
result: undefined,
|
|
7737
7791
|
aborted: false,
|
|
7738
|
-
willRetry:
|
|
7792
|
+
willRetry: willRetry && !continuationSkipReason,
|
|
7739
7793
|
skipped: true,
|
|
7794
|
+
continuationSkipReason,
|
|
7740
7795
|
});
|
|
7741
7796
|
if (willRetry) {
|
|
7742
|
-
this.#
|
|
7743
|
-
if (this.#isResumableAgentTail()) {
|
|
7744
|
-
this.#scheduleAgentContinue({
|
|
7745
|
-
delayMs: 100,
|
|
7746
|
-
generation,
|
|
7747
|
-
onSkip: skipReason => this.#logCompactionContinuationSkipped("overflow_retry", skipReason),
|
|
7748
|
-
onError: error => this.#logCompactionContinuationError("overflow_retry", error),
|
|
7749
|
-
});
|
|
7750
|
-
} else {
|
|
7751
|
-
const tail = this.agent.state.messages.at(-1) as AssistantMessage | undefined;
|
|
7752
|
-
logger.warn("Auto-compaction continuation skipped", {
|
|
7753
|
-
source: "overflow_retry",
|
|
7754
|
-
reason: "not_resumable_tail",
|
|
7755
|
-
role: tail?.role,
|
|
7756
|
-
stopReason: tail?.stopReason,
|
|
7757
|
-
});
|
|
7758
|
-
}
|
|
7797
|
+
this.#scheduleOverflowRetryContinuation(generation);
|
|
7759
7798
|
} else if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
7760
7799
|
this.#scheduleAgentContinue({
|
|
7761
7800
|
delayMs: 100,
|
|
@@ -7966,26 +8005,18 @@ export class AgentSession {
|
|
|
7966
8005
|
details,
|
|
7967
8006
|
preserveData,
|
|
7968
8007
|
};
|
|
7969
|
-
|
|
8008
|
+
const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
|
|
8009
|
+
await this.#emitSessionEvent({
|
|
8010
|
+
type: "auto_compaction_end",
|
|
8011
|
+
action,
|
|
8012
|
+
result,
|
|
8013
|
+
aborted: false,
|
|
8014
|
+
willRetry: willRetry && !continuationSkipReason,
|
|
8015
|
+
continuationSkipReason,
|
|
8016
|
+
});
|
|
7970
8017
|
|
|
7971
8018
|
if (willRetry) {
|
|
7972
|
-
this.#
|
|
7973
|
-
if (!this.#isResumableAgentTail()) {
|
|
7974
|
-
const tail = this.agent.state.messages.at(-1) as AssistantMessage | undefined;
|
|
7975
|
-
logger.warn("Auto-compaction continuation skipped", {
|
|
7976
|
-
source: "overflow_retry",
|
|
7977
|
-
reason: "not_resumable_tail",
|
|
7978
|
-
role: tail?.role,
|
|
7979
|
-
stopReason: tail?.stopReason,
|
|
7980
|
-
});
|
|
7981
|
-
} else {
|
|
7982
|
-
this.#scheduleAgentContinue({
|
|
7983
|
-
delayMs: 100,
|
|
7984
|
-
generation,
|
|
7985
|
-
onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
|
|
7986
|
-
onError: error => this.#logCompactionContinuationError("overflow_retry", error),
|
|
7987
|
-
});
|
|
7988
|
-
}
|
|
8019
|
+
this.#scheduleOverflowRetryContinuation(generation);
|
|
7989
8020
|
} else if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
7990
8021
|
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
7991
8022
|
// Kick the loop so queued messages are actually delivered.
|
package/src/tools/fetch.ts
CHANGED
|
@@ -16,6 +16,8 @@ import { renderStatusLine } from "../tui";
|
|
|
16
16
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
17
17
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
18
18
|
import { ensureTool } from "../utils/tools-manager";
|
|
19
|
+
import { INSANE_NOTES, tryInsaneFetch } from "../web/insane/bridge";
|
|
20
|
+
import { validatePublicHttpUrlForInsane } from "../web/insane/url-guard";
|
|
19
21
|
import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../web/parallel";
|
|
20
22
|
import { specialHandlers } from "../web/scrapers";
|
|
21
23
|
import type { RenderResult } from "../web/scrapers/types";
|
|
@@ -705,6 +707,55 @@ async function handleSpecialUrls(
|
|
|
705
707
|
// Main Render Function
|
|
706
708
|
// =============================================================================
|
|
707
709
|
|
|
710
|
+
/**
|
|
711
|
+
* Opt-in insane-search fallback for blocked / degraded public URL reads.
|
|
712
|
+
*
|
|
713
|
+
* Returns a finalized `method: "insane"` result on success, or null (so the
|
|
714
|
+
* caller continues with its normal degraded behavior). Fail-closed: no note,
|
|
715
|
+
* guard DNS, dependency probe, or subprocess when raw mode or the opt-in
|
|
716
|
+
* setting is off. The public-URL guard runs BEFORE any probe/spawn.
|
|
717
|
+
*/
|
|
718
|
+
export async function tryInsaneFallback(args: {
|
|
719
|
+
url: string;
|
|
720
|
+
finalUrl: string;
|
|
721
|
+
timeout: number;
|
|
722
|
+
raw: boolean;
|
|
723
|
+
settings: Settings;
|
|
724
|
+
signal: AbortSignal | undefined;
|
|
725
|
+
fetchedAt: string;
|
|
726
|
+
notes: string[];
|
|
727
|
+
}): Promise<FetchRenderResult | null> {
|
|
728
|
+
if (args.raw) return null;
|
|
729
|
+
if (args.settings.get("web.insaneFallback") !== true) return null;
|
|
730
|
+
|
|
731
|
+
const target = args.finalUrl || args.url;
|
|
732
|
+
const guard = await validatePublicHttpUrlForInsane(target);
|
|
733
|
+
if (!guard.ok) {
|
|
734
|
+
args.notes.push(INSANE_NOTES.guardBlocked(guard.reason));
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const result = await tryInsaneFetch(guard.url.toString(), {
|
|
739
|
+
timeoutMs: args.timeout * 1000,
|
|
740
|
+
signal: args.signal,
|
|
741
|
+
});
|
|
742
|
+
if (result.ok) {
|
|
743
|
+
const output = finalizeOutput(result.content);
|
|
744
|
+
return {
|
|
745
|
+
url: args.url,
|
|
746
|
+
finalUrl: target,
|
|
747
|
+
contentType: "text/markdown",
|
|
748
|
+
method: "insane",
|
|
749
|
+
content: output.content,
|
|
750
|
+
fetchedAt: args.fetchedAt,
|
|
751
|
+
truncated: output.truncated,
|
|
752
|
+
notes: [...args.notes, ...result.notes],
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
for (const note of result.notes) args.notes.push(note);
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
|
|
708
759
|
/**
|
|
709
760
|
* Main render function implementing the full pipeline
|
|
710
761
|
*/
|
|
@@ -751,6 +802,19 @@ async function renderUrl(
|
|
|
751
802
|
throw new ToolAbortError();
|
|
752
803
|
}
|
|
753
804
|
if (!response.ok) {
|
|
805
|
+
const failureNote = response.status ? `Failed to fetch URL (HTTP ${response.status})` : "Failed to fetch URL";
|
|
806
|
+
notes.push(failureNote);
|
|
807
|
+
const insane = await tryInsaneFallback({
|
|
808
|
+
url,
|
|
809
|
+
finalUrl: response.finalUrl || url,
|
|
810
|
+
timeout,
|
|
811
|
+
raw,
|
|
812
|
+
settings,
|
|
813
|
+
signal,
|
|
814
|
+
fetchedAt,
|
|
815
|
+
notes,
|
|
816
|
+
});
|
|
817
|
+
if (insane) return insane;
|
|
754
818
|
return {
|
|
755
819
|
url,
|
|
756
820
|
finalUrl: response.finalUrl || url,
|
|
@@ -759,7 +823,7 @@ async function renderUrl(
|
|
|
759
823
|
content: "",
|
|
760
824
|
fetchedAt,
|
|
761
825
|
truncated: false,
|
|
762
|
-
notes
|
|
826
|
+
notes,
|
|
763
827
|
};
|
|
764
828
|
}
|
|
765
829
|
|
|
@@ -1062,6 +1126,8 @@ async function renderUrl(
|
|
|
1062
1126
|
const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, settings, signal, storage);
|
|
1063
1127
|
if (!htmlResult.ok) {
|
|
1064
1128
|
notes.push("html rendering failed (lynx/html2text unavailable)");
|
|
1129
|
+
const insane = await tryInsaneFallback({ url, finalUrl, timeout, raw, settings, signal, fetchedAt, notes });
|
|
1130
|
+
if (insane) return insane;
|
|
1065
1131
|
const output = finalizeOutput(rawContent);
|
|
1066
1132
|
return {
|
|
1067
1133
|
url,
|
|
@@ -1122,6 +1188,17 @@ async function renderUrl(
|
|
|
1122
1188
|
};
|
|
1123
1189
|
}
|
|
1124
1190
|
|
|
1191
|
+
const insaneLowQuality = await tryInsaneFallback({
|
|
1192
|
+
url,
|
|
1193
|
+
finalUrl,
|
|
1194
|
+
timeout,
|
|
1195
|
+
raw,
|
|
1196
|
+
settings,
|
|
1197
|
+
signal,
|
|
1198
|
+
fetchedAt,
|
|
1199
|
+
notes,
|
|
1200
|
+
});
|
|
1201
|
+
if (insaneLowQuality) return insaneLowQuality;
|
|
1125
1202
|
notes.push("Page appears to require JavaScript or is mostly navigation");
|
|
1126
1203
|
}
|
|
1127
1204
|
|
package/src/tools/index.ts
CHANGED
|
@@ -58,6 +58,7 @@ import { SearchToolBm25Tool } from "./search-tool-bm25";
|
|
|
58
58
|
import { SkillTool } from "./skill";
|
|
59
59
|
import { loadSshTool } from "./ssh";
|
|
60
60
|
import { SubagentTool } from "./subagent";
|
|
61
|
+
import { TelegramSendTool } from "./telegram-send";
|
|
61
62
|
import { type TodoPhase, TodoWriteTool } from "./todo-write";
|
|
62
63
|
import { WriteTool } from "./write";
|
|
63
64
|
import { YieldTool } from "./yield";
|
|
@@ -96,6 +97,7 @@ export * from "./search-tool-bm25";
|
|
|
96
97
|
export * from "./skill";
|
|
97
98
|
export * from "./ssh";
|
|
98
99
|
export * from "./subagent";
|
|
100
|
+
export * from "./telegram-send";
|
|
99
101
|
export * from "./todo-write";
|
|
100
102
|
export * from "./vim";
|
|
101
103
|
export * from "./write";
|
|
@@ -402,6 +404,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
402
404
|
todo_write: s => new TodoWriteTool(s),
|
|
403
405
|
web_search: s => new WebSearchTool(s),
|
|
404
406
|
search_tool_bm25: SearchToolBm25Tool.createIf,
|
|
407
|
+
telegram_send: TelegramSendTool.createIf,
|
|
405
408
|
write: s => new WriteTool(s),
|
|
406
409
|
skill: SkillTool.createIf,
|
|
407
410
|
goal: s => new GoalTool(s),
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
|
|
4
|
+
import { z } from "zod/v4";
|
|
5
|
+
import { getTelegramFileSink } from "../notifications/attachment-registry";
|
|
6
|
+
import { getNotificationConfig, isGloballyConfigured } from "../notifications/config";
|
|
7
|
+
import type { ToolSession } from "./index";
|
|
8
|
+
|
|
9
|
+
const telegramSendSchema = z.object({
|
|
10
|
+
path: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe("file path (absolute or relative to cwd) to send to Telegram; must resolve inside the workspace"),
|
|
13
|
+
caption: z.string().optional().describe("optional caption"),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
type TelegramSendParams = z.infer<typeof telegramSendSchema>;
|
|
17
|
+
|
|
18
|
+
interface TelegramSendDetails {
|
|
19
|
+
path: string;
|
|
20
|
+
caption?: string;
|
|
21
|
+
ok: boolean;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class TelegramSendTool implements AgentTool<typeof telegramSendSchema, TelegramSendDetails> {
|
|
26
|
+
readonly name = "telegram_send";
|
|
27
|
+
readonly label = "TelegramSend";
|
|
28
|
+
readonly summary = "Send a workspace file to Telegram";
|
|
29
|
+
readonly loadMode = "discoverable";
|
|
30
|
+
readonly description =
|
|
31
|
+
"Send a file from the current workspace to the connected Telegram chat as a document. The path must resolve " +
|
|
32
|
+
"(after following symlinks) to a regular file inside the project root; paths outside the workspace are rejected.";
|
|
33
|
+
readonly parameters = telegramSendSchema;
|
|
34
|
+
readonly strict = true;
|
|
35
|
+
|
|
36
|
+
constructor(private readonly session: ToolSession) {}
|
|
37
|
+
|
|
38
|
+
static createIf(session: ToolSession): TelegramSendTool | null {
|
|
39
|
+
return isGloballyConfigured(getNotificationConfig(session.settings)) ? new TelegramSendTool(session) : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve `requested` against the workspace root and confine it via realpath:
|
|
44
|
+
* blocks absolute paths outside the project, `..` traversal, and symlinks that
|
|
45
|
+
* escape the root. Returns the resolved real path of a regular file, or an
|
|
46
|
+
* error message. This is the egress safety boundary — the model can only send
|
|
47
|
+
* files that genuinely live inside the session workspace.
|
|
48
|
+
*/
|
|
49
|
+
private async resolveContainedFile(
|
|
50
|
+
requested: string,
|
|
51
|
+
): Promise<{ ok: true; path: string } | { ok: false; error: string }> {
|
|
52
|
+
let root: string;
|
|
53
|
+
try {
|
|
54
|
+
root = await fs.promises.realpath(this.session.cwd);
|
|
55
|
+
} catch {
|
|
56
|
+
return { ok: false, error: "workspace root is unavailable" };
|
|
57
|
+
}
|
|
58
|
+
const absolute = path.isAbsolute(requested) ? requested : path.resolve(root, requested);
|
|
59
|
+
let real: string;
|
|
60
|
+
try {
|
|
61
|
+
real = await fs.promises.realpath(absolute);
|
|
62
|
+
} catch {
|
|
63
|
+
return { ok: false, error: `file not found: ${requested}` };
|
|
64
|
+
}
|
|
65
|
+
const rel = path.relative(root, real);
|
|
66
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
|
|
67
|
+
return { ok: false, error: "path escapes the workspace root; only files inside the project can be sent" };
|
|
68
|
+
}
|
|
69
|
+
let stat: fs.Stats;
|
|
70
|
+
try {
|
|
71
|
+
stat = await fs.promises.stat(real);
|
|
72
|
+
} catch {
|
|
73
|
+
return { ok: false, error: `file not found: ${requested}` };
|
|
74
|
+
}
|
|
75
|
+
if (!stat.isFile()) {
|
|
76
|
+
return { ok: false, error: "not a regular file" };
|
|
77
|
+
}
|
|
78
|
+
return { ok: true, path: real };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async execute(
|
|
82
|
+
_toolCallId: string,
|
|
83
|
+
params: TelegramSendParams,
|
|
84
|
+
_signal?: AbortSignal,
|
|
85
|
+
_onUpdate?: AgentToolUpdateCallback<TelegramSendDetails>,
|
|
86
|
+
_context?: AgentToolContext,
|
|
87
|
+
): Promise<AgentToolResult<TelegramSendDetails>> {
|
|
88
|
+
const sessionId = this.session.getSessionId?.();
|
|
89
|
+
if (!sessionId) {
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: "telegram_send: no active session id" }],
|
|
92
|
+
details: { path: params.path, caption: params.caption, ok: false, error: "no active session id" },
|
|
93
|
+
isError: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const contained = await this.resolveContainedFile(params.path);
|
|
98
|
+
if (!contained.ok) {
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text: `telegram_send: ${contained.error}` }],
|
|
101
|
+
details: { path: params.path, caption: params.caption, ok: false, error: contained.error },
|
|
102
|
+
isError: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const abs = contained.path;
|
|
106
|
+
|
|
107
|
+
const sink = getTelegramFileSink(sessionId);
|
|
108
|
+
if (!sink) {
|
|
109
|
+
return {
|
|
110
|
+
content: [
|
|
111
|
+
{ type: "text", text: "telegram_send: Telegram notifications are not connected for this session" },
|
|
112
|
+
],
|
|
113
|
+
details: {
|
|
114
|
+
path: abs,
|
|
115
|
+
caption: params.caption,
|
|
116
|
+
ok: false,
|
|
117
|
+
error: "Telegram notifications are not connected",
|
|
118
|
+
},
|
|
119
|
+
isError: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = await sink({ path: abs, caption: params.caption });
|
|
124
|
+
if (result.ok) {
|
|
125
|
+
return {
|
|
126
|
+
content: [{ type: "text", text: `Sent ${path.basename(abs)} to Telegram.` }],
|
|
127
|
+
details: { path: abs, caption: params.caption, ok: true },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: `telegram_send failed: ${result.error}` }],
|
|
133
|
+
details: { path: abs, caption: params.caption, ok: false, error: result.error },
|
|
134
|
+
isError: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|