mixdog 0.7.5 → 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 +112 -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,9 +6,11 @@
|
|
|
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,
|
|
@@ -17,6 +19,10 @@ import {
|
|
|
17
19
|
} from './config-merge.mjs';
|
|
18
20
|
import { DEFAULT_MODELS } from '../src/search/lib/config.mjs';
|
|
19
21
|
|
|
22
|
+
let _linuxSecretsCapable;
|
|
23
|
+
const KEYTAR_SERVICE = 'mixdog';
|
|
24
|
+
const KEYTAR_PROBE_TIMEOUT_MS = 8000;
|
|
25
|
+
|
|
20
26
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
27
|
const REPO_ROOT = join(__dirname, '..');
|
|
22
28
|
const DEFAULT_USER_WORKFLOW = JSON.parse(
|
|
@@ -52,12 +58,59 @@ const SEARCH_OAUTH_ALIASES = Object.freeze({
|
|
|
52
58
|
});
|
|
53
59
|
function pluginDataDir() {
|
|
54
60
|
const dir = process.env.CLAUDE_PLUGIN_DATA;
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
'CLAUDE_PLUGIN_DATA must be set before running the setup wizard (install.mjs sets it unconditionally)',
|
|
58
|
-
);
|
|
61
|
+
if (dir && typeof dir === 'string' && String(dir).trim()) {
|
|
62
|
+
return String(dir).trim();
|
|
59
63
|
}
|
|
60
|
-
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');
|
|
61
114
|
}
|
|
62
115
|
|
|
63
116
|
function sanitizeName(n) {
|
|
@@ -193,20 +246,27 @@ function presetIdsFromAgent(agentSection) {
|
|
|
193
246
|
return presets.map((p) => p.id || p.name).filter(Boolean);
|
|
194
247
|
}
|
|
195
248
|
|
|
196
|
-
export async function stepDiscordToken(io, { updateSection, readSection }) {
|
|
249
|
+
export async function stepDiscordToken(io, { updateSection, readSection, secretsCapable = true }) {
|
|
197
250
|
const { hasStoredSecret, SECRET_ACCOUNTS, getDiscordToken } = await import('../src/shared/config.mjs');
|
|
198
251
|
io.say('\n── Step 2/9: Discord ──');
|
|
199
252
|
io.say('Bot token (keychain), application ID, and optional main channel.');
|
|
200
253
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
}
|
|
210
270
|
}
|
|
211
271
|
|
|
212
272
|
const channels = readSection('channels') || {};
|
|
@@ -326,7 +386,7 @@ async function stepAddressForm(io, { updateSection, readSection }) {
|
|
|
326
386
|
io.say('• Saved memory.user (title/name).');
|
|
327
387
|
}
|
|
328
388
|
|
|
329
|
-
export async function stepWebhookReceiver(io, { updateSection, readSection }) {
|
|
389
|
+
export async function stepWebhookReceiver(io, { updateSection, readSection, secretsCapable = true }) {
|
|
330
390
|
io.say('\n── Step 3/9: Inbound webhooks (ngrok receiver) ──');
|
|
331
391
|
io.say('Global webhook tunnel for inbound HTTP (channels.webhook). Per-endpoint registration is configured later in the UI.');
|
|
332
392
|
const enableRaw = await io.ask('Enable inbound webhooks? [y/N]: ');
|
|
@@ -353,17 +413,27 @@ export async function stepWebhookReceiver(io, { updateSection, readSection }) {
|
|
|
353
413
|
if (!isSkippableAnswer(domainRaw)) {
|
|
354
414
|
webhook.domain = String(domainRaw).trim();
|
|
355
415
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
+
}
|
|
360
424
|
const secrets = {};
|
|
361
425
|
updateSection('channels', (current) => mergeConfig(current, { webhook }, secrets));
|
|
362
|
-
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).');
|
|
363
429
|
}
|
|
364
430
|
|
|
365
|
-
async function stepProviderKeys(io, { updateSection }) {
|
|
431
|
+
async function stepProviderKeys(io, { updateSection, secretsCapable = true }) {
|
|
366
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
|
+
}
|
|
367
437
|
io.say('Optional API keys (hidden). Enter to skip a provider.');
|
|
368
438
|
const providers = {};
|
|
369
439
|
for (const p of AG_API_PROVIDERS) {
|
|
@@ -458,7 +528,7 @@ function parseYesNo(raw) {
|
|
|
458
528
|
}
|
|
459
529
|
|
|
460
530
|
/** Mirrors POST /search/config → mergeSearchConfig. */
|
|
461
|
-
export async function stepSearchBackend(io, { updateSection, readSection }) {
|
|
531
|
+
export async function stepSearchBackend(io, { updateSection, readSection, secretsCapable = true }) {
|
|
462
532
|
io.say('\n── Step 7/9: Search backend ──');
|
|
463
533
|
io.say('Active provider: anthropic-oauth | openai-oauth | grok-oauth (OAuth — uses Agent credentials).');
|
|
464
534
|
const { hasStoredSecret, SECRET_ACCOUNTS, getSearchApiKey } = await import('../src/shared/config.mjs');
|
|
@@ -481,15 +551,19 @@ export async function stepSearchBackend(io, { updateSection, readSection }) {
|
|
|
481
551
|
if (provider !== curProvider) payload.provider = provider;
|
|
482
552
|
|
|
483
553
|
const searchProviders = {};
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
+
}
|
|
492
564
|
}
|
|
565
|
+
} else {
|
|
566
|
+
io.say('• Search provider API keys: skipped (Linux keychain unavailable).');
|
|
493
567
|
}
|
|
494
568
|
if (Object.keys(searchProviders).length) payload.searchProviders = searchProviders;
|
|
495
569
|
|
|
@@ -597,15 +671,21 @@ export async function stepExplorerPreset(io, { readSection, updateSection, DEFAU
|
|
|
597
671
|
* @param {(prompt:string)=>Promise<string>} [ioOverride.ask]
|
|
598
672
|
* @param {(prompt:string)=>Promise<string>} [ioOverride.askSecret]
|
|
599
673
|
* @param {(line:string)=>void} [ioOverride.say]
|
|
674
|
+
* @param {object} [options]
|
|
675
|
+
* @param {boolean} [options.secretsCapable] — override Linux keytar preflight (tests).
|
|
600
676
|
*/
|
|
601
|
-
export async function runSetupWizard(ioOverride = null) {
|
|
677
|
+
export async function runSetupWizard(ioOverride = null, options = {}) {
|
|
602
678
|
const io = ioOverride ? { ...defaultIo(), ...ioOverride } : defaultIo();
|
|
603
679
|
if (!io.interactive) return { skipped: true };
|
|
604
680
|
|
|
681
|
+
const secretsCapable = options.secretsCapable ?? probeLinuxSecretsCapable();
|
|
682
|
+
if (!secretsCapable) io.say(linuxKeychainUnavailableMessage());
|
|
683
|
+
|
|
605
684
|
io.say('\nMixdog setup wizard — configure before opening Claude Code.');
|
|
606
685
|
io.say('Press Enter on any step to skip it.\n');
|
|
607
686
|
|
|
608
687
|
const ctx = await loadConfigModules();
|
|
688
|
+
ctx.secretsCapable = secretsCapable;
|
|
609
689
|
try {
|
|
610
690
|
await stepAddressForm(io, ctx);
|
|
611
691
|
const discordSaved = await stepDiscordToken(io, ctx);
|