mixdog 0.7.4 → 0.7.6
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/.claude-plugin/plugin.json +1 -1
- package/README.md +14 -0
- package/hooks/hooks.json +6 -6
- package/hooks/shim-launcher.cjs +51 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +2 -2
- package/setup/install.mjs +42 -15
- package/setup/mixdog-cli.mjs +131 -0
- package/setup/wizard.mjs +321 -32
package/README.md
CHANGED
|
@@ -48,6 +48,20 @@ as JSON you can diff.
|
|
|
48
48
|
|
|
49
49
|
## Install
|
|
50
50
|
|
|
51
|
+
**From npm (terminal):**
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx mixdog setup # register plugin + run the setup wizard
|
|
55
|
+
npm i -g mixdog # then launch Claude Code with mixdog pre-loaded:
|
|
56
|
+
mixdog # → claude --dangerously-load-development-channels plugin:mixdog@trib-plugin
|
|
57
|
+
mixdog --dangerously-skip-permissions # extra Claude flags pass through
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`mixdog` (no subcommand) starts Claude Code with mixdog pre-loaded; the `claude`
|
|
61
|
+
CLI must be on your PATH.
|
|
62
|
+
|
|
63
|
+
**Inside Claude Code (slash commands):**
|
|
64
|
+
|
|
51
65
|
```
|
|
52
66
|
/plugin marketplace add trib-plugin/mixdog
|
|
53
67
|
/plugin install mixdog@trib-plugin
|
package/hooks/hooks.json
CHANGED
|
@@ -7,17 +7,17 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
|
-
"command": "\"${CLAUDE_PLUGIN_ROOT}/
|
|
10
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/shim-launcher.cjs\" --part=rules",
|
|
11
11
|
"timeout": 30
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"type": "command",
|
|
15
|
-
"command": "\"${CLAUDE_PLUGIN_ROOT}/
|
|
15
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/shim-launcher.cjs\" --part=core",
|
|
16
16
|
"timeout": 130
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"type": "command",
|
|
20
|
-
"command": "\"${CLAUDE_PLUGIN_ROOT}/
|
|
20
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/shim-launcher.cjs\" --part=recap",
|
|
21
21
|
"timeout": 130
|
|
22
22
|
}
|
|
23
23
|
]
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"hooks": [
|
|
30
30
|
{
|
|
31
31
|
"type": "command",
|
|
32
|
-
"command": "\"${CLAUDE_PLUGIN_ROOT}/
|
|
32
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/shim-launcher.cjs\"",
|
|
33
33
|
"timeout": 10
|
|
34
34
|
}
|
|
35
35
|
]
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"hooks": [
|
|
42
42
|
{
|
|
43
43
|
"type": "command",
|
|
44
|
-
"command": "\"${CLAUDE_PLUGIN_ROOT}/
|
|
44
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/shim-launcher.cjs\" --kind=post-tool",
|
|
45
45
|
"timeout": 5
|
|
46
46
|
}
|
|
47
47
|
]
|
|
@@ -70,4 +70,4 @@
|
|
|
70
70
|
}
|
|
71
71
|
]
|
|
72
72
|
}
|
|
73
|
-
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const SHIM_ARCH_ALIAS = Object.freeze({
|
|
8
|
+
x64: 'x86_64',
|
|
9
|
+
arm64: 'aarch64',
|
|
10
|
+
ia32: 'i686',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function shimBinExt() {
|
|
14
|
+
return process.platform === 'win32' ? '.exe' : '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function shimArchTag() {
|
|
18
|
+
const arch = SHIM_ARCH_ALIAS[process.arch] || process.arch;
|
|
19
|
+
const os = process.platform === 'win32' ? 'windows'
|
|
20
|
+
: process.platform === 'darwin' ? 'macos'
|
|
21
|
+
: 'linux';
|
|
22
|
+
return `${os}-${arch}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveShim(pluginRoot) {
|
|
26
|
+
const ext = shimBinExt();
|
|
27
|
+
const name = 'mixdog-shim' + ext;
|
|
28
|
+
const candidates = [
|
|
29
|
+
path.join(pluginRoot, 'native', 'mixdog-shim', 'target', 'release', name),
|
|
30
|
+
path.join(pluginRoot, 'native', 'prebuilt', shimArchTag(), name),
|
|
31
|
+
];
|
|
32
|
+
for (const candidate of candidates) {
|
|
33
|
+
try {
|
|
34
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
35
|
+
} catch { /* ignore */ }
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
|
|
41
|
+
if (!pluginRoot) process.exit(0);
|
|
42
|
+
|
|
43
|
+
const shimPath = resolveShim(pluginRoot);
|
|
44
|
+
if (!shimPath) process.exit(0);
|
|
45
|
+
|
|
46
|
+
const child = spawnSync(shimPath, process.argv.slice(2), {
|
|
47
|
+
stdio: 'inherit',
|
|
48
|
+
windowsHide: true,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
process.exit(child.status ?? 0);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "Claude Code all-in-one bridge plugin: role-based bridge workers, continuous memory, and syntax-aware code editing.",
|
|
5
5
|
"author": "mixdog contributors <dev@tribgames.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"bin": {
|
|
10
10
|
"mcfg": "./setup/launch.mjs",
|
|
11
11
|
"mixdog-install": "./setup/install.mjs",
|
|
12
|
-
"mixdog": "./setup/
|
|
12
|
+
"mixdog": "./setup/mixdog-cli.mjs"
|
|
13
13
|
},
|
|
14
14
|
"engines": {
|
|
15
15
|
"bun": ">=1.1.0"
|
package/setup/install.mjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// install.mjs — one-shot bootstrapper that registers the mixdog plugin in the
|
|
3
3
|
// user's Claude Code settings so it auto-loads on the next session start.
|
|
4
4
|
//
|
|
5
|
-
// Run via: npx
|
|
6
|
-
// or: node setup/install.mjs
|
|
5
|
+
// Run via: npx mixdog setup | mixdog-install (after the package is published)
|
|
6
|
+
// or: node setup/install.mjs (from a checkout)
|
|
7
7
|
//
|
|
8
8
|
// It merges two keys into the user-scope settings file (preserving everything
|
|
9
9
|
// else that is already there):
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
} from 'node:fs';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
24
|
import { homedir } from 'node:os';
|
|
25
|
+
import { realpathSync } from 'node:fs';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
25
27
|
import { createInterface } from 'node:readline';
|
|
26
28
|
import { spawn } from 'node:child_process';
|
|
27
29
|
import { DEFAULT_MARKETPLACE, DEFAULT_PLUGIN } from '../src/shared/plugin-paths.mjs';
|
|
@@ -31,9 +33,23 @@ const PLUGIN_REF = `${DEFAULT_PLUGIN}@${DEFAULT_MARKETPLACE}`;
|
|
|
31
33
|
const REPO = 'trib-plugin/mixdog'; // github owner/repo
|
|
32
34
|
const REPO_URL = 'https://github.com/trib-plugin/mixdog';
|
|
33
35
|
|
|
36
|
+
/** Claude config root — matches Claude Code (settings + plugins tree). */
|
|
37
|
+
export function claudeConfigBaseDir() {
|
|
38
|
+
return process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
|
39
|
+
}
|
|
40
|
+
|
|
34
41
|
// Claude Code honours CLAUDE_CONFIG_DIR; otherwise the user scope is ~/.claude.
|
|
35
42
|
function settingsDir() {
|
|
36
|
-
return
|
|
43
|
+
return claudeConfigBaseDir();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function defaultPluginDataDir() {
|
|
47
|
+
return join(
|
|
48
|
+
claudeConfigBaseDir(),
|
|
49
|
+
'plugins',
|
|
50
|
+
'data',
|
|
51
|
+
`${DEFAULT_PLUGIN}-${MARKETPLACE}`,
|
|
52
|
+
);
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
function loadSettings(file) {
|
|
@@ -91,17 +107,14 @@ function registerPluginInSettings() {
|
|
|
91
107
|
console.log(`\nNext: restart Claude Code (or run /reload-plugins). mixdog loads automatically.`);
|
|
92
108
|
}
|
|
93
109
|
|
|
94
|
-
async function
|
|
110
|
+
export async function runInstall() {
|
|
95
111
|
registerPluginInSettings();
|
|
96
112
|
|
|
97
113
|
// npx / node setup/install.mjs runs outside Claude Code — config.mjs needs a data dir.
|
|
98
|
-
process.env.CLAUDE_PLUGIN_DATA
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
'data',
|
|
103
|
-
'mixdog-trib-plugin',
|
|
104
|
-
);
|
|
114
|
+
const pluginData = process.env.CLAUDE_PLUGIN_DATA;
|
|
115
|
+
if (!pluginData || !String(pluginData).trim()) {
|
|
116
|
+
process.env.CLAUDE_PLUGIN_DATA = defaultPluginDataDir();
|
|
117
|
+
}
|
|
105
118
|
|
|
106
119
|
const { runSetupWizard } = await import('./wizard.mjs');
|
|
107
120
|
await runSetupWizard();
|
|
@@ -134,7 +147,21 @@ function openRepo() {
|
|
|
134
147
|
}
|
|
135
148
|
}
|
|
136
149
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
150
|
+
function isInstallerEntry() {
|
|
151
|
+
const entry = process.argv[1];
|
|
152
|
+
if (!entry) return false;
|
|
153
|
+
try {
|
|
154
|
+
const self = realpathSync(fileURLToPath(import.meta.url));
|
|
155
|
+
const invoked = realpathSync(entry);
|
|
156
|
+
return self === invoked;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (isInstallerEntry()) {
|
|
163
|
+
runInstall().catch((err) => {
|
|
164
|
+
console.error(err?.stack || err?.message || String(err));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// mixdog-cli.mjs — `mixdog` bin dispatcher: launch Claude Code with the dev
|
|
3
|
+
// plugin load flags, or run setup/install on demand.
|
|
4
|
+
|
|
5
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { realpathSync } from 'node:fs';
|
|
9
|
+
import { constants as osConstants } from 'node:os';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { DEFAULT_MARKETPLACE, DEFAULT_PLUGIN } from '../src/shared/plugin-paths.mjs';
|
|
12
|
+
import { runInstall } from './install.mjs';
|
|
13
|
+
|
|
14
|
+
const PLUGIN_LOAD_ARG = `plugin:${DEFAULT_PLUGIN}@${DEFAULT_MARKETPLACE}`;
|
|
15
|
+
const CLAUDE_PREFIX = ['--dangerously-load-development-channels', PLUGIN_LOAD_ARG];
|
|
16
|
+
|
|
17
|
+
function isSetupCommand(first) {
|
|
18
|
+
return first === 'setup' || first === 'install';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildClaudeLaunchArgv(passthrough = []) {
|
|
22
|
+
return [...CLAUDE_PREFIX, ...passthrough];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveClaudeExecutable() {
|
|
26
|
+
const win32 = process.platform === 'win32';
|
|
27
|
+
try {
|
|
28
|
+
const cmd = win32 ? 'where' : 'which';
|
|
29
|
+
const out = execFileSync(cmd, ['claude'], { encoding: 'utf8', windowsHide: true }).trim();
|
|
30
|
+
const first = out.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
|
|
31
|
+
if (first && existsSync(first)) return first;
|
|
32
|
+
} catch { /* PATH scan */ }
|
|
33
|
+
|
|
34
|
+
const pathSep = win32 ? ';' : ':';
|
|
35
|
+
const dirs = String(process.env.PATH || '').split(pathSep).filter(Boolean);
|
|
36
|
+
const pathext = win32
|
|
37
|
+
? String(process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';').map((e) => e.toLowerCase())
|
|
38
|
+
: [''];
|
|
39
|
+
const bases = win32 ? ['claude'] : ['claude'];
|
|
40
|
+
for (const dir of dirs) {
|
|
41
|
+
for (const base of bases) {
|
|
42
|
+
if (win32) {
|
|
43
|
+
for (const ext of pathext) {
|
|
44
|
+
const candidate = join(dir, base + ext);
|
|
45
|
+
if (existsSync(candidate)) return candidate;
|
|
46
|
+
}
|
|
47
|
+
const bare = join(dir, base);
|
|
48
|
+
if (existsSync(bare)) return bare;
|
|
49
|
+
} else {
|
|
50
|
+
const candidate = join(dir, base);
|
|
51
|
+
if (existsSync(candidate)) return candidate;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function dispatchMixdogCli(argv = process.argv.slice(2)) {
|
|
59
|
+
const [first] = argv;
|
|
60
|
+
if (isSetupCommand(first)) {
|
|
61
|
+
if (process.env.MIXDOG_CLI_DRY_RUN === '1') {
|
|
62
|
+
process.stdout.write('mixdog-cli: route=setup\n');
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
await runInstall();
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const claudeArgs = buildClaudeLaunchArgv(argv);
|
|
70
|
+
if (process.env.MIXDOG_CLI_DRY_RUN === '1') {
|
|
71
|
+
process.stdout.write(`mixdog-cli: claude ${JSON.stringify(claudeArgs)}\n`);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const claudePath = resolveClaudeExecutable();
|
|
76
|
+
if (!claudePath) {
|
|
77
|
+
process.stderr.write(
|
|
78
|
+
'\n✗ `claude` was not found on PATH. Install Claude Code first: https://docs.anthropic.com/en/docs/claude-code\n',
|
|
79
|
+
);
|
|
80
|
+
return 127;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return launchClaude(claudePath, claudeArgs);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function launchClaude(claudePath, claudeArgs) {
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
const win32 = process.platform === 'win32';
|
|
89
|
+
const needsShell = win32 && /\.(cmd|bat)$/i.test(claudePath);
|
|
90
|
+
const child = spawn(claudePath, claudeArgs, {
|
|
91
|
+
stdio: 'inherit',
|
|
92
|
+
shell: needsShell,
|
|
93
|
+
windowsHide: true,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
child.on('error', (err) => {
|
|
97
|
+
process.stderr.write(`${err?.stack || err?.message || String(err)}\n`);
|
|
98
|
+
resolve(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
child.on('close', (code, signal) => {
|
|
102
|
+
if (signal) {
|
|
103
|
+
const sigNum = osConstants.signals?.[signal] ?? 0;
|
|
104
|
+
resolve(128 + sigNum);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
resolve(code ?? 0);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isDirectCliEntry() {
|
|
113
|
+
const entry = process.argv[1];
|
|
114
|
+
if (!entry) return false;
|
|
115
|
+
try {
|
|
116
|
+
const self = realpathSync(fileURLToPath(import.meta.url));
|
|
117
|
+
const invoked = realpathSync(entry);
|
|
118
|
+
return self === invoked;
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (isDirectCliEntry()) {
|
|
125
|
+
dispatchMixdogCli()
|
|
126
|
+
.then((code) => process.exit(code))
|
|
127
|
+
.catch((err) => {
|
|
128
|
+
console.error(err?.stack || err?.message || String(err));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
});
|
|
131
|
+
}
|
package/setup/wizard.mjs
CHANGED
|
@@ -6,14 +6,22 @@
|
|
|
6
6
|
* module (install.mjs does that) so src/shared/config.mjs can resolve paths.
|
|
7
7
|
*/
|
|
8
8
|
import { createInterface } from 'node:readline';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
9
10
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from 'node:fs';
|
|
10
11
|
import { join, dirname, basename } from 'node:path';
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { defaultPluginDataDir } from './install.mjs';
|
|
12
14
|
import {
|
|
13
15
|
mergeAgentConfig,
|
|
14
16
|
mergeMemoryConfig,
|
|
15
17
|
mergeConfig,
|
|
18
|
+
mergeSearchConfig,
|
|
16
19
|
} from './config-merge.mjs';
|
|
20
|
+
import { DEFAULT_MODELS } from '../src/search/lib/config.mjs';
|
|
21
|
+
|
|
22
|
+
let _linuxSecretsCapable;
|
|
23
|
+
const KEYTAR_SERVICE = 'mixdog';
|
|
24
|
+
const KEYTAR_PROBE_TIMEOUT_MS = 8000;
|
|
17
25
|
|
|
18
26
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
27
|
const REPO_ROOT = join(__dirname, '..');
|
|
@@ -32,14 +40,77 @@ const AG_API_PROVIDERS = [
|
|
|
32
40
|
|
|
33
41
|
const WORKFLOW_ROLES = ['worker', 'reviewer', 'debugger', 'tester'];
|
|
34
42
|
|
|
43
|
+
/** Raw SERP API keys (setup.html SR_KEY_PROVIDERS) — independent of active provider. */
|
|
44
|
+
const SEARCH_RAW_KEY_PROVIDERS = [
|
|
45
|
+
{ id: 'firecrawl', name: 'Firecrawl' },
|
|
46
|
+
{ id: 'tavily', name: 'Tavily' },
|
|
47
|
+
{ id: 'exa', name: 'Exa' },
|
|
48
|
+
];
|
|
49
|
+
const SEARCH_OAUTH_PROVIDERS = new Set(['anthropic-oauth', 'openai-oauth', 'grok-oauth']);
|
|
50
|
+
const OPENAI_SEARCH_EFFORT_VALUES = new Set(['low', 'medium', 'high']);
|
|
51
|
+
const SEARCH_OAUTH_ALIASES = Object.freeze({
|
|
52
|
+
'anthropic-oauth': 'anthropic-oauth',
|
|
53
|
+
anthropic: 'anthropic-oauth',
|
|
54
|
+
'openai-oauth': 'openai-oauth',
|
|
55
|
+
openai: 'openai-oauth',
|
|
56
|
+
'grok-oauth': 'grok-oauth',
|
|
57
|
+
grok: 'grok-oauth',
|
|
58
|
+
});
|
|
35
59
|
function pluginDataDir() {
|
|
36
60
|
const dir = process.env.CLAUDE_PLUGIN_DATA;
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
'CLAUDE_PLUGIN_DATA must be set before running the setup wizard (install.mjs sets it unconditionally)',
|
|
40
|
-
);
|
|
61
|
+
if (dir && typeof dir === 'string' && String(dir).trim()) {
|
|
62
|
+
return String(dir).trim();
|
|
41
63
|
}
|
|
42
|
-
return
|
|
64
|
+
return defaultPluginDataDir();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One-time Linux preflight: optional keytar must load before any secret prompt. */
|
|
68
|
+
export function resetLinuxSecretsCapableCache() {
|
|
69
|
+
_linuxSecretsCapable = undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function runLinuxKeytarOperationalProbe() {
|
|
73
|
+
const script = [
|
|
74
|
+
'try {',
|
|
75
|
+
'const keytar = require("keytar");',
|
|
76
|
+
'if (process.env.MIXDOG_KEYTAR_PROBE_INJECT_FAIL === "1") process.exit(3);',
|
|
77
|
+
`keytar.findCredentials(${JSON.stringify(KEYTAR_SERVICE)})`,
|
|
78
|
+
' .then(() => process.exit(0))',
|
|
79
|
+
' .catch((e) => { process.stderr.write(String(e && e.message ? e.message : e)); process.exit(1); });',
|
|
80
|
+
'} catch (e) { process.stderr.write(String(e && e.message ? e.message : e)); process.exit(2); }',
|
|
81
|
+
].join(' ');
|
|
82
|
+
const r = spawnSync(process.execPath, ['-e', script], {
|
|
83
|
+
env: { ...process.env },
|
|
84
|
+
encoding: 'utf8',
|
|
85
|
+
timeout: KEYTAR_PROBE_TIMEOUT_MS,
|
|
86
|
+
windowsHide: true,
|
|
87
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
88
|
+
});
|
|
89
|
+
if (r.error) return false;
|
|
90
|
+
return r.status === 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {{ treatAsLinux?: boolean }} [probeOptions] — .scratch harness only (operational probe).
|
|
95
|
+
*/
|
|
96
|
+
export function probeLinuxSecretsCapable(probeOptions = null) {
|
|
97
|
+
const isLinux = probeOptions?.treatAsLinux === true || process.platform === 'linux';
|
|
98
|
+
if (!isLinux) return true;
|
|
99
|
+
if (_linuxSecretsCapable !== undefined) return _linuxSecretsCapable;
|
|
100
|
+
_linuxSecretsCapable = runLinuxKeytarOperationalProbe();
|
|
101
|
+
return _linuxSecretsCapable;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function linuxKeychainUnavailableMessage() {
|
|
105
|
+
return [
|
|
106
|
+
'',
|
|
107
|
+
'⚠ Linux keychain unavailable (optional `keytar` not installed or libsecret missing).',
|
|
108
|
+
' Secret prompts are skipped; non-secret setup continues. Install keytar later, then use the Mixdog UI:',
|
|
109
|
+
' Debian/Ubuntu: sudo apt install libsecret-1-dev then: npm install keytar',
|
|
110
|
+
' Fedora/RHEL: sudo dnf install libsecret-devel then: npm install keytar',
|
|
111
|
+
' Arch: sudo pacman -S libsecret then: npm install keytar',
|
|
112
|
+
'',
|
|
113
|
+
].join('\n');
|
|
43
114
|
}
|
|
44
115
|
|
|
45
116
|
function sanitizeName(n) {
|
|
@@ -121,11 +192,11 @@ function readHiddenLine(prompt) {
|
|
|
121
192
|
async function loadConfigModules() {
|
|
122
193
|
const { ensureDataSeeds } = await import('../src/shared/seed.mjs');
|
|
123
194
|
const { readSection, updateSection } = await import('../src/shared/config.mjs');
|
|
124
|
-
const { DEFAULT_PRESETS } = await import('../src/agent/orchestrator/config.mjs');
|
|
195
|
+
const { DEFAULT_PRESETS, DEFAULT_MAINTENANCE } = await import('../src/agent/orchestrator/config.mjs');
|
|
125
196
|
const dataDir = pluginDataDir();
|
|
126
197
|
mkdirSync(dataDir, { recursive: true });
|
|
127
198
|
ensureDataSeeds(dataDir);
|
|
128
|
-
return { readSection, updateSection, DEFAULT_PRESETS, dataDir };
|
|
199
|
+
return { readSection, updateSection, DEFAULT_PRESETS, DEFAULT_MAINTENANCE, dataDir };
|
|
129
200
|
}
|
|
130
201
|
|
|
131
202
|
function readUserWorkflow(dataDir) {
|
|
@@ -175,18 +246,66 @@ function presetIdsFromAgent(agentSection) {
|
|
|
175
246
|
return presets.map((p) => p.id || p.name).filter(Boolean);
|
|
176
247
|
}
|
|
177
248
|
|
|
178
|
-
async function stepDiscordToken(io, { updateSection }) {
|
|
179
|
-
|
|
180
|
-
io.say('
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
249
|
+
export async function stepDiscordToken(io, { updateSection, readSection, secretsCapable = true }) {
|
|
250
|
+
const { hasStoredSecret, SECRET_ACCOUNTS, getDiscordToken } = await import('../src/shared/config.mjs');
|
|
251
|
+
io.say('\n── Step 2/9: Discord ──');
|
|
252
|
+
io.say('Bot token (keychain), application ID, and optional main channel.');
|
|
253
|
+
|
|
254
|
+
let hadStoredToken = false;
|
|
255
|
+
let token = '';
|
|
256
|
+
let enteredToken = false;
|
|
257
|
+
if (!secretsCapable) {
|
|
258
|
+
io.say('• Discord bot token: skipped (Linux keychain unavailable).');
|
|
259
|
+
} else {
|
|
260
|
+
hadStoredToken = hasStoredSecret(SECRET_ACCOUNTS.discordToken);
|
|
261
|
+
const tokenPrompt = hadStoredToken
|
|
262
|
+
? 'Discord bot token (stored, Enter=keep): '
|
|
263
|
+
: 'Discord bot token [hidden] (Enter=skip whole step): ';
|
|
264
|
+
token = (await io.askSecret(tokenPrompt)).trim();
|
|
265
|
+
enteredToken = !isSkippableAnswer(token);
|
|
266
|
+
if (!enteredToken && !hadStoredToken) {
|
|
267
|
+
io.say('• Skipped Discord setup.');
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
185
270
|
}
|
|
271
|
+
|
|
272
|
+
const channels = readSection('channels') || {};
|
|
273
|
+
const curDiscord = channels.discord && typeof channels.discord === 'object' ? channels.discord : {};
|
|
274
|
+
const curAppId = String(curDiscord.applicationId || '').trim();
|
|
275
|
+
const appIdBase = 'Application ID';
|
|
276
|
+
const appIdPrompt = curAppId
|
|
277
|
+
? `${appIdBase} (current: ${curAppId}, Enter=keep): `
|
|
278
|
+
: `${appIdBase}: `;
|
|
279
|
+
const appIdRaw = await io.ask(appIdPrompt);
|
|
280
|
+
const appIdToSet = isSkippableAnswer(appIdRaw) ? '' : String(appIdRaw).trim();
|
|
281
|
+
// The main channel is conventionally named "main" with interactive mode;
|
|
282
|
+
// only the channel ID varies, so we ask just that. Extra channels and
|
|
283
|
+
// monitor mode are configured later in the UI.
|
|
284
|
+
const chIdRaw = await io.ask('Main channel ID (Enter=skip channel): ');
|
|
285
|
+
const channelId = isSkippableAnswer(chIdRaw) ? '' : String(chIdRaw).trim();
|
|
286
|
+
const channelName = 'main';
|
|
287
|
+
const mode = 'interactive';
|
|
186
288
|
const secrets = {};
|
|
187
|
-
updateSection('channels', (current) =>
|
|
188
|
-
|
|
189
|
-
|
|
289
|
+
updateSection('channels', (current) => {
|
|
290
|
+
const payload = {};
|
|
291
|
+
const discord = {};
|
|
292
|
+
if (enteredToken) discord.token = token;
|
|
293
|
+
if (appIdToSet) discord.applicationId = appIdToSet;
|
|
294
|
+
if (Object.keys(discord).length > 0) payload.discord = discord;
|
|
295
|
+
if (channelId) {
|
|
296
|
+
const existingCfg = current.channelsConfig && typeof current.channelsConfig === 'object'
|
|
297
|
+
? { ...current.channelsConfig }
|
|
298
|
+
: {};
|
|
299
|
+
existingCfg[channelName] = { channelId, mode };
|
|
300
|
+
payload.channelsConfig = existingCfg;
|
|
301
|
+
payload.mainChannel = channelName;
|
|
302
|
+
}
|
|
303
|
+
return mergeConfig(current, payload, secrets);
|
|
304
|
+
});
|
|
305
|
+
if (enteredToken) io.say('• Discord token saved to keychain.');
|
|
306
|
+
if (appIdToSet) io.say('• Application ID saved.');
|
|
307
|
+
if (channelId) io.say(`• Main channel "${channelName}" configured (${mode}).`);
|
|
308
|
+
return enteredToken || hadStoredToken || !!getDiscordToken();
|
|
190
309
|
}
|
|
191
310
|
|
|
192
311
|
function formatVoiceProgress(p) {
|
|
@@ -221,7 +340,7 @@ async function installVoiceRuntime(dataDir, io) {
|
|
|
221
340
|
/** Mirrors setup.html channels save: `voice` via POST /config → mergeConfig. */
|
|
222
341
|
async function stepVoiceTranscription(io, ctx, discordTokenSaved) {
|
|
223
342
|
if (!discordTokenSaved) return;
|
|
224
|
-
io.say('\n── Step 2a/
|
|
343
|
+
io.say('\n── Step 2a/9: Voice transcription (음성 전사) ──');
|
|
225
344
|
io.say('Install local Speech-to-text (whisper.cpp) for Discord voice messages.');
|
|
226
345
|
const raw = await io.ask('Enable voice transcription? [y/N] (Enter=skip): ');
|
|
227
346
|
if (isSkippableAnswer(raw)) {
|
|
@@ -247,7 +366,7 @@ async function stepVoiceTranscription(io, ctx, discordTokenSaved) {
|
|
|
247
366
|
}
|
|
248
367
|
|
|
249
368
|
async function stepAddressForm(io, { updateSection, readSection }) {
|
|
250
|
-
io.say('\n── Step 1/
|
|
369
|
+
io.say('\n── Step 1/9: Address form (호칭) ──');
|
|
251
370
|
const memory = readSection('memory');
|
|
252
371
|
const curTitle = memory?.user?.title || '';
|
|
253
372
|
const curName = memory?.user?.name || '';
|
|
@@ -267,8 +386,8 @@ async function stepAddressForm(io, { updateSection, readSection }) {
|
|
|
267
386
|
io.say('• Saved memory.user (title/name).');
|
|
268
387
|
}
|
|
269
388
|
|
|
270
|
-
export async function stepWebhookReceiver(io, { updateSection, readSection }) {
|
|
271
|
-
io.say('\n── Step 3/
|
|
389
|
+
export async function stepWebhookReceiver(io, { updateSection, readSection, secretsCapable = true }) {
|
|
390
|
+
io.say('\n── Step 3/9: Inbound webhooks (ngrok receiver) ──');
|
|
272
391
|
io.say('Global webhook tunnel for inbound HTTP (channels.webhook). Per-endpoint registration is configured later in the UI.');
|
|
273
392
|
const enableRaw = await io.ask('Enable inbound webhooks? [y/N]: ');
|
|
274
393
|
if (isSkippableAnswer(enableRaw)) {
|
|
@@ -294,17 +413,27 @@ export async function stepWebhookReceiver(io, { updateSection, readSection }) {
|
|
|
294
413
|
if (!isSkippableAnswer(domainRaw)) {
|
|
295
414
|
webhook.domain = String(domainRaw).trim();
|
|
296
415
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
416
|
+
if (secretsCapable) {
|
|
417
|
+
const authPrompt = hasStoredSecret(SECRET_ACCOUNTS.webhookAuth)
|
|
418
|
+
? 'Auth Token (stored, Enter=keep): '
|
|
419
|
+
: 'ngrok Auth Token [hidden]: ';
|
|
420
|
+
webhook.authtoken = (await io.askSecret(authPrompt)).trim();
|
|
421
|
+
} else {
|
|
422
|
+
io.say('• ngrok Auth Token: skipped (Linux keychain unavailable).');
|
|
423
|
+
}
|
|
301
424
|
const secrets = {};
|
|
302
425
|
updateSection('channels', (current) => mergeConfig(current, { webhook }, secrets));
|
|
303
|
-
io.say(
|
|
426
|
+
io.say(secretsCapable
|
|
427
|
+
? '• Inbound webhook receiver saved (channels.webhook enabled/domain; authtoken in keychain).'
|
|
428
|
+
: '• Inbound webhook receiver saved (channels.webhook enabled/domain; authtoken not collected).');
|
|
304
429
|
}
|
|
305
430
|
|
|
306
|
-
async function stepProviderKeys(io, { updateSection }) {
|
|
307
|
-
io.say('\n── Step 4/
|
|
431
|
+
async function stepProviderKeys(io, { updateSection, secretsCapable = true }) {
|
|
432
|
+
io.say('\n── Step 4/9: Provider API keys ──');
|
|
433
|
+
if (!secretsCapable) {
|
|
434
|
+
io.say('• Skipped provider API keys (Linux keychain unavailable).');
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
308
437
|
io.say('Optional API keys (hidden). Enter to skip a provider.');
|
|
309
438
|
const providers = {};
|
|
310
439
|
for (const p of AG_API_PROVIDERS) {
|
|
@@ -323,7 +452,7 @@ async function stepProviderKeys(io, { updateSection }) {
|
|
|
323
452
|
}
|
|
324
453
|
|
|
325
454
|
async function stepPresets(io, { readSection, updateSection, DEFAULT_PRESETS }) {
|
|
326
|
-
io.say('\n── Step 5/
|
|
455
|
+
io.say('\n── Step 5/9: Agent presets ──');
|
|
327
456
|
const agent = readSection('agent');
|
|
328
457
|
const existing = presetIdsFromAgent(agent);
|
|
329
458
|
if (existing.length > 0) {
|
|
@@ -350,11 +479,11 @@ async function stepPresets(io, { readSection, updateSection, DEFAULT_PRESETS })
|
|
|
350
479
|
}
|
|
351
480
|
|
|
352
481
|
async function stepRolePresetMapping(io, { readSection, dataDir }) {
|
|
353
|
-
io.say('\n── Step 6/
|
|
482
|
+
io.say('\n── Step 6/9: Role → preset mapping ──');
|
|
354
483
|
const agent = readSection('agent');
|
|
355
484
|
const presetIds = presetIdsFromAgent(agent);
|
|
356
485
|
if (presetIds.length === 0) {
|
|
357
|
-
io.say('No presets on disk — run step
|
|
486
|
+
io.say('No presets on disk — run step 5 first or configure presets in the Mixdog UI later.');
|
|
358
487
|
return;
|
|
359
488
|
}
|
|
360
489
|
io.say(`Available presets: ${presetIds.join(', ')}`);
|
|
@@ -384,21 +513,179 @@ async function stepRolePresetMapping(io, { readSection, dataDir }) {
|
|
|
384
513
|
io.say('• Role → preset mapping saved to user-workflow.json.');
|
|
385
514
|
}
|
|
386
515
|
|
|
516
|
+
function resolveSearchBackendInput(raw) {
|
|
517
|
+
if (isSkippableAnswer(raw)) return null;
|
|
518
|
+
const key = String(raw).trim().toLowerCase();
|
|
519
|
+
return SEARCH_OAUTH_ALIASES[key] || null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function parseYesNo(raw) {
|
|
523
|
+
if (isSkippableAnswer(raw)) return null;
|
|
524
|
+
const v = String(raw).trim().toLowerCase();
|
|
525
|
+
if (v === 'y' || v === 'yes' || v === 'true' || v === '1') return true;
|
|
526
|
+
if (v === 'n' || v === 'no' || v === 'false' || v === '0') return false;
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Mirrors POST /search/config → mergeSearchConfig. */
|
|
531
|
+
export async function stepSearchBackend(io, { updateSection, readSection, secretsCapable = true }) {
|
|
532
|
+
io.say('\n── Step 7/9: Search backend ──');
|
|
533
|
+
io.say('Active provider: anthropic-oauth | openai-oauth | grok-oauth (OAuth — uses Agent credentials).');
|
|
534
|
+
const { hasStoredSecret, SECRET_ACCOUNTS, getSearchApiKey } = await import('../src/shared/config.mjs');
|
|
535
|
+
const search = readSection('search') || {};
|
|
536
|
+
const curProvider = String(search.provider || 'anthropic-oauth').trim() || 'anthropic-oauth';
|
|
537
|
+
const backendRaw = await io.ask(`Search provider [${curProvider}]: `);
|
|
538
|
+
let provider = curProvider;
|
|
539
|
+
if (!isSkippableAnswer(backendRaw)) {
|
|
540
|
+
const resolved = resolveSearchBackendInput(backendRaw);
|
|
541
|
+
if (!resolved) {
|
|
542
|
+
io.say(` ! Unknown provider "${String(backendRaw).trim()}" — keeping ${curProvider}.`);
|
|
543
|
+
} else if (!SEARCH_OAUTH_PROVIDERS.has(resolved)) {
|
|
544
|
+
io.say(` ! Provider "${resolved}" is not a supported OAuth backend — keeping ${curProvider}.`);
|
|
545
|
+
} else {
|
|
546
|
+
provider = resolved;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const payload = {};
|
|
551
|
+
if (provider !== curProvider) payload.provider = provider;
|
|
552
|
+
|
|
553
|
+
const searchProviders = {};
|
|
554
|
+
if (secretsCapable) {
|
|
555
|
+
for (const p of SEARCH_RAW_KEY_PROVIDERS) {
|
|
556
|
+
const hadKey = hasStoredSecret(SECRET_ACCOUNTS.searchApiKey(p.id));
|
|
557
|
+
const keyPrompt = hadKey
|
|
558
|
+
? `${p.name} API key (stored, Enter=keep): `
|
|
559
|
+
: `${p.name} API key [hidden] (Enter=skip): `;
|
|
560
|
+
const key = (await io.askSecret(keyPrompt)).trim();
|
|
561
|
+
if (!isSkippableAnswer(key)) {
|
|
562
|
+
searchProviders[p.id] = key;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
} else {
|
|
566
|
+
io.say('• Search provider API keys: skipped (Linux keychain unavailable).');
|
|
567
|
+
}
|
|
568
|
+
if (Object.keys(searchProviders).length) payload.searchProviders = searchProviders;
|
|
569
|
+
|
|
570
|
+
if (provider === 'openai-oauth') {
|
|
571
|
+
const curModel = (search.models && search.models.openai)
|
|
572
|
+
|| DEFAULT_MODELS.openai
|
|
573
|
+
|| '';
|
|
574
|
+
const modelRaw = await io.ask(`OpenAI model [${curModel}] (Enter=keep): `);
|
|
575
|
+
if (!isSkippableAnswer(modelRaw)) {
|
|
576
|
+
const model = String(modelRaw).trim();
|
|
577
|
+
if (model) payload.models = { ...(payload.models || {}), openai: model };
|
|
578
|
+
}
|
|
579
|
+
const curEffort = String(search.modelOptions?.openai?.effort || 'medium').trim() || 'medium';
|
|
580
|
+
const effortRaw = await io.ask(`OpenAI effort (low/medium/high) [${curEffort}] (Enter=keep): `);
|
|
581
|
+
if (!isSkippableAnswer(effortRaw)) {
|
|
582
|
+
const effort = String(effortRaw).trim().toLowerCase();
|
|
583
|
+
if (!OPENAI_SEARCH_EFFORT_VALUES.has(effort)) {
|
|
584
|
+
io.say(` ! Unknown effort "${effortRaw.trim()}" — keeping ${curEffort}.`);
|
|
585
|
+
} else {
|
|
586
|
+
const openaiOpts = { ...(search.modelOptions?.openai || {}), effort };
|
|
587
|
+
payload.modelOptions = { ...(payload.modelOptions || {}), openai: openaiOpts };
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const curFast = !!search.modelOptions?.openai?.fast;
|
|
591
|
+
const fastRaw = await io.ask(`OpenAI Fast mode [${curFast ? 'y' : 'N'}] (y/N, Enter=keep): `);
|
|
592
|
+
const fastParsed = parseYesNo(fastRaw);
|
|
593
|
+
if (fastParsed === undefined && !isSkippableAnswer(fastRaw)) {
|
|
594
|
+
io.say(' ! Fast mode: answer y or n — keeping current.');
|
|
595
|
+
} else if (fastParsed !== null) {
|
|
596
|
+
const base = payload.modelOptions?.openai || search.modelOptions?.openai || {};
|
|
597
|
+
const openaiOpts = { ...base };
|
|
598
|
+
if (fastParsed) openaiOpts.fast = true;
|
|
599
|
+
else delete openaiOpts.fast;
|
|
600
|
+
// Write even an empty object: mergeSearchConfig treats an empty per-family
|
|
601
|
+
// entry as "clear this family", which is how an explicit Fast=n drops a
|
|
602
|
+
// lone fast flag (no effort left to keep the object non-empty).
|
|
603
|
+
payload.modelOptions = { ...(payload.modelOptions || {}), openai: openaiOpts };
|
|
604
|
+
}
|
|
605
|
+
} else if (provider === 'grok-oauth') {
|
|
606
|
+
const curModel = (search.models && search.models.xai)
|
|
607
|
+
|| DEFAULT_MODELS.xai
|
|
608
|
+
|| '';
|
|
609
|
+
const modelRaw = await io.ask(`xAI model [${curModel}] (Enter=keep): `);
|
|
610
|
+
if (!isSkippableAnswer(modelRaw)) {
|
|
611
|
+
const model = String(modelRaw).trim();
|
|
612
|
+
if (model) payload.models = { ...(payload.models || {}), xai: model };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const secrets = {};
|
|
617
|
+
updateSection('search', (current) => mergeSearchConfig(current, payload, secrets));
|
|
618
|
+
const after = readSection('search') || {};
|
|
619
|
+
const savedProvider = after.provider || curProvider;
|
|
620
|
+
io.say(`• Search provider: ${savedProvider}.`);
|
|
621
|
+
for (const p of SEARCH_RAW_KEY_PROVIDERS) {
|
|
622
|
+
io.say(`• ${p.name} API key: ${getSearchApiKey(p.id) ? 'stored' : 'not set'}.`);
|
|
623
|
+
}
|
|
624
|
+
if (savedProvider === 'openai-oauth' && after.models?.openai) {
|
|
625
|
+
io.say(`• OpenAI model: ${after.models.openai}.`);
|
|
626
|
+
}
|
|
627
|
+
if (savedProvider === 'grok-oauth' && after.models?.xai) {
|
|
628
|
+
io.say(`• xAI model: ${after.models.xai}.`);
|
|
629
|
+
}
|
|
630
|
+
if (savedProvider === 'openai-oauth' && after.modelOptions?.openai?.effort) {
|
|
631
|
+
io.say(`• OpenAI effort: ${after.modelOptions.openai.effort}.`);
|
|
632
|
+
}
|
|
633
|
+
if (savedProvider === 'openai-oauth' && after.modelOptions?.openai?.fast) {
|
|
634
|
+
io.say('• OpenAI Fast mode: on.');
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** Mirrors POST /agent/maintenance for the explore slot. */
|
|
639
|
+
export async function stepExplorerPreset(io, { readSection, updateSection, DEFAULT_PRESETS, DEFAULT_MAINTENANCE }) {
|
|
640
|
+
io.say('\n── Step 8/9: Explorer model (explore tool) ──');
|
|
641
|
+
const agent = readSection('agent') || {};
|
|
642
|
+
const presetIds = presetIdsFromAgent(agent);
|
|
643
|
+
const validIds = new Set([
|
|
644
|
+
...presetIds,
|
|
645
|
+
...DEFAULT_PRESETS.map((p) => p.id).filter(Boolean),
|
|
646
|
+
]);
|
|
647
|
+
const curExplore = String(agent.maintenance?.explore || DEFAULT_MAINTENANCE.explore || 'haiku').trim() || 'haiku';
|
|
648
|
+
if (validIds.size > 0) {
|
|
649
|
+
io.say(`Available presets: ${[...validIds].join(', ')}`);
|
|
650
|
+
}
|
|
651
|
+
const raw = await io.ask(`Preset for explorer (explore tool) [${curExplore}]: `);
|
|
652
|
+
if (isSkippableAnswer(raw)) {
|
|
653
|
+
io.say(`• Explorer preset unchanged (${curExplore}).`);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const preset = String(raw).trim();
|
|
657
|
+
if (!validIds.has(preset)) {
|
|
658
|
+
io.say(` ! Unknown preset "${preset}" — keeping ${curExplore}.`);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
updateSection('agent', (current) => ({
|
|
662
|
+
...current,
|
|
663
|
+
maintenance: { ...(current.maintenance || {}), explore: preset },
|
|
664
|
+
}));
|
|
665
|
+
io.say(`• Explorer maintenance preset: ${preset}.`);
|
|
666
|
+
}
|
|
667
|
+
|
|
387
668
|
/**
|
|
388
669
|
* @param {object} [ioOverride]
|
|
389
670
|
* @param {boolean} [ioOverride.interactive]
|
|
390
671
|
* @param {(prompt:string)=>Promise<string>} [ioOverride.ask]
|
|
391
672
|
* @param {(prompt:string)=>Promise<string>} [ioOverride.askSecret]
|
|
392
673
|
* @param {(line:string)=>void} [ioOverride.say]
|
|
674
|
+
* @param {object} [options]
|
|
675
|
+
* @param {boolean} [options.secretsCapable] — override Linux keytar preflight (tests).
|
|
393
676
|
*/
|
|
394
|
-
export async function runSetupWizard(ioOverride = null) {
|
|
677
|
+
export async function runSetupWizard(ioOverride = null, options = {}) {
|
|
395
678
|
const io = ioOverride ? { ...defaultIo(), ...ioOverride } : defaultIo();
|
|
396
679
|
if (!io.interactive) return { skipped: true };
|
|
397
680
|
|
|
681
|
+
const secretsCapable = options.secretsCapable ?? probeLinuxSecretsCapable();
|
|
682
|
+
if (!secretsCapable) io.say(linuxKeychainUnavailableMessage());
|
|
683
|
+
|
|
398
684
|
io.say('\nMixdog setup wizard — configure before opening Claude Code.');
|
|
399
685
|
io.say('Press Enter on any step to skip it.\n');
|
|
400
686
|
|
|
401
687
|
const ctx = await loadConfigModules();
|
|
688
|
+
ctx.secretsCapable = secretsCapable;
|
|
402
689
|
try {
|
|
403
690
|
await stepAddressForm(io, ctx);
|
|
404
691
|
const discordSaved = await stepDiscordToken(io, ctx);
|
|
@@ -407,6 +694,8 @@ export async function runSetupWizard(ioOverride = null) {
|
|
|
407
694
|
await stepProviderKeys(io, ctx);
|
|
408
695
|
await stepPresets(io, ctx);
|
|
409
696
|
await stepRolePresetMapping(io, ctx);
|
|
697
|
+
await stepSearchBackend(io, ctx);
|
|
698
|
+
await stepExplorerPreset(io, ctx);
|
|
410
699
|
io.say('\n✓ Wizard complete. Restart Claude Code (or /reload-plugins) to load mixdog.');
|
|
411
700
|
} catch (err) {
|
|
412
701
|
io.say(`\n✗ Wizard error: ${err?.message || err}`);
|