mixdog 0.7.14 → 0.7.16
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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/hooks/session-start.cjs +10 -2
- package/hooks/shim-launcher.cjs +15 -1
- package/lib/keychain-cjs.cjs +32 -5
- package/lib/plugin-paths.cjs +10 -2
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +1 -1
- package/setup/config-merge.mjs +1 -8
- package/setup/launch-core.mjs +184 -73
- package/setup/locate-claude.mjs +22 -4
- package/setup/setup-server.mjs +37 -21
- package/src/agent/orchestrator/config.mjs +7 -11
- package/src/agent/orchestrator/tools/builtin/path-utils.mjs +23 -0
- package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +36 -10
- package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +13 -4
- package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +11 -1
- package/src/agent/orchestrator/tools/graph-manifest.json +7 -7
- package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +11 -1
- package/src/agent/orchestrator/tools/patch-manifest.json +7 -7
- package/src/channels/index.mjs +10 -1
- package/src/channels/lib/executor.mjs +59 -24
- package/src/channels/lib/hook-pipe-server.mjs +8 -1
- package/src/memory/lib/runtime-fetcher.mjs +19 -3
- package/src/search/lib/web-tools.mjs +13 -1
- package/src/shared/config.mjs +26 -4
- package/src/shared/open-url.mjs +35 -10
- package/src/shared/user-cwd.mjs +14 -2
- package/src/shared/wsl.mjs +35 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolvePluginData } from '../../shared/plugin-paths.mjs';
|
|
2
|
-
import { readSection, updateSection, getAgentApiKey } from '../../shared/config.mjs';
|
|
2
|
+
import { readSection, updateSection, getAgentApiKey, AGENT_PROVIDER_ENV } from '../../shared/config.mjs';
|
|
3
3
|
import { OPENAI_COMPAT_PRESETS } from './providers/openai-compat.mjs';
|
|
4
4
|
import { hasAnthropicOAuthCredentials } from './providers/anthropic-oauth.mjs';
|
|
5
5
|
import { hasOpenAIOAuthCredentials } from './providers/openai-oauth.mjs';
|
|
@@ -10,13 +10,9 @@ import { hasGrokOAuthCredentials } from './providers/grok-oauth.mjs';
|
|
|
10
10
|
export function getPluginData() {
|
|
11
11
|
return resolvePluginData();
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
gemini: 'GEMINI_API_KEY',
|
|
17
|
-
deepseek: 'DEEPSEEK_API_KEY',
|
|
18
|
-
xai: 'XAI_API_KEY',
|
|
19
|
-
};
|
|
13
|
+
// First-class agent API-key providers: imported from the shared SSOT
|
|
14
|
+
// (src/shared/config.mjs) so default-config and overlay paths cannot drift
|
|
15
|
+
// from the env names the runtime key loader (getAgentApiKey) actually uses.
|
|
20
16
|
// Canonical maintenance defaults. Single source of truth — imported by
|
|
21
17
|
// llm/index.mjs and setup-server.mjs so UI/runtime cannot drift from config.
|
|
22
18
|
//
|
|
@@ -73,7 +69,7 @@ export const DEFAULT_PRESETS = Object.freeze([
|
|
|
73
69
|
function buildDefaultConfig() {
|
|
74
70
|
const providers = {};
|
|
75
71
|
// API providers — enabled if env key exists
|
|
76
|
-
for (const [name, envKey] of Object.entries(
|
|
72
|
+
for (const [name, envKey] of Object.entries(AGENT_PROVIDER_ENV)) {
|
|
77
73
|
const apiKey = process.env[envKey];
|
|
78
74
|
providers[name] = {
|
|
79
75
|
enabled: !!apiKey,
|
|
@@ -138,11 +134,11 @@ export function loadConfig() {
|
|
|
138
134
|
// Provider API keys live in the OS keychain (std env / MIXDOG_AGENT_*
|
|
139
135
|
// -> keychain), never plaintext in config. Overlay them so the
|
|
140
136
|
// provider clients see config.apiKey populated.
|
|
141
|
-
//
|
|
137
|
+
// AGENT_PROVIDER_ENV covers first-class key providers; OPENAI_COMPAT_PRESETS
|
|
142
138
|
// covers compat providers (opencode-go, …) whose key also lives in
|
|
143
139
|
// the keychain. Without the union, a compat provider with a valid
|
|
144
140
|
// stored key still ships 'no-key' → 401.
|
|
145
|
-
for (const name of new Set([...Object.keys(
|
|
141
|
+
for (const name of new Set([...Object.keys(AGENT_PROVIDER_ENV), ...Object.keys(OPENAI_COMPAT_PRESETS)])) {
|
|
146
142
|
const kc = getAgentApiKey(name);
|
|
147
143
|
if (kc) mergedProviders[name] = { ...(mergedProviders[name] || {}), apiKey: kc, enabled: true };
|
|
148
144
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from 'os';
|
|
2
2
|
import { isAbsolute, relative, resolve } from 'path';
|
|
3
3
|
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { isWSL } from '../../../../shared/wsl.mjs';
|
|
4
5
|
|
|
5
6
|
// Restore the on-disk casing of a path (win32 only). rg relativizes candidate
|
|
6
7
|
// paths against its process cwd with a CASE-SENSITIVE prefix strip before
|
|
@@ -28,6 +29,21 @@ export function posixPathToWindowsPath(posixPath) {
|
|
|
28
29
|
return posixPath;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
// Reverse of posixPathToWindowsPath, for WSL: a Windows-HOST drive path that
|
|
33
|
+
// arrives from the host (e.g. `C:\Users\x` or `C:/Users/x`) is unreachable as
|
|
34
|
+
// a literal under WSL — the drive surfaces at `/mnt/<letter>/...`. Map
|
|
35
|
+
// `<drive>:` → `/mnt/<lowercase-drive>` and flip backslashes to slashes. Only
|
|
36
|
+
// drive-letter paths are touched; anything else is returned unchanged.
|
|
37
|
+
export function windowsPathToPosixPath(winPath) {
|
|
38
|
+
if (typeof winPath !== 'string') return winPath;
|
|
39
|
+
const m = winPath.match(/^([a-zA-Z]):[\\/](.*)$/);
|
|
40
|
+
if (m) return `/mnt/${m[1].toLowerCase()}/${m[2].replace(/\\/g, '/')}`;
|
|
41
|
+
// Bare `C:` with no separator → drive root.
|
|
42
|
+
const root = winPath.match(/^([a-zA-Z]):$/);
|
|
43
|
+
if (root) return `/mnt/${root[1].toLowerCase()}/`;
|
|
44
|
+
return winPath;
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
export function normalizeInputPath(p) {
|
|
32
48
|
if (typeof p !== 'string') return p;
|
|
33
49
|
// Trim leading/trailing whitespace — LLMs intermittently emit paths with
|
|
@@ -49,6 +65,13 @@ export function normalizeInputPath(p) {
|
|
|
49
65
|
if (looksPosixDrive || looksCygdrive || looksWsl || looksUnc) {
|
|
50
66
|
out = posixPathToWindowsPath(out);
|
|
51
67
|
}
|
|
68
|
+
} else if (isWSL()) {
|
|
69
|
+
// Reverse direction: a Windows-style drive path from the host maps to
|
|
70
|
+
// the /mnt/<letter> mount. Native-Linux behavior is untouched (gated
|
|
71
|
+
// on isWSL()); a path already in /mnt/... POSIX form is left as-is.
|
|
72
|
+
if (/^[a-zA-Z]:([\\/]|$)/.test(out)) {
|
|
73
|
+
out = windowsPathToPosixPath(out);
|
|
74
|
+
}
|
|
52
75
|
}
|
|
53
76
|
try { out = out.normalize('NFC'); } catch {}
|
|
54
77
|
return out;
|
|
@@ -712,7 +712,11 @@ export function adoptForegroundShellJob({ command, cwd, pid, timeoutMs, mergeStd
|
|
|
712
712
|
}
|
|
713
713
|
|
|
714
714
|
function _startBackgroundShellJobImpl({ command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, shellArg, shellType, clientHostPid }) {
|
|
715
|
-
|
|
715
|
+
// Route ANY PowerShell shell to the PS wrapper, regardless of platform.
|
|
716
|
+
// Gating on win32 sent shell:'powershell' on macOS/Linux down the POSIX
|
|
717
|
+
// path below, which spawns `pwsh <wrapper>.sh` — pwsh then tries to run a
|
|
718
|
+
// bash script. isPowerShellShell already matches pwsh/powershell by stem.
|
|
719
|
+
if (isPowerShellShell(shell, shellType)) {
|
|
716
720
|
return startBackgroundPowerShellJob({ command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, clientHostPid });
|
|
717
721
|
}
|
|
718
722
|
|
|
@@ -738,9 +742,11 @@ function _startBackgroundShellJobImpl({ command, timeoutMs, workDir, mergeStderr
|
|
|
738
742
|
// only the setTimeout below enforced; a mixdog restart between spawn
|
|
739
743
|
// and deadline would orphan the runaway. --preserve-status keeps the
|
|
740
744
|
// user command's exit code on success; on timeout the wrapper exits 124.
|
|
741
|
-
// `timeout` ships with GNU coreutils on Linux
|
|
742
|
-
//
|
|
743
|
-
//
|
|
745
|
+
// `timeout` ships with GNU coreutils on Linux. On macOS, Homebrew
|
|
746
|
+
// coreutils installs it as `gtimeout` (the un-prefixed name is NOT created
|
|
747
|
+
// by default), so the wrapper picks `timeout` if present else `gtimeout`.
|
|
748
|
+
// When neither exists it falls through to the inner command (the parent
|
|
749
|
+
// setTimeout still calls refreshShellJob to clean up).
|
|
744
750
|
const userCmdQuoted = shellQuoteSingle(command);
|
|
745
751
|
// P2 fix: invoke the resolved shell (not bash -c) so zsh / dash /
|
|
746
752
|
// alternate shells run snapshot-aware commands correctly. Drop
|
|
@@ -762,7 +768,7 @@ function _startBackgroundShellJobImpl({ command, timeoutMs, workDir, mergeStderr
|
|
|
762
768
|
// status for processes that actually exited 0. `rm -- "$0"` removes
|
|
763
769
|
// the staged wrapper .cmd.sh after donePath is published so a host
|
|
764
770
|
// crash before this point still leaves the file for the sweep to GC.
|
|
765
|
-
const wrapped = `{ if command -v timeout >/dev/null 2>&1; then touch ${shellQuoteSingle(enforcedPath)};
|
|
771
|
+
const wrapped = `{ if command -v timeout >/dev/null 2>&1; then _to=timeout; elif command -v gtimeout >/dev/null 2>&1; then _to=gtimeout; else _to=; fi; if [ -n "$_to" ]; then touch ${shellQuoteSingle(enforcedPath)}; "$_to" ${timeoutSeconds} ${innerShellQ} ${innerArgQ} ${userCmdQuoted}; else ${innerShellQ} ${innerArgQ} ${userCmdQuoted}; fi; rc=$?; printf '%s' "$rc" > ${shellQuoteSingle(exitPath)}; touch ${shellQuoteSingle(donePath)}; rm -- "$0" 2>/dev/null; exit $rc; }`;
|
|
766
772
|
// Stage the wrapped command to a .sh and let the script open its own
|
|
767
773
|
// output files via `exec > … 2> …`. The parent does NOT pass file
|
|
768
774
|
// descriptors via stdio inheritance (`stdio: 'ignore'` for all three).
|
|
@@ -841,9 +847,24 @@ function startBackgroundPowerShellJob({ command, timeoutMs, workDir, mergeStderr
|
|
|
841
847
|
'$code = 1',
|
|
842
848
|
'try {',
|
|
843
849
|
" $argList = @('-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', $encoded)",
|
|
844
|
-
|
|
850
|
+
// -WindowStyle is a Windows-only Start-Process parameter; pwsh on
|
|
851
|
+
// macOS/Linux throws "not supported on this platform". Add it only on win32.
|
|
852
|
+
' $spArgs = @{ FilePath = $exe; ArgumentList = $argList; RedirectStandardOutput = $stdoutPath; RedirectStandardError = $stderrPath; PassThru = $true }',
|
|
853
|
+
' if ($IsWindows -or $null -eq $IsWindows) { $spArgs[\'WindowStyle\'] = \'Hidden\' }',
|
|
854
|
+
' $p = Start-Process @spArgs',
|
|
845
855
|
' if ($timeoutMs -gt 0 -and -not $p.WaitForExit($timeoutMs)) {',
|
|
846
|
-
|
|
856
|
+
// Kill the whole process TREE, not just the direct child: Start-Process
|
|
857
|
+
// launches an intermediate pwsh that spawns the user command, so
|
|
858
|
+
// Stop-Process on $p.Id alone orphans the grandchildren. On Windows use
|
|
859
|
+
// `taskkill /T /F` (tree-terminate); on macOS/Linux pwsh, fall back to
|
|
860
|
+
// Stop-Process (no taskkill there).
|
|
861
|
+
' try {',
|
|
862
|
+
' if ($IsWindows -or $null -eq $IsWindows) {',
|
|
863
|
+
' & taskkill.exe /pid $p.Id /t /f 2>$null | Out-Null',
|
|
864
|
+
' } else {',
|
|
865
|
+
' try { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue } catch {}',
|
|
866
|
+
' }',
|
|
867
|
+
' } catch {}',
|
|
847
868
|
' $code = 124',
|
|
848
869
|
' } else {',
|
|
849
870
|
' try { $p.WaitForExit() } catch {}',
|
|
@@ -875,9 +896,14 @@ function startBackgroundPowerShellJob({ command, timeoutMs, workDir, mergeStderr
|
|
|
875
896
|
}
|
|
876
897
|
|
|
877
898
|
const shellStem = basename(String(shell || '')).toLowerCase().replace(/\.exe$/, '');
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
899
|
+
// `-WindowStyle Hidden` is a Windows-only CLI switch; pwsh on macOS/Linux
|
|
900
|
+
// rejects it. `-ExecutionPolicy` likewise only applies to Windows
|
|
901
|
+
// PowerShell. Build args per-platform so cross-OS pwsh background jobs run.
|
|
902
|
+
const isWin = process.platform === 'win32';
|
|
903
|
+
const wrapperArgs = ['-NoLogo', '-NoProfile', '-NonInteractive'];
|
|
904
|
+
if (isWin) wrapperArgs.push('-WindowStyle', 'Hidden');
|
|
905
|
+
if (isWin && shellStem === 'powershell') wrapperArgs.push('-ExecutionPolicy', 'Bypass');
|
|
906
|
+
wrapperArgs.push('-File', wrappedTempPath);
|
|
881
907
|
// Spawn the staged wrapper directly. detached MUST be false on Windows:
|
|
882
908
|
// a native pwsh launched with detached:true + stdio:'ignore' exits
|
|
883
909
|
// immediately without running -File (verified — the detached child dies
|
|
@@ -19,10 +19,15 @@ function shellTypeFor(shell) {
|
|
|
19
19
|
|
|
20
20
|
function shellSpec(shell, shellType = shellTypeFor(shell)) {
|
|
21
21
|
if (shellType === 'powershell') {
|
|
22
|
+
// `-WindowStyle Hidden` is a Windows-only switch. PowerShell Core
|
|
23
|
+
// (pwsh) on macOS/Linux rejects it, so only append it on win32.
|
|
24
|
+
const psArgs = ['-NoLogo', '-NoProfile', '-NonInteractive'];
|
|
25
|
+
if (process.platform === 'win32') psArgs.push('-WindowStyle', 'Hidden');
|
|
26
|
+
psArgs.push('-Command');
|
|
22
27
|
return {
|
|
23
28
|
shell,
|
|
24
29
|
shellArg: '-Command',
|
|
25
|
-
shellArgs:
|
|
30
|
+
shellArgs: psArgs,
|
|
26
31
|
shellType,
|
|
27
32
|
};
|
|
28
33
|
}
|
|
@@ -60,8 +65,10 @@ function resolveWindowsPowerShell() {
|
|
|
60
65
|
|
|
61
66
|
export function resolveShell() {
|
|
62
67
|
if (_resolvedShell) return _resolvedShell;
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
// Gate on the actual platform, NOT WINDIR/SystemRoot env presence: under
|
|
69
|
+
// WSL those vars can be inherited via interop while process.platform is
|
|
70
|
+
// 'linux', and WSL must resolve /bin/sh — not Windows PowerShell.
|
|
71
|
+
const isWindows = process.platform === 'win32';
|
|
65
72
|
if (!isWindows) {
|
|
66
73
|
_resolvedShell = shellSpec('/bin/sh', 'posix');
|
|
67
74
|
return _resolvedShell;
|
|
@@ -76,7 +83,9 @@ export function resolveShell() {
|
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
function _isWindows() {
|
|
79
|
-
|
|
86
|
+
// Real-platform check only (see resolveShell): env presence would make
|
|
87
|
+
// WSL (process.platform 'linux') mis-resolve to Windows Git Bash.
|
|
88
|
+
return process.platform === 'win32';
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
// Resolve Git Bash on Windows. Strategy (invariant-based, no silent fallback):
|
|
@@ -118,7 +118,17 @@ export function ensureGraphBinary(dataDir) {
|
|
|
118
118
|
const pkey = platformKey();
|
|
119
119
|
const asset = manifest.assets?.[pkey];
|
|
120
120
|
if (!asset || !asset.url || !asset.sha256) {
|
|
121
|
-
|
|
121
|
+
// Unsupported platform/arch (e.g. win32-arm64): the manifest has no
|
|
122
|
+
// downloadable asset for this {os}-{arch}. The code graph has NO JS
|
|
123
|
+
// parsing fallback, so this is terminal — surface a single clear,
|
|
124
|
+
// actionable message instead of a cryptic crash downstream.
|
|
125
|
+
const supported = Object.keys(manifest.assets || {}).join(', ') || '(none)';
|
|
126
|
+
throw new Error(
|
|
127
|
+
`[graph-fetcher] no prebuilt mixdog-graph binary for platform ${pkey} `
|
|
128
|
+
+ `(unsupported platform/arch — there is no JS parsing fallback). `
|
|
129
|
+
+ `Supported platforms: ${supported}. `
|
|
130
|
+
+ `Build it locally: cargo build --release in native/mixdog-graph.`,
|
|
131
|
+
);
|
|
122
132
|
}
|
|
123
133
|
const version = String(manifest.version || '0');
|
|
124
134
|
const dir = graphBinDir(dataDir);
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.7.
|
|
2
|
+
"version": "0.7.16",
|
|
3
3
|
"_comment": "Rewritten by .github/workflows/graph-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-graph binary on the GitHub release. A local cargo build under native/mixdog-graph/target/release always takes precedence at runtime. (v0.5.236 entries were filled manually after CI's commit step hit detached HEAD; the workflow now checks out ref: main so future releases self-update.)",
|
|
4
4
|
"assets": {
|
|
5
5
|
"darwin-arm64": {
|
|
6
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
6
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-graph-darwin-arm64",
|
|
7
7
|
"sha256": "53f45f8373000fb55ccb49f9b03af0275ca2a663f924b68ee8ded4535e14445a"
|
|
8
8
|
},
|
|
9
9
|
"darwin-x64": {
|
|
10
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
10
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-graph-darwin-x64",
|
|
11
11
|
"sha256": "347dc5e1b39dfc33ae07e01319b085c1f1ee775a70be1ac09f7b0d2954b36591"
|
|
12
12
|
},
|
|
13
13
|
"linux-arm64": {
|
|
14
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
14
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-graph-linux-arm64",
|
|
15
15
|
"sha256": "7648be2c4bc89bef837b4843639a9ab38c382ba103518ad52d4439587521542f"
|
|
16
16
|
},
|
|
17
17
|
"linux-x64": {
|
|
18
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
18
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-graph-linux-x64",
|
|
19
19
|
"sha256": "6e5a1b548c69c3a51170eb11c769985be7ca7029ede2597b19304c8dc106eded"
|
|
20
20
|
},
|
|
21
21
|
"win32-x64": {
|
|
22
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
23
|
-
"sha256": "
|
|
22
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-graph-win32-x64.exe",
|
|
23
|
+
"sha256": "6e53cdf523871f818942678d95e2235105b939ceeafc56a3b83b8190eeeba026"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -107,7 +107,17 @@ export function ensurePatchBinary(dataDir) {
|
|
|
107
107
|
const pkey = platformKey();
|
|
108
108
|
const asset = manifest.assets?.[pkey];
|
|
109
109
|
if (!asset || !asset.url || !asset.sha256) {
|
|
110
|
-
|
|
110
|
+
// Unsupported platform/arch (e.g. win32-arm64): the manifest has no
|
|
111
|
+
// downloadable asset for this {os}-{arch}. apply_patch is native-only
|
|
112
|
+
// (no JS apply fallback), so this is terminal — surface a single clear,
|
|
113
|
+
// actionable message instead of a cryptic crash downstream.
|
|
114
|
+
const supported = Object.keys(manifest.assets || {}).join(', ') || '(none)';
|
|
115
|
+
throw new Error(
|
|
116
|
+
`[patch-fetcher] no prebuilt mixdog-patch binary for platform ${pkey} `
|
|
117
|
+
+ `(unsupported platform/arch — apply_patch is native-only, no JS apply fallback). `
|
|
118
|
+
+ `Supported platforms: ${supported}. `
|
|
119
|
+
+ `Build it locally: cargo build --release in native/mixdog-patch.`,
|
|
120
|
+
);
|
|
111
121
|
}
|
|
112
122
|
const version = String(manifest.version || '0');
|
|
113
123
|
const dir = patchBinDir(dataDir);
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.7.
|
|
2
|
+
"version": "0.7.16",
|
|
3
3
|
"_comment": "Rewritten by .github/workflows/patch-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-patch binary on the GitHub release. A local cargo build under native/mixdog-patch/target/release always takes precedence; otherwise the binary is fetched per this manifest into the data dir (apply is native-only — no JS apply engine).",
|
|
4
4
|
"assets": {
|
|
5
5
|
"darwin-arm64": {
|
|
6
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
6
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-patch-darwin-arm64",
|
|
7
7
|
"sha256": "836a0b60a443b0a6a8c1bbe24d15a79ed70ee92c2f0fbc05374c4e9ed2536415"
|
|
8
8
|
},
|
|
9
9
|
"darwin-x64": {
|
|
10
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
10
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-patch-darwin-x64",
|
|
11
11
|
"sha256": "cab10c4e1e8b72d3958241dffdff764712ed74f4861d105bafa8258961215c98"
|
|
12
12
|
},
|
|
13
13
|
"linux-arm64": {
|
|
14
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
14
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-patch-linux-arm64",
|
|
15
15
|
"sha256": "a90c32ce3417a7d853f2723f82f3613cf2cd030fe885cf710cfc9f8e4b193264"
|
|
16
16
|
},
|
|
17
17
|
"linux-x64": {
|
|
18
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
18
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-patch-linux-x64",
|
|
19
19
|
"sha256": "0fea40ab98acd35bfb47515756024d1882a2abbaddce8a0b51642d20ac405577"
|
|
20
20
|
},
|
|
21
21
|
"win32-x64": {
|
|
22
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
23
|
-
"sha256": "
|
|
22
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.16/mixdog-patch-win32-x64.exe",
|
|
23
|
+
"sha256": "81fbabab9aa53f5fa57b1691b5ab24cbc1cf6ac1c2a36482c336411abe452d14"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
}
|
package/src/channels/index.mjs
CHANGED
|
@@ -1826,12 +1826,21 @@ function wireWebhookHandlers() {
|
|
|
1826
1826
|
// partial `silent_to_agent` semantics that still audit to Discord.
|
|
1827
1827
|
const raw = String(text);
|
|
1828
1828
|
if (/^\s*(?:\[[^\]\n]+\]\s*)*\[meta:silent\]/.test(raw)) return;
|
|
1829
|
+
// Deterministic findings-count drop. Code-review handlers emit a
|
|
1830
|
+
// structured `[[findings:N]]` token (N = number of issues). The RELAY —
|
|
1831
|
+
// not the worker's prose — decides: N==0 => clean review, drop entirely
|
|
1832
|
+
// (no Lead inject, no Discord forward). Token absent => fail-safe forward
|
|
1833
|
+
// so a real finding is never silently dropped if the worker omits it.
|
|
1834
|
+
const fc = raw.match(/\[\[findings:(\d+)\]\]/i);
|
|
1835
|
+
if (fc && Number(fc[1]) === 0) return;
|
|
1829
1836
|
// Lifecycle pings (started / iter echoes, marked silent_to_agent) are
|
|
1830
1837
|
// channel noise for an automated webhook review — drop them entirely so
|
|
1831
1838
|
// a skip stays fully silent and only the final answer reaches the
|
|
1832
1839
|
// channel. The final [meta:silent] skip result is already dropped above.
|
|
1833
1840
|
if (meta?.silent_to_agent === true) return;
|
|
1834
|
-
|
|
1841
|
+
// Strip the verdict token before surfacing (findings present, N>0).
|
|
1842
|
+
const surfaced = raw.replace(/\[\[findings:\d+\]\]/gi, "").replace(/[^\S\n]{2,}/g, " ").trim();
|
|
1843
|
+
injectAndRecord(channelId, label, surfaced || raw, {
|
|
1835
1844
|
type: "webhook",
|
|
1836
1845
|
instruction,
|
|
1837
1846
|
});
|
|
@@ -130,30 +130,65 @@ function runScript(name, scriptName, onResult) {
|
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
132
|
const ext = extname(scriptName).toLowerCase();
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
133
|
+
// Pick interpreter candidates. `python3` does not exist on a default
|
|
134
|
+
// Windows install — the Python launcher `py` (and often `python`) does —
|
|
135
|
+
// so on win32 try py → python → python3, falling through on ENOENT.
|
|
136
|
+
// POSIX keeps python3 → python.
|
|
137
|
+
let candidates;
|
|
138
|
+
if (ext === ".py") {
|
|
139
|
+
candidates = process.platform === "win32"
|
|
140
|
+
? ["py", "python", "python3"]
|
|
141
|
+
: ["python3", "python"];
|
|
142
|
+
} else {
|
|
143
|
+
candidates = ["node"];
|
|
144
|
+
}
|
|
145
|
+
// onResult MUST fire exactly once across the whole candidate chain. A failed
|
|
146
|
+
// ENOENT spawn emits BOTH 'error' (→ we advance) AND 'close', so guard the
|
|
147
|
+
// final callback at the chain level and mark each attempt that advanced so
|
|
148
|
+
// its own 'close' is ignored.
|
|
149
|
+
let resultSent = false;
|
|
150
|
+
const finish = (out, code) => {
|
|
151
|
+
if (resultSent) return;
|
|
152
|
+
resultSent = true;
|
|
153
|
+
onResult(out, code);
|
|
154
|
+
};
|
|
155
|
+
const trySpawn = (idx) => {
|
|
156
|
+
const cmd = candidates[idx];
|
|
157
|
+
let advanced = false;
|
|
158
|
+
const proc = spawn(cmd, [scriptPath], {
|
|
159
|
+
timeout: 3e4,
|
|
160
|
+
env: { ...process.env },
|
|
161
|
+
windowsHide: true
|
|
162
|
+
});
|
|
163
|
+
let stdout = "";
|
|
164
|
+
let stderr = "";
|
|
165
|
+
if (proc.stdout) proc.stdout.on("data", (d) => {
|
|
166
|
+
stdout += d;
|
|
167
|
+
});
|
|
168
|
+
if (proc.stderr) proc.stderr.on("data", (d) => {
|
|
169
|
+
stderr += d;
|
|
170
|
+
});
|
|
171
|
+
proc.on("close", (code) => {
|
|
172
|
+
// This attempt ENOENT'd and handed off to the next candidate — its
|
|
173
|
+
// 'close' is spurious and must not report a result.
|
|
174
|
+
if (advanced) return;
|
|
175
|
+
if (code !== 0) {
|
|
176
|
+
logEvent(`${name}: script exited ${code}: ${stderr.substring(0, 500)}`);
|
|
177
|
+
}
|
|
178
|
+
finish(stdout.substring(0, 2e3), code);
|
|
179
|
+
});
|
|
180
|
+
proc.on("error", (err) => {
|
|
181
|
+
// Interpreter not found → try the next candidate before giving up.
|
|
182
|
+
if (err.code === "ENOENT" && idx + 1 < candidates.length) {
|
|
183
|
+
advanced = true;
|
|
184
|
+
trySpawn(idx + 1);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
logEvent(`${name}: script spawn error: ${err.message}`);
|
|
188
|
+
finish("", null);
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
trySpawn(0);
|
|
157
192
|
}
|
|
158
193
|
export {
|
|
159
194
|
applyParser,
|
|
@@ -35,7 +35,14 @@ const {
|
|
|
35
35
|
// net.createServer().listen() accepts both transparently.
|
|
36
36
|
const PIPE_PATH = moduleRequire('../../../lib/hook-pipe-path.cjs')()
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// Honor MIXDOG_RUNTIME_ROOT consistently with runtime-paths.mjs (the consumer
|
|
39
|
+
// of these tool-exec signals): when the override is set, the signal PRODUCER
|
|
40
|
+
// here must write into the same root the channels worker watches, or signals
|
|
41
|
+
// are silently dropped. Default stays tmpdir()/mixdog so non-override installs
|
|
42
|
+
// are unchanged.
|
|
43
|
+
const RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT
|
|
44
|
+
? pathResolve(process.env.MIXDOG_RUNTIME_ROOT)
|
|
45
|
+
: join(tmpdir(), 'mixdog')
|
|
39
46
|
const SIGNAL_CONSUMER_MARKER = join(RUNTIME_ROOT, '.tool-exec-consumer')
|
|
40
47
|
const SUBAGENT_SIGNAL_CONSUMER_MARKER = join(RUNTIME_ROOT, '.tool-exec-subagent-consumer')
|
|
41
48
|
const SIGNAL_RE_GENERIC = /^tool-exec-\d+-[0-9a-f]+\.signal$/
|
|
@@ -341,13 +341,29 @@ export async function ensureRuntime(dataDir) {
|
|
|
341
341
|
const pkey = platformKey()
|
|
342
342
|
const asset = manifest.assets?.[pkey]
|
|
343
343
|
if (!asset) {
|
|
344
|
+
// Platform/arch absent from the manifest entirely (e.g. win32-arm64).
|
|
345
|
+
// The memory PG runtime cannot start here; fail with a single clear,
|
|
346
|
+
// actionable message. The memory worker's init().catch reports this as
|
|
347
|
+
// degraded and the rest of mixdog (agent, tools) keeps working without
|
|
348
|
+
// memory.
|
|
349
|
+
const supported = Object.keys(manifest.assets || {})
|
|
350
|
+
.filter((k) => isUsableAsset(manifest.assets[k]))
|
|
351
|
+
.join(', ') || '(none)'
|
|
344
352
|
throw new Error(
|
|
345
|
-
`[runtime-fetcher]
|
|
346
|
-
`
|
|
353
|
+
`[runtime-fetcher] memory runtime not available on ${pkey}: ` +
|
|
354
|
+
`no runtime asset for this platform/arch in the manifest. ` +
|
|
355
|
+
`Supported: ${supported}. ` +
|
|
356
|
+
`Memory is disabled on this platform; the rest of mixdog continues to work.`
|
|
347
357
|
)
|
|
348
358
|
}
|
|
349
359
|
if (!isUsableAsset(asset)) {
|
|
350
|
-
|
|
360
|
+
// Platform/arch present but explicitly marked unsupported or carrying a
|
|
361
|
+
// placeholder/TBD payload (e.g. linux-arm64). Same graceful-degrade path.
|
|
362
|
+
throw new Error(
|
|
363
|
+
`[runtime-fetcher] memory runtime not available on ${pkey}: ` +
|
|
364
|
+
`this platform/arch is marked unsupported (no validated runtime asset). ` +
|
|
365
|
+
`Memory is disabled on this platform; the rest of mixdog continues to work.`
|
|
366
|
+
)
|
|
351
367
|
}
|
|
352
368
|
|
|
353
369
|
const { url, sha256, size } = asset
|
|
@@ -6,6 +6,7 @@ import { Agent, fetch as undiciFetch } from 'undici'
|
|
|
6
6
|
import { JSDOM } from 'jsdom'
|
|
7
7
|
import puppeteer from 'puppeteer-core'
|
|
8
8
|
import { Readability } from '@mozilla/readability'
|
|
9
|
+
import { isWSL } from '../../shared/wsl.mjs'
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
const PKG_VERSION = (() => { try { return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version } catch { return '0.0.1' } })()
|
|
@@ -38,13 +39,24 @@ const COMMON_BROWSER_PATHS = (() => {
|
|
|
38
39
|
].filter(Boolean)
|
|
39
40
|
}
|
|
40
41
|
if (platform === 'linux') {
|
|
41
|
-
|
|
42
|
+
// Native-Linux Chromium/Chrome binaries first. The /mnt/c Windows .exe
|
|
43
|
+
// entries are reachable from WSL's filesystem but puppeteer-core CANNOT
|
|
44
|
+
// drive a Windows GUI browser as a Linux child process (CDP over a pipe to
|
|
45
|
+
// a Win32 binary launched from the Linux ABI does not work), so advertising
|
|
46
|
+
// puppeteer-available off a Windows .exe yields launch failures at runtime.
|
|
47
|
+
// Only offer the Windows .exe fallbacks on plain Linux (Wine/dual-mount
|
|
48
|
+
// edge cases), never under WSL.
|
|
49
|
+
const linuxNative = [
|
|
42
50
|
'/usr/bin/google-chrome',
|
|
43
51
|
'/usr/bin/google-chrome-stable',
|
|
44
52
|
'/usr/bin/chromium',
|
|
45
53
|
'/usr/bin/chromium-browser',
|
|
46
54
|
'/snap/bin/chromium',
|
|
47
55
|
'/usr/bin/microsoft-edge',
|
|
56
|
+
]
|
|
57
|
+
if (isWSL()) return linuxNative
|
|
58
|
+
return [
|
|
59
|
+
...linuxNative,
|
|
48
60
|
'/mnt/c/Program Files/Google/Chrome/Application/chrome.exe',
|
|
49
61
|
'/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe',
|
|
50
62
|
'/mnt/c/Program Files/Microsoft/Edge/Application/msedge.exe',
|
package/src/shared/config.mjs
CHANGED
|
@@ -297,16 +297,19 @@ export function getSearchApiKey(provider) {
|
|
|
297
297
|
|
|
298
298
|
// Standard provider env names take precedence so existing OPENAI_API_KEY-style
|
|
299
299
|
// exports keep working, then MIXDOG_AGENT_<P>_APIKEY, then the OS keychain.
|
|
300
|
-
|
|
300
|
+
// SSOT for agent API-key providers: setup-server.mjs and config-merge.mjs import
|
|
301
|
+
// this so UI key-presence detection uses the exact same predicate the runtime
|
|
302
|
+
// loads from. Add a provider here and every path picks it up.
|
|
303
|
+
export const AGENT_PROVIDER_ENV = Object.freeze({
|
|
301
304
|
openai: 'OPENAI_API_KEY', anthropic: 'ANTHROPIC_API_KEY', gemini: 'GEMINI_API_KEY',
|
|
302
|
-
deepseek: 'DEEPSEEK_API_KEY', xai: 'XAI_API_KEY',
|
|
305
|
+
deepseek: 'DEEPSEEK_API_KEY', xai: 'XAI_API_KEY', 'opencode-go': 'OPENCODE_API_KEY',
|
|
303
306
|
})
|
|
304
307
|
|
|
305
308
|
// Last-resort env aliases honored AFTER the standard env / MIXDOG_AGENT_* /
|
|
306
309
|
// keychain sources. GROK_API_KEY is the established xAI alias elsewhere in the
|
|
307
310
|
// repo (search discovery, xai-api/grok-oauth backends), so honoring it here
|
|
308
311
|
// keeps provider discovery and dispatch resolving the same credential.
|
|
309
|
-
const AGENT_PROVIDER_ENV_ALIASES = Object.freeze({
|
|
312
|
+
export const AGENT_PROVIDER_ENV_ALIASES = Object.freeze({
|
|
310
313
|
xai: ['GROK_API_KEY'],
|
|
311
314
|
})
|
|
312
315
|
|
|
@@ -331,7 +334,26 @@ export function getAgentApiKey(provider) {
|
|
|
331
334
|
* Never writes to mixdog-config.json.
|
|
332
335
|
*/
|
|
333
336
|
export function saveSecret(account, value) {
|
|
334
|
-
|
|
337
|
+
try {
|
|
338
|
+
_setSecret(account, value)
|
|
339
|
+
} catch (err) {
|
|
340
|
+
// On WSL/headless Linux (and any host missing a usable keychain backend,
|
|
341
|
+
// e.g. keytar/libsecret not installed or no running Secret Service) the OS
|
|
342
|
+
// write throws a cryptic backend error. Surface an actionable message that
|
|
343
|
+
// points at the env-var read path the getters already honor, instead of
|
|
344
|
+
// silently writing the plaintext secret to mixdog-config.json.
|
|
345
|
+
const envKey = _envKey(account)
|
|
346
|
+
const agentMatch = String(account || '').match(/^agent\.([^.]+)\.apiKey$/)
|
|
347
|
+
const stdEnv = agentMatch ? AGENT_PROVIDER_ENV[agentMatch[1]] : null
|
|
348
|
+
const envHint = stdEnv ? `${stdEnv} (or ${envKey})` : envKey
|
|
349
|
+
const e = new Error(
|
|
350
|
+
`[config] could not save secret to the OS keychain: ${err && err.message ? err.message : err}\n` +
|
|
351
|
+
` No usable keychain backend on this host (common on WSL / headless Linux without libsecret).\n` +
|
|
352
|
+
` Set the ${envHint} environment variable instead — the runtime reads it directly.`
|
|
353
|
+
)
|
|
354
|
+
e.cause = err
|
|
355
|
+
throw e
|
|
356
|
+
}
|
|
335
357
|
}
|
|
336
358
|
|
|
337
359
|
export function deleteSecret(account) {
|