opencode-dux 1.0.0
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/LICENSE +21 -0
- package/README.md +452 -0
- package/dist/agents/descriptions.d.ts +6 -0
- package/dist/agents/designer.d.ts +2 -0
- package/dist/agents/explorer.d.ts +2 -0
- package/dist/agents/fixer.d.ts +2 -0
- package/dist/agents/index.d.ts +22 -0
- package/dist/agents/interpreter.d.ts +2 -0
- package/dist/agents/librarian.d.ts +2 -0
- package/dist/agents/oracle.d.ts +2 -0
- package/dist/agents/orchestrator.d.ts +27 -0
- package/dist/agents/overrides.d.ts +18 -0
- package/dist/agents/prompt-blocks.d.ts +97 -0
- package/dist/agents/steward.d.ts +3 -0
- package/dist/cli/config-io.d.ts +24 -0
- package/dist/cli/config-manager.d.ts +4 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +1006 -0
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/mcps.d.ts +13 -0
- package/dist/cli/model-key-normalization.d.ts +1 -0
- package/dist/cli/paths.d.ts +35 -0
- package/dist/cli/providers.d.ts +137 -0
- package/dist/cli/skills.d.ts +22 -0
- package/dist/cli/system.d.ts +5 -0
- package/dist/cli/types.d.ts +38 -0
- package/dist/config/constants.d.ts +12 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/loader.d.ts +40 -0
- package/dist/config/runtime-preset.d.ts +12 -0
- package/dist/config/schema.d.ts +281 -0
- package/dist/config/utils.d.ts +10 -0
- package/dist/discovery/local/types.d.ts +79 -0
- package/dist/discovery/local.d.ts +73 -0
- package/dist/discovery/mcp-servers.d.ts +88 -0
- package/dist/discovery/skills.d.ts +94 -0
- package/dist/hooks/apply-patch/codec.d.ts +7 -0
- package/dist/hooks/apply-patch/errors.d.ts +25 -0
- package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
- package/dist/hooks/apply-patch/index.d.ts +15 -0
- package/dist/hooks/apply-patch/matching.d.ts +26 -0
- package/dist/hooks/apply-patch/operations.d.ts +3 -0
- package/dist/hooks/apply-patch/patch.d.ts +2 -0
- package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
- package/dist/hooks/apply-patch/resolution.d.ts +19 -0
- package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
- package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
- package/dist/hooks/apply-patch/types.d.ts +80 -0
- package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
- package/dist/hooks/auto-update-checker/checker.d.ts +32 -0
- package/dist/hooks/auto-update-checker/constants.d.ts +11 -0
- package/dist/hooks/auto-update-checker/index.d.ts +18 -0
- package/dist/hooks/auto-update-checker/types.d.ts +22 -0
- package/dist/hooks/chat-headers.d.ts +16 -0
- package/dist/hooks/context-pressure-reminder/index.d.ts +33 -0
- package/dist/hooks/delegate-task-retry/guidance.d.ts +2 -0
- package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
- package/dist/hooks/delegate-task-retry/index.d.ts +4 -0
- package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
- package/dist/hooks/filter-available-skills/index.d.ts +32 -0
- package/dist/hooks/foreground-fallback/index.d.ts +72 -0
- package/dist/hooks/image-hook.d.ts +5 -0
- package/dist/hooks/index.d.ts +14 -0
- package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
- package/dist/hooks/json-error-recovery/index.d.ts +1 -0
- package/dist/hooks/phase-reminder/index.d.ts +26 -0
- package/dist/hooks/post-file-tool-nudge/index.d.ts +19 -0
- package/dist/hooks/task-session-manager/index.d.ts +52 -0
- package/dist/hooks/todo-continuation/index.d.ts +53 -0
- package/dist/hooks/todo-continuation/todo-hygiene.d.ts +35 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +31782 -0
- package/dist/mcp/context7.d.ts +6 -0
- package/dist/mcp/grep-app.d.ts +6 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/types.d.ts +12 -0
- package/dist/mcp/websearch.d.ts +9 -0
- package/dist/skills/registry.d.ts +29 -0
- package/dist/subscriptions/accounts-store.d.ts +57 -0
- package/dist/subscriptions/index.d.ts +13 -0
- package/dist/subscriptions/neuralwatt-scraper.d.ts +14 -0
- package/dist/subscriptions/opencode-go-scraper.d.ts +27 -0
- package/dist/subscriptions/types.d.ts +115 -0
- package/dist/subscriptions/usage-service.d.ts +74 -0
- package/dist/tools/ast-grep/cli.d.ts +15 -0
- package/dist/tools/ast-grep/constants.d.ts +25 -0
- package/dist/tools/ast-grep/downloader.d.ts +5 -0
- package/dist/tools/ast-grep/index.d.ts +10 -0
- package/dist/tools/ast-grep/tools.d.ts +3 -0
- package/dist/tools/ast-grep/types.d.ts +30 -0
- package/dist/tools/ast-grep/utils.d.ts +4 -0
- package/dist/tools/delegate.d.ts +14 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/preset-manager.d.ts +27 -0
- package/dist/tools/smartfetch/binary.d.ts +3 -0
- package/dist/tools/smartfetch/cache.d.ts +6 -0
- package/dist/tools/smartfetch/constants.d.ts +12 -0
- package/dist/tools/smartfetch/index.d.ts +3 -0
- package/dist/tools/smartfetch/network.d.ts +38 -0
- package/dist/tools/smartfetch/secondary-model.d.ts +28 -0
- package/dist/tools/smartfetch/tool.d.ts +3 -0
- package/dist/tools/smartfetch/types.d.ts +122 -0
- package/dist/tools/smartfetch/utils.d.ts +18 -0
- package/dist/tui-state.d.ts +168 -0
- package/dist/tui.d.ts +37 -0
- package/dist/tui.js +1896 -0
- package/dist/utils/agent-variant.d.ts +63 -0
- package/dist/utils/compat.d.ts +30 -0
- package/dist/utils/env.d.ts +1 -0
- package/dist/utils/index.d.ts +9 -0
- package/dist/utils/internal-initiator.d.ts +6 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/polling.d.ts +21 -0
- package/dist/utils/session-manager.d.ts +55 -0
- package/dist/utils/session.d.ts +90 -0
- package/dist/utils/subagent-depth.d.ts +35 -0
- package/dist/utils/system-collapse.d.ts +6 -0
- package/dist/utils/task.d.ts +4 -0
- package/dist/utils/zip-extractor.d.ts +1 -0
- package/index.ts +1 -0
- package/opencode-dux.schema.json +634 -0
- package/package.json +103 -0
- package/src/agents/descriptions.ts +55 -0
- package/src/agents/designer.test.ts +86 -0
- package/src/agents/designer.ts +154 -0
- package/src/agents/display-name.test.ts +186 -0
- package/src/agents/explorer.test.ts +79 -0
- package/src/agents/explorer.ts +144 -0
- package/src/agents/fixer.test.ts +79 -0
- package/src/agents/fixer.ts +145 -0
- package/src/agents/index.test.ts +472 -0
- package/src/agents/index.ts +248 -0
- package/src/agents/interpreter.ts +136 -0
- package/src/agents/librarian.test.ts +80 -0
- package/src/agents/librarian.ts +145 -0
- package/src/agents/oracle.test.ts +89 -0
- package/src/agents/oracle.ts +184 -0
- package/src/agents/orchestrator.test.ts +116 -0
- package/src/agents/orchestrator.ts +574 -0
- package/src/agents/overrides.ts +95 -0
- package/src/agents/prompt-blocks.test.ts +114 -0
- package/src/agents/prompt-blocks.ts +640 -0
- package/src/agents/steward.ts +146 -0
- package/src/cli/config-io.test.ts +536 -0
- package/src/cli/config-io.ts +473 -0
- package/src/cli/config-manager.test.ts +141 -0
- package/src/cli/config-manager.ts +4 -0
- package/src/cli/index.ts +88 -0
- package/src/cli/install.ts +282 -0
- package/src/cli/mcps.test.ts +62 -0
- package/src/cli/mcps.ts +39 -0
- package/src/cli/model-key-normalization.test.ts +21 -0
- package/src/cli/model-key-normalization.ts +60 -0
- package/src/cli/paths.test.ts +167 -0
- package/src/cli/paths.ts +144 -0
- package/src/cli/providers.test.ts +118 -0
- package/src/cli/providers.ts +141 -0
- package/src/cli/skills.test.ts +111 -0
- package/src/cli/skills.ts +103 -0
- package/src/cli/system.test.ts +91 -0
- package/src/cli/system.ts +180 -0
- package/src/cli/types.ts +43 -0
- package/src/config/constants.ts +58 -0
- package/src/config/index.ts +4 -0
- package/src/config/loader.test.ts +1194 -0
- package/src/config/loader.ts +269 -0
- package/src/config/model-resolution.test.ts +176 -0
- package/src/config/runtime-preset.test.ts +61 -0
- package/src/config/runtime-preset.ts +37 -0
- package/src/config/schema.ts +248 -0
- package/src/config/utils.test.ts +41 -0
- package/src/config/utils.ts +23 -0
- package/src/discovery/local/types.ts +85 -0
- package/src/discovery/local.ts +322 -0
- package/src/discovery/mcp-servers.ts +804 -0
- package/src/discovery/skills.ts +959 -0
- package/src/hooks/apply-patch/codec.test.ts +184 -0
- package/src/hooks/apply-patch/codec.ts +352 -0
- package/src/hooks/apply-patch/errors.ts +117 -0
- package/src/hooks/apply-patch/execution-context.ts +432 -0
- package/src/hooks/apply-patch/hook.test.ts +768 -0
- package/src/hooks/apply-patch/index.ts +126 -0
- package/src/hooks/apply-patch/matching.test.ts +215 -0
- package/src/hooks/apply-patch/matching.ts +586 -0
- package/src/hooks/apply-patch/operations.test.ts +1535 -0
- package/src/hooks/apply-patch/operations.ts +3 -0
- package/src/hooks/apply-patch/patch.ts +9 -0
- package/src/hooks/apply-patch/prepared-changes.ts +400 -0
- package/src/hooks/apply-patch/resolution.test.ts +420 -0
- package/src/hooks/apply-patch/resolution.ts +437 -0
- package/src/hooks/apply-patch/rewrite.ts +496 -0
- package/src/hooks/apply-patch/test-helpers.ts +52 -0
- package/src/hooks/apply-patch/types.ts +111 -0
- package/src/hooks/auto-update-checker/cache.test.ts +179 -0
- package/src/hooks/auto-update-checker/cache.ts +188 -0
- package/src/hooks/auto-update-checker/checker.test.ts +159 -0
- package/src/hooks/auto-update-checker/checker.ts +308 -0
- package/src/hooks/auto-update-checker/constants.ts +33 -0
- package/src/hooks/auto-update-checker/index.test.ts +282 -0
- package/src/hooks/auto-update-checker/index.ts +225 -0
- package/src/hooks/auto-update-checker/types.ts +26 -0
- package/src/hooks/chat-headers.test.ts +236 -0
- package/src/hooks/chat-headers.ts +97 -0
- package/src/hooks/context-pressure-reminder/index.test.ts +179 -0
- package/src/hooks/context-pressure-reminder/index.ts +137 -0
- package/src/hooks/delegate-task-retry/guidance.ts +41 -0
- package/src/hooks/delegate-task-retry/hook.ts +23 -0
- package/src/hooks/delegate-task-retry/index.test.ts +38 -0
- package/src/hooks/delegate-task-retry/index.ts +7 -0
- package/src/hooks/delegate-task-retry/patterns.ts +79 -0
- package/src/hooks/filter-available-skills/index.test.ts +297 -0
- package/src/hooks/filter-available-skills/index.ts +160 -0
- package/src/hooks/foreground-fallback/index.test.ts +624 -0
- package/src/hooks/foreground-fallback/index.ts +374 -0
- package/src/hooks/image-hook.ts +6 -0
- package/src/hooks/index.ts +17 -0
- package/src/hooks/json-error-recovery/hook.ts +73 -0
- package/src/hooks/json-error-recovery/index.test.ts +111 -0
- package/src/hooks/json-error-recovery/index.ts +6 -0
- package/src/hooks/phase-reminder/index.test.ts +74 -0
- package/src/hooks/phase-reminder/index.ts +85 -0
- package/src/hooks/post-file-tool-nudge/index.test.ts +94 -0
- package/src/hooks/post-file-tool-nudge/index.ts +63 -0
- package/src/hooks/task-session-manager/index.test.ts +833 -0
- package/src/hooks/task-session-manager/index.ts +434 -0
- package/src/hooks/todo-continuation/index.test.ts +3026 -0
- package/src/hooks/todo-continuation/index.ts +878 -0
- package/src/hooks/todo-continuation/todo-hygiene.test.ts +204 -0
- package/src/hooks/todo-continuation/todo-hygiene.ts +207 -0
- package/src/index.ts +1672 -0
- package/src/mcp/context7.ts +14 -0
- package/src/mcp/grep-app.ts +11 -0
- package/src/mcp/index.test.ts +96 -0
- package/src/mcp/index.ts +66 -0
- package/src/mcp/types.ts +16 -0
- package/src/mcp/websearch.ts +47 -0
- package/src/skills/codemap/README.md +60 -0
- package/src/skills/codemap/SKILL.md +174 -0
- package/src/skills/codemap/scripts/codemap.mjs +483 -0
- package/src/skills/codemap/scripts/codemap.test.ts +129 -0
- package/src/skills/registry.ts +218 -0
- package/src/skills/simplify/README.md +19 -0
- package/src/skills/simplify/SKILL.md +138 -0
- package/src/subscriptions/accounts-store.test.ts +236 -0
- package/src/subscriptions/accounts-store.ts +184 -0
- package/src/subscriptions/index.ts +30 -0
- package/src/subscriptions/neuralwatt-scraper.ts +108 -0
- package/src/subscriptions/opencode-go-scraper.ts +301 -0
- package/src/subscriptions/types.ts +145 -0
- package/src/subscriptions/usage-service.test.ts +202 -0
- package/src/subscriptions/usage-service.ts +651 -0
- package/src/tools/ast-grep/cli.ts +257 -0
- package/src/tools/ast-grep/constants.ts +214 -0
- package/src/tools/ast-grep/downloader.ts +131 -0
- package/src/tools/ast-grep/index.ts +24 -0
- package/src/tools/ast-grep/tools.ts +117 -0
- package/src/tools/ast-grep/types.ts +51 -0
- package/src/tools/ast-grep/utils.ts +126 -0
- package/src/tools/delegate-handoff.test.ts +18 -0
- package/src/tools/delegate.ts +508 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/preset-manager.test.ts +795 -0
- package/src/tools/preset-manager.ts +332 -0
- package/src/tools/smartfetch/binary.ts +58 -0
- package/src/tools/smartfetch/cache.test.ts +34 -0
- package/src/tools/smartfetch/cache.ts +112 -0
- package/src/tools/smartfetch/constants.ts +29 -0
- package/src/tools/smartfetch/index.ts +8 -0
- package/src/tools/smartfetch/network.test.ts +178 -0
- package/src/tools/smartfetch/network.ts +614 -0
- package/src/tools/smartfetch/secondary-model.test.ts +85 -0
- package/src/tools/smartfetch/secondary-model.ts +276 -0
- package/src/tools/smartfetch/tool.test.ts +60 -0
- package/src/tools/smartfetch/tool.ts +832 -0
- package/src/tools/smartfetch/types.ts +135 -0
- package/src/tools/smartfetch/utils.test.ts +24 -0
- package/src/tools/smartfetch/utils.ts +456 -0
- package/src/tui-state.test.ts +867 -0
- package/src/tui-state.ts +1255 -0
- package/src/tui.test.ts +336 -0
- package/src/tui.ts +1539 -0
- package/src/utils/agent-variant.test.ts +244 -0
- package/src/utils/agent-variant.ts +187 -0
- package/src/utils/compat.ts +91 -0
- package/src/utils/env.ts +12 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/internal-initiator.ts +28 -0
- package/src/utils/logger.test.ts +220 -0
- package/src/utils/logger.ts +136 -0
- package/src/utils/polling.test.ts +191 -0
- package/src/utils/polling.ts +67 -0
- package/src/utils/session-manager.test.ts +173 -0
- package/src/utils/session-manager.ts +356 -0
- package/src/utils/session.test.ts +110 -0
- package/src/utils/session.ts +389 -0
- package/src/utils/subagent-depth.test.ts +170 -0
- package/src/utils/subagent-depth.ts +75 -0
- package/src/utils/system-collapse.test.ts +86 -0
- package/src/utils/system-collapse.ts +24 -0
- package/src/utils/task.test.ts +24 -0
- package/src/utils/task.ts +20 -0
- package/src/utils/zip-extractor.ts +102 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import type { PluginInput } from '@opencode-ai/plugin';
|
|
5
|
+
import { stripJsonComments } from '../../cli/config-io';
|
|
6
|
+
import { getConfigSearchDirs } from '../../cli/paths';
|
|
7
|
+
import { loadPluginConfig } from '../../config/loader';
|
|
8
|
+
import { MAX_MODEL_CONTENT_CHARS } from './constants';
|
|
9
|
+
import type { CachedFetch, SecondaryModel } from './types';
|
|
10
|
+
|
|
11
|
+
type OpenCodeClient = PluginInput['client'];
|
|
12
|
+
|
|
13
|
+
function parseModelRef(value: string | undefined) {
|
|
14
|
+
if (!value) return undefined;
|
|
15
|
+
const [providerID, ...rest] = value.split('/');
|
|
16
|
+
const modelID = rest.join('/');
|
|
17
|
+
if (!providerID || !modelID) return undefined;
|
|
18
|
+
return { providerID, modelID };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function pickAgentModelRef(value: unknown): string | undefined {
|
|
22
|
+
if (typeof value === 'string') return value;
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
for (const entry of value) {
|
|
25
|
+
if (typeof entry === 'string') return entry;
|
|
26
|
+
if (
|
|
27
|
+
entry &&
|
|
28
|
+
typeof entry === 'object' &&
|
|
29
|
+
'id' in entry &&
|
|
30
|
+
typeof (entry as { id?: unknown }).id === 'string'
|
|
31
|
+
) {
|
|
32
|
+
return (entry as { id: string }).id;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function findPreferredOpenCodeConfigPath(baseDir: string) {
|
|
40
|
+
for (const file of ['opencode.jsonc', 'opencode.json']) {
|
|
41
|
+
const fullPath = path.join(baseDir, file);
|
|
42
|
+
if (existsSync(fullPath)) return fullPath;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readOpenCodeConfigFile(configPath: string | undefined) {
|
|
48
|
+
if (!configPath) return undefined;
|
|
49
|
+
try {
|
|
50
|
+
const content = await readFile(configPath, 'utf8');
|
|
51
|
+
return JSON.parse(stripJsonComments(content)) as {
|
|
52
|
+
small_model?: unknown;
|
|
53
|
+
};
|
|
54
|
+
} catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function readEffectiveOpenCodeConfig(directory: string) {
|
|
60
|
+
const projectDir = path.join(directory, '.opencode');
|
|
61
|
+
const userDirs = getConfigSearchDirs();
|
|
62
|
+
const projectPath = findPreferredOpenCodeConfigPath(projectDir);
|
|
63
|
+
const userPath = userDirs
|
|
64
|
+
.map((configDir) => findPreferredOpenCodeConfigPath(configDir))
|
|
65
|
+
.find(Boolean);
|
|
66
|
+
|
|
67
|
+
const userConfig = await readOpenCodeConfigFile(userPath);
|
|
68
|
+
const projectConfig = await readOpenCodeConfigFile(projectPath);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
small_model: projectConfig?.small_model ?? userConfig?.small_model,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function readSecondaryModelFromConfig(directory: string) {
|
|
76
|
+
try {
|
|
77
|
+
const models: SecondaryModel[] = [];
|
|
78
|
+
const seen = new Set<string>();
|
|
79
|
+
const pushModel = (value: unknown) => {
|
|
80
|
+
if (typeof value !== 'string') return;
|
|
81
|
+
const parsedModel = parseModelRef(value);
|
|
82
|
+
if (!parsedModel) return;
|
|
83
|
+
const key = `${parsedModel.providerID}/${parsedModel.modelID}`;
|
|
84
|
+
if (seen.has(key)) return;
|
|
85
|
+
seen.add(key);
|
|
86
|
+
models.push(parsedModel);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const opencodeConfig = await readEffectiveOpenCodeConfig(directory);
|
|
90
|
+
pushModel(
|
|
91
|
+
typeof opencodeConfig.small_model === 'string'
|
|
92
|
+
? opencodeConfig.small_model
|
|
93
|
+
: undefined,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const pluginConfig = loadPluginConfig(directory);
|
|
97
|
+
const explorerModel = pickAgentModelRef(
|
|
98
|
+
pluginConfig.agents?.explorer?.model,
|
|
99
|
+
);
|
|
100
|
+
const librarianModel = pickAgentModelRef(
|
|
101
|
+
pluginConfig.agents?.librarian?.model,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
pushModel(explorerModel);
|
|
105
|
+
pushModel(librarianModel);
|
|
106
|
+
|
|
107
|
+
return models;
|
|
108
|
+
} catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildPrompt(content: string, prompt: string) {
|
|
114
|
+
return [
|
|
115
|
+
'Use only the fetched content below.',
|
|
116
|
+
'Do not use tools, outside knowledge, or unstated assumptions.',
|
|
117
|
+
'Answer concisely and directly.',
|
|
118
|
+
'If the requested information is missing from the content, say that clearly.',
|
|
119
|
+
'Preserve code examples or exact values only when they are relevant to the task.',
|
|
120
|
+
'',
|
|
121
|
+
'Fetched content:',
|
|
122
|
+
'---',
|
|
123
|
+
content,
|
|
124
|
+
'---',
|
|
125
|
+
'',
|
|
126
|
+
'Task:',
|
|
127
|
+
prompt,
|
|
128
|
+
].join('\n');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function decideSecondaryModelUse(
|
|
132
|
+
fetchResult: CachedFetch,
|
|
133
|
+
prompt: string | undefined,
|
|
134
|
+
secondaryModels: SecondaryModel[],
|
|
135
|
+
) {
|
|
136
|
+
if (!prompt?.trim()) return { use: false, reason: 'no_prompt' as const };
|
|
137
|
+
if (!secondaryModels.length) {
|
|
138
|
+
return {
|
|
139
|
+
use: false,
|
|
140
|
+
reason: 'no_secondary_model_configured' as const,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (!fetchResult.markdown.trim()) {
|
|
144
|
+
return { use: false, reason: 'empty_content' as const };
|
|
145
|
+
}
|
|
146
|
+
if (fetchResult.wordCount > 0 && fetchResult.wordCount < 25) {
|
|
147
|
+
return { use: false, reason: 'content_too_short' as const };
|
|
148
|
+
}
|
|
149
|
+
return { use: true, reason: 'prompt_present' as const };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isUsableSecondaryText(text: string) {
|
|
153
|
+
const trimmed = text.trim();
|
|
154
|
+
if (!trimmed) return false;
|
|
155
|
+
if (/^no response from secondary model\.?$/i.test(trimmed)) return false;
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function runSecondaryModel(
|
|
160
|
+
client: OpenCodeClient,
|
|
161
|
+
directory: string,
|
|
162
|
+
model: SecondaryModel,
|
|
163
|
+
prompt: string,
|
|
164
|
+
content: string,
|
|
165
|
+
) {
|
|
166
|
+
const session = await client.session.create({
|
|
167
|
+
responseStyle: 'data',
|
|
168
|
+
throwOnError: true,
|
|
169
|
+
query: { directory },
|
|
170
|
+
body: { title: 'smartfetch-secondary' },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const sessionId =
|
|
174
|
+
(session as { data?: { id?: string }; id?: string })?.data?.id ??
|
|
175
|
+
(session as { data?: { id?: string }; id?: string })?.id;
|
|
176
|
+
if (!sessionId) {
|
|
177
|
+
throw new Error('Secondary model session did not return an id');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const sourceChars = content.length;
|
|
181
|
+
const truncatedContent = content.slice(0, MAX_MODEL_CONTENT_CHARS);
|
|
182
|
+
const inputChars = truncatedContent.length;
|
|
183
|
+
const inputTruncated = inputChars < sourceChars;
|
|
184
|
+
const effectivePrompt = inputTruncated
|
|
185
|
+
? `${prompt}\n\nNote: only the first ${inputChars} characters of a longer fetched document were provided.`
|
|
186
|
+
: prompt;
|
|
187
|
+
try {
|
|
188
|
+
const toolIDsResponse = await client.tool.ids({
|
|
189
|
+
responseStyle: 'data',
|
|
190
|
+
throwOnError: true,
|
|
191
|
+
});
|
|
192
|
+
const toolIDsData = toolIDsResponse as { data?: unknown };
|
|
193
|
+
const toolIDs = Array.isArray(toolIDsData.data)
|
|
194
|
+
? (toolIDsData.data as string[])
|
|
195
|
+
: Array.isArray(toolIDsResponse)
|
|
196
|
+
? toolIDsResponse
|
|
197
|
+
: [];
|
|
198
|
+
const disabledTools = Object.fromEntries(
|
|
199
|
+
(toolIDs || []).map((id: string) => [id, false]),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const result = await client.session.prompt({
|
|
203
|
+
responseStyle: 'data',
|
|
204
|
+
throwOnError: true,
|
|
205
|
+
path: { id: sessionId },
|
|
206
|
+
query: { directory },
|
|
207
|
+
body: {
|
|
208
|
+
model,
|
|
209
|
+
system:
|
|
210
|
+
'Answer only from the supplied content. Do not use tools or outside knowledge.',
|
|
211
|
+
tools: disabledTools,
|
|
212
|
+
parts: [
|
|
213
|
+
{
|
|
214
|
+
type: 'text',
|
|
215
|
+
text: buildPrompt(truncatedContent, effectivePrompt),
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const parts =
|
|
222
|
+
(result as { data?: { parts?: Array<{ type?: string; text?: string }> } })
|
|
223
|
+
?.data?.parts ??
|
|
224
|
+
(result as { parts?: Array<{ type?: string; text?: string }> })?.parts ??
|
|
225
|
+
[];
|
|
226
|
+
const text = parts
|
|
227
|
+
.map((part) => (part?.type === 'text' ? part.text || '' : ''))
|
|
228
|
+
.join('')
|
|
229
|
+
.trim();
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
text,
|
|
233
|
+
inputTruncated,
|
|
234
|
+
inputChars,
|
|
235
|
+
sourceChars,
|
|
236
|
+
};
|
|
237
|
+
} finally {
|
|
238
|
+
await client.session
|
|
239
|
+
.delete({
|
|
240
|
+
path: { id: sessionId },
|
|
241
|
+
query: { directory },
|
|
242
|
+
})
|
|
243
|
+
.catch(() => undefined);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function runSecondaryModelWithFallback(
|
|
248
|
+
client: OpenCodeClient,
|
|
249
|
+
directory: string,
|
|
250
|
+
models: SecondaryModel[],
|
|
251
|
+
prompt: string,
|
|
252
|
+
content: string,
|
|
253
|
+
) {
|
|
254
|
+
let lastError: unknown;
|
|
255
|
+
for (const model of models) {
|
|
256
|
+
try {
|
|
257
|
+
const result = await runSecondaryModel(
|
|
258
|
+
client,
|
|
259
|
+
directory,
|
|
260
|
+
model,
|
|
261
|
+
prompt,
|
|
262
|
+
content,
|
|
263
|
+
);
|
|
264
|
+
if (!isUsableSecondaryText(result.text)) {
|
|
265
|
+
lastError = new Error('Secondary model returned no usable text');
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
return { ...result, model };
|
|
269
|
+
} catch (error) {
|
|
270
|
+
lastError = error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
throw lastError instanceof Error
|
|
274
|
+
? lastError
|
|
275
|
+
: new Error(String(lastError ?? 'Secondary model failed'));
|
|
276
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { afterEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import { createWebfetchTool } from './tool';
|
|
3
|
+
|
|
4
|
+
function createExecutionContext() {
|
|
5
|
+
return {
|
|
6
|
+
ask: mock(async () => undefined),
|
|
7
|
+
metadata: mock(() => undefined),
|
|
8
|
+
abort: new AbortController().signal,
|
|
9
|
+
directory: '/tmp/smartfetch-test',
|
|
10
|
+
} as any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('smartfetch/tool', () => {
|
|
14
|
+
const originalFetch = globalThis.fetch;
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
globalThis.fetch = originalFetch;
|
|
18
|
+
mock.restore();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('returns a required llms.txt message when prefer_llms_txt is always and no llms.txt is available', async () => {
|
|
22
|
+
const fetchMock = mock(async (input: string | URL | Request) => {
|
|
23
|
+
const url = typeof input === 'string' ? input : input.toString();
|
|
24
|
+
|
|
25
|
+
if (
|
|
26
|
+
url === 'https://docs.example.com/llms-full.txt' ||
|
|
27
|
+
url === 'https://docs.example.com/llms.txt'
|
|
28
|
+
) {
|
|
29
|
+
return new Response('not found', {
|
|
30
|
+
status: 404,
|
|
31
|
+
headers: { 'content-type': 'text/plain' },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw new Error(`Unexpected fetch URL: ${url}`);
|
|
36
|
+
});
|
|
37
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
38
|
+
|
|
39
|
+
const webfetch = createWebfetchTool({ client: {} } as any);
|
|
40
|
+
const ctx = createExecutionContext();
|
|
41
|
+
const result = await webfetch.execute(
|
|
42
|
+
{
|
|
43
|
+
url: 'https://docs.example.com/page',
|
|
44
|
+
format: 'markdown',
|
|
45
|
+
extract_main: true,
|
|
46
|
+
prefer_llms_txt: 'always',
|
|
47
|
+
include_metadata: true,
|
|
48
|
+
save_binary: false,
|
|
49
|
+
},
|
|
50
|
+
ctx,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(result).toContain('Required llms.txt content was unavailable.');
|
|
54
|
+
expect(result).toContain('Original URL: https://docs.example.com/page');
|
|
55
|
+
expect(result).toContain('prefer_llms_txt: "always"');
|
|
56
|
+
expect(result).toContain('used_llms_txt: false');
|
|
57
|
+
expect(ctx.ask).toHaveBeenCalledTimes(1);
|
|
58
|
+
expect(ctx.metadata).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
});
|