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,269 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { stripJsonComments } from '../cli/config-io';
|
|
4
|
+
import { getConfigSearchDirs } from '../cli/paths';
|
|
5
|
+
import { type PluginConfig, PluginConfigSchema } from './schema';
|
|
6
|
+
|
|
7
|
+
const PROMPTS_DIR_NAME = 'opencode-dux';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load and validate plugin configuration from a specific file path.
|
|
11
|
+
* Supports both .json and .jsonc formats (JSON with comments).
|
|
12
|
+
* Returns null if the file doesn't exist, is invalid, or cannot be read.
|
|
13
|
+
* Logs warnings for validation errors and unexpected read errors.
|
|
14
|
+
*
|
|
15
|
+
* @param configPath - Absolute path to the config file
|
|
16
|
+
* @returns Validated config object, or null if loading failed
|
|
17
|
+
*/
|
|
18
|
+
function loadConfigFromPath(configPath: string): PluginConfig | null {
|
|
19
|
+
try {
|
|
20
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
21
|
+
// Use stripJsonComments to support JSONC format (comments and trailing commas)
|
|
22
|
+
const rawConfig = JSON.parse(stripJsonComments(content));
|
|
23
|
+
const result = PluginConfigSchema.safeParse(rawConfig);
|
|
24
|
+
|
|
25
|
+
if (!result.success) {
|
|
26
|
+
console.warn(`[opencode-dux] Invalid config at ${configPath}:`);
|
|
27
|
+
console.warn(result.error.format());
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return result.data;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// File doesn't exist or isn't readable - this is expected and fine
|
|
34
|
+
if (
|
|
35
|
+
error instanceof Error &&
|
|
36
|
+
'code' in error &&
|
|
37
|
+
(error as NodeJS.ErrnoException).code !== 'ENOENT'
|
|
38
|
+
) {
|
|
39
|
+
console.warn(
|
|
40
|
+
`[opencode-dux] Error reading config from ${configPath}:`,
|
|
41
|
+
error.message,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Find existing config file path, preferring .jsonc over .json.
|
|
50
|
+
* Checks for .jsonc first, then falls back to .json.
|
|
51
|
+
*
|
|
52
|
+
* @param basePath - Base path without extension (e.g., /path/to/opencode-dux)
|
|
53
|
+
* @returns Path to existing config file, or null if neither exists
|
|
54
|
+
*/
|
|
55
|
+
function findConfigPath(basePath: string): string | null {
|
|
56
|
+
const jsoncPath = `${basePath}.jsonc`;
|
|
57
|
+
const jsonPath = `${basePath}.json`;
|
|
58
|
+
|
|
59
|
+
// Prefer .jsonc over .json
|
|
60
|
+
if (fs.existsSync(jsoncPath)) {
|
|
61
|
+
return jsoncPath;
|
|
62
|
+
}
|
|
63
|
+
if (fs.existsSync(jsonPath)) {
|
|
64
|
+
return jsonPath;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function findConfigPathInDirs(
|
|
70
|
+
configDirs: string[],
|
|
71
|
+
baseName: string,
|
|
72
|
+
): string | null {
|
|
73
|
+
for (const configDir of configDirs) {
|
|
74
|
+
const configPath = findConfigPath(path.join(configDir, baseName));
|
|
75
|
+
if (configPath) {
|
|
76
|
+
return configPath;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Recursively merge two objects, with override values taking precedence.
|
|
85
|
+
* For nested objects, merges recursively. For arrays and primitives, override replaces base.
|
|
86
|
+
*
|
|
87
|
+
* @param base - Base object to merge into
|
|
88
|
+
* @param override - Override object whose values take precedence
|
|
89
|
+
* @returns Merged object, or undefined if both inputs are undefined
|
|
90
|
+
*/
|
|
91
|
+
export function deepMerge<T extends Record<string, unknown>>(
|
|
92
|
+
base?: T,
|
|
93
|
+
override?: T,
|
|
94
|
+
): T | undefined {
|
|
95
|
+
if (!base) return override;
|
|
96
|
+
if (!override) return base;
|
|
97
|
+
|
|
98
|
+
const result = { ...base } as T;
|
|
99
|
+
for (const key of Object.keys(override) as (keyof T)[]) {
|
|
100
|
+
const baseVal = base[key];
|
|
101
|
+
const overrideVal = override[key];
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
typeof baseVal === 'object' &&
|
|
105
|
+
baseVal !== null &&
|
|
106
|
+
typeof overrideVal === 'object' &&
|
|
107
|
+
overrideVal !== null &&
|
|
108
|
+
!Array.isArray(baseVal) &&
|
|
109
|
+
!Array.isArray(overrideVal)
|
|
110
|
+
) {
|
|
111
|
+
result[key] = deepMerge(
|
|
112
|
+
baseVal as Record<string, unknown>,
|
|
113
|
+
overrideVal as Record<string, unknown>,
|
|
114
|
+
) as T[keyof T];
|
|
115
|
+
} else {
|
|
116
|
+
result[key] = overrideVal;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Load plugin configuration from user and project config files, merging them appropriately.
|
|
124
|
+
*
|
|
125
|
+
* Configuration is loaded from two locations:
|
|
126
|
+
* 1. User config: $OPENCODE_CONFIG_DIR/opencode-dux.jsonc or .json,
|
|
127
|
+
* or ~/.config/opencode/opencode-dux.jsonc or .json (or $XDG_CONFIG_HOME)
|
|
128
|
+
* 2. Project config: <directory>/.opencode/opencode-dux.jsonc or .json
|
|
129
|
+
*
|
|
130
|
+
* JSONC format is preferred over JSON (allows comments and trailing commas).
|
|
131
|
+
* Project config takes precedence over user config. Nested objects (agents) are
|
|
132
|
+
* deep-merged, while top-level arrays are replaced entirely by project config.
|
|
133
|
+
*
|
|
134
|
+
* @param directory - Project directory to search for .opencode config
|
|
135
|
+
* @returns Merged plugin configuration (empty object if no configs found)
|
|
136
|
+
*/
|
|
137
|
+
export function loadPluginConfig(directory: string): PluginConfig {
|
|
138
|
+
const userConfigPath = findConfigPathInDirs(
|
|
139
|
+
getConfigSearchDirs(),
|
|
140
|
+
'opencode-dux',
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const projectConfigBasePath = path.join(
|
|
144
|
+
directory,
|
|
145
|
+
'.opencode',
|
|
146
|
+
'opencode-dux',
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Find existing config files (preferring .jsonc over .json)
|
|
150
|
+
const projectConfigPath = findConfigPath(projectConfigBasePath);
|
|
151
|
+
|
|
152
|
+
let config: PluginConfig = userConfigPath
|
|
153
|
+
? (loadConfigFromPath(userConfigPath) ?? {})
|
|
154
|
+
: {};
|
|
155
|
+
|
|
156
|
+
const projectConfig = projectConfigPath
|
|
157
|
+
? loadConfigFromPath(projectConfigPath)
|
|
158
|
+
: null;
|
|
159
|
+
if (projectConfig) {
|
|
160
|
+
config = {
|
|
161
|
+
...config,
|
|
162
|
+
...projectConfig,
|
|
163
|
+
agents: deepMerge(config.agents, projectConfig.agents),
|
|
164
|
+
sessionManager: deepMerge(
|
|
165
|
+
config.sessionManager,
|
|
166
|
+
projectConfig.sessionManager,
|
|
167
|
+
),
|
|
168
|
+
contextPressure: deepMerge(
|
|
169
|
+
config.contextPressure,
|
|
170
|
+
projectConfig.contextPressure,
|
|
171
|
+
),
|
|
172
|
+
fallback: deepMerge(config.fallback, projectConfig.fallback),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Override preset from environment variable if set
|
|
177
|
+
const envPreset = process.env.OH_MY_OPENCODE_SLIM_PRESET;
|
|
178
|
+
if (envPreset) {
|
|
179
|
+
config.preset = envPreset;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Resolve preset and merge with root agents
|
|
183
|
+
if (config.preset) {
|
|
184
|
+
const preset = config.presets?.[config.preset];
|
|
185
|
+
if (preset) {
|
|
186
|
+
// Merge preset agents with root agents (root overrides)
|
|
187
|
+
config.agents = deepMerge(preset, config.agents);
|
|
188
|
+
} else {
|
|
189
|
+
// Preset name specified but doesn't exist - warn user
|
|
190
|
+
const presetSource =
|
|
191
|
+
envPreset === config.preset ? 'environment variable' : 'config file';
|
|
192
|
+
const availablePresets = config.presets
|
|
193
|
+
? Object.keys(config.presets).join(', ')
|
|
194
|
+
: 'none';
|
|
195
|
+
console.warn(
|
|
196
|
+
`[opencode-dux] Preset "${config.preset}" not found (from ${presetSource}). Available presets: ${availablePresets}`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return config;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Load custom prompt for an agent from the prompts directory.
|
|
206
|
+
* Checks for {agent}.md (replaces default) and {agent}_append.md (appends to default).
|
|
207
|
+
* If preset is provided and safe for paths, it first checks {preset}/ subdirectory,
|
|
208
|
+
* then falls back to the root prompts directory.
|
|
209
|
+
*
|
|
210
|
+
* @param agentName - Name of the agent (e.g., "orchestrator", "explorer")
|
|
211
|
+
* @param preset - Optional preset name for preset-scoped prompt lookup
|
|
212
|
+
* @returns Object with prompt and/or appendPrompt if files exist
|
|
213
|
+
*/
|
|
214
|
+
export function loadAgentPrompt(
|
|
215
|
+
agentName: string,
|
|
216
|
+
preset?: string,
|
|
217
|
+
): {
|
|
218
|
+
prompt?: string;
|
|
219
|
+
appendPrompt?: string;
|
|
220
|
+
} {
|
|
221
|
+
const presetDirName =
|
|
222
|
+
preset && /^[a-zA-Z0-9_-]+$/.test(preset) ? preset : undefined;
|
|
223
|
+
const promptSearchDirs = getConfigSearchDirs().flatMap((configDir) => {
|
|
224
|
+
const promptsDir = path.join(configDir, PROMPTS_DIR_NAME);
|
|
225
|
+
return presetDirName
|
|
226
|
+
? [path.join(promptsDir, presetDirName), promptsDir]
|
|
227
|
+
: [promptsDir];
|
|
228
|
+
});
|
|
229
|
+
const result: { prompt?: string; appendPrompt?: string } = {};
|
|
230
|
+
|
|
231
|
+
const readFirstPrompt = (
|
|
232
|
+
fileName: string,
|
|
233
|
+
errorPrefix: string,
|
|
234
|
+
): string | undefined => {
|
|
235
|
+
for (const dir of promptSearchDirs) {
|
|
236
|
+
const promptPath = path.join(dir, fileName);
|
|
237
|
+
if (!fs.existsSync(promptPath)) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
return fs.readFileSync(promptPath, 'utf-8');
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.warn(
|
|
245
|
+
`[opencode-dux] ${errorPrefix} ${promptPath}:`,
|
|
246
|
+
error instanceof Error ? error.message : String(error),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return undefined;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// Check for replacement prompt
|
|
255
|
+
result.prompt = readFirstPrompt(
|
|
256
|
+
`${agentName}.md`,
|
|
257
|
+
'Error reading prompt file',
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Check for append prompt
|
|
261
|
+
result.appendPrompt = readFirstPrompt(
|
|
262
|
+
`${agentName}_append.md`,
|
|
263
|
+
'Error reading append prompt file',
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { ModelEntry } from '../config/schema';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Test the model array resolution logic that runs in the config hook.
|
|
6
|
+
* This logic determines which model to use from an effective model array.
|
|
7
|
+
*
|
|
8
|
+
* The resolver always picks the first model in the effective array,
|
|
9
|
+
* regardless of provider configuration. This is correct because:
|
|
10
|
+
* - Not all providers require entries in opencodeConfig.provider - some are
|
|
11
|
+
* loaded automatically by opencode (e.g. github-copilot, openrouter).
|
|
12
|
+
* - We cannot distinguish "auto-loaded provider" from "provider not configured"
|
|
13
|
+
* without calling the API, which isn't available at config-hook time.
|
|
14
|
+
* - Runtime failover (rate-limit handling) is handled separately by
|
|
15
|
+
* ForegroundFallbackManager.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
describe('model array resolution', () => {
|
|
19
|
+
/**
|
|
20
|
+
* Simulates the resolution logic from src/index.ts.
|
|
21
|
+
* Always returns the first model in the array.
|
|
22
|
+
*/
|
|
23
|
+
function resolveModelFromArray(
|
|
24
|
+
modelArray: Array<{ id: string; variant?: string }>,
|
|
25
|
+
): { model: string; variant?: string } | null {
|
|
26
|
+
if (!modelArray || modelArray.length === 0) return null;
|
|
27
|
+
|
|
28
|
+
const chosen = modelArray[0];
|
|
29
|
+
return {
|
|
30
|
+
model: chosen.id,
|
|
31
|
+
variant: chosen.variant,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test('uses first model when no provider config exists', () => {
|
|
36
|
+
const modelArray: ModelEntry[] = [
|
|
37
|
+
{ id: 'opencode/big-pickle', variant: 'high' },
|
|
38
|
+
{ id: 'iflowcn/qwen3-235b-a22b-thinking-2507', variant: 'high' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const result = resolveModelFromArray(modelArray);
|
|
42
|
+
|
|
43
|
+
expect(result?.model).toBe('opencode/big-pickle');
|
|
44
|
+
expect(result?.variant).toBe('high');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('uses first model even when other providers are configured', () => {
|
|
48
|
+
const modelArray: ModelEntry[] = [
|
|
49
|
+
{ id: 'github-copilot/claude-opus-4.6', variant: 'high' },
|
|
50
|
+
{ id: 'zai-coding-plan/glm-5' },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const result = resolveModelFromArray(modelArray);
|
|
54
|
+
|
|
55
|
+
// Auto-loaded provider should not be skipped in favor of configured one
|
|
56
|
+
expect(result?.model).toBe('github-copilot/claude-opus-4.6');
|
|
57
|
+
expect(result?.variant).toBe('high');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('returns null for empty model array', () => {
|
|
61
|
+
const modelArray: ModelEntry[] = [];
|
|
62
|
+
|
|
63
|
+
const result = resolveModelFromArray(modelArray);
|
|
64
|
+
|
|
65
|
+
expect(result).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Tests for the fallback.chains merging logic that runs in the config hook.
|
|
71
|
+
* Mirrors the effectiveArrays construction in src/index.ts.
|
|
72
|
+
*/
|
|
73
|
+
describe('fallback.chains merging for foreground agents', () => {
|
|
74
|
+
/**
|
|
75
|
+
* Simulates the effectiveArrays construction + resolution from src/index.ts.
|
|
76
|
+
* Returns the resolved model string or null.
|
|
77
|
+
*/
|
|
78
|
+
function resolveWithChains(opts: {
|
|
79
|
+
modelArray?: Array<{ id: string; variant?: string }>;
|
|
80
|
+
currentModel?: string;
|
|
81
|
+
chainModels?: string[];
|
|
82
|
+
fallbackEnabled?: boolean;
|
|
83
|
+
}): string | null {
|
|
84
|
+
const {
|
|
85
|
+
modelArray,
|
|
86
|
+
currentModel,
|
|
87
|
+
chainModels,
|
|
88
|
+
fallbackEnabled = true,
|
|
89
|
+
} = opts;
|
|
90
|
+
|
|
91
|
+
// Build effectiveArrays (mirrors index.ts logic)
|
|
92
|
+
const effectiveArray: Array<{ id: string; variant?: string }> = modelArray
|
|
93
|
+
? [...modelArray]
|
|
94
|
+
: [];
|
|
95
|
+
|
|
96
|
+
if (fallbackEnabled && chainModels && chainModels.length > 0) {
|
|
97
|
+
if (effectiveArray.length === 0 && currentModel) {
|
|
98
|
+
effectiveArray.push({ id: currentModel });
|
|
99
|
+
}
|
|
100
|
+
const seen = new Set(effectiveArray.map((m) => m.id));
|
|
101
|
+
for (const chainModel of chainModels) {
|
|
102
|
+
if (!seen.has(chainModel)) {
|
|
103
|
+
seen.add(chainModel);
|
|
104
|
+
effectiveArray.push({ id: chainModel });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (effectiveArray.length === 0) return null;
|
|
110
|
+
|
|
111
|
+
// Resolution: always use first model in effective array
|
|
112
|
+
return effectiveArray[0].id;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
test('primary model wins regardless of provider config', () => {
|
|
116
|
+
const result = resolveWithChains({
|
|
117
|
+
currentModel: 'anthropic/claude-opus-4-5',
|
|
118
|
+
chainModels: ['openai/gpt-4o'],
|
|
119
|
+
});
|
|
120
|
+
expect(result).toBe('anthropic/claude-opus-4-5');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('chain is ignored when fallback disabled', () => {
|
|
124
|
+
const result = resolveWithChains({
|
|
125
|
+
currentModel: 'anthropic/claude-opus-4-5',
|
|
126
|
+
chainModels: ['openai/gpt-4o'],
|
|
127
|
+
fallbackEnabled: false,
|
|
128
|
+
});
|
|
129
|
+
// chain not applied; no effectiveArray entry → falls through to null (no _modelArray either)
|
|
130
|
+
expect(result).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('_modelArray entries take precedence and chain appends after', () => {
|
|
134
|
+
const result = resolveWithChains({
|
|
135
|
+
modelArray: [
|
|
136
|
+
{ id: 'anthropic/claude-opus-4-5' },
|
|
137
|
+
{ id: 'anthropic/claude-sonnet-4-5' },
|
|
138
|
+
],
|
|
139
|
+
chainModels: ['openai/gpt-4o'],
|
|
140
|
+
});
|
|
141
|
+
// First entry in _modelArray wins; chain only used for runtime failover
|
|
142
|
+
expect(result).toBe('anthropic/claude-opus-4-5');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('duplicate model ids across array and chain are deduplicated', () => {
|
|
146
|
+
const result = resolveWithChains({
|
|
147
|
+
modelArray: [
|
|
148
|
+
{ id: 'anthropic/claude-opus-4-5' },
|
|
149
|
+
{ id: 'openai/gpt-4o' },
|
|
150
|
+
],
|
|
151
|
+
chainModels: ['openai/gpt-4o', 'google/gemini-pro'],
|
|
152
|
+
});
|
|
153
|
+
expect(result).toBe('anthropic/claude-opus-4-5');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('no currentModel and no _modelArray with chain still resolves', () => {
|
|
157
|
+
const result = resolveWithChains({
|
|
158
|
+
chainModels: ['openai/gpt-4o', 'anthropic/claude-sonnet-4-5'],
|
|
159
|
+
});
|
|
160
|
+
expect(result).toBe('openai/gpt-4o');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('built-in provider not skipped when other providers are configured', () => {
|
|
164
|
+
// Regression test: github-copilot is auto-loaded by opencode and doesn't
|
|
165
|
+
// need an entry in opencodeConfig.provider. The resolver must not skip
|
|
166
|
+
// it in favor of a configured provider later in the chain.
|
|
167
|
+
const result = resolveWithChains({
|
|
168
|
+
currentModel: 'github-copilot/claude-opus-4.6',
|
|
169
|
+
chainModels: [
|
|
170
|
+
'github-copilot/gemini-3.1-pro-preview',
|
|
171
|
+
'zai-coding-plan/glm-5',
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
expect(result).toBe('github-copilot/claude-opus-4.6');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
getActiveRuntimePreset,
|
|
4
|
+
getPreviousRuntimePreset,
|
|
5
|
+
rollbackRuntimePreset,
|
|
6
|
+
setActiveRuntimePreset,
|
|
7
|
+
setActiveRuntimePresetWithPrevious,
|
|
8
|
+
} from './runtime-preset';
|
|
9
|
+
|
|
10
|
+
describe('runtime-preset', () => {
|
|
11
|
+
// Cleanup after each test to avoid state leakage
|
|
12
|
+
test('getActiveRuntimePreset returns null initially', () => {
|
|
13
|
+
setActiveRuntimePreset(null);
|
|
14
|
+
expect(getActiveRuntimePreset()).toBeNull();
|
|
15
|
+
setActiveRuntimePreset(null);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('setActiveRuntimePreset sets the active preset', () => {
|
|
19
|
+
setActiveRuntimePreset(null);
|
|
20
|
+
setActiveRuntimePreset('foo');
|
|
21
|
+
expect(getActiveRuntimePreset()).toBe('foo');
|
|
22
|
+
setActiveRuntimePreset(null);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('setActiveRuntimePresetWithPrevious sets active and previous', () => {
|
|
26
|
+
setActiveRuntimePreset(null);
|
|
27
|
+
setActiveRuntimePreset('old');
|
|
28
|
+
setActiveRuntimePresetWithPrevious('new');
|
|
29
|
+
expect(getActiveRuntimePreset()).toBe('new');
|
|
30
|
+
expect(getPreviousRuntimePreset()).toBe('old');
|
|
31
|
+
setActiveRuntimePreset(null);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('setActiveRuntimePresetWithPrevious with null sets previous to old', () => {
|
|
35
|
+
setActiveRuntimePreset(null);
|
|
36
|
+
setActiveRuntimePreset('old');
|
|
37
|
+
setActiveRuntimePresetWithPrevious(null);
|
|
38
|
+
expect(getActiveRuntimePreset()).toBeNull();
|
|
39
|
+
expect(getPreviousRuntimePreset()).toBe('old');
|
|
40
|
+
setActiveRuntimePreset(null);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('rollbackRuntimePreset restores active and clears previous', () => {
|
|
44
|
+
setActiveRuntimePreset(null);
|
|
45
|
+
setActiveRuntimePreset('old');
|
|
46
|
+
setActiveRuntimePresetWithPrevious('new');
|
|
47
|
+
rollbackRuntimePreset('old');
|
|
48
|
+
expect(getActiveRuntimePreset()).toBe('old');
|
|
49
|
+
expect(getPreviousRuntimePreset()).toBeNull();
|
|
50
|
+
setActiveRuntimePreset(null);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('rollbackRuntimePreset with null clears active and previous', () => {
|
|
54
|
+
setActiveRuntimePreset(null);
|
|
55
|
+
setActiveRuntimePresetWithPrevious('new');
|
|
56
|
+
rollbackRuntimePreset(null);
|
|
57
|
+
expect(getActiveRuntimePreset()).toBeNull();
|
|
58
|
+
expect(getPreviousRuntimePreset()).toBeNull();
|
|
59
|
+
setActiveRuntimePreset(null);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-level runtime preset state.
|
|
3
|
+
*
|
|
4
|
+
* Survives plugin re-inits triggered by client.config.update() →
|
|
5
|
+
* Instance.dispose(). The plugin function re-runs but this module-level
|
|
6
|
+
* variable persists within the same Node.js process.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
let activeRuntimePreset: string | null = null;
|
|
10
|
+
|
|
11
|
+
export function setActiveRuntimePreset(name: string | null): void {
|
|
12
|
+
activeRuntimePreset = name;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getActiveRuntimePreset(): string | null {
|
|
16
|
+
return activeRuntimePreset;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns the name of the previously active runtime preset (before the
|
|
21
|
+
* current one), used to compute reset diffs when switching presets.
|
|
22
|
+
*/
|
|
23
|
+
let previousRuntimePreset: string | null = null;
|
|
24
|
+
|
|
25
|
+
export function getPreviousRuntimePreset(): string | null {
|
|
26
|
+
return previousRuntimePreset;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function setActiveRuntimePresetWithPrevious(name: string | null): void {
|
|
30
|
+
previousRuntimePreset = activeRuntimePreset;
|
|
31
|
+
activeRuntimePreset = name;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function rollbackRuntimePreset(previous: string | null): void {
|
|
35
|
+
activeRuntimePreset = previous;
|
|
36
|
+
previousRuntimePreset = null;
|
|
37
|
+
}
|