@selesai/code 0.9.6 → 0.9.9
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 +18 -0
- package/dist/cli/credential-print.js +4 -4
- package/dist/cli.js +0 -0
- package/dist/core/agent-session-runtime.js +1 -4
- package/dist/core/agent-session.d.ts +2 -0
- package/dist/core/agent-session.js +26 -0
- package/dist/core/compaction/branch-summarization.js +30 -30
- package/dist/core/compaction/compaction.js +81 -81
- package/dist/core/compaction/utils.js +2 -2
- package/dist/core/export-html/template.css +1066 -1066
- package/dist/core/export-html/template.html +55 -55
- package/dist/core/export-html/template.js +1864 -1864
- package/dist/core/export-html/vendor/highlight.min.js +1212 -1212
- package/dist/core/export-html/vendor/marked.min.js +78 -78
- package/dist/core/handoff.d.ts +11 -0
- package/dist/core/handoff.js +49 -0
- package/dist/core/messages.js +7 -7
- package/dist/extensions/context-compaction-reminder.test.ts +82 -82
- package/dist/extensions/context-compaction-reminder.ts +28 -28
- package/dist/extensions/handoff-new.ts +14 -75
- package/dist/extensions/pi-intercom/LICENSE +21 -21
- package/dist/extensions/pi-intercom/broker/client.test.ts +83 -83
- package/dist/extensions/pi-intercom/broker/extension.test.ts +387 -387
- package/dist/extensions/pi-intercom/broker/framing.test.ts +114 -114
- package/dist/extensions/pi-intercom/broker/paths.test.ts +153 -153
- package/dist/extensions/pi-intercom/broker/paths.ts +134 -134
- package/dist/extensions/pi-intercom/broker/runtime-claim.test.ts +34 -34
- package/dist/extensions/pi-intercom/broker/runtime-claim.ts +21 -21
- package/dist/extensions/pi-intercom/cwd.test.ts +40 -40
- package/dist/extensions/pi-intercom/cwd.ts +31 -31
- package/dist/extensions/pi-intercom/extension-api.ts +44 -44
- package/dist/extensions/pi-intercom/format-context.test.ts +31 -31
- package/dist/extensions/pi-intercom/format-context.ts +32 -32
- package/dist/extensions/pi-intercom/test/overlay-width.test.ts +66 -66
- package/dist/extensions/pi-intercom/ui/compose.ts +143 -143
- package/dist/extensions/pi-intercom/ui/session-list.ts +166 -166
- package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +286 -286
- package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +108 -108
- package/dist/extensions/pi-powerline-footer/separators.ts +57 -57
- package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +47 -47
- package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +39 -39
- package/dist/extensions/pi-powerline-footer/theme.example.json +24 -24
- package/dist/extensions/pi-powerline-footer/theme.json +12 -12
- package/dist/extensions/pi-powerline-footer/tps.ts +345 -345
- package/dist/extensions/pi-rewind-hook/README.md +245 -245
- package/dist/extensions/pi-rewind-hook/index.ts +1445 -1445
- package/dist/extensions/pi-rewind-hook/package.json +29 -29
- package/dist/extensions/pi-subagents/install.mjs +0 -0
- package/dist/extensions/pi-web-agent/package.json +31 -31
- package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -205
- package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -136
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -152
- package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -25
- package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -28
- package/dist/extensions/pi-web-agent/src/changelog-notice.ts +136 -136
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -946
- package/dist/extensions/pi-web-agent/src/extension.ts +126 -126
- package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -118
- package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -199
- package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -100
- package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -117
- package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -67
- package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -60
- package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -50
- package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -52
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -105
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -45
- package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -28
- package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -47
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -376
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -64
- package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -181
- package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -101
- package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -81
- package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +210 -210
- package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -75
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -61
- package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -54
- package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -41
- package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -20
- package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -63
- package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -114
- package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -72
- package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -96
- package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -71
- package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -31
- package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -31
- package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -157
- package/dist/extensions/pi-web-agent/src/types.ts +88 -88
- package/dist/extensions/ponytail/package.json +8 -8
- package/dist/extensions/question/batch.ts +103 -103
- package/dist/extensions/question/constants.ts +30 -30
- package/dist/extensions/question/helpers.ts +58 -58
- package/dist/extensions/question/navigation.ts +14 -14
- package/dist/extensions/question/package.json +19 -19
- package/dist/extensions/question/schemas.ts +43 -43
- package/dist/extensions/question/selection-mode.ts +46 -46
- package/dist/extensions/question/shortcuts.ts +44 -44
- package/dist/extensions/question/types.ts +132 -132
- package/dist/extensions/test-resolve-hook-impl.mjs +6 -6
- package/dist/extensions/test-resolve-hook.mjs +6 -6
- package/dist/extensions/web-agent-onboarding.ts +222 -222
- package/dist/extensions/workflow/package.json +17 -17
- package/dist/modes/rpc/rpc-mode.js +24 -0
- package/dist/modes/rpc/rpc-types.d.ts +19 -0
- package/dist/package-manager-cli.js +71 -71
- package/dist/rpc-entry.js +0 -0
- package/dist/skills/agent-browser/SKILL.md +52 -52
- package/dist/skills/batch-grill-me/SKILL.md +19 -19
- package/dist/skills/grill-me/SKILL.md +10 -10
- package/dist/skills/handoff/SKILL.md +16 -16
- package/dist/skills/handoff-text/SKILL.md +14 -14
- package/dist/skills/implanger/SKILL.md +69 -69
- package/dist/skills/improve-codebase/REFERENCE.md +78 -78
- package/dist/skills/improve-codebase/SKILL.md +178 -178
- package/dist/skills/planger/SKILL.md +166 -166
- package/dist/skills/ponytail/SKILL.md +116 -116
- package/dist/skills/ponytail-audit/SKILL.md +42 -42
- package/dist/skills/ponytail-debt/SKILL.md +45 -45
- package/dist/skills/ponytail-gain/SKILL.md +51 -51
- package/dist/skills/ponytail-help/SKILL.md +70 -70
- package/dist/skills/ponytail-review/SKILL.md +58 -58
- package/dist/skills/selesai-handoff/SKILL.md +20 -20
- package/dist/skills/workflow-creation/SKILL.md +73 -73
- package/dist/themes/powerline-footer/theme.json +33 -33
- package/docs/compaction.md +396 -396
- package/docs/containerization.md +111 -111
- package/docs/development.md +71 -71
- package/docs/docs.json +164 -164
- package/docs/environment-variables.md +86 -86
- package/docs/index.md +83 -83
- package/docs/json.md +82 -82
- package/docs/models.md +502 -502
- package/docs/packages.md +227 -227
- package/docs/plans/subagent-delegation/phase-0-correctness.md +264 -264
- package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +485 -485
- package/docs/plans/subagent-delegation/phase-2-context-controls.md +281 -281
- package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +361 -361
- package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +380 -380
- package/docs/prompt-templates.md +95 -95
- package/docs/providers.md +293 -293
- package/docs/sdk.md +1143 -1143
- package/docs/security.md +59 -59
- package/docs/session-format.md +414 -414
- package/docs/sessions.md +145 -145
- package/docs/shared-host-extensions.md +109 -109
- package/docs/shell-aliases.md +13 -13
- package/docs/skills.md +231 -231
- package/docs/terminal-setup.md +142 -142
- package/docs/termux.md +127 -127
- package/docs/themes.md +295 -295
- package/docs/tmux.md +63 -63
- package/docs/tui.md +927 -927
- package/docs/windows.md +17 -17
- package/examples/README.md +25 -25
- package/examples/extensions/README.md +211 -211
- package/examples/extensions/auto-commit-on-exit.ts +49 -49
- package/examples/extensions/bash-spawn-hook.ts +30 -30
- package/examples/extensions/bookmark.ts +50 -50
- package/examples/extensions/border-status-editor.ts +150 -150
- package/examples/extensions/built-in-tool-renderer.ts +249 -249
- package/examples/extensions/claude-rules.ts +86 -86
- package/examples/extensions/commands.ts +72 -72
- package/examples/extensions/confirm-destructive.ts +59 -59
- package/examples/extensions/custom-compaction.ts +130 -130
- package/examples/extensions/custom-footer.ts +64 -64
- package/examples/extensions/custom-header.ts +73 -73
- package/examples/extensions/custom-provider-anthropic/index.ts +610 -610
- package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -24
- package/examples/extensions/custom-provider-anthropic/package.json +19 -19
- package/examples/extensions/custom-provider-gitlab-duo/index.ts +404 -404
- package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -16
- package/examples/extensions/custom-provider-gitlab-duo/test.ts +82 -82
- package/examples/extensions/dirty-repo-guard.ts +56 -56
- package/examples/extensions/doom-overlay/README.md +46 -46
- package/examples/extensions/doom-overlay/doom/build/doom.js +21 -21
- package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
- package/examples/extensions/doom-overlay/doom/build.sh +152 -152
- package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -72
- package/examples/extensions/doom-overlay/doom-component.ts +132 -132
- package/examples/extensions/doom-overlay/doom-engine.ts +173 -173
- package/examples/extensions/doom-overlay/doom-keys.ts +104 -104
- package/examples/extensions/doom-overlay/index.ts +74 -74
- package/examples/extensions/doom-overlay/wad-finder.ts +51 -51
- package/examples/extensions/dynamic-resources/SKILL.md +8 -8
- package/examples/extensions/dynamic-resources/dynamic.json +79 -79
- package/examples/extensions/dynamic-resources/dynamic.md +5 -5
- package/examples/extensions/dynamic-resources/index.ts +15 -15
- package/examples/extensions/dynamic-tools.ts +74 -74
- package/examples/extensions/event-bus.ts +43 -43
- package/examples/extensions/file-trigger.ts +41 -41
- package/examples/extensions/git-checkpoint.ts +53 -53
- package/examples/extensions/git-merge-and-resolve.ts +115 -115
- package/examples/extensions/github-issue-autocomplete.ts +185 -185
- package/examples/extensions/gondolin/index.ts +531 -531
- package/examples/extensions/gondolin/package-lock.json +185 -185
- package/examples/extensions/gondolin/package.json +19 -19
- package/examples/extensions/handoff.ts +199 -199
- package/examples/extensions/hello.ts +26 -26
- package/examples/extensions/hidden-thinking-label.ts +53 -53
- package/examples/extensions/inline-bash.ts +94 -94
- package/examples/extensions/input-transform-streaming.ts +39 -39
- package/examples/extensions/input-transform.ts +43 -43
- package/examples/extensions/interactive-shell.ts +196 -196
- package/examples/extensions/mac-system-theme.ts +47 -47
- package/examples/extensions/message-renderer.ts +59 -59
- package/examples/extensions/minimal-mode.ts +426 -426
- package/examples/extensions/modal-editor.ts +85 -85
- package/examples/extensions/model-status.ts +31 -31
- package/examples/extensions/notify.ts +55 -55
- package/examples/extensions/overlay-qa-tests.ts +1450 -1450
- package/examples/extensions/overlay-test.ts +153 -153
- package/examples/extensions/permission-gate.ts +34 -34
- package/examples/extensions/pirate.ts +47 -47
- package/examples/extensions/plan-mode/README.md +66 -66
- package/examples/extensions/plan-mode/index.ts +390 -390
- package/examples/extensions/plan-mode/utils.ts +168 -168
- package/examples/extensions/preset.ts +436 -436
- package/examples/extensions/project-trust.ts +64 -64
- package/examples/extensions/prompt-customizer.ts +97 -97
- package/examples/extensions/protected-paths.ts +30 -30
- package/examples/extensions/provider-payload.ts +18 -18
- package/examples/extensions/qna.ts +122 -122
- package/examples/extensions/question.ts +285 -285
- package/examples/extensions/questionnaire.ts +448 -448
- package/examples/extensions/rainbow-editor.ts +88 -88
- package/examples/extensions/reload-runtime.ts +37 -37
- package/examples/extensions/rpc-demo.ts +118 -118
- package/examples/extensions/sandbox/index.ts +321 -321
- package/examples/extensions/sandbox/package-lock.json +92 -92
- package/examples/extensions/sandbox/package.json +19 -19
- package/examples/extensions/send-user-message.ts +97 -97
- package/examples/extensions/session-name.ts +27 -27
- package/examples/extensions/shutdown-command.ts +63 -63
- package/examples/extensions/snake.ts +343 -343
- package/examples/extensions/space-invaders.ts +560 -560
- package/examples/extensions/ssh.ts +220 -220
- package/examples/extensions/status-line.ts +32 -32
- package/examples/extensions/structured-output.ts +65 -65
- package/examples/extensions/subagent/README.md +175 -175
- package/examples/extensions/subagent/agents/planner.md +37 -37
- package/examples/extensions/subagent/agents/reviewer.md +35 -35
- package/examples/extensions/subagent/agents/scout.md +50 -50
- package/examples/extensions/subagent/agents/worker.md +24 -24
- package/examples/extensions/subagent/agents.ts +126 -126
- package/examples/extensions/subagent/index.ts +1015 -1015
- package/examples/extensions/subagent/prompts/implement-and-review.md +10 -10
- package/examples/extensions/subagent/prompts/implement.md +10 -10
- package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -9
- package/examples/extensions/summarize.ts +209 -209
- package/examples/extensions/system-prompt-header.ts +17 -17
- package/examples/extensions/tic-tac-toe.ts +1008 -1008
- package/examples/extensions/timed-confirm.ts +70 -70
- package/examples/extensions/titlebar-spinner.ts +58 -58
- package/examples/extensions/todo.ts +297 -297
- package/examples/extensions/tool-override.ts +144 -144
- package/examples/extensions/tools.ts +146 -146
- package/examples/extensions/trigger-compact.ts +50 -50
- package/examples/extensions/truncated-tool.ts +195 -195
- package/examples/extensions/widget-placement.ts +9 -9
- package/examples/extensions/with-deps/index.ts +32 -32
- package/examples/extensions/with-deps/package-lock.json +31 -31
- package/examples/extensions/with-deps/package.json +22 -22
- package/examples/extensions/working-indicator.ts +123 -123
- package/examples/extensions/working-message-test.ts +25 -25
- package/examples/rpc-extension-ui.ts +632 -632
- package/examples/sdk/01-minimal.ts +26 -26
- package/examples/sdk/02-custom-model.ts +53 -53
- package/examples/sdk/03-custom-prompt.ts +75 -75
- package/examples/sdk/04-skills.ts +55 -55
- package/examples/sdk/05-tools.ts +48 -48
- package/examples/sdk/06-extensions.ts +99 -99
- package/examples/sdk/07-context-files.ts +47 -47
- package/examples/sdk/08-prompt-templates.ts +51 -51
- package/examples/sdk/09-api-keys-and-oauth.ts +52 -52
- package/examples/sdk/10-settings.ts +53 -53
- package/examples/sdk/11-sessions.ts +52 -52
- package/examples/sdk/12-full-control.ts +79 -79
- package/examples/sdk/13-session-runtime.ts +67 -67
- package/examples/sdk/README.md +144 -144
- package/package.json +1 -1
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
import { chromium } from 'playwright';
|
|
2
|
-
import { extractReadableContentSafely } from '../extract/readability.js';
|
|
3
|
-
import { resolveBrowserExecutable, type BrowserResolutionResult } from './browser-resolution.js';
|
|
4
|
-
import type { WebFetchHeadlessResponse } from '../types.js';
|
|
5
|
-
|
|
6
|
-
function cleanupRenderedText(text: string): string {
|
|
7
|
-
let cleaned = text.replace(/(Show more)(\s+\1){1,}/gi, '$1');
|
|
8
|
-
cleaned = cleaned.replace(/(Privacy Terms)(\s+\1){1,}/gi, '$1');
|
|
9
|
-
cleaned = cleaned.replace(/\s+/g, ' ').trim();
|
|
10
|
-
return cleaned;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function headlessFetch(
|
|
14
|
-
url: string,
|
|
15
|
-
{
|
|
16
|
-
configuredPath,
|
|
17
|
-
resolveBrowser = (options?: { configuredPath?: string }) =>
|
|
18
|
-
resolveBrowserExecutable({ configuredPath: options?.configuredPath }),
|
|
19
|
-
launchBrowser = ({ executablePath, headless }: { executablePath?: string; headless: true }) =>
|
|
20
|
-
chromium.launch(executablePath ? { executablePath, headless } : { headless }),
|
|
21
|
-
now = () => Date.now()
|
|
22
|
-
}: {
|
|
23
|
-
configuredPath?: string;
|
|
24
|
-
resolveBrowser?: (options?: { configuredPath?: string }) => Promise<BrowserResolutionResult>;
|
|
25
|
-
launchBrowser?: (options: { executablePath?: string; headless: true }) => Promise<{
|
|
26
|
-
newContext: () => Promise<{ newPage: () => Promise<any>; close: () => Promise<void> }>;
|
|
27
|
-
close: () => Promise<void>;
|
|
28
|
-
}>;
|
|
29
|
-
now?: () => number;
|
|
30
|
-
} = {}
|
|
31
|
-
): Promise<WebFetchHeadlessResponse> {
|
|
32
|
-
const resolved = await resolveBrowser({ configuredPath });
|
|
33
|
-
if (!resolved.ok && resolved.error.code === 'CONFIGURED_BROWSER_NOT_FOUND') {
|
|
34
|
-
return {
|
|
35
|
-
status: 'error',
|
|
36
|
-
url,
|
|
37
|
-
metadata: { method: 'headless', cacheHit: false },
|
|
38
|
-
error: resolved.error
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const browserName = resolved.ok ? resolved.browser : 'chromium';
|
|
43
|
-
const launchOptions = resolved.ok
|
|
44
|
-
? { executablePath: resolved.executablePath, headless: true as const }
|
|
45
|
-
: { headless: true as const };
|
|
46
|
-
|
|
47
|
-
let browser: Awaited<ReturnType<typeof launchBrowser>> | undefined;
|
|
48
|
-
let context: Awaited<ReturnType<Awaited<ReturnType<typeof launchBrowser>>['newContext']>> | undefined;
|
|
49
|
-
let page: Awaited<ReturnType<Awaited<ReturnType<Awaited<ReturnType<typeof launchBrowser>>['newContext']>>['newPage']>> | undefined;
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
browser = await launchBrowser(launchOptions);
|
|
53
|
-
context = await browser.newContext();
|
|
54
|
-
page = await context.newPage();
|
|
55
|
-
|
|
56
|
-
const startedAt = now();
|
|
57
|
-
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });
|
|
58
|
-
await page.waitForLoadState('load', { timeout: 10000 });
|
|
59
|
-
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => undefined);
|
|
60
|
-
const html = await page.content();
|
|
61
|
-
const finishedAt = now();
|
|
62
|
-
|
|
63
|
-
const extraction = extractReadableContentSafely(html);
|
|
64
|
-
const cleanedContent = {
|
|
65
|
-
...extraction.content,
|
|
66
|
-
text: cleanupRenderedText(extraction.content.text)
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
if (!cleanedContent.text || cleanedContent.text.length < 40) {
|
|
70
|
-
return {
|
|
71
|
-
status: 'blocked',
|
|
72
|
-
url,
|
|
73
|
-
metadata: {
|
|
74
|
-
method: 'headless',
|
|
75
|
-
cacheHit: false,
|
|
76
|
-
browser: browserName,
|
|
77
|
-
navigationMs: finishedAt - startedAt
|
|
78
|
-
},
|
|
79
|
-
error: {
|
|
80
|
-
code: 'HEADLESS_EXTRACTION_WEAK',
|
|
81
|
-
message: 'Rendered page did not produce enough readable content.'
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
status: 'ok',
|
|
88
|
-
url,
|
|
89
|
-
content: cleanedContent,
|
|
90
|
-
metadata: {
|
|
91
|
-
method: 'headless',
|
|
92
|
-
cacheHit: false,
|
|
93
|
-
browser: browserName,
|
|
94
|
-
navigationMs: finishedAt - startedAt,
|
|
95
|
-
truncated: cleanedContent.text.length >= 4000
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
} catch (error) {
|
|
99
|
-
return {
|
|
100
|
-
status: 'error',
|
|
101
|
-
url,
|
|
102
|
-
metadata: {
|
|
103
|
-
method: 'headless',
|
|
104
|
-
cacheHit: false,
|
|
105
|
-
browser: browserName
|
|
106
|
-
},
|
|
107
|
-
error: {
|
|
108
|
-
code: 'HEADLESS_NAVIGATION_FAILED',
|
|
109
|
-
message: error instanceof Error ? error.message : 'Unknown headless navigation failure.'
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
} finally {
|
|
113
|
-
await page?.close?.().catch(() => undefined);
|
|
114
|
-
await context?.close?.().catch(() => undefined);
|
|
115
|
-
await browser?.close?.().catch(() => undefined);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import { extractReadableContentSafely } from '../extract/readability.js';
|
|
3
|
+
import { resolveBrowserExecutable, type BrowserResolutionResult } from './browser-resolution.js';
|
|
4
|
+
import type { WebFetchHeadlessResponse } from '../types.js';
|
|
5
|
+
|
|
6
|
+
function cleanupRenderedText(text: string): string {
|
|
7
|
+
let cleaned = text.replace(/(Show more)(\s+\1){1,}/gi, '$1');
|
|
8
|
+
cleaned = cleaned.replace(/(Privacy Terms)(\s+\1){1,}/gi, '$1');
|
|
9
|
+
cleaned = cleaned.replace(/\s+/g, ' ').trim();
|
|
10
|
+
return cleaned;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function headlessFetch(
|
|
14
|
+
url: string,
|
|
15
|
+
{
|
|
16
|
+
configuredPath,
|
|
17
|
+
resolveBrowser = (options?: { configuredPath?: string }) =>
|
|
18
|
+
resolveBrowserExecutable({ configuredPath: options?.configuredPath }),
|
|
19
|
+
launchBrowser = ({ executablePath, headless }: { executablePath?: string; headless: true }) =>
|
|
20
|
+
chromium.launch(executablePath ? { executablePath, headless } : { headless }),
|
|
21
|
+
now = () => Date.now()
|
|
22
|
+
}: {
|
|
23
|
+
configuredPath?: string;
|
|
24
|
+
resolveBrowser?: (options?: { configuredPath?: string }) => Promise<BrowserResolutionResult>;
|
|
25
|
+
launchBrowser?: (options: { executablePath?: string; headless: true }) => Promise<{
|
|
26
|
+
newContext: () => Promise<{ newPage: () => Promise<any>; close: () => Promise<void> }>;
|
|
27
|
+
close: () => Promise<void>;
|
|
28
|
+
}>;
|
|
29
|
+
now?: () => number;
|
|
30
|
+
} = {}
|
|
31
|
+
): Promise<WebFetchHeadlessResponse> {
|
|
32
|
+
const resolved = await resolveBrowser({ configuredPath });
|
|
33
|
+
if (!resolved.ok && resolved.error.code === 'CONFIGURED_BROWSER_NOT_FOUND') {
|
|
34
|
+
return {
|
|
35
|
+
status: 'error',
|
|
36
|
+
url,
|
|
37
|
+
metadata: { method: 'headless', cacheHit: false },
|
|
38
|
+
error: resolved.error
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const browserName = resolved.ok ? resolved.browser : 'chromium';
|
|
43
|
+
const launchOptions = resolved.ok
|
|
44
|
+
? { executablePath: resolved.executablePath, headless: true as const }
|
|
45
|
+
: { headless: true as const };
|
|
46
|
+
|
|
47
|
+
let browser: Awaited<ReturnType<typeof launchBrowser>> | undefined;
|
|
48
|
+
let context: Awaited<ReturnType<Awaited<ReturnType<typeof launchBrowser>>['newContext']>> | undefined;
|
|
49
|
+
let page: Awaited<ReturnType<Awaited<ReturnType<Awaited<ReturnType<typeof launchBrowser>>['newContext']>>['newPage']>> | undefined;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
browser = await launchBrowser(launchOptions);
|
|
53
|
+
context = await browser.newContext();
|
|
54
|
+
page = await context.newPage();
|
|
55
|
+
|
|
56
|
+
const startedAt = now();
|
|
57
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });
|
|
58
|
+
await page.waitForLoadState('load', { timeout: 10000 });
|
|
59
|
+
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => undefined);
|
|
60
|
+
const html = await page.content();
|
|
61
|
+
const finishedAt = now();
|
|
62
|
+
|
|
63
|
+
const extraction = extractReadableContentSafely(html);
|
|
64
|
+
const cleanedContent = {
|
|
65
|
+
...extraction.content,
|
|
66
|
+
text: cleanupRenderedText(extraction.content.text)
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (!cleanedContent.text || cleanedContent.text.length < 40) {
|
|
70
|
+
return {
|
|
71
|
+
status: 'blocked',
|
|
72
|
+
url,
|
|
73
|
+
metadata: {
|
|
74
|
+
method: 'headless',
|
|
75
|
+
cacheHit: false,
|
|
76
|
+
browser: browserName,
|
|
77
|
+
navigationMs: finishedAt - startedAt
|
|
78
|
+
},
|
|
79
|
+
error: {
|
|
80
|
+
code: 'HEADLESS_EXTRACTION_WEAK',
|
|
81
|
+
message: 'Rendered page did not produce enough readable content.'
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
status: 'ok',
|
|
88
|
+
url,
|
|
89
|
+
content: cleanedContent,
|
|
90
|
+
metadata: {
|
|
91
|
+
method: 'headless',
|
|
92
|
+
cacheHit: false,
|
|
93
|
+
browser: browserName,
|
|
94
|
+
navigationMs: finishedAt - startedAt,
|
|
95
|
+
truncated: cleanedContent.text.length >= 4000
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return {
|
|
100
|
+
status: 'error',
|
|
101
|
+
url,
|
|
102
|
+
metadata: {
|
|
103
|
+
method: 'headless',
|
|
104
|
+
cacheHit: false,
|
|
105
|
+
browser: browserName
|
|
106
|
+
},
|
|
107
|
+
error: {
|
|
108
|
+
code: 'HEADLESS_NAVIGATION_FAILED',
|
|
109
|
+
message: error instanceof Error ? error.message : 'Unknown headless navigation failure.'
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
} finally {
|
|
113
|
+
await page?.close?.().catch(() => undefined);
|
|
114
|
+
await context?.close?.().catch(() => undefined);
|
|
115
|
+
await browser?.close?.().catch(() => undefined);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
import { extractReadableContentSafely } from '../extract/readability.js';
|
|
2
|
-
import type { WebFetchResponse } from '../types.js';
|
|
3
|
-
|
|
4
|
-
function looksLikeScriptShell(html: string): boolean {
|
|
5
|
-
const lower = html.toLowerCase();
|
|
6
|
-
return lower.includes('<script') && (lower.includes('id="app"') || lower.includes('id="root"'));
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function isWeakHttpContent(options: { html: string; title?: string; text: string }): boolean {
|
|
10
|
-
const normalizedText = options.text.replace(/\s+/g, ' ').trim();
|
|
11
|
-
const normalizedHtml = options.html.replace(/\s+/g, ' ').trim();
|
|
12
|
-
const textLength = normalizedText.length;
|
|
13
|
-
const htmlLength = normalizedHtml.length;
|
|
14
|
-
const hasGenericShellMarker = /enable javascript|javascript required|please turn on javascript/i.test(
|
|
15
|
-
options.html
|
|
16
|
-
);
|
|
17
|
-
const veryShortBody = textLength > 0 && textLength < 120;
|
|
18
|
-
const lowDensity = htmlLength > 0 && textLength / htmlLength < 0.02;
|
|
19
|
-
|
|
20
|
-
return veryShortBody && (lowDensity || hasGenericShellMarker);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function createHttpFetcher({
|
|
24
|
-
fetchImpl = fetch
|
|
25
|
-
}: {
|
|
26
|
-
fetchImpl?: typeof fetch;
|
|
27
|
-
} = {}) {
|
|
28
|
-
return async function httpFetch(url: string): Promise<WebFetchResponse> {
|
|
29
|
-
const response = await fetchImpl(url);
|
|
30
|
-
const contentType = response.headers.get('content-type') ?? '';
|
|
31
|
-
|
|
32
|
-
if (!contentType.includes('text/html')) {
|
|
33
|
-
return {
|
|
34
|
-
status: 'unsupported',
|
|
35
|
-
url: response.url,
|
|
36
|
-
metadata: { method: 'http', cacheHit: false, contentType }
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const html = await response.text();
|
|
41
|
-
const extraction = extractReadableContentSafely(html);
|
|
42
|
-
const content = extraction.content;
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
looksLikeScriptShell(html) ||
|
|
46
|
-
content.text.length < 40 ||
|
|
47
|
-
isWeakHttpContent({ html, title: content.title, text: content.text })
|
|
48
|
-
) {
|
|
49
|
-
return {
|
|
50
|
-
status: 'needs_headless',
|
|
51
|
-
url: response.url,
|
|
52
|
-
metadata: { method: 'http', cacheHit: false, contentType },
|
|
53
|
-
error: {
|
|
54
|
-
code: 'WEAK_EXTRACTION',
|
|
55
|
-
message: 'HTTP extraction was not reliable enough.'
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
status: 'ok',
|
|
62
|
-
url: response.url,
|
|
63
|
-
content,
|
|
64
|
-
metadata: { method: 'http', cacheHit: false, contentType, truncated: content.text.length >= 4000 }
|
|
65
|
-
};
|
|
66
|
-
};
|
|
67
|
-
}
|
|
1
|
+
import { extractReadableContentSafely } from '../extract/readability.js';
|
|
2
|
+
import type { WebFetchResponse } from '../types.js';
|
|
3
|
+
|
|
4
|
+
function looksLikeScriptShell(html: string): boolean {
|
|
5
|
+
const lower = html.toLowerCase();
|
|
6
|
+
return lower.includes('<script') && (lower.includes('id="app"') || lower.includes('id="root"'));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isWeakHttpContent(options: { html: string; title?: string; text: string }): boolean {
|
|
10
|
+
const normalizedText = options.text.replace(/\s+/g, ' ').trim();
|
|
11
|
+
const normalizedHtml = options.html.replace(/\s+/g, ' ').trim();
|
|
12
|
+
const textLength = normalizedText.length;
|
|
13
|
+
const htmlLength = normalizedHtml.length;
|
|
14
|
+
const hasGenericShellMarker = /enable javascript|javascript required|please turn on javascript/i.test(
|
|
15
|
+
options.html
|
|
16
|
+
);
|
|
17
|
+
const veryShortBody = textLength > 0 && textLength < 120;
|
|
18
|
+
const lowDensity = htmlLength > 0 && textLength / htmlLength < 0.02;
|
|
19
|
+
|
|
20
|
+
return veryShortBody && (lowDensity || hasGenericShellMarker);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createHttpFetcher({
|
|
24
|
+
fetchImpl = fetch
|
|
25
|
+
}: {
|
|
26
|
+
fetchImpl?: typeof fetch;
|
|
27
|
+
} = {}) {
|
|
28
|
+
return async function httpFetch(url: string): Promise<WebFetchResponse> {
|
|
29
|
+
const response = await fetchImpl(url);
|
|
30
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
31
|
+
|
|
32
|
+
if (!contentType.includes('text/html')) {
|
|
33
|
+
return {
|
|
34
|
+
status: 'unsupported',
|
|
35
|
+
url: response.url,
|
|
36
|
+
metadata: { method: 'http', cacheHit: false, contentType }
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const html = await response.text();
|
|
41
|
+
const extraction = extractReadableContentSafely(html);
|
|
42
|
+
const content = extraction.content;
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
looksLikeScriptShell(html) ||
|
|
46
|
+
content.text.length < 40 ||
|
|
47
|
+
isWeakHttpContent({ html, title: content.title, text: content.text })
|
|
48
|
+
) {
|
|
49
|
+
return {
|
|
50
|
+
status: 'needs_headless',
|
|
51
|
+
url: response.url,
|
|
52
|
+
metadata: { method: 'http', cacheHit: false, contentType },
|
|
53
|
+
error: {
|
|
54
|
+
code: 'WEAK_EXTRACTION',
|
|
55
|
+
message: 'HTTP extraction was not reliable enough.'
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
status: 'ok',
|
|
62
|
+
url: response.url,
|
|
63
|
+
content,
|
|
64
|
+
metadata: { method: 'http', cacheHit: false, contentType, truncated: content.text.length >= 4000 }
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
import type { ResearchEvidence } from './research-types.js';
|
|
2
|
-
import type { EvidenceCaveatReason } from './evidence-quality.js';
|
|
3
|
-
|
|
4
|
-
function normalizeSummary(summary: string) {
|
|
5
|
-
return summary.replace(/\s+/g, ' ').trim();
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function sentenceForReason(reason: EvidenceCaveatReason): string {
|
|
9
|
-
switch (reason) {
|
|
10
|
-
case 'community-only':
|
|
11
|
-
return 'the strongest readable sources were mostly community/practical context';
|
|
12
|
-
case 'low-diversity':
|
|
13
|
-
return 'the source set was narrow';
|
|
14
|
-
case 'unreadable-direct-source':
|
|
15
|
-
return 'one or more linked sources could not be read reliably';
|
|
16
|
-
case 'unreadable-thread-source':
|
|
17
|
-
return 'one or more thread sources could not be read reliably';
|
|
18
|
-
case 'possible-conflict':
|
|
19
|
-
return 'readable sources include cautionary or possibly conflicting guidance';
|
|
20
|
-
case 'bot-check':
|
|
21
|
-
return 'some candidate sources showed bot-check or security verification pages';
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function joinReasons(reasons: string[]) {
|
|
26
|
-
if (reasons.length === 0) return '';
|
|
27
|
-
if (reasons.length === 1) return reasons[0];
|
|
28
|
-
return `${reasons.slice(0, -1).join(', ')}, and ${reasons.at(-1)}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function caveatText(partial: boolean, caveatReasons: EvidenceCaveatReason[] = []) {
|
|
32
|
-
if (!partial) return undefined;
|
|
33
|
-
const specificReasons = caveatReasons.map(sentenceForReason);
|
|
34
|
-
if (specificReasons.length > 0) {
|
|
35
|
-
return `Evidence is partial: ${joinReasons(specificReasons)}.`;
|
|
36
|
-
}
|
|
37
|
-
return 'Evidence is partial, so this answer is based on the strongest source found within the bounded research budget.';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function synthesizeAnswer({
|
|
41
|
-
evidence,
|
|
42
|
-
partial,
|
|
43
|
-
caveatReasons = []
|
|
44
|
-
}: {
|
|
45
|
-
evidence: ResearchEvidence[];
|
|
46
|
-
partial: boolean;
|
|
47
|
-
caveatReasons?: EvidenceCaveatReason[];
|
|
48
|
-
}) {
|
|
49
|
-
const findings = evidence.slice(0, 5).map((item) => {
|
|
50
|
-
const summary = normalizeSummary(item.summary);
|
|
51
|
-
return item.sourceKind === 'community' || item.sourceKind === 'issue-thread'
|
|
52
|
-
? `Community/practical context: ${summary}`
|
|
53
|
-
: summary;
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
findings,
|
|
58
|
-
caveat: caveatText(partial, caveatReasons)
|
|
59
|
-
};
|
|
60
|
-
}
|
|
1
|
+
import type { ResearchEvidence } from './research-types.js';
|
|
2
|
+
import type { EvidenceCaveatReason } from './evidence-quality.js';
|
|
3
|
+
|
|
4
|
+
function normalizeSummary(summary: string) {
|
|
5
|
+
return summary.replace(/\s+/g, ' ').trim();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function sentenceForReason(reason: EvidenceCaveatReason): string {
|
|
9
|
+
switch (reason) {
|
|
10
|
+
case 'community-only':
|
|
11
|
+
return 'the strongest readable sources were mostly community/practical context';
|
|
12
|
+
case 'low-diversity':
|
|
13
|
+
return 'the source set was narrow';
|
|
14
|
+
case 'unreadable-direct-source':
|
|
15
|
+
return 'one or more linked sources could not be read reliably';
|
|
16
|
+
case 'unreadable-thread-source':
|
|
17
|
+
return 'one or more thread sources could not be read reliably';
|
|
18
|
+
case 'possible-conflict':
|
|
19
|
+
return 'readable sources include cautionary or possibly conflicting guidance';
|
|
20
|
+
case 'bot-check':
|
|
21
|
+
return 'some candidate sources showed bot-check or security verification pages';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function joinReasons(reasons: string[]) {
|
|
26
|
+
if (reasons.length === 0) return '';
|
|
27
|
+
if (reasons.length === 1) return reasons[0];
|
|
28
|
+
return `${reasons.slice(0, -1).join(', ')}, and ${reasons.at(-1)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function caveatText(partial: boolean, caveatReasons: EvidenceCaveatReason[] = []) {
|
|
32
|
+
if (!partial) return undefined;
|
|
33
|
+
const specificReasons = caveatReasons.map(sentenceForReason);
|
|
34
|
+
if (specificReasons.length > 0) {
|
|
35
|
+
return `Evidence is partial: ${joinReasons(specificReasons)}.`;
|
|
36
|
+
}
|
|
37
|
+
return 'Evidence is partial, so this answer is based on the strongest source found within the bounded research budget.';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function synthesizeAnswer({
|
|
41
|
+
evidence,
|
|
42
|
+
partial,
|
|
43
|
+
caveatReasons = []
|
|
44
|
+
}: {
|
|
45
|
+
evidence: ResearchEvidence[];
|
|
46
|
+
partial: boolean;
|
|
47
|
+
caveatReasons?: EvidenceCaveatReason[];
|
|
48
|
+
}) {
|
|
49
|
+
const findings = evidence.slice(0, 5).map((item) => {
|
|
50
|
+
const summary = normalizeSummary(item.summary);
|
|
51
|
+
return item.sourceKind === 'community' || item.sourceKind === 'issue-thread'
|
|
52
|
+
? `Community/practical context: ${summary}`
|
|
53
|
+
: summary;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
findings,
|
|
58
|
+
caveat: caveatText(partial, caveatReasons)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
import type { SearchResult } from '../types.js';
|
|
2
|
-
import { classifySourceProfile } from './source-profile.js';
|
|
3
|
-
|
|
4
|
-
function wantsDiscussionSources(query = '') {
|
|
5
|
-
return /reddit|forum|forums|discussion|thread|comments|community|user experience|people recommend/i.test(query);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function candidateScore(result: SearchResult, query?: string) {
|
|
9
|
-
const url = result.url.toLowerCase();
|
|
10
|
-
const profile = classifySourceProfile(result.url);
|
|
11
|
-
const wantsThreads = wantsDiscussionSources(query);
|
|
12
|
-
|
|
13
|
-
if (profile.kind === 'official-docs') return 0;
|
|
14
|
-
if (profile.kind === 'official-api') return 1;
|
|
15
|
-
|
|
16
|
-
if (wantsThreads) {
|
|
17
|
-
if (profile.kind === 'forum-thread') return 2;
|
|
18
|
-
if (profile.kind === 'issue-thread') return 3;
|
|
19
|
-
if (url.includes('github.com/')) return 4;
|
|
20
|
-
if (profile.kind === 'package-page') return 6;
|
|
21
|
-
return 5;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (profile.kind === 'issue-thread') return 2;
|
|
25
|
-
if (url.includes('github.com/')) return 3;
|
|
26
|
-
if (profile.kind === 'package-page') return 5;
|
|
27
|
-
return 4;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function selectCandidates({
|
|
31
|
-
query,
|
|
32
|
-
results,
|
|
33
|
-
seenUrls,
|
|
34
|
-
maxCandidates
|
|
35
|
-
}: {
|
|
36
|
-
query?: string;
|
|
37
|
-
results: SearchResult[];
|
|
38
|
-
seenUrls: Set<string>;
|
|
39
|
-
maxCandidates: number;
|
|
40
|
-
}) {
|
|
41
|
-
const deduped = new Map<string, SearchResult>();
|
|
42
|
-
for (const result of results) {
|
|
43
|
-
if (seenUrls.has(result.url)) continue;
|
|
44
|
-
if (!deduped.has(result.url)) deduped.set(result.url, result);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return [...deduped.values()]
|
|
48
|
-
.sort((left, right) => candidateScore(left, query) - candidateScore(right, query))
|
|
49
|
-
.slice(0, maxCandidates);
|
|
50
|
-
}
|
|
1
|
+
import type { SearchResult } from '../types.js';
|
|
2
|
+
import { classifySourceProfile } from './source-profile.js';
|
|
3
|
+
|
|
4
|
+
function wantsDiscussionSources(query = '') {
|
|
5
|
+
return /reddit|forum|forums|discussion|thread|comments|community|user experience|people recommend/i.test(query);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function candidateScore(result: SearchResult, query?: string) {
|
|
9
|
+
const url = result.url.toLowerCase();
|
|
10
|
+
const profile = classifySourceProfile(result.url);
|
|
11
|
+
const wantsThreads = wantsDiscussionSources(query);
|
|
12
|
+
|
|
13
|
+
if (profile.kind === 'official-docs') return 0;
|
|
14
|
+
if (profile.kind === 'official-api') return 1;
|
|
15
|
+
|
|
16
|
+
if (wantsThreads) {
|
|
17
|
+
if (profile.kind === 'forum-thread') return 2;
|
|
18
|
+
if (profile.kind === 'issue-thread') return 3;
|
|
19
|
+
if (url.includes('github.com/')) return 4;
|
|
20
|
+
if (profile.kind === 'package-page') return 6;
|
|
21
|
+
return 5;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (profile.kind === 'issue-thread') return 2;
|
|
25
|
+
if (url.includes('github.com/')) return 3;
|
|
26
|
+
if (profile.kind === 'package-page') return 5;
|
|
27
|
+
return 4;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function selectCandidates({
|
|
31
|
+
query,
|
|
32
|
+
results,
|
|
33
|
+
seenUrls,
|
|
34
|
+
maxCandidates
|
|
35
|
+
}: {
|
|
36
|
+
query?: string;
|
|
37
|
+
results: SearchResult[];
|
|
38
|
+
seenUrls: Set<string>;
|
|
39
|
+
maxCandidates: number;
|
|
40
|
+
}) {
|
|
41
|
+
const deduped = new Map<string, SearchResult>();
|
|
42
|
+
for (const result of results) {
|
|
43
|
+
if (seenUrls.has(result.url)) continue;
|
|
44
|
+
if (!deduped.has(result.url)) deduped.set(result.url, result);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return [...deduped.values()]
|
|
48
|
+
.sort((left, right) => candidateScore(left, query) - candidateScore(right, query))
|
|
49
|
+
.slice(0, maxCandidates);
|
|
50
|
+
}
|