@gajae-code/coding-agent 0.11.7 → 0.11.8
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 +55 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/config/keybindings.d.ts +33 -3
- package/dist/types/config/model-profile-activation.d.ts +4 -3
- package/dist/types/config/model-profile-contract.d.ts +38 -0
- package/dist/types/config/settings-schema.d.ts +15 -2
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
- package/dist/types/main.d.ts +8 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +3 -2
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +5 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +25 -0
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +1 -1
- package/dist/types/sdk/host/query/handlers.d.ts +9 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/startup-capability.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +11 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +17 -2
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-storage.d.ts +18 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +94 -0
- package/dist/types/tools/ask.d.ts +454 -174
- package/dist/types/tools/image-gen.d.ts +47 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/package.json +8 -8
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/cli/args.ts +2 -92
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli.ts +18 -0
- package/src/commands/sdk.ts +2 -2
- package/src/config/keybindings.ts +157 -24
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +17 -2
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/py/executor.ts +30 -11
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/gjc-runtime/launch-tmux.ts +1 -1
- package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/gjc-runtime/ralplan-runtime.ts +75 -3
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
- package/src/gjc-runtime/tmux-sessions.ts +45 -6
- package/src/gjc-runtime/ultragoal-guard.ts +5 -5
- package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/internal-urls/local-protocol.ts +33 -16
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +25 -10
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/components/model-selector.ts +42 -8
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/tool-status-header.ts +20 -4
- package/src/modes/components/welcome.ts +22 -18
- package/src/modes/controllers/command-controller.ts +97 -54
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/controllers/input-controller.ts +7 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +46 -54
- package/src/modes/interactive-mode.ts +41 -27
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +5 -3
- package/src/prompts/agents/architect.md +0 -1
- package/src/prompts/agents/critic.md +0 -1
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/runtime-mcp/manager.ts +51 -53
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/transports/http.ts +13 -6
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/bus/chat-daemon-control.ts +18 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +9 -1
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/index.ts +240 -65
- package/src/sdk/bus/lifecycle-commands.ts +47 -3
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +27 -2
- package/src/sdk/bus/prompt-reconciliation.ts +239 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/telegram-daemon-contract.ts +13 -3
- package/src/sdk/bus/telegram-daemon-control.ts +145 -33
- package/src/sdk/bus/telegram-daemon.ts +835 -110
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/host/control/dispatch.ts +1 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +2 -6
- package/src/sdk/host/query/handlers.ts +70 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +47 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +2 -0
- package/src/sdk/startup-capability.ts +9 -1
- package/src/session/agent-session.ts +52 -5
- package/src/session/blob-store.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +854 -84
- package/src/session/internal/managed-session-storage.ts +251 -199
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +310 -327
- package/src/session/session-storage.ts +128 -11
- package/src/task/gjc-command.ts +2 -2
- package/src/task/index.ts +121 -15
- package/src/task/receipt.ts +3 -0
- package/src/task/types.ts +34 -3
- package/src/tools/ask.ts +198 -88
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/image-gen.ts +178 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/url-guard.ts +3 -1
- package/src/web/scrapers/docs-rs.ts +30 -5
package/src/tools/image-gen.ts
CHANGED
|
@@ -41,10 +41,12 @@ const OPENAI_IMAGE_OUTPUT_FORMAT = "webp";
|
|
|
41
41
|
const OPENAI_IMAGE_MIME_TYPE = "image/webp";
|
|
42
42
|
|
|
43
43
|
const ANTIGRAVITY_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
|
44
|
+
const ALIBABA_TOKEN_PLAN_HOST = "https://token-plan.ap-southeast-1.maas.aliyuncs.com";
|
|
45
|
+
const ALIBABA_IMAGE_GENERATION_URL = `${ALIBABA_TOKEN_PLAN_HOST}/api/v1/services/aigc/multimodal-generation/generation`;
|
|
44
46
|
const IMAGE_SYSTEM_INSTRUCTION =
|
|
45
47
|
"You are an AI image generator. Generate images based on user descriptions. Focus on creating high-quality, visually appealing images that match the user's request.";
|
|
46
48
|
|
|
47
|
-
type ImageProvider = "antigravity" | "gemini" | "openai" | "openai-codex" | "openrouter";
|
|
49
|
+
type ImageProvider = "alibaba" | "antigravity" | "gemini" | "openai" | "openai-codex" | "openrouter";
|
|
48
50
|
interface ImageApiKey {
|
|
49
51
|
provider: ImageProvider;
|
|
50
52
|
apiKey: string;
|
|
@@ -296,6 +298,81 @@ interface AntigravityResponseChunk {
|
|
|
296
298
|
};
|
|
297
299
|
}
|
|
298
300
|
|
|
301
|
+
interface AlibabaImageContentPart {
|
|
302
|
+
type?: string;
|
|
303
|
+
text?: string;
|
|
304
|
+
image?: string;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
interface AlibabaImageResponse {
|
|
308
|
+
code?: string;
|
|
309
|
+
message?: string;
|
|
310
|
+
output?: {
|
|
311
|
+
choices?: Array<{ message?: { content?: AlibabaImageContentPart[] } }>;
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Map the tool's image_size values onto wan2.7's 1K/2K size classes. */
|
|
316
|
+
export function resolveAlibabaImageSize(imageSize: string | undefined): string | undefined {
|
|
317
|
+
switch (imageSize) {
|
|
318
|
+
case "1024x1024":
|
|
319
|
+
return "1K";
|
|
320
|
+
case "1536x1024":
|
|
321
|
+
case "1024x1536":
|
|
322
|
+
return "2K";
|
|
323
|
+
default:
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Build the Bailian multimodal-generation request body for wan2.7 image
|
|
330
|
+
* generation/editing. Input images ride along as data URLs; generation-only
|
|
331
|
+
* calls send just the prompt text. The sync endpoint returns OSS URLs inline.
|
|
332
|
+
*/
|
|
333
|
+
export function buildAlibabaImageRequest(
|
|
334
|
+
model: string,
|
|
335
|
+
promptText: string,
|
|
336
|
+
inputImages: InlineImageData[],
|
|
337
|
+
imageSize: string | undefined,
|
|
338
|
+
): {
|
|
339
|
+
model: string;
|
|
340
|
+
input: { messages: Array<{ role: "user"; content: Array<{ text: string } | { image: string }> }> };
|
|
341
|
+
parameters: { n: number; watermark: boolean; size?: string };
|
|
342
|
+
} {
|
|
343
|
+
const content: Array<{ text: string } | { image: string }> = [];
|
|
344
|
+
for (const image of inputImages) {
|
|
345
|
+
content.push({ image: toDataUrl(image) });
|
|
346
|
+
}
|
|
347
|
+
content.push({ text: promptText });
|
|
348
|
+
const size = resolveAlibabaImageSize(imageSize);
|
|
349
|
+
return {
|
|
350
|
+
model,
|
|
351
|
+
input: { messages: [{ role: "user", content }] },
|
|
352
|
+
parameters: { n: 1, watermark: false, ...(size ? { size } : {}) },
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Collect OSS image URLs and any text parts from a Bailian image response. */
|
|
357
|
+
export function collectAlibabaImageResult(response: AlibabaImageResponse): {
|
|
358
|
+
imageUrls: string[];
|
|
359
|
+
responseText?: string;
|
|
360
|
+
} {
|
|
361
|
+
const imageUrls: string[] = [];
|
|
362
|
+
const textParts: string[] = [];
|
|
363
|
+
for (const choice of response.output?.choices ?? []) {
|
|
364
|
+
for (const part of choice.message?.content ?? []) {
|
|
365
|
+
if (part.image) {
|
|
366
|
+
imageUrls.push(part.image);
|
|
367
|
+
} else if (part.text) {
|
|
368
|
+
textParts.push(part.text);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const responseText = textParts.join("\n").trim();
|
|
373
|
+
return { imageUrls, responseText: responseText.length > 0 ? responseText : undefined };
|
|
374
|
+
}
|
|
375
|
+
|
|
299
376
|
interface ImageGenToolDetails {
|
|
300
377
|
provider: ImageProvider;
|
|
301
378
|
model: string;
|
|
@@ -585,6 +662,7 @@ export function setPreferredImageProvider(provider: ImageProvider | "auto"): voi
|
|
|
585
662
|
/** Provider → default image model mapping for auto-binding */
|
|
586
663
|
export const IMAGE_PROVIDER_DEFAULTS: Record<string, string> = {
|
|
587
664
|
openai: "gpt-image-2",
|
|
665
|
+
alibaba: "wan2.7-image",
|
|
588
666
|
"openai-codex": "gpt-image-2",
|
|
589
667
|
antigravity: "gemini-3-pro-image",
|
|
590
668
|
gemini: "gemini-3-pro-image-preview",
|
|
@@ -704,6 +782,18 @@ async function findOpenAIHostedImageCredentials(
|
|
|
704
782
|
};
|
|
705
783
|
}
|
|
706
784
|
|
|
785
|
+
async function findAlibabaImageCredentials(
|
|
786
|
+
modelRegistry: ModelRegistry | undefined,
|
|
787
|
+
sessionId?: string,
|
|
788
|
+
): Promise<ImageApiKey | null> {
|
|
789
|
+
const envKey = getEnvApiKey("alibaba-token-plan");
|
|
790
|
+
if (envKey) return { provider: "alibaba", apiKey: envKey };
|
|
791
|
+
if (!modelRegistry) return null;
|
|
792
|
+
const apiKey = await modelRegistry.getApiKeyForProvider("alibaba-token-plan", sessionId);
|
|
793
|
+
if (!isAuthenticated(apiKey)) return null;
|
|
794
|
+
return { provider: "alibaba", apiKey };
|
|
795
|
+
}
|
|
796
|
+
|
|
707
797
|
async function findImageApiKey(
|
|
708
798
|
modelRegistry?: ModelRegistry,
|
|
709
799
|
activeModel?: Model,
|
|
@@ -744,6 +834,9 @@ async function findImageApiKey(
|
|
|
744
834
|
const openRouterKey = getEnvApiKey("openrouter");
|
|
745
835
|
return openRouterKey ? { provider: "openrouter", apiKey: openRouterKey } : null;
|
|
746
836
|
}
|
|
837
|
+
if (config.provider === "alibaba") {
|
|
838
|
+
return await findAlibabaImageCredentials(modelRegistry, sessionId);
|
|
839
|
+
}
|
|
747
840
|
}
|
|
748
841
|
|
|
749
842
|
// If a specific provider is preferred (legacy path), try it first.
|
|
@@ -762,9 +855,11 @@ async function findImageApiKey(
|
|
|
762
855
|
} else if (preferredImageProvider === "openrouter") {
|
|
763
856
|
const openRouterKey = getEnvApiKey("openrouter");
|
|
764
857
|
return openRouterKey ? { provider: "openrouter", apiKey: openRouterKey } : null;
|
|
858
|
+
} else if (preferredImageProvider === "alibaba") {
|
|
859
|
+
return await findAlibabaImageCredentials(modelRegistry, sessionId);
|
|
765
860
|
}
|
|
766
861
|
|
|
767
|
-
// Auto-detect: GPT hosted image generation, then Antigravity, OpenRouter, Gemini.
|
|
862
|
+
// Auto-detect: GPT hosted image generation, then Antigravity, OpenRouter, Gemini, Alibaba.
|
|
768
863
|
const openAI = await findOpenAIHostedImageCredentials(modelRegistry, activeModel, sessionId);
|
|
769
864
|
if (openAI) return openAI;
|
|
770
865
|
|
|
@@ -782,6 +877,9 @@ async function findImageApiKey(
|
|
|
782
877
|
const googleKey = $env.GOOGLE_API_KEY;
|
|
783
878
|
if (googleKey) return { provider: "gemini", apiKey: googleKey };
|
|
784
879
|
|
|
880
|
+
const alibaba = await findAlibabaImageCredentials(modelRegistry, sessionId);
|
|
881
|
+
if (alibaba) return alibaba;
|
|
882
|
+
|
|
785
883
|
return null;
|
|
786
884
|
}
|
|
787
885
|
|
|
@@ -1218,7 +1316,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1218
1316
|
const apiKey = await findImageApiKey(ctx.modelRegistry, ctx.model, sessionId);
|
|
1219
1317
|
if (!apiKey) {
|
|
1220
1318
|
throw new Error(
|
|
1221
|
-
"No image API credentials found. Use a GPT Responses/Codex model with OpenAI credentials, login with google-antigravity, or set OPENROUTER_API_KEY, GEMINI_API_KEY, or
|
|
1319
|
+
"No image API credentials found. Use a GPT Responses/Codex model with OpenAI credentials, login with google-antigravity, or set OPENROUTER_API_KEY, GEMINI_API_KEY, GOOGLE_API_KEY, or ALIBABA_TOKEN_PLAN_API_KEY.",
|
|
1222
1320
|
);
|
|
1223
1321
|
}
|
|
1224
1322
|
|
|
@@ -1230,9 +1328,11 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1230
1328
|
? (apiKey.model?.id ?? resolveImageModel(provider, null))
|
|
1231
1329
|
: provider === "antigravity"
|
|
1232
1330
|
? resolveImageModel("antigravity", null)
|
|
1233
|
-
: provider === "
|
|
1234
|
-
? resolveImageModel("
|
|
1235
|
-
:
|
|
1331
|
+
: provider === "alibaba"
|
|
1332
|
+
? resolveImageModel("alibaba", null)
|
|
1333
|
+
: provider === "openrouter"
|
|
1334
|
+
? resolveImageModel("openrouter", null)
|
|
1335
|
+
: resolveImageModel("gemini", null);
|
|
1236
1336
|
const resolvedModel = provider === "openrouter" ? resolveOpenRouterModel(model) : model;
|
|
1237
1337
|
const cwd = ctx.sessionManager.getCwd();
|
|
1238
1338
|
|
|
@@ -1450,6 +1550,78 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1450
1550
|
};
|
|
1451
1551
|
}
|
|
1452
1552
|
|
|
1553
|
+
if (provider === "alibaba") {
|
|
1554
|
+
const requestBody = buildAlibabaImageRequest(
|
|
1555
|
+
model,
|
|
1556
|
+
assemblePrompt(params),
|
|
1557
|
+
resolvedImages,
|
|
1558
|
+
params.image_size,
|
|
1559
|
+
);
|
|
1560
|
+
|
|
1561
|
+
const response = await fetch(ALIBABA_IMAGE_GENERATION_URL, {
|
|
1562
|
+
method: "POST",
|
|
1563
|
+
headers: {
|
|
1564
|
+
"Content-Type": "application/json",
|
|
1565
|
+
Authorization: `Bearer ${apiKey.apiKey}`,
|
|
1566
|
+
},
|
|
1567
|
+
body: JSON.stringify(requestBody),
|
|
1568
|
+
signal: requestSignal,
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
const rawText = await response.text();
|
|
1572
|
+
if (!response.ok) {
|
|
1573
|
+
let message = rawText;
|
|
1574
|
+
try {
|
|
1575
|
+
const parsed = JSON.parse(rawText) as { message?: string; error?: { message?: string } };
|
|
1576
|
+
message = parsed.error?.message ?? parsed.message ?? message;
|
|
1577
|
+
} catch {
|
|
1578
|
+
// Keep raw text.
|
|
1579
|
+
}
|
|
1580
|
+
throw new Error(`Alibaba image request failed (${response.status}): ${message}`);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
const data = JSON.parse(rawText) as AlibabaImageResponse;
|
|
1584
|
+
if (data.code) {
|
|
1585
|
+
throw new Error(`Alibaba image request failed: ${data.code}: ${data.message ?? ""}`.trim());
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
const { imageUrls, responseText } = collectAlibabaImageResult(data);
|
|
1589
|
+
// Result URLs are short-lived OSS-signed URLs (24h); download immediately.
|
|
1590
|
+
const inlineImages: InlineImageData[] = [];
|
|
1591
|
+
for (const imageUrl of imageUrls) {
|
|
1592
|
+
inlineImages.push(await loadImageFromUrl(imageUrl, requestSignal));
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
if (inlineImages.length === 0) {
|
|
1596
|
+
const messageText = responseText ? `\n\n${responseText}` : "";
|
|
1597
|
+
return {
|
|
1598
|
+
content: [{ type: "text", text: `No image data returned.${messageText}` }],
|
|
1599
|
+
details: {
|
|
1600
|
+
provider,
|
|
1601
|
+
model,
|
|
1602
|
+
imageCount: 0,
|
|
1603
|
+
imagePaths: [],
|
|
1604
|
+
images: [],
|
|
1605
|
+
responseText,
|
|
1606
|
+
},
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
const imagePaths = await saveImagesToTemp(inlineImages);
|
|
1611
|
+
|
|
1612
|
+
return {
|
|
1613
|
+
content: [{ type: "text", text: buildResponseSummary(provider, model, imagePaths, responseText) }],
|
|
1614
|
+
details: {
|
|
1615
|
+
provider,
|
|
1616
|
+
model,
|
|
1617
|
+
imageCount: inlineImages.length,
|
|
1618
|
+
imagePaths,
|
|
1619
|
+
images: inlineImages,
|
|
1620
|
+
responseText,
|
|
1621
|
+
},
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1453
1625
|
const parts = [] as Array<{ text?: string; inlineData?: InlineImageData }>;
|
|
1454
1626
|
for (const image of resolvedImages) {
|
|
1455
1627
|
parts.push({ inlineData: image });
|
package/src/tools/index.ts
CHANGED
|
@@ -137,6 +137,8 @@ export interface AskAnswerRequest {
|
|
|
137
137
|
options: string[];
|
|
138
138
|
interaction: "selector" | "custom_editor" | "clarification_editor";
|
|
139
139
|
controls: readonly AskRemoteControl[];
|
|
140
|
+
/** Optional zero-based recommendation into the authoritative raw options. */
|
|
141
|
+
recommendedIndex?: number;
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
export type AskRemoteInteraction =
|
|
@@ -232,6 +234,8 @@ export interface ToolSession {
|
|
|
232
234
|
* its terminal-phase chain guard. Returns the raw phase string or undefined
|
|
233
235
|
* when no active skill (or accessor) is available. */
|
|
234
236
|
getActiveSkillPhase?: () => string | undefined;
|
|
237
|
+
/** Restrict provider-facing deep-interview ask metadata to the active workflow stage. */
|
|
238
|
+
getDeepInterviewAskStage?: () => "topology" | "post-topology" | undefined;
|
|
235
239
|
/** Pre-loaded prompt templates */
|
|
236
240
|
promptTemplates?: PromptTemplate[];
|
|
237
241
|
/** Whether LSP integrations are enabled */
|
|
@@ -571,7 +571,13 @@ export function truncateDiffByHunk(
|
|
|
571
571
|
export function shortenPath(filePath: string, homeDir?: string): string {
|
|
572
572
|
if (typeof filePath !== "string") return "";
|
|
573
573
|
const home = homeDir ?? os.homedir();
|
|
574
|
-
if (home
|
|
574
|
+
if (!home) return filePath;
|
|
575
|
+
// Only abbreviate paths that are exactly home or genuinely inside it. A bare
|
|
576
|
+
// startsWith(home) also matches siblings that merely share the prefix
|
|
577
|
+
// (e.g. "/home/woodyx/notes.txt" for home "/home/woody"), which would corrupt
|
|
578
|
+
// them into "~x/notes.txt"; require a path-separator boundary after home.
|
|
579
|
+
const boundary = home.endsWith(path.sep) ? home : home + path.sep;
|
|
580
|
+
if (filePath === home || filePath.startsWith(boundary)) {
|
|
575
581
|
return `~${filePath.slice(home.length)}`;
|
|
576
582
|
}
|
|
577
583
|
return filePath;
|
package/src/utils/edit-mode.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $
|
|
1
|
+
import { $pickenv } from "@gajae-code/utils/env";
|
|
2
2
|
|
|
3
3
|
export type EditMode = "replace" | "patch" | "hashline" | "vim" | "apply_patch";
|
|
4
4
|
|
|
@@ -34,7 +34,7 @@ export function resolveEditMode(session: EditModeSessionLike): EditMode {
|
|
|
34
34
|
const modelVariant = session.settings.getEditVariantForModel?.(activeModel);
|
|
35
35
|
if (modelVariant) return modelVariant;
|
|
36
36
|
|
|
37
|
-
const envMode = normalizeEditMode($
|
|
37
|
+
const envMode = normalizeEditMode($pickenv("GJC_EDIT_VARIANT", "PI_EDIT_VARIANT"));
|
|
38
38
|
if (envMode) return envMode;
|
|
39
39
|
|
|
40
40
|
const settingsMode = normalizeEditMode(String(session.settings.get("edit.mode") ?? ""));
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Predict the user's likely next prompt after an agent turn using a smol,
|
|
3
|
+
* fast model. Native GJC port of the ghost-text prompt-suggestion behavior
|
|
4
|
+
* from Claude Code: the prediction renders as dim ghost text in the empty
|
|
5
|
+
* composer and Tab accepts it.
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentMessage } from "@gajae-code/agent-core";
|
|
8
|
+
import { type Api, type AssistantMessage, completeSimple, type Model } from "@gajae-code/ai";
|
|
9
|
+
import { logger, prompt } from "@gajae-code/utils";
|
|
10
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
11
|
+
import { resolveRoleSelection } from "../config/model-resolver";
|
|
12
|
+
import type { Settings } from "../config/settings";
|
|
13
|
+
import suggestionSystemPrompt from "../prompts/system/prompt-suggestion-system.md" with { type: "text" };
|
|
14
|
+
|
|
15
|
+
const SUGGESTION_SYSTEM_PROMPT = prompt.render(suggestionSystemPrompt);
|
|
16
|
+
|
|
17
|
+
export const PROMPT_SUGGESTION_MAX_WORDS = 12;
|
|
18
|
+
export const PROMPT_SUGGESTION_MAX_CHARS = 100;
|
|
19
|
+
|
|
20
|
+
const SUGGESTION_MAX_TOKENS = 64;
|
|
21
|
+
const REASONING_SAFE_MAX_TOKENS = 1024;
|
|
22
|
+
const MAX_CONTEXT_MESSAGES = 12;
|
|
23
|
+
const MAX_MESSAGE_CHARS = 1500;
|
|
24
|
+
const MAX_CONTEXT_CHARS = 8000;
|
|
25
|
+
|
|
26
|
+
/** Single words a user plausibly types on their own; anything else needs >= 2 words. */
|
|
27
|
+
const SINGLE_WORD_ALLOWLIST = new Set([
|
|
28
|
+
"yes",
|
|
29
|
+
"yeah",
|
|
30
|
+
"yep",
|
|
31
|
+
"yea",
|
|
32
|
+
"yup",
|
|
33
|
+
"sure",
|
|
34
|
+
"ok",
|
|
35
|
+
"okay",
|
|
36
|
+
"push",
|
|
37
|
+
"commit",
|
|
38
|
+
"deploy",
|
|
39
|
+
"stop",
|
|
40
|
+
"continue",
|
|
41
|
+
"check",
|
|
42
|
+
"exit",
|
|
43
|
+
"quit",
|
|
44
|
+
"no",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/** Model responses that are meta-statements about staying silent, not suggestions. */
|
|
48
|
+
const META_TEXT_PATTERN = /\bsilence is\b|\bstay(s|ing)? silent\b/;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Strip wrapper tags and label prefixes a model sometimes adds despite being
|
|
52
|
+
* told to reply with only the suggestion.
|
|
53
|
+
*/
|
|
54
|
+
export function sanitizePromptSuggestion(raw: string): string {
|
|
55
|
+
return raw
|
|
56
|
+
.trim()
|
|
57
|
+
.replace(/^<(suggestion|response|output|answer|result)>([\s\S]*)<\/\1>$/i, (match, tag: string, inner: string) =>
|
|
58
|
+
// Keep the original when the inner text itself contains the closing
|
|
59
|
+
// tag (nested/ambiguous markup) instead of producing a mangled strip.
|
|
60
|
+
inner.includes(`</${tag.toLowerCase()}>`) || inner.includes(`</${tag.toUpperCase()}>`) ? match : inner,
|
|
61
|
+
)
|
|
62
|
+
.replace(
|
|
63
|
+
/^\s*(suggested\s+(response|reply|input|prompt)|suggestion|response|reply|answer|output|result)\s*:\s*/i,
|
|
64
|
+
"",
|
|
65
|
+
)
|
|
66
|
+
.trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Heuristic gate rejecting model output that would make a bad ghost-text
|
|
71
|
+
* suggestion. Returns the rejection reason, or null when the text is usable.
|
|
72
|
+
*/
|
|
73
|
+
export function suppressPromptSuggestionReason(text: string): string | null {
|
|
74
|
+
if (!text) return "empty";
|
|
75
|
+
const lower = text.toLowerCase();
|
|
76
|
+
const wordCount = text.trim().split(/\s+/).length;
|
|
77
|
+
|
|
78
|
+
if (lower === "done") return "done";
|
|
79
|
+
if (
|
|
80
|
+
lower === "nothing found" ||
|
|
81
|
+
lower === "nothing found." ||
|
|
82
|
+
lower.startsWith("nothing to suggest") ||
|
|
83
|
+
lower.startsWith("no suggestion") ||
|
|
84
|
+
META_TEXT_PATTERN.test(lower) ||
|
|
85
|
+
/^\W*silence\W*$/.test(lower)
|
|
86
|
+
) {
|
|
87
|
+
return "meta_text";
|
|
88
|
+
}
|
|
89
|
+
if (/^\(.*\)$|^\[.*\]$/.test(text)) return "meta_wrapped";
|
|
90
|
+
if (
|
|
91
|
+
lower.startsWith("api error:") ||
|
|
92
|
+
lower.startsWith("prompt is too long") ||
|
|
93
|
+
lower.startsWith("request timed out") ||
|
|
94
|
+
lower.startsWith("invalid api key")
|
|
95
|
+
) {
|
|
96
|
+
return "error_message";
|
|
97
|
+
}
|
|
98
|
+
if (/^\w+:\s/.test(text)) return "prefixed_label";
|
|
99
|
+
if (wordCount < 2 && !text.startsWith("/") && !SINGLE_WORD_ALLOWLIST.has(lower)) return "too_few_words";
|
|
100
|
+
if (wordCount > PROMPT_SUGGESTION_MAX_WORDS) return "too_many_words";
|
|
101
|
+
if (text.length >= PROMPT_SUGGESTION_MAX_CHARS) return "too_long";
|
|
102
|
+
if (/[.!?]\s+[A-Z]/.test(text)) return "multiple_sentences";
|
|
103
|
+
if (/[\n*]|\*\*/.test(text)) return "has_formatting";
|
|
104
|
+
if (
|
|
105
|
+
/thanks|thank you|looks good|sounds good|that works|that worked|that's all|nice|great|perfect|makes sense|awesome|excellent/.test(
|
|
106
|
+
lower,
|
|
107
|
+
)
|
|
108
|
+
) {
|
|
109
|
+
return "evaluative";
|
|
110
|
+
}
|
|
111
|
+
if (
|
|
112
|
+
/^(let me|i'll|i've|i'm|i can|i would|i think|i notice|here's|here is|here are|that's|this is|this will|you can|you should|you could|sure,|of course|certainly)/i.test(
|
|
113
|
+
text,
|
|
114
|
+
)
|
|
115
|
+
) {
|
|
116
|
+
return "claude_voice";
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractTranscriptEntry(message: AgentMessage): { role: "User" | "Assistant"; text: string } | null {
|
|
122
|
+
if (message.role === "user") {
|
|
123
|
+
const content = message.content;
|
|
124
|
+
const text =
|
|
125
|
+
typeof content === "string"
|
|
126
|
+
? content
|
|
127
|
+
: content
|
|
128
|
+
.filter(block => block.type === "text")
|
|
129
|
+
.map(block => block.text)
|
|
130
|
+
.join("\n");
|
|
131
|
+
const trimmed = text.trim();
|
|
132
|
+
return trimmed ? { role: "User", text: trimmed } : null;
|
|
133
|
+
}
|
|
134
|
+
if (message.role === "assistant") {
|
|
135
|
+
const text = (message as AssistantMessage).content
|
|
136
|
+
.filter(block => block.type === "text")
|
|
137
|
+
.map(block => block.text)
|
|
138
|
+
.join("\n")
|
|
139
|
+
.trim();
|
|
140
|
+
return text ? { role: "Assistant", text } : null;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Build a compact recent-conversation transcript for the prediction request.
|
|
147
|
+
* Returns null when there is no user message to predict from.
|
|
148
|
+
*/
|
|
149
|
+
export function buildPromptSuggestionContext(messages: AgentMessage[]): string | null {
|
|
150
|
+
const entries: { role: "User" | "Assistant"; text: string }[] = [];
|
|
151
|
+
let totalChars = 0;
|
|
152
|
+
for (let index = messages.length - 1; index >= 0 && entries.length < MAX_CONTEXT_MESSAGES; index--) {
|
|
153
|
+
const message = messages[index];
|
|
154
|
+
if (!message) continue;
|
|
155
|
+
const entry = extractTranscriptEntry(message);
|
|
156
|
+
if (!entry) continue;
|
|
157
|
+
const text = entry.text.length > MAX_MESSAGE_CHARS ? `${entry.text.slice(0, MAX_MESSAGE_CHARS)}…` : entry.text;
|
|
158
|
+
if (totalChars + text.length > MAX_CONTEXT_CHARS && entries.length > 0) break;
|
|
159
|
+
entries.push({ role: entry.role, text });
|
|
160
|
+
totalChars += text.length;
|
|
161
|
+
}
|
|
162
|
+
if (!entries.some(entry => entry.role === "User")) return null;
|
|
163
|
+
entries.reverse();
|
|
164
|
+
const transcript = entries.map(entry => `${entry.role}: ${entry.text}`).join("\n\n");
|
|
165
|
+
return `<conversation>\n${transcript}\n</conversation>\n\nPredict the user's next message.`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getSuggestionModel(
|
|
169
|
+
registry: ModelRegistry,
|
|
170
|
+
settings: Settings,
|
|
171
|
+
currentModel?: Model<Api>,
|
|
172
|
+
): Model<Api> | undefined {
|
|
173
|
+
const availableModels = registry.getAvailable();
|
|
174
|
+
if (availableModels.length === 0) return undefined;
|
|
175
|
+
const model = resolveRoleSelection(["default"], settings, availableModels, registry)?.model;
|
|
176
|
+
if (model) return model;
|
|
177
|
+
return currentModel;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function extractSuggestionText(contentBlocks: AssistantMessage["content"]): string {
|
|
181
|
+
let text = "";
|
|
182
|
+
for (const content of contentBlocks) {
|
|
183
|
+
if (content.type === "text") {
|
|
184
|
+
text += content.text;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return text;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Generate a predicted next user prompt from the recent conversation.
|
|
192
|
+
* Returns null when no usable suggestion is produced (silence is the
|
|
193
|
+
* expected steady state).
|
|
194
|
+
*/
|
|
195
|
+
export async function generatePromptSuggestion(
|
|
196
|
+
messages: AgentMessage[],
|
|
197
|
+
registry: ModelRegistry,
|
|
198
|
+
settings: Settings,
|
|
199
|
+
sessionId?: string,
|
|
200
|
+
currentModel?: Model<Api>,
|
|
201
|
+
metadataResolver?: (provider: string) => Record<string, unknown> | undefined,
|
|
202
|
+
): Promise<string | null> {
|
|
203
|
+
const context = buildPromptSuggestionContext(messages);
|
|
204
|
+
if (!context) return null;
|
|
205
|
+
|
|
206
|
+
const model = getSuggestionModel(registry, settings, currentModel);
|
|
207
|
+
if (!model) {
|
|
208
|
+
logger.debug("prompt-suggestion: no model available");
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const apiKey = await registry.getApiKey(model, sessionId);
|
|
213
|
+
if (!apiKey) {
|
|
214
|
+
logger.debug("prompt-suggestion: no API key", { provider: model.provider, id: model.id });
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
// Resolve metadata after getApiKey so the session-sticky credential for
|
|
218
|
+
// this request is already recorded (same contract as title-generator).
|
|
219
|
+
const metadata = metadataResolver?.(model.provider);
|
|
220
|
+
|
|
221
|
+
// A suggestion is a 2-12 word task, but some reasoning backends ignore
|
|
222
|
+
// disableReasoning; reserve output room for the answer after any
|
|
223
|
+
// unavoidable thinking tokens.
|
|
224
|
+
const maxTokens = model.reasoning
|
|
225
|
+
? Math.max(SUGGESTION_MAX_TOKENS, REASONING_SAFE_MAX_TOKENS)
|
|
226
|
+
: SUGGESTION_MAX_TOKENS;
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const response = await completeSimple(
|
|
230
|
+
model,
|
|
231
|
+
{
|
|
232
|
+
systemPrompt: [SUGGESTION_SYSTEM_PROMPT],
|
|
233
|
+
messages: [{ role: "user", content: context, timestamp: Date.now() }],
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
apiKey,
|
|
237
|
+
maxTokens,
|
|
238
|
+
disableReasoning: true,
|
|
239
|
+
metadata,
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (response.stopReason === "error") {
|
|
244
|
+
logger.debug("prompt-suggestion: response error", {
|
|
245
|
+
model: `${model.provider}/${model.id}`,
|
|
246
|
+
errorMessage: response.errorMessage,
|
|
247
|
+
});
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const suggestion = sanitizePromptSuggestion(extractSuggestionText(response.content));
|
|
252
|
+
const suppressReason = suppressPromptSuggestionReason(suggestion);
|
|
253
|
+
if (suppressReason) {
|
|
254
|
+
logger.debug("prompt-suggestion: suppressed", { reason: suppressReason });
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
return suggestion;
|
|
258
|
+
} catch (err) {
|
|
259
|
+
logger.debug("prompt-suggestion: error", {
|
|
260
|
+
model: `${model.provider}/${model.id}`,
|
|
261
|
+
error: err instanceof Error ? err.message : String(err),
|
|
262
|
+
});
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
package/src/utils/sixel.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $
|
|
1
|
+
import { $pickenv, $pickflag } from "@gajae-code/utils";
|
|
2
2
|
|
|
3
3
|
const SIXEL_START_REGEX = /\x1bP(?:[0-9;]*)q/u;
|
|
4
4
|
const SIXEL_END_SEQUENCE = "\x1b\\";
|
|
@@ -14,8 +14,8 @@ const SIXEL_PLACEHOLDER_PREFIX = "__GJC_SIXEL_SEQUENCE_";
|
|
|
14
14
|
* - PI_ALLOW_SIXEL_PASSTHROUGH=1
|
|
15
15
|
*/
|
|
16
16
|
export function isSixelPassthroughEnabled(): boolean {
|
|
17
|
-
const forcedProtocol = $
|
|
18
|
-
return forcedProtocol === "sixel" && $
|
|
17
|
+
const forcedProtocol = $pickenv("GJC_FORCE_IMAGE_PROTOCOL", "PI_FORCE_IMAGE_PROTOCOL")?.trim().toLowerCase();
|
|
18
|
+
return forcedProtocol === "sixel" && $pickflag("GJC_ALLOW_SIXEL_PASSTHROUGH", "PI_ALLOW_SIXEL_PASSTHROUGH");
|
|
19
19
|
}
|
|
20
20
|
/** Returns true when the text contains a SIXEL start sequence. */
|
|
21
21
|
export function containsSixelSequence(text: string): boolean {
|
|
@@ -244,8 +244,10 @@ export async function guardedPublicFetch(
|
|
|
244
244
|
logicalUrl.protocol === "https:"
|
|
245
245
|
? { rejectUnauthorized: true, ...(net.isIP(hostname) === 0 ? { serverName: hostname } : {}) }
|
|
246
246
|
: undefined;
|
|
247
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
248
|
+
const addresses = method === "GET" || method === "HEAD" ? guard.addresses : guard.addresses.slice(0, 1);
|
|
247
249
|
let lastError: unknown;
|
|
248
|
-
for (const address of
|
|
250
|
+
for (const address of addresses) {
|
|
249
251
|
if (hasConfiguredProxy(process.env)) {
|
|
250
252
|
return { ok: false, reason: "proxy routing appeared during resolution", logicalUrl: rawUrl };
|
|
251
253
|
}
|
|
@@ -299,7 +299,21 @@ async function readCachedRustdocCrate(
|
|
|
299
299
|
): Promise<{ crate: RustdocCrate; fetchedAt: string } | null> {
|
|
300
300
|
const cachePath = getDocsRsCachePath(target);
|
|
301
301
|
try {
|
|
302
|
-
const
|
|
302
|
+
const stat = await fs.stat(cachePath);
|
|
303
|
+
if (stat.size > MAX_BYTES) {
|
|
304
|
+
await fs.rm(cachePath, { force: true });
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const cacheBytes = await Bun.file(cachePath)
|
|
309
|
+
.slice(0, MAX_BYTES + 1)
|
|
310
|
+
.arrayBuffer();
|
|
311
|
+
if (cacheBytes.byteLength > MAX_BYTES) {
|
|
312
|
+
await fs.rm(cachePath, { force: true });
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const jsonStr = Buffer.from(cacheBytes).toString("utf-8");
|
|
303
317
|
const crate = tryParseJson<RustdocCrate>(jsonStr);
|
|
304
318
|
if (!crate?.index) return null;
|
|
305
319
|
return { crate, fetchedAt: stat.mtime.toISOString() };
|
|
@@ -378,11 +392,22 @@ export const handleDocsRs: SpecialHandler = async (
|
|
|
378
392
|
const requestSignal = ptree.combineSignals(signal, timeout * 1000);
|
|
379
393
|
const response = await fetch(jsonUrl, {
|
|
380
394
|
signal: requestSignal,
|
|
381
|
-
headers: {
|
|
395
|
+
headers: {
|
|
396
|
+
"User-Agent": "gjc-web-fetch/1.0",
|
|
397
|
+
Accept: "application/gzip",
|
|
398
|
+
"Accept-Encoding": "identity",
|
|
399
|
+
},
|
|
382
400
|
redirect: "follow",
|
|
401
|
+
decompress: false,
|
|
383
402
|
});
|
|
384
403
|
if (!response.ok) return null;
|
|
385
404
|
|
|
405
|
+
const contentEncoding = response.headers.get("Content-Encoding");
|
|
406
|
+
if (contentEncoding && contentEncoding.trim().toLowerCase() !== "identity") {
|
|
407
|
+
await response.body?.cancel();
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
|
|
386
411
|
const reader = response.body?.getReader();
|
|
387
412
|
if (!reader) return null;
|
|
388
413
|
|
|
@@ -394,13 +419,13 @@ export const handleDocsRs: SpecialHandler = async (
|
|
|
394
419
|
chunks.push(value);
|
|
395
420
|
totalSize += value.length;
|
|
396
421
|
if (totalSize > MAX_BYTES) {
|
|
397
|
-
reader.cancel();
|
|
398
|
-
|
|
422
|
+
await reader.cancel();
|
|
423
|
+
return null;
|
|
399
424
|
}
|
|
400
425
|
}
|
|
401
426
|
|
|
402
427
|
const compressed = Buffer.concat(chunks);
|
|
403
|
-
const jsonStr = gunzipSync(compressed).toString("utf-8");
|
|
428
|
+
const jsonStr = gunzipSync(compressed, { maxOutputLength: MAX_BYTES }).toString("utf-8");
|
|
404
429
|
crate_ = tryParseJson<RustdocCrate>(jsonStr);
|
|
405
430
|
if (crate_?.index) {
|
|
406
431
|
await writeCachedRustdocCrate(target, jsonStr);
|