mixdog 0.7.14 → 0.7.15
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/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 +140 -73
- package/setup/setup-server.mjs +15 -21
- package/src/agent/orchestrator/config.mjs +7 -11
- package/src/agent/orchestrator/tools/graph-manifest.json +7 -7
- package/src/agent/orchestrator/tools/patch-manifest.json +7 -7
- package/src/channels/index.mjs +10 -1
- package/src/shared/config.mjs +6 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.15",
|
|
4
4
|
"description": "Claude Code all-in-one agent plugin — autonomous agents, continuous memory, cost-aware sub-agents, and syntax-aware code editing.",
|
|
5
5
|
"hooks": "./hooks/hooks.json",
|
|
6
6
|
"mcpServers": {
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.15",
|
|
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",
|
package/setup/config-merge.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getSearchApiKey,
|
|
13
13
|
getAgentApiKey,
|
|
14
14
|
diagnoseDiscordTokenValue,
|
|
15
|
+
AGENT_PROVIDER_ENV,
|
|
15
16
|
} from '../src/shared/config.mjs';
|
|
16
17
|
|
|
17
18
|
const SUPPORTED_PROVIDERS = [
|
|
@@ -19,14 +20,6 @@ const SUPPORTED_PROVIDERS = [
|
|
|
19
20
|
'tavily', 'firecrawl', 'exa',
|
|
20
21
|
];
|
|
21
22
|
|
|
22
|
-
const AGENT_PROVIDER_ENV = Object.freeze({
|
|
23
|
-
openai: 'OPENAI_API_KEY',
|
|
24
|
-
anthropic: 'ANTHROPIC_API_KEY',
|
|
25
|
-
gemini: 'GEMINI_API_KEY',
|
|
26
|
-
deepseek: 'DEEPSEEK_API_KEY',
|
|
27
|
-
xai: 'XAI_API_KEY',
|
|
28
|
-
});
|
|
29
|
-
|
|
30
23
|
function envSecretPresent(account) {
|
|
31
24
|
const key = 'MIXDOG_' + String(account || '').replace(/[.\s]+/g, '_').toUpperCase();
|
|
32
25
|
if (process.env[key]) return true;
|
package/setup/launch-core.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// is derived inside launchConfigUi().
|
|
15
15
|
|
|
16
16
|
import { spawn, execSync } from 'child_process';
|
|
17
|
-
import { openSync, closeSync, readFileSync, writeSync, writeFileSync, appendFileSync } from 'fs';
|
|
17
|
+
import { openSync, closeSync, readFileSync, writeSync, writeFileSync, appendFileSync, statSync, unlinkSync } from 'fs';
|
|
18
18
|
import { join, dirname } from 'path';
|
|
19
19
|
import { fileURLToPath } from 'url';
|
|
20
20
|
import http from 'http';
|
|
@@ -122,6 +122,110 @@ function sleep(ms) {
|
|
|
122
122
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// Cross-process launch mutex. Only ONE launcher runs the version-check →
|
|
126
|
+
// reclaim critical section at a time. Now that prewarm takes over every
|
|
127
|
+
// session, two concurrent session starts can both observe the same stale
|
|
128
|
+
// server; without serialization the slower one kills the faster one's
|
|
129
|
+
// freshly-spawned correct server. Exclusive-create lock file + stale-takeover
|
|
130
|
+
// (a holder that crashed mid-launch leaves a lock no older than STALE_MS).
|
|
131
|
+
// Best-effort: on timeout or an unexpected fs error we proceed lock-free
|
|
132
|
+
// rather than deadlock the launcher.
|
|
133
|
+
const LAUNCH_LOCK_PATH = join(tmpdir(), `mixdog-setup-launch-${PORT}.lock`);
|
|
134
|
+
const LAUNCH_LOCK_STALE_MS = 30000;
|
|
135
|
+
|
|
136
|
+
async function acquireLaunchLock(trace, timeoutMs = 20000) {
|
|
137
|
+
const deadline = Date.now() + timeoutMs;
|
|
138
|
+
while (Date.now() <= deadline) {
|
|
139
|
+
try {
|
|
140
|
+
const fd = openSync(LAUNCH_LOCK_PATH, 'wx'); // exclusive create — EEXIST if held
|
|
141
|
+
writeSync(fd, `${process.pid} ${new Date().toISOString()}\n`);
|
|
142
|
+
closeSync(fd);
|
|
143
|
+
return true;
|
|
144
|
+
} catch (e) {
|
|
145
|
+
if (e?.code !== 'EEXIST') { trace(`launch-lock: unexpected ${e?.code || e} → proceeding lock-free`); return false; }
|
|
146
|
+
let stale = false;
|
|
147
|
+
try { stale = (Date.now() - statSync(LAUNCH_LOCK_PATH).mtimeMs) > LAUNCH_LOCK_STALE_MS; } catch {}
|
|
148
|
+
if (stale) { trace(`launch-lock: stale lock → reclaiming`); try { unlinkSync(LAUNCH_LOCK_PATH); } catch {} continue; }
|
|
149
|
+
await sleep(200);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
trace(`launch-lock: wait timeout → proceeding lock-free`);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function releaseLaunchLock() {
|
|
157
|
+
try { unlinkSync(LAUNCH_LOCK_PATH); } catch {}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Reclaim PORT from a version-mismatched setup-server: resolve the LISTENER
|
|
161
|
+
// pid, kill it (tree on win32, process-group on unix), wait for the port to
|
|
162
|
+
// free, then spawn the current-version server window-free. Throws LaunchError
|
|
163
|
+
// if the port cannot be reclaimed. Caller MUST hold the launch lock so two
|
|
164
|
+
// launchers never race the kill+respawn.
|
|
165
|
+
async function reclaimPort(pluginRoot, pluginData, remoteRoot, trace) {
|
|
166
|
+
let stalePid = null;
|
|
167
|
+
let pidResolveError = null;
|
|
168
|
+
let killError = null;
|
|
169
|
+
try {
|
|
170
|
+
if (process.platform === 'win32') {
|
|
171
|
+
// -State Listen is REQUIRED: Get-NetTCPConnection -LocalPort returns the
|
|
172
|
+
// LISTENING socket AND every ESTABLISHED/TIME_WAIT connection to it, so a
|
|
173
|
+
// bare `-First 1` can resolve a client/proxy PID (or 0) instead of the
|
|
174
|
+
// server — taskkill then misses the real owner and the port never frees.
|
|
175
|
+
const out = execSync(
|
|
176
|
+
`powershell -NoProfile -Command "(Get-NetTCPConnection -LocalPort ${PORT} -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess"`,
|
|
177
|
+
{ encoding: 'utf8', timeout: 3000, windowsHide: true },
|
|
178
|
+
).trim();
|
|
179
|
+
const n = parseInt(out, 10);
|
|
180
|
+
if (Number.isFinite(n) && n > 0) stalePid = n;
|
|
181
|
+
} else {
|
|
182
|
+
const out = execSync(`lsof -ti :${PORT} -sTCP:LISTEN`, { encoding: 'utf8', timeout: 3000 }).trim();
|
|
183
|
+
const n = parseInt(out.split('\n')[0], 10);
|
|
184
|
+
if (Number.isFinite(n) && n > 0) stalePid = n;
|
|
185
|
+
}
|
|
186
|
+
} catch (e) { pidResolveError = e; }
|
|
187
|
+
if (stalePid !== null) {
|
|
188
|
+
try {
|
|
189
|
+
if (process.platform === 'win32') {
|
|
190
|
+
// /T kills the whole process tree — the setup-server spawns child
|
|
191
|
+
// browser/server descendants that survive a PID-only Stop-Process.
|
|
192
|
+
execSync(`taskkill /T /F /PID ${stalePid}`, { timeout: 3000, windowsHide: true });
|
|
193
|
+
} else {
|
|
194
|
+
// Detached setup-server is its own process-group leader; the negative
|
|
195
|
+
// PID signals the group, reaping descendants a bare kill would orphan.
|
|
196
|
+
try { execSync(`kill -KILL -${stalePid}`, { timeout: 3000 }); }
|
|
197
|
+
catch { execSync(`kill -KILL ${stalePid}`, { timeout: 3000 }); }
|
|
198
|
+
}
|
|
199
|
+
} catch (e) { killError = e; }
|
|
200
|
+
}
|
|
201
|
+
// Poll until the port stops answering (250ms ticks, 5000ms deadline — a hair
|
|
202
|
+
// longer than the kill's own 3000ms timeout so a slow OS port release is not
|
|
203
|
+
// misread as a kill failure).
|
|
204
|
+
const killDeadline = Date.now() + 5000;
|
|
205
|
+
let portFree = false;
|
|
206
|
+
while (Date.now() < killDeadline) {
|
|
207
|
+
await sleep(250);
|
|
208
|
+
portFree = !(await ping(300));
|
|
209
|
+
if (portFree) break;
|
|
210
|
+
}
|
|
211
|
+
if (!portFree) {
|
|
212
|
+
const errorDetails = [
|
|
213
|
+
pidResolveError && ` PID resolution failure: ${pidResolveError?.message || String(pidResolveError)}`,
|
|
214
|
+
killError && ` kill failure: ${killError?.message || String(killError)}`,
|
|
215
|
+
].filter(Boolean).join('\n');
|
|
216
|
+
throw new LaunchError(
|
|
217
|
+
`Port ${PORT} is in use by a different mixdog plugin instance and could not be reclaimed (kill of PID ${stalePid ?? 'unknown'} failed or timed out).\n` +
|
|
218
|
+
` expected plugin root: ${pluginRoot}\n` +
|
|
219
|
+
` actual plugin root: ${remoteRoot}\n` +
|
|
220
|
+
(errorDetails ? errorDetails + '\n' : '') +
|
|
221
|
+
`Stop the other setup-server (or change PORT) and retry.\n`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
trace(`reclaimPort: killed stalePid=${stalePid}, port freed, spawning current server`);
|
|
225
|
+
await spawnServerWithLog(pluginRoot, pluginData, { openOnStart: false });
|
|
226
|
+
trace(`reclaimPort: spawnServerWithLog completed`);
|
|
227
|
+
}
|
|
228
|
+
|
|
125
229
|
async function waitForServer(timeoutMs = 4000, shouldStop = () => false) {
|
|
126
230
|
const deadline = Date.now() + timeoutMs;
|
|
127
231
|
let ready = false;
|
|
@@ -386,22 +490,13 @@ export async function launchConfigUi(options = {}) {
|
|
|
386
490
|
const alive = await aliveProbe();
|
|
387
491
|
__trace(`aliveProbe result alive=${alive}`);
|
|
388
492
|
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
__trace(`prewarm: server already alive → no-op`);
|
|
397
|
-
return CONFIG_UI_URL;
|
|
398
|
-
}
|
|
399
|
-
__trace(`prewarm: !alive → spawnServerWithLog (window-free)`);
|
|
400
|
-
await spawnServerWithLog(pluginRoot, pluginData, { openOnStart: false });
|
|
401
|
-
__trace(`prewarm: spawnServerWithLog completed`);
|
|
402
|
-
return CONFIG_UI_URL;
|
|
403
|
-
}
|
|
404
|
-
|
|
493
|
+
// Singleton / current-version invariant is enforced below for BOTH prewarm
|
|
494
|
+
// and interactive launches: prewarm no longer early-returns on a live
|
|
495
|
+
// server, so a stale old-version setup-server left running across a plugin
|
|
496
|
+
// update is taken over and replaced window-free at session start — instead
|
|
497
|
+
// of lingering until a manual /mixdog:config (the recurrence we hit). PORT
|
|
498
|
+
// is a fixed constant, so exactly one setup-server can own it; reclaiming a
|
|
499
|
+
// version-mismatched owner is the correct singleton behaviour even here.
|
|
405
500
|
if (!alive) {
|
|
406
501
|
// Boot window-free; the unconditional requestOpen below owns opening the
|
|
407
502
|
// window. Spawning with openOnStart:1 here would double-open and races a
|
|
@@ -425,68 +520,40 @@ export async function launchConfigUi(options = {}) {
|
|
|
425
520
|
const actual = normalize(remoteRoot);
|
|
426
521
|
__trace(`compare expected=${expected} actual=${actual} mismatch=${expected !== actual}`);
|
|
427
522
|
if (expected !== actual) {
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
523
|
+
// Serialize the reclaim across processes: prewarm now takes over every
|
|
524
|
+
// session, so two concurrent launchers can both observe this stale
|
|
525
|
+
// server. Under the lock the recheck is authoritative — whoever acquires
|
|
526
|
+
// first reclaims, and the next acquirer re-reads a now-current server and
|
|
527
|
+
// skips, so a freshly-spawned correct server is never killed.
|
|
528
|
+
const locked = await acquireLaunchLock(__trace);
|
|
434
529
|
try {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
{ encoding: 'utf8', timeout: 3000, windowsHide: true },
|
|
439
|
-
).trim();
|
|
440
|
-
const n = parseInt(out, 10);
|
|
441
|
-
if (Number.isFinite(n) && n > 0) stalePid = n;
|
|
530
|
+
const recheckRoot = await fetchPluginPath();
|
|
531
|
+
if (recheckRoot && normalize(recheckRoot) !== expected) {
|
|
532
|
+
await reclaimPort(pluginRoot, pluginData, recheckRoot, __trace);
|
|
442
533
|
} else {
|
|
443
|
-
|
|
444
|
-
const n = parseInt(out.split('\n')[0], 10);
|
|
445
|
-
if (Number.isFinite(n) && n > 0) stalePid = n;
|
|
534
|
+
__trace(`takeover: server current under lock (recheckRoot=${recheckRoot}) → skip`);
|
|
446
535
|
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
try {
|
|
450
|
-
if (process.platform === 'win32') {
|
|
451
|
-
// /T kills the whole process tree — the setup-server spawns child
|
|
452
|
-
// browser/server descendants that survive a PID-only Stop-Process,
|
|
453
|
-
// leaking processes and holding the port.
|
|
454
|
-
execSync(`taskkill /T /F /PID ${stalePid}`, { timeout: 3000, windowsHide: true });
|
|
455
|
-
} else {
|
|
456
|
-
// setup-server is launched detached (its own process-group leader),
|
|
457
|
-
// so the port-owning PID equals the group id. Negative PID signals
|
|
458
|
-
// the whole group, reaping descendants a bare kill would orphan.
|
|
459
|
-
try { execSync(`kill -KILL -${stalePid}`, { timeout: 3000 }); }
|
|
460
|
-
catch { execSync(`kill -KILL ${stalePid}`, { timeout: 3000 }); }
|
|
461
|
-
}
|
|
462
|
-
} catch (e) { killError = e; }
|
|
536
|
+
} finally {
|
|
537
|
+
if (locked) releaseLaunchLock();
|
|
463
538
|
}
|
|
464
|
-
// Poll until port is free (250ms ticks, 3000ms deadline).
|
|
465
|
-
const killDeadline = Date.now() + 3000;
|
|
466
|
-
let portFree = false;
|
|
467
|
-
while (Date.now() < killDeadline) {
|
|
468
|
-
await sleep(250);
|
|
469
|
-
portFree = !(await ping(300));
|
|
470
|
-
if (portFree) break;
|
|
471
|
-
}
|
|
472
|
-
if (!portFree) {
|
|
473
|
-
const errorDetails = [
|
|
474
|
-
pidResolveError && ` PID resolution failure: ${pidResolveError?.message || String(pidResolveError)}`,
|
|
475
|
-
killError && ` kill failure: ${killError?.message || String(killError)}`,
|
|
476
|
-
].filter(Boolean).join('\n');
|
|
477
|
-
throw new LaunchError(
|
|
478
|
-
`Port ${PORT} is in use by a different mixdog plugin instance and could not be reclaimed (kill of PID ${stalePid ?? 'unknown'} failed or timed out).\n` +
|
|
479
|
-
` expected plugin root: ${pluginRoot}\n` +
|
|
480
|
-
` actual plugin root: ${remoteRoot}\n` +
|
|
481
|
-
(errorDetails ? errorDetails + '\n' : '') +
|
|
482
|
-
`Stop the other setup-server (or change PORT) and retry.\n`,
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
__trace(`takeover: killed stalePid=${stalePid}, port freed, spawning new server`);
|
|
486
|
-
await spawnServerWithLog(pluginRoot, pluginData, { openOnStart: false });
|
|
487
|
-
__trace(`takeover: spawnServerWithLog completed`);
|
|
488
539
|
}
|
|
489
540
|
}
|
|
541
|
+
|
|
542
|
+
// Ensure the server (just spawned, taken over, or concurrently swapped by
|
|
543
|
+
// another launcher) is actually answering before we report success or open a
|
|
544
|
+
// window — absorbs the brief port-down window during a concurrent reclaim so
|
|
545
|
+
// requestOpen never hits a refused port and prewarm never returns early.
|
|
546
|
+
if (!await waitForServer(15000)) {
|
|
547
|
+
throw new LaunchError(`setup-server did not become ready at ${CONFIG_UI_URL}/ before launch.\n`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Prewarm stops here: the current-version server is up (spawned or taken
|
|
551
|
+
// over above) and must NOT open a browser window.
|
|
552
|
+
if (prewarm) {
|
|
553
|
+
__trace(`prewarm: current-version server ensured → return`);
|
|
554
|
+
return CONFIG_UI_URL;
|
|
555
|
+
}
|
|
556
|
+
|
|
490
557
|
// Invariant: a non-prewarm launch ALWAYS opens the window through the
|
|
491
558
|
// idempotent /open endpoint once the server is ready — regardless of which
|
|
492
559
|
// process (this launcher, a concurrent every-session prewarm, or a
|
package/setup/setup-server.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { listSchedules } from '../src/shared/schedules-store.mjs';
|
|
|
15
15
|
import { ensureDataSeeds } from '../src/shared/seed.mjs';
|
|
16
16
|
import { backupUserData, markUserDataInitialized, shouldSeedMissingUserData } from '../src/shared/user-data-guard.mjs';
|
|
17
17
|
import { tmpdir } from 'os';
|
|
18
|
-
import { readSection, writeSection, updateSection, saveSecret, deleteSecret, hasStoredSecret, SECRET_ACCOUNTS, getSearchApiKey, getAgentApiKey, getDiscordToken, getWebhookAuthtoken, diagnoseDiscordTokenValue } from '../src/shared/config.mjs';
|
|
18
|
+
import { readSection, writeSection, updateSection, saveSecret, deleteSecret, hasStoredSecret, SECRET_ACCOUNTS, getSearchApiKey, getAgentApiKey, getDiscordToken, getWebhookAuthtoken, diagnoseDiscordTokenValue, AGENT_PROVIDER_ENV, AGENT_PROVIDER_ENV_ALIASES } from '../src/shared/config.mjs';
|
|
19
19
|
import { applyDefaults as applyChannelsDefaults } from '../src/channels/lib/config.mjs';
|
|
20
20
|
import { validateCronExpression } from '../src/channels/lib/scheduler.mjs';
|
|
21
21
|
import { mergeAgentConfig, mergeMemoryConfig, mergeSearchConfig, mergeConfig, mergeEndpointConfig, mergeWebhookEndpointConfig } from './config-merge.mjs';
|
|
@@ -142,15 +142,11 @@ const SUPPORTED_PROVIDERS = [
|
|
|
142
142
|
'anthropic-oauth', 'openai-oauth', 'openai-api', 'gemini-api', 'xai-api', 'grok-oauth',
|
|
143
143
|
'tavily', 'firecrawl', 'exa',
|
|
144
144
|
];
|
|
145
|
-
|
|
145
|
+
// AGENT_PROVIDER_ENV / AGENT_PROVIDER_ENV_ALIASES are the SSOT (src/shared/config.mjs);
|
|
146
|
+
// derive the id list so a provider added there flows through every detection path
|
|
147
|
+
// (keyStored, envKeys, secret presence) here automatically.
|
|
148
|
+
const AGENT_KEY_PROVIDER_IDS = Object.keys(AGENT_PROVIDER_ENV);
|
|
146
149
|
const SEARCH_KEY_PROVIDER_IDS = ['firecrawl', 'tavily', 'exa'];
|
|
147
|
-
const AGENT_PROVIDER_ENV = Object.freeze({
|
|
148
|
-
openai: 'OPENAI_API_KEY',
|
|
149
|
-
anthropic: 'ANTHROPIC_API_KEY',
|
|
150
|
-
gemini: 'GEMINI_API_KEY',
|
|
151
|
-
deepseek: 'DEEPSEEK_API_KEY',
|
|
152
|
-
xai: 'XAI_API_KEY',
|
|
153
|
-
});
|
|
154
150
|
|
|
155
151
|
function envSecretPresent(account) {
|
|
156
152
|
const key = 'MIXDOG_' + String(account || '').replace(/[.\s]+/g, '_').toUpperCase();
|
|
@@ -465,21 +461,19 @@ async function detectAuth(config = {}) {
|
|
|
465
461
|
result.copilot = existsSync(join(configDir, 'github-copilot', 'hosts.json'))
|
|
466
462
|
|| existsSync(join(configDir, 'github-copilot', 'apps.json'));
|
|
467
463
|
result.envKeys = {};
|
|
468
|
-
for (const [
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
// shows xAI as available. keyStored semantics (keychain-only) stay unchanged.
|
|
476
|
-
result.envKeys.xai = result.envKeys.xai || !!process.env.GROK_API_KEY;
|
|
464
|
+
for (const [id, env] of Object.entries(AGENT_PROVIDER_ENV)) result.envKeys[id] = !!process.env[env];
|
|
465
|
+
// Honor the shared last-resort env aliases (e.g. GROK_API_KEY for xai) so a
|
|
466
|
+
// GROK_API_KEY-only env still shows xAI as available, matching getAgentApiKey.
|
|
467
|
+
// keyStored semantics (keychain-only) stay unchanged.
|
|
468
|
+
for (const [id, aliases] of Object.entries(AGENT_PROVIDER_ENV_ALIASES)) {
|
|
469
|
+
if (!result.envKeys[id]) result.envKeys[id] = aliases.some((a) => !!process.env[a]);
|
|
470
|
+
}
|
|
477
471
|
// Keychain-stored provider keys. Only this boolean is sent to the browser —
|
|
478
472
|
// the secret value never leaves the server, so the UI can show "Set" without
|
|
479
473
|
// exposing the key.
|
|
480
474
|
result.keyStored = {};
|
|
481
|
-
for (const
|
|
482
|
-
result.keyStored[
|
|
475
|
+
for (const id of AGENT_KEY_PROVIDER_IDS) {
|
|
476
|
+
result.keyStored[id] = hasStoredSecret(SECRET_ACCOUNTS.agentApiKey(id));
|
|
483
477
|
}
|
|
484
478
|
const ollamaUrl = config?.providers?.ollama?.baseURL || 'http://localhost:11434/v1';
|
|
485
479
|
const lmstudioUrl = config?.providers?.lmstudio?.baseURL || 'http://localhost:1234/v1';
|
|
@@ -518,7 +512,7 @@ async function detectAuth(config = {}) {
|
|
|
518
512
|
// in sync with src/agent/orchestrator/providers/registry.mjs.
|
|
519
513
|
const _RUNTIME_PROVIDER_NAMES = [
|
|
520
514
|
'anthropic', 'anthropic-oauth', 'openai', 'openai-oauth',
|
|
521
|
-
'gemini', 'deepseek', 'xai', 'grok-oauth',
|
|
515
|
+
'gemini', 'deepseek', 'xai', 'opencode-go', 'grok-oauth',
|
|
522
516
|
'ollama', 'lmstudio',
|
|
523
517
|
];
|
|
524
518
|
|
|
@@ -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,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.7.
|
|
2
|
+
"version": "0.7.15",
|
|
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.15/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.15/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.15/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.15/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.15/mixdog-graph-win32-x64.exe",
|
|
23
|
+
"sha256": "e2933fcf6ad0d7ab517c0beda4f8c7258638602bca73170dbda2cd8311c8637f"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.7.
|
|
2
|
+
"version": "0.7.15",
|
|
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.15/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.15/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.15/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.15/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.15/mixdog-patch-win32-x64.exe",
|
|
23
|
+
"sha256": "bc57bc029667bd62ce3798b4bfe23a42e4278148a6bbb778236769b3713397d8"
|
|
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
|
});
|
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
|
|