@gajae-code/coding-agent 0.11.6 → 0.11.7
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 +37 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +56 -6
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +5 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +19 -5
- package/dist/types/main.d.ts +24 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-status-header.d.ts +1 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +61 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +28 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +21 -7
- package/dist/types/session/internal/managed-session-storage.d.ts +10 -8
- package/dist/types/session/session-manager.d.ts +25 -4
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/tools/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +16 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +34 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +42 -9
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profiles.ts +18 -0
- package/src/config/settings-schema.ts +57 -6
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -1
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/gjc-runtime/ultragoal-guard.ts +88 -10
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +275 -12
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/internal-urls/local-protocol.ts +32 -6
- package/src/main.ts +86 -27
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +31 -3
- package/src/modes/components/tool-status-header.ts +6 -1
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/input-controller.ts +31 -0
- package/src/modes/controllers/selector-controller.ts +106 -12
- package/src/modes/interactive-mode.ts +16 -7
- package/src/modes/types.ts +1 -0
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- package/src/runtime-mcp/transports/http.ts +50 -16
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/index.ts +4 -5
- package/src/sdk/bus/lifecycle-commands.ts +16 -7
- package/src/sdk/bus/notification-service.ts +41 -18
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -3
- package/src/sdk/bus/telegram-daemon-control.ts +2 -2
- package/src/sdk/bus/telegram-daemon.ts +1663 -375
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +29 -5
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +240 -149
- package/src/session/blob-store.ts +19 -6
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +313 -57
- package/src/session/internal/managed-session-storage.ts +146 -35
- package/src/session/session-manager.ts +319 -85
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/tools/ask.ts +1 -1
- package/src/tools/fetch.ts +13 -131
- package/src/tools/image-gen.ts +301 -20
- package/src/tools/read.ts +2 -0
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +138 -27
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
|
@@ -391,7 +391,7 @@ export function formatCredentialSummary(credential: ImportableCredential): strin
|
|
|
391
391
|
const identityPart = identity ? ` ${identity}` : "";
|
|
392
392
|
let expiry = "";
|
|
393
393
|
if (credential.kind === "oauth" && credential.expiresAt !== undefined) {
|
|
394
|
-
expiry = credential.expiresAt
|
|
394
|
+
expiry = credential.expiresAt <= Date.now() ? " [expired]" : "";
|
|
395
395
|
}
|
|
396
396
|
return `${provider} · ${kind}${identityPart} · token ${credential.redactedToken}${expiry} (from ${credential.source})`;
|
|
397
397
|
}
|
|
@@ -413,7 +413,10 @@ export function formatDiscoverySummary(result: CredentialDiscoveryResult): strin
|
|
|
413
413
|
return lines;
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
-
|
|
416
|
+
/** Reason shown when automatic OAuth import rejects an unusable discovery record. */
|
|
417
|
+
export const INVALID_AUTO_IMPORT_OAUTH_EXPIRY_REASON = "OAuth credential has no valid future expiry";
|
|
418
|
+
|
|
419
|
+
function hasAutoImportOAuthProviderOrigin(credential: ImportableCredential): boolean {
|
|
417
420
|
return (
|
|
418
421
|
AUTO_IMPORT_OAUTH_PROVIDER_ORIGINS[credential.provider]?.has(credential.origin) === true &&
|
|
419
422
|
credential.kind === "oauth" &&
|
|
@@ -421,8 +424,30 @@ export function isAutoImportOAuthCredential(credential: ImportableCredential): b
|
|
|
421
424
|
);
|
|
422
425
|
}
|
|
423
426
|
|
|
427
|
+
export function isAutoImportOAuthCredential(credential: ImportableCredential, now: number = Date.now()): boolean {
|
|
428
|
+
return (
|
|
429
|
+
hasAutoImportOAuthProviderOrigin(credential) &&
|
|
430
|
+
typeof credential.expiresAt === "number" &&
|
|
431
|
+
Number.isFinite(credential.expiresAt) &&
|
|
432
|
+
credential.expiresAt > now
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Keep rejected OAuth discoveries visible to operators without offering them for automatic import. */
|
|
437
|
+
export function getAutoImportOAuthCredentialSkips(
|
|
438
|
+
credentials: readonly ImportableCredential[],
|
|
439
|
+
now: number = Date.now(),
|
|
440
|
+
): SkippedCredential[] {
|
|
441
|
+
return credentials.flatMap(credential =>
|
|
442
|
+
hasAutoImportOAuthProviderOrigin(credential) && !isAutoImportOAuthCredential(credential, now)
|
|
443
|
+
? [{ origin: credential.origin, source: credential.source, reason: INVALID_AUTO_IMPORT_OAUTH_EXPIRY_REASON }]
|
|
444
|
+
: [],
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
424
448
|
export function filterAutoImportOAuthCredentials(credentials: readonly ImportableCredential[]): ImportableCredential[] {
|
|
425
|
-
|
|
449
|
+
const now = Date.now();
|
|
450
|
+
return credentials.filter(credential => isAutoImportOAuthCredential(credential, now));
|
|
426
451
|
}
|
|
427
452
|
|
|
428
453
|
/**
|
|
@@ -49,6 +49,7 @@ interface ProviderPreset {
|
|
|
49
49
|
baseUrl: string;
|
|
50
50
|
apiKeyEnv: string;
|
|
51
51
|
models: readonly string[];
|
|
52
|
+
modelApi?: Readonly<Record<string, ProviderSetupApi>>;
|
|
52
53
|
compat?: ProviderCompatConfig;
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -113,6 +114,7 @@ function resolvePresetInput(input: ProviderSetupInput): {
|
|
|
113
114
|
apiKey?: string;
|
|
114
115
|
apiKeyEnv?: string;
|
|
115
116
|
models: readonly string[];
|
|
117
|
+
modelApi?: Readonly<Record<string, ProviderSetupApi>>;
|
|
116
118
|
api: ProviderSetupApi;
|
|
117
119
|
compat?: ProviderCompatConfig;
|
|
118
120
|
} {
|
|
@@ -152,11 +154,32 @@ function resolvePresetInput(input: ProviderSetupInput): {
|
|
|
152
154
|
apiKey: input.apiKey,
|
|
153
155
|
apiKeyEnv: input.apiKeyEnv ?? preset?.apiKeyEnv,
|
|
154
156
|
models: input.models && input.models.length > 0 ? input.models : (preset?.models ?? []),
|
|
157
|
+
modelApi: preset?.modelApi,
|
|
155
158
|
api: preset?.api ?? apiForCompatibility(compatibility),
|
|
156
159
|
compat: preset?.compat,
|
|
157
160
|
};
|
|
158
161
|
}
|
|
159
162
|
|
|
163
|
+
export function validateModelApi(
|
|
164
|
+
modelApi: Readonly<Record<string, string>> | undefined,
|
|
165
|
+
models: readonly string[],
|
|
166
|
+
presetId: string,
|
|
167
|
+
): void {
|
|
168
|
+
if (!modelApi) return;
|
|
169
|
+
const modelSet = new Set(models);
|
|
170
|
+
const validApis: readonly string[] = ["openai-responses", "openai-completions", "anthropic-messages"];
|
|
171
|
+
for (const [key, value] of Object.entries(modelApi)) {
|
|
172
|
+
if (!modelSet.has(key)) {
|
|
173
|
+
throw new Error(`Provider preset '${presetId}' declares modelApi for unknown model '${key}'.`);
|
|
174
|
+
}
|
|
175
|
+
if (!validApis.includes(value)) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Provider preset '${presetId}' declares invalid modelApi value '${value}' for model '${key}'.`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
160
183
|
function validateSetupInput(input: ProviderSetupInput): {
|
|
161
184
|
providerId: string;
|
|
162
185
|
baseUrl: string;
|
|
@@ -166,6 +189,7 @@ function validateSetupInput(input: ProviderSetupInput): {
|
|
|
166
189
|
compatibility: ProviderCompatibility;
|
|
167
190
|
api: ProviderSetupApi;
|
|
168
191
|
compat?: ProviderCompatConfig;
|
|
192
|
+
modelApi?: Readonly<Record<string, ProviderSetupApi>>;
|
|
169
193
|
preset?: ProviderPreset;
|
|
170
194
|
} {
|
|
171
195
|
const resolved = resolvePresetInput(input);
|
|
@@ -201,6 +225,7 @@ function validateSetupInput(input: ProviderSetupInput): {
|
|
|
201
225
|
|
|
202
226
|
const models = parseModelList(resolved.models);
|
|
203
227
|
if (models.length === 0) throw new Error("At least one model id is required.");
|
|
228
|
+
validateModelApi(resolved.modelApi, models, resolved.preset?.id ?? resolved.providerId);
|
|
204
229
|
|
|
205
230
|
return {
|
|
206
231
|
providerId,
|
|
@@ -210,6 +235,7 @@ function validateSetupInput(input: ProviderSetupInput): {
|
|
|
210
235
|
models,
|
|
211
236
|
compatibility: resolved.compatibility,
|
|
212
237
|
api: resolved.api,
|
|
238
|
+
modelApi: resolved.modelApi,
|
|
213
239
|
compat: resolved.compat,
|
|
214
240
|
preset: resolved.preset,
|
|
215
241
|
};
|
|
@@ -275,7 +301,10 @@ export async function addApiCompatibleProvider(input: ProviderSetupInput): Promi
|
|
|
275
301
|
baseUrl: validated.baseUrl,
|
|
276
302
|
api: validated.api,
|
|
277
303
|
auth: "apiKey",
|
|
278
|
-
models: validated.models.map(id =>
|
|
304
|
+
models: validated.models.map(id => {
|
|
305
|
+
const api = validated.modelApi?.[id];
|
|
306
|
+
return api ? { id, api } : { id };
|
|
307
|
+
}),
|
|
279
308
|
};
|
|
280
309
|
if (validated.compat) provider.compat = validated.compat;
|
|
281
310
|
if (validated.credentialSource === "env") {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
[
|
|
2
2
|
{"id":"minimax","aliases":["minimax-code"],"name":"MiniMax Coding Plan","description":"OpenAI-compatible MiniMax Coding Plan endpoint","compatibility":"openai","api":"openai-completions","providerId":"minimax-code","baseUrl":"https://api.minimax.io/v1","apiKeyEnv":"MINIMAX_CODE_API_KEY","models":["minimax-m3"],"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"reasoningContentField":"reasoning_content"}},
|
|
3
3
|
{"id":"minimax-cn","aliases":["minimax-code-cn","minimaxi"],"name":"MiniMax Coding Plan (China)","description":"OpenAI-compatible MiniMax China endpoint","compatibility":"openai","api":"openai-completions","providerId":"minimax-code-cn","baseUrl":"https://api.minimaxi.com/v1","apiKeyEnv":"MINIMAX_CODE_CN_API_KEY","models":["minimax-m3"],"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"reasoningContentField":"reasoning_content"}},
|
|
4
|
-
{"id":"glm","aliases":["zai","z-ai","bigmodel"],"name":"GLM / zAI","description":"OpenAI-compatible GLM endpoint from zAI/BigModel","compatibility":"openai","api":"openai-completions","providerId":"glm-proxy","baseUrl":"https://api.z.ai/api/paas/v4","apiKeyEnv":"ZAI_API_KEY","models":["glm-4.6"],"compat":{"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","reasoningContentField":"reasoning_content"}}
|
|
4
|
+
{"id":"glm","aliases":["zai","z-ai","bigmodel"],"name":"GLM / zAI","description":"OpenAI-compatible GLM endpoint from zAI/BigModel","compatibility":"openai","api":"openai-completions","providerId":"glm-proxy","baseUrl":"https://api.z.ai/api/paas/v4","apiKeyEnv":"ZAI_API_KEY","models":["glm-4.6"],"compat":{"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","reasoningContentField":"reasoning_content"}},
|
|
5
|
+
{"id":"alibaba-token-plan","aliases":["alibaba","token-plan"],"name":"Alibaba Token Plan","description":"OpenAI-compatible Alibaba Token Plan endpoint","compatibility":"openai","api":"openai-completions","providerId":"alibaba-token-plan","baseUrl":"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1","apiKeyEnv":"ALIBABA_TOKEN_PLAN_API_KEY","models":["qwen3.8-max-preview","glm-5.2","deepseek-v4-pro"],"modelApi":{"qwen3.8-max-preview":"openai-responses","glm-5.2":"openai-completions","deepseek-v4-pro":"openai-completions"},"compat":{"supportsDeveloperRole":false}}
|
|
5
6
|
]
|
|
@@ -1515,7 +1515,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1515
1515
|
},
|
|
1516
1516
|
{
|
|
1517
1517
|
name: "btw",
|
|
1518
|
-
description: "
|
|
1518
|
+
description: "Start an ephemeral multi-turn side chat using the current session context",
|
|
1519
1519
|
inlineHint: "<question>",
|
|
1520
1520
|
allowArgs: true,
|
|
1521
1521
|
handleTui: async (command, runtime) => {
|
package/src/ssh/utils.ts
CHANGED
|
@@ -3,6 +3,20 @@ export function sanitizeHostName(name: string): string {
|
|
|
3
3
|
return sanitized.length > 0 ? sanitized : "remote";
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
export function validateSshDestination(username: string | undefined, host: string): string | undefined {
|
|
7
|
+
if (host.startsWith("-") || username?.startsWith("-")) {
|
|
8
|
+
return "username and host must not begin with '-'";
|
|
9
|
+
}
|
|
10
|
+
if (/\0|\r|\n/.test(host) || (username !== undefined && /\0|\r|\n/.test(username))) {
|
|
11
|
+
return "username and host must not contain NUL or line breaks";
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
export function buildSshTarget(username: string | undefined, host: string): string {
|
|
17
|
+
const validationError = validateSshDestination(username, host);
|
|
18
|
+
if (validationError) {
|
|
19
|
+
throw new Error(`Invalid SSH destination: ${validationError}`);
|
|
20
|
+
}
|
|
7
21
|
return username ? `${username}@${host}` : host;
|
|
8
22
|
}
|
package/src/task/discovery.ts
CHANGED
|
@@ -14,6 +14,7 @@ import * as path from "node:path";
|
|
|
14
14
|
import { logger } from "@gajae-code/utils";
|
|
15
15
|
import { isProviderEnabled } from "../capability";
|
|
16
16
|
import { findAllNearestProjectConfigDirs, getConfigDirs } from "../config";
|
|
17
|
+
import type { Settings } from "../config/settings";
|
|
17
18
|
import { listClaudePluginRoots } from "../discovery/helpers";
|
|
18
19
|
import { rootContainsGjcManifest } from "../extensibility/gjc-plugins/paths";
|
|
19
20
|
import { loadBundledAgents, parseAgent } from "./agents";
|
|
@@ -57,8 +58,13 @@ async function loadAgentsFromDir(dir: string, source: AgentSource): Promise<Agen
|
|
|
57
58
|
* Precedence (highest wins): .gjc project, .gjc user, GJC plugins, then bundled
|
|
58
59
|
*
|
|
59
60
|
* @param cwd - Current working directory for project agent discovery
|
|
61
|
+
* @param activeSettings - Settings used to resolve enabled providers.
|
|
60
62
|
*/
|
|
61
|
-
export async function discoverAgents(
|
|
63
|
+
export async function discoverAgents(
|
|
64
|
+
cwd: string,
|
|
65
|
+
home: string = os.homedir(),
|
|
66
|
+
activeSettings?: Settings,
|
|
67
|
+
): Promise<DiscoveryResult> {
|
|
62
68
|
const resolvedCwd = path.resolve(cwd);
|
|
63
69
|
const agentSources = Array.from(new Set(getConfigDirs("", { project: false }).map(entry => entry.source)));
|
|
64
70
|
|
|
@@ -91,7 +97,7 @@ export async function discoverAgents(cwd: string, home: string = os.homedir()):
|
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
// Load agents from GJC marketplace plugins.
|
|
94
|
-
const { roots: pluginRoots } = isProviderEnabled("claude-plugins")
|
|
100
|
+
const { roots: pluginRoots } = isProviderEnabled("claude-plugins", activeSettings)
|
|
95
101
|
? await listClaudePluginRoots(home, resolvedCwd)
|
|
96
102
|
: { roots: [] };
|
|
97
103
|
const nonGjcPluginRoots = [];
|
package/src/tools/ask.ts
CHANGED
|
@@ -317,7 +317,7 @@ function isRoundZeroRecoveryCandidate(value: unknown): boolean {
|
|
|
317
317
|
const question = parseEncodedContainer(rawQuestion);
|
|
318
318
|
if (typeof question !== "object" || question === null || !Object.hasOwn(question, "deepInterview")) return false;
|
|
319
319
|
const deepInterview = parseEncodedContainer((question as Record<string, unknown>).deepInterview);
|
|
320
|
-
if (typeof deepInterview !== "object" || deepInterview === null) return
|
|
320
|
+
if (typeof deepInterview !== "object" || deepInterview === null) return false;
|
|
321
321
|
const metadata = deepInterview as Record<string, unknown>;
|
|
322
322
|
return Object.hasOwn(metadata, "intent_contract") || Object.hasOwn(metadata, "intent_review");
|
|
323
323
|
});
|
package/src/tools/fetch.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { AgentToolResult } from "@gajae-code/agent-core";
|
|
|
4
4
|
import type { ImageContent, TextContent } from "@gajae-code/ai";
|
|
5
5
|
import { htmlToMarkdown } from "@gajae-code/natives";
|
|
6
6
|
import { type Component, Text } from "@gajae-code/tui";
|
|
7
|
-
import {
|
|
7
|
+
import { ptree, truncate } from "@gajae-code/utils";
|
|
8
8
|
import type { Settings } from "../config/settings";
|
|
9
9
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
10
|
import { type Theme, theme } from "../modes/theme/theme";
|
|
@@ -15,10 +15,8 @@ import { renderStatusLine } from "../tui";
|
|
|
15
15
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
16
16
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
17
17
|
import { parseHtmlLazy } from "../utils/linkedom";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { validatePublicHttpUrl, validatePublicHttpUrlForInsane } from "../web/insane/url-guard";
|
|
21
|
-
import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../web/parallel";
|
|
18
|
+
import { INSANE_NOTES } from "../web/insane/bridge";
|
|
19
|
+
import { validatePublicHttpUrl } from "../web/insane/url-guard";
|
|
22
20
|
import { specialHandlers } from "../web/scrapers";
|
|
23
21
|
import type { RenderResult } from "../web/scrapers/types";
|
|
24
22
|
import { finalizeOutput, loadPage, looksLikeHtml, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
|
|
@@ -93,13 +91,6 @@ const MAX_INLINE_IMAGE_OUTPUT_BYTES = 300 * 1024;
|
|
|
93
91
|
// Utilities
|
|
94
92
|
// =============================================================================
|
|
95
93
|
|
|
96
|
-
/**
|
|
97
|
-
* Check if a command exists (cross-platform)
|
|
98
|
-
*/
|
|
99
|
-
function hasCommand(cmd: string): boolean {
|
|
100
|
-
return Boolean($which(cmd));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
94
|
/**
|
|
104
95
|
* Build llms.txt candidates scoped to the requested URL
|
|
105
96
|
*/
|
|
@@ -537,96 +528,18 @@ async function parseFeedToMarkdown(content: string, maxItems = 10): Promise<stri
|
|
|
537
528
|
return content; // Fall back to raw content
|
|
538
529
|
}
|
|
539
530
|
|
|
540
|
-
|
|
541
|
-
* Render HTML to markdown using Parallel, jina, trafilatura, lynx (in order of preference)
|
|
542
|
-
*/
|
|
543
|
-
async function renderHtmlToText(
|
|
544
|
-
url: string,
|
|
531
|
+
export async function renderHtmlToText(
|
|
545
532
|
html: string,
|
|
546
|
-
|
|
547
|
-
settings: Settings,
|
|
548
|
-
userSignal: AbortSignal | undefined,
|
|
549
|
-
storage: AgentStorage | null,
|
|
533
|
+
signal?: AbortSignal,
|
|
550
534
|
): Promise<{ content: string; ok: boolean; method: string }> {
|
|
551
|
-
const signal = ptree.combineSignals(userSignal, timeout * 1000);
|
|
552
|
-
const execOptions = {
|
|
553
|
-
mode: "group" as const,
|
|
554
|
-
allowNonZero: true,
|
|
555
|
-
allowAbort: true,
|
|
556
|
-
stderr: "full" as const,
|
|
557
|
-
signal,
|
|
558
|
-
};
|
|
559
|
-
|
|
560
|
-
// Try Parallel extract first when credentials are configured
|
|
561
|
-
if (settings.get("providers.parallelFetch") && findParallelApiKey(storage)) {
|
|
562
|
-
try {
|
|
563
|
-
const parallelResult = await extractWithParallel(
|
|
564
|
-
[url],
|
|
565
|
-
{
|
|
566
|
-
objective: "Extract the main content",
|
|
567
|
-
excerpts: true,
|
|
568
|
-
fullContent: false,
|
|
569
|
-
signal,
|
|
570
|
-
},
|
|
571
|
-
storage,
|
|
572
|
-
);
|
|
573
|
-
const firstDocument = parallelResult.results[0];
|
|
574
|
-
if (firstDocument) {
|
|
575
|
-
const content = getParallelExtractContent(firstDocument);
|
|
576
|
-
if (content.trim().length > 100 && !isLowQualityOutput(content)) {
|
|
577
|
-
return { content, ok: true, method: "parallel" };
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
} catch {
|
|
581
|
-
// Parallel extract failed, continue to next method
|
|
582
|
-
signal?.throwIfAborted();
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// Try jina first (reader API)
|
|
587
535
|
try {
|
|
588
|
-
const jinaUrl = `https://r.jina.ai/${url}`;
|
|
589
|
-
const response = await fetch(jinaUrl, {
|
|
590
|
-
headers: { Accept: "text/markdown" },
|
|
591
|
-
signal,
|
|
592
|
-
});
|
|
593
|
-
if (response.ok) {
|
|
594
|
-
const content = await response.text();
|
|
595
|
-
if (content.trim().length > 100 && !isLowQualityOutput(content)) {
|
|
596
|
-
return { content, ok: true, method: "jina" };
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
} catch {
|
|
600
|
-
// Jina failed, continue to next method
|
|
601
536
|
signal?.throwIfAborted();
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Try trafilatura (auto-install via uv/pip)
|
|
605
|
-
const trafilatura = await ensureTool("trafilatura", { signal, silent: true });
|
|
606
|
-
if (trafilatura) {
|
|
607
|
-
const result = await ptree.exec([trafilatura, "-u", url, "--output-format", "markdown"], execOptions);
|
|
608
|
-
if (result.ok && result.stdout.trim().length > 100) {
|
|
609
|
-
return { content: result.stdout, ok: true, method: "trafilatura" };
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Try lynx (can't auto-install, system package)
|
|
614
|
-
const lynx = hasCommand("lynx");
|
|
615
|
-
if (lynx) {
|
|
616
|
-
const result = await ptree.exec(["lynx", "-dump", "-nolist", "-width", "250", url], execOptions);
|
|
617
|
-
if (result.ok) {
|
|
618
|
-
return { content: result.stdout, ok: true, method: "lynx" };
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Fall back to native converter (fastest, no network/subprocess)
|
|
623
|
-
try {
|
|
624
537
|
const content = await htmlToMarkdown(html, { cleanContent: true });
|
|
625
538
|
if (content.trim().length > 100 && !isLowQualityOutput(content)) {
|
|
626
539
|
return { content, ok: true, method: "native" };
|
|
627
540
|
}
|
|
628
541
|
} catch {
|
|
629
|
-
// Native
|
|
542
|
+
// Native conversion failed; the caller returns its bounded raw HTML.
|
|
630
543
|
signal?.throwIfAborted();
|
|
631
544
|
}
|
|
632
545
|
return { content: "", ok: false, method: "none" };
|
|
@@ -707,14 +620,6 @@ async function handleSpecialUrls(
|
|
|
707
620
|
// Main Render Function
|
|
708
621
|
// =============================================================================
|
|
709
622
|
|
|
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
623
|
export async function tryInsaneFallback(args: {
|
|
719
624
|
url: string;
|
|
720
625
|
finalUrl: string;
|
|
@@ -727,32 +632,7 @@ export async function tryInsaneFallback(args: {
|
|
|
727
632
|
}): Promise<FetchRenderResult | null> {
|
|
728
633
|
if (args.raw) return null;
|
|
729
634
|
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);
|
|
635
|
+
args.notes.push(INSANE_NOTES.securityDisabled);
|
|
756
636
|
return null;
|
|
757
637
|
}
|
|
758
638
|
|
|
@@ -772,6 +652,7 @@ async function renderUrl(
|
|
|
772
652
|
if (signal?.aborted) {
|
|
773
653
|
throw new ToolAbortError();
|
|
774
654
|
}
|
|
655
|
+
const preflightSignal = ptree.combineSignals(signal, timeout * 1000);
|
|
775
656
|
|
|
776
657
|
// Handle internal protocol URLs (e.g., pi-internal://) - return empty
|
|
777
658
|
if (url.startsWith("pi-internal://")) {
|
|
@@ -789,7 +670,8 @@ async function renderUrl(
|
|
|
789
670
|
|
|
790
671
|
// Step 0: Normalize URL (ensure scheme for special handlers)
|
|
791
672
|
url = normalizeUrl(url);
|
|
792
|
-
const publicUrl = await validatePublicHttpUrl(url);
|
|
673
|
+
const publicUrl = await validatePublicHttpUrl(url, { signal: preflightSignal });
|
|
674
|
+
if (signal?.aborted) throw new ToolAbortError();
|
|
793
675
|
if (!publicUrl.ok) {
|
|
794
676
|
notes.push(`Blocked URL fetch: target URL is not public HTTP(S): ${publicUrl.reason}`);
|
|
795
677
|
return {
|
|
@@ -1138,10 +1020,10 @@ async function renderUrl(
|
|
|
1138
1020
|
throw new ToolAbortError();
|
|
1139
1021
|
}
|
|
1140
1022
|
|
|
1141
|
-
// 5E: Render
|
|
1142
|
-
const htmlResult = await renderHtmlToText(
|
|
1023
|
+
// 5E: Render only the bytes already fetched above.
|
|
1024
|
+
const htmlResult = await renderHtmlToText(rawContent, signal);
|
|
1143
1025
|
if (!htmlResult.ok) {
|
|
1144
|
-
notes.push("
|
|
1026
|
+
notes.push("native HTML rendering failed");
|
|
1145
1027
|
const insane = await tryInsaneFallback({ url, finalUrl, timeout, raw, settings, signal, fetchedAt, notes });
|
|
1146
1028
|
if (insane) return insane;
|
|
1147
1029
|
const output = finalizeOutput(rawContent);
|