mixdog 0.9.20 → 0.9.21

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.
Files changed (36) hide show
  1. package/README.md +14 -12
  2. package/package.json +1 -1
  3. package/scripts/build-runtime-windows.ps1 +242 -242
  4. package/scripts/recall-usecase-cases.json +1 -1
  5. package/scripts/smoke-runtime-negative.ps1 +106 -106
  6. package/scripts/tool-efficiency-diag.mjs +1 -1
  7. package/src/defaults/skills/setup/SKILL.md +327 -0
  8. package/src/rules/lead/02-channels.md +3 -3
  9. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -2
  10. package/src/runtime/agent/orchestrator/providers/gemini.mjs +3 -0
  11. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -10
  12. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -0
  13. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +40 -3
  14. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +73 -3
  15. package/src/runtime/channels/lib/owned-runtime.mjs +95 -15
  16. package/src/runtime/channels/lib/runtime-paths.mjs +20 -9
  17. package/src/runtime/channels/lib/worker-main.mjs +7 -1
  18. package/src/runtime/memory/index.mjs +90 -0
  19. package/src/runtime/memory/lib/http-router.mjs +39 -0
  20. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +6 -0
  21. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +46 -9
  22. package/src/runtime/memory/lib/memory-cycle2.mjs +87 -11
  23. package/src/runtime/memory/lib/memory.mjs +39 -0
  24. package/src/runtime/shared/atomic-file.mjs +138 -80
  25. package/src/runtime/shared/child-guardian.mjs +61 -3
  26. package/src/session-runtime/runtime-core.mjs +75 -17
  27. package/src/standalone/channel-worker.mjs +63 -7
  28. package/src/standalone/folder-dialog.mjs +4 -1
  29. package/src/standalone/memory-runtime-proxy.mjs +98 -11
  30. package/src/standalone/seeds.mjs +28 -1
  31. package/src/tui/App.jsx +1 -0
  32. package/src/tui/app/doctor.mjs +175 -0
  33. package/src/tui/app/slash-commands.mjs +1 -0
  34. package/src/tui/app/slash-dispatch.mjs +9 -0
  35. package/src/tui/dist/index.mjs +297 -71
  36. package/src/tui/engine/session-api.mjs +19 -0
@@ -158,7 +158,7 @@ import React21 from "react";
158
158
  import { render } from "../../../vendor/ink/build/index.js";
159
159
  import { closeSync as closeSync2, constants as fsConstants, createWriteStream, mkdirSync as mkdirSync7, openSync as openSync2, readSync } from "node:fs";
160
160
  import { tmpdir as tmpdir3 } from "node:os";
161
- import { dirname as dirname7, join as join9 } from "node:path";
161
+ import { dirname as dirname9, join as join11 } from "node:path";
162
162
  import { performance as performance3 } from "node:perf_hooks";
163
163
 
164
164
  // src/tui/App.jsx
@@ -7509,7 +7509,10 @@ namespace Mixdog {
7509
7509
  }
7510
7510
  }
7511
7511
  '@`,
7512
- "Add-Type -TypeDefinition $code -ReferencedAssemblies System.Windows.Forms | Out-Null",
7512
+ // System.Drawing is required too: GetDialogOwnerCenter uses
7513
+ // Screen.PrimaryScreen.WorkingArea (System.Drawing.Rectangle) — without it
7514
+ // the Add-Type compile fails and the dialog silently falls back to typing.
7515
+ "Add-Type -TypeDefinition $code -ReferencedAssemblies System.Windows.Forms,System.Drawing | Out-Null",
7513
7516
  // Invisible TopMost owner anchored to the TUI console (or foreground window)
7514
7517
  // so IFileOpenDialog is modal, centered, and not detached on another monitor.
7515
7518
  "$owner = New-Object System.Windows.Forms.Form",
@@ -7682,6 +7685,7 @@ var SLASH_COMMANDS = [
7682
7685
  { name: "settings", usage: "/setting", aliases: ["setting", "config"], aliasUsage: ["settings", "config"], showAliasUsage: false, description: "Open runtime settings" },
7683
7686
  { name: "profile", usage: "/profile", description: "Set your title and response language" },
7684
7687
  { name: "update", usage: "/update", description: "Check version and update mixdog" },
7688
+ { name: "doctor", usage: "/doctor", description: "Diagnose installation health" },
7685
7689
  { name: "quit", usage: "/quit", aliases: ["exit", "q"], aliasUsage: ["exit", "q"], description: "Quit the TUI" }
7686
7690
  ];
7687
7691
  function slashQuery(value) {
@@ -13733,6 +13737,7 @@ var RETRY_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY", "EEXIST"]
13733
13737
  var LOCK_WAIT_CODES = /* @__PURE__ */ new Set(["EEXIST", "EPERM", "EACCES", "EBUSY"]);
13734
13738
  var DEFAULT_BACKOFFS_MS = Object.freeze([25, 50, 100, 200, 400, 800, 1200, 1600]);
13735
13739
  var DEFAULT_LOCK_TIMEOUT_MS = 8e3;
13740
+ var _OWNER_TOKEN = randomBytes2(12).toString("hex");
13736
13741
  function sleepSync(ms) {
13737
13742
  try {
13738
13743
  const buf = new SharedArrayBuffer(4);
@@ -13780,34 +13785,7 @@ function withFileLockSync(lockPath, fn, opts = {}) {
13780
13785
  throw contErr;
13781
13786
  }
13782
13787
  try {
13783
- const st = statSync2(lockPath);
13784
- const ownerPidEarly = _readLockOwnerPid(lockPath);
13785
- const ownerDead = ownerPidEarly !== null && _pidIsDead(ownerPidEarly);
13786
- if (ownerDead || Date.now() - st.mtimeMs > staleMs) {
13787
- const deadPid = ownerPidEarly;
13788
- if (ownerDead) {
13789
- const reclaim = _tryAcquireReclaimGuard(lockPath, staleMs);
13790
- if (reclaim !== null) {
13791
- let reclaimed = false;
13792
- try {
13793
- const currentSt = statSync2(lockPath);
13794
- if (ownerDead || Date.now() - currentSt.mtimeMs > staleMs) {
13795
- const currentPid = _readLockOwnerPid(lockPath);
13796
- if (currentPid === deadPid && _pidIsDead(currentPid)) {
13797
- try {
13798
- unlinkSync2(lockPath);
13799
- reclaimed = true;
13800
- } catch {
13801
- }
13802
- }
13803
- }
13804
- } finally {
13805
- _releaseReclaimGuard(reclaim);
13806
- }
13807
- if (reclaimed) continue;
13808
- }
13809
- }
13810
- }
13788
+ if (_tryReclaimStaleLock(lockPath, staleMs)) continue;
13811
13789
  } catch {
13812
13790
  }
13813
13791
  if (Date.now() >= deadline) break;
@@ -13818,7 +13796,7 @@ function withFileLockSync(lockPath, fn, opts = {}) {
13818
13796
  continue;
13819
13797
  }
13820
13798
  try {
13821
- writeFileSync2(fd, `${process.pid} ${Date.now()}
13799
+ writeFileSync2(fd, `${process.pid} ${Date.now()} ${_OWNER_TOKEN}
13822
13800
  `, "utf8");
13823
13801
  } catch {
13824
13802
  }
@@ -13831,7 +13809,7 @@ function withFileLockSync(lockPath, fn, opts = {}) {
13831
13809
  } catch {
13832
13810
  }
13833
13811
  try {
13834
- if (_lockOwnedByPid(lockPath, process.pid)) unlinkSync2(lockPath);
13812
+ if (_lockOwnedBySelf(lockPath)) unlinkSync2(lockPath);
13835
13813
  } catch {
13836
13814
  }
13837
13815
  }
@@ -13861,8 +13839,77 @@ function _pidIsDead(pid) {
13861
13839
  return err?.code === "ESRCH";
13862
13840
  }
13863
13841
  }
13864
- function _lockOwnedByPid(lockPath, pid) {
13865
- return _readLockOwnerPid(lockPath) === pid;
13842
+ function _readLockOwner(lockPath) {
13843
+ try {
13844
+ const raw = readFileSync2(lockPath, "utf8");
13845
+ const parts = String(raw).trim().split(/\s+/);
13846
+ const pid = Number.parseInt(parts[0], 10);
13847
+ return {
13848
+ pid: Number.isFinite(pid) && pid > 0 ? pid : null,
13849
+ token: parts.length >= 3 ? parts[2] : null
13850
+ };
13851
+ } catch {
13852
+ return { pid: null, token: null };
13853
+ }
13854
+ }
13855
+ function _ownerIsLive(owner) {
13856
+ if (owner.pid === null) return false;
13857
+ if (owner.pid === process.pid) return true;
13858
+ try {
13859
+ process.kill(owner.pid, 0);
13860
+ return true;
13861
+ } catch (err) {
13862
+ return err?.code !== "ESRCH";
13863
+ }
13864
+ }
13865
+ function _ownerIsSelfForeign(owner) {
13866
+ return owner.pid === process.pid && owner.token !== null && owner.token !== _OWNER_TOKEN;
13867
+ }
13868
+ function _lockOwnedBySelf(lockPath) {
13869
+ const owner = _readLockOwner(lockPath);
13870
+ if (owner.pid !== process.pid) return false;
13871
+ return owner.token === null ? true : owner.token === _OWNER_TOKEN;
13872
+ }
13873
+ function _tryReclaimStaleLock(lockPath, staleMs) {
13874
+ let st;
13875
+ try {
13876
+ st = statSync2(lockPath);
13877
+ } catch {
13878
+ return false;
13879
+ }
13880
+ const owner = _readLockOwner(lockPath);
13881
+ const stale = Date.now() - st.mtimeMs > staleMs;
13882
+ const dead = owner.pid !== null && !_ownerIsLive(owner);
13883
+ const pidless = owner.pid === null;
13884
+ const selfForeign = _ownerIsSelfForeign(owner);
13885
+ if (!dead && !((pidless || selfForeign) && stale)) return false;
13886
+ const reclaim = _tryAcquireReclaimGuard(lockPath, staleMs);
13887
+ if (reclaim === null) return false;
13888
+ try {
13889
+ let cur;
13890
+ try {
13891
+ cur = statSync2(lockPath);
13892
+ } catch {
13893
+ return false;
13894
+ }
13895
+ const curOwner = _readLockOwner(lockPath);
13896
+ if (curOwner.pid !== owner.pid || curOwner.token !== owner.token) return false;
13897
+ const curStale = Date.now() - cur.mtimeMs > staleMs;
13898
+ const curDead = curOwner.pid !== null && !_ownerIsLive(curOwner);
13899
+ const curPidless = curOwner.pid === null;
13900
+ const curSelfForeign = _ownerIsSelfForeign(curOwner);
13901
+ if (curDead || (curPidless || curSelfForeign) && curStale) {
13902
+ try {
13903
+ unlinkSync2(lockPath);
13904
+ return true;
13905
+ } catch {
13906
+ return false;
13907
+ }
13908
+ }
13909
+ return false;
13910
+ } finally {
13911
+ _releaseReclaimGuard(reclaim);
13912
+ }
13866
13913
  }
13867
13914
  function _reclaimGuardIsUnreadableAndStale(guardPath, staleMs) {
13868
13915
  if (_readLockOwnerPid(guardPath) !== null) return false;
@@ -13901,7 +13948,14 @@ function _unlinkUnreadableStaleReclaimGuard(guardPath, staleMs) {
13901
13948
  function _tryClearStaleReclaimGuard(guardPath, staleMs) {
13902
13949
  const guardPid = _readLockOwnerPid(guardPath);
13903
13950
  if (guardPid !== null) {
13904
- if (_pidIsDead(guardPid)) _unlinkReclaimGuardIfPidMatches(guardPath, guardPid);
13951
+ let guardStale = false;
13952
+ try {
13953
+ guardStale = Date.now() - statSync2(guardPath).mtimeMs > staleMs;
13954
+ } catch {
13955
+ }
13956
+ if (_pidIsDead(guardPid) || guardStale) {
13957
+ _unlinkReclaimGuardIfPidMatches(guardPath, guardPid);
13958
+ }
13905
13959
  return;
13906
13960
  }
13907
13961
  _unlinkUnreadableStaleReclaimGuard(guardPath, staleMs);
@@ -13963,32 +14017,7 @@ async function withFileLock(lockPath, fn, opts = {}) {
13963
14017
  throw contErr;
13964
14018
  }
13965
14019
  try {
13966
- const st = statSync2(lockPath);
13967
- if (Date.now() - st.mtimeMs > staleMs) {
13968
- const deadPid = _readLockOwnerPid(lockPath);
13969
- if (deadPid !== null && _pidIsDead(deadPid)) {
13970
- const reclaim = _tryAcquireReclaimGuard(lockPath, staleMs);
13971
- if (reclaim !== null) {
13972
- let reclaimed = false;
13973
- try {
13974
- const currentSt = statSync2(lockPath);
13975
- if (Date.now() - currentSt.mtimeMs > staleMs) {
13976
- const currentPid = _readLockOwnerPid(lockPath);
13977
- if (currentPid === deadPid && _pidIsDead(currentPid)) {
13978
- try {
13979
- unlinkSync2(lockPath);
13980
- reclaimed = true;
13981
- } catch {
13982
- }
13983
- }
13984
- }
13985
- } finally {
13986
- _releaseReclaimGuard(reclaim);
13987
- }
13988
- if (reclaimed) continue;
13989
- }
13990
- }
13991
- }
14020
+ if (_tryReclaimStaleLock(lockPath, staleMs)) continue;
13992
14021
  } catch {
13993
14022
  }
13994
14023
  if (Date.now() >= deadline) break;
@@ -13999,7 +14028,7 @@ async function withFileLock(lockPath, fn, opts = {}) {
13999
14028
  continue;
14000
14029
  }
14001
14030
  try {
14002
- writeFileSync2(fd, `${process.pid} ${Date.now()}
14031
+ writeFileSync2(fd, `${process.pid} ${Date.now()} ${_OWNER_TOKEN}
14003
14032
  `, "utf8");
14004
14033
  } catch {
14005
14034
  }
@@ -14012,7 +14041,7 @@ async function withFileLock(lockPath, fn, opts = {}) {
14012
14041
  } catch {
14013
14042
  }
14014
14043
  try {
14015
- if (_lockOwnedByPid(lockPath, process.pid)) unlinkSync2(lockPath);
14044
+ if (_lockOwnedBySelf(lockPath)) unlinkSync2(lockPath);
14016
14045
  } catch {
14017
14046
  }
14018
14047
  }
@@ -16808,6 +16837,7 @@ function createSlashDispatch({
16808
16837
  openContextPicker,
16809
16838
  openProfilePicker,
16810
16839
  openUpdatePicker,
16840
+ runDoctor,
16811
16841
  requestExit
16812
16842
  }) {
16813
16843
  const runSlashCommand = (cmd, arg = "") => {
@@ -17123,6 +17153,13 @@ function createSlashDispatch({
17123
17153
  case "update":
17124
17154
  openUpdatePicker();
17125
17155
  return true;
17156
+ case "doctor":
17157
+ if (state.commandBusy) {
17158
+ store.pushNotice("wait for the current command to finish before /doctor", "warn");
17159
+ return false;
17160
+ }
17161
+ void Promise.resolve(runDoctor?.()).catch((e) => store.pushNotice(`doctor failed: ${e?.message || e}`, "error"));
17162
+ return true;
17126
17163
  case "quit":
17127
17164
  requestExit();
17128
17165
  return true;
@@ -18980,6 +19017,7 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
18980
19017
  openContextPicker: (...a) => openContextPicker(...a),
18981
19018
  openProfilePicker: (...a) => openProfilePicker(...a),
18982
19019
  openUpdatePicker: (...a) => openUpdatePicker(...a),
19020
+ runDoctor: (...a) => store.runDoctor?.(...a),
18983
19021
  requestExit: (...a) => requestExit(...a)
18984
19022
  });
18985
19023
  const promptHintTimerRef = useRef9(null);
@@ -24441,6 +24479,178 @@ function createEngineApiB(bag) {
24441
24479
  };
24442
24480
  }
24443
24481
 
24482
+ // src/runtime/shared/update-checker.mjs
24483
+ import { dirname as dirname7, join as join8 } from "node:path";
24484
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
24485
+ var PACKAGE_NAME = "mixdog";
24486
+ var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
24487
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
24488
+ var _MODULE_DIR = dirname7(fileURLToPath2(import.meta.url));
24489
+ var _PACKAGE_JSON_PATH = join8(_MODULE_DIR, "..", "..", "..", "package.json");
24490
+ function parseSemver(value) {
24491
+ const text = String(value || "").trim().replace(/^v/i, "");
24492
+ const [core, ...preParts] = text.split("-");
24493
+ const parts = core.split(".").map((n) => {
24494
+ const num2 = Number.parseInt(n, 10);
24495
+ return Number.isFinite(num2) ? num2 : 0;
24496
+ });
24497
+ while (parts.length < 3) parts.push(0);
24498
+ return { parts, prerelease: preParts.join("-") };
24499
+ }
24500
+ function compareSemver(a, b) {
24501
+ const pa = parseSemver(a);
24502
+ const pb = parseSemver(b);
24503
+ for (let i = 0; i < 3; i++) {
24504
+ const diff = (pa.parts[i] || 0) - (pb.parts[i] || 0);
24505
+ if (diff !== 0) return diff;
24506
+ }
24507
+ if (pa.prerelease && !pb.prerelease) return -1;
24508
+ if (!pa.prerelease && pb.prerelease) return 1;
24509
+ if (pa.prerelease !== pb.prerelease) return pa.prerelease < pb.prerelease ? -1 : 1;
24510
+ return 0;
24511
+ }
24512
+
24513
+ // src/tui/app/doctor.mjs
24514
+ import { readFileSync as readFileSync6 } from "node:fs";
24515
+ import { dirname as dirname8, join as join9 } from "node:path";
24516
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
24517
+ var GLYPH = { ok: "\u2713", warn: "\u26A0", fail: "\u2717" };
24518
+ function readPackageJson() {
24519
+ try {
24520
+ const dir = dirname8(fileURLToPath3(import.meta.url));
24521
+ const raw = JSON.parse(readFileSync6(join9(dir, "..", "..", "..", "package.json"), "utf8"));
24522
+ return raw && typeof raw === "object" ? raw : null;
24523
+ } catch {
24524
+ return null;
24525
+ }
24526
+ }
24527
+ async function buildDoctorReport(runtime = {}, getState = () => ({})) {
24528
+ const rows = [];
24529
+ const check = async (label, fn) => {
24530
+ const row = (level, detail) => {
24531
+ rows.push(`${GLYPH[level] || GLYPH.warn} ${label}: ${detail}`);
24532
+ };
24533
+ try {
24534
+ await fn(row);
24535
+ } catch (e) {
24536
+ row("fail", `check failed: ${e?.message || e}`);
24537
+ }
24538
+ };
24539
+ const pkg = readPackageJson();
24540
+ await check("mixdog", async (row) => {
24541
+ const upd = await runtime.checkForUpdate?.({}) || {};
24542
+ const current = upd.currentVersion || pkg?.version || "unknown";
24543
+ const latest = upd.latestVersion;
24544
+ if (latest == null) {
24545
+ row("warn", `v${current} \xB7 update check skipped (registry unreachable)`);
24546
+ return;
24547
+ }
24548
+ if (upd.updateAvailable) row("warn", `v${current} \xB7 update available \u2192 v${latest}`);
24549
+ else row("ok", `v${current} \xB7 up to date`);
24550
+ });
24551
+ await check("node", async (row) => {
24552
+ const nodeVer = process.versions?.node || "0.0.0";
24553
+ const engines = pkg?.engines?.node;
24554
+ if (!engines) {
24555
+ row("ok", `v${nodeVer}`);
24556
+ return;
24557
+ }
24558
+ const m = String(engines).match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
24559
+ if (!m) {
24560
+ row("warn", `v${nodeVer} \xB7 engines "${engines}" unparsed`);
24561
+ return;
24562
+ }
24563
+ const required = `${m[1]}.${m[2] || 0}.${m[3] || 0}`;
24564
+ const ok = compareSemver(nodeVer, required) >= 0;
24565
+ row(ok ? "ok" : "fail", `v${nodeVer} \xB7 requires node ${engines}`);
24566
+ });
24567
+ await check("providers", async (row) => {
24568
+ const setup = await runtime.getProviderSetup?.() || {};
24569
+ const lists = [...setup.api || [], ...setup.oauth || [], ...setup.local || []];
24570
+ const isAuthed = (p) => Boolean(p && (p.authenticated || p.enabled || p.detected));
24571
+ const authed = lists.filter(isAuthed);
24572
+ const active = getState()?.provider || "";
24573
+ const activeEntry = active ? lists.find((p) => p.id === active) : null;
24574
+ if (activeEntry && !isAuthed(activeEntry)) {
24575
+ row("fail", `route ${active} has no auth \xB7 ${authed.length} configured`);
24576
+ return;
24577
+ }
24578
+ if (active && !activeEntry) {
24579
+ row("warn", `${authed.length} authed \xB7 route ${active} (not listed)`);
24580
+ return;
24581
+ }
24582
+ row("ok", `${authed.length} authed \xB7 route ${active || "unknown"}`);
24583
+ });
24584
+ await check("mcp", async (row) => {
24585
+ const m = runtime.mcpStatus?.() || {};
24586
+ const servers = Array.isArray(m.servers) ? m.servers : [];
24587
+ const conn = Number(m.connectedCount || 0);
24588
+ const conf = Number(m.configuredCount || 0);
24589
+ if (conf === 0) {
24590
+ row("ok", "no servers configured");
24591
+ return;
24592
+ }
24593
+ const failed = servers.filter((s) => s && (s.error || s.status === "failed")).map((s) => s.name || s.id).filter(Boolean);
24594
+ const detail = `${conn}/${conf} connected${failed.length ? ` \xB7 failed: ${failed.join(", ")}` : ""}`;
24595
+ row(failed.length || conn < conf ? "warn" : "ok", detail);
24596
+ });
24597
+ await check("memory", async (row) => {
24598
+ const mem = runtime.getMemorySettings?.() || {};
24599
+ const enabled = mem.enabled !== false;
24600
+ let detail = enabled ? "enabled" : "disabled";
24601
+ if (mem.backend) detail += ` \xB7 backend ${mem.backend}`;
24602
+ const health = mem.backendHealth || mem.health;
24603
+ if (health != null) {
24604
+ detail += ` \xB7 ${typeof health === "string" ? health : health.ok ? "healthy" : "unhealthy"}`;
24605
+ }
24606
+ row(enabled ? "ok" : "warn", detail);
24607
+ });
24608
+ await check("channels", async (row) => {
24609
+ const settings = runtime.getChannelSettings?.({ includeStatus: true }) || {};
24610
+ const enabled = settings.enabled !== false;
24611
+ if (!enabled) {
24612
+ row("ok", "disabled");
24613
+ return;
24614
+ }
24615
+ const worker = settings.status || runtime.getChannelWorkerStatus?.() || {};
24616
+ const setup = runtime.getChannelSetup?.() || {};
24617
+ const tokens = [];
24618
+ if (setup.discord?.authenticated) tokens.push("discord");
24619
+ if (setup.telegram?.authenticated) tokens.push("telegram");
24620
+ if (setup.webhook?.authenticated) tokens.push("webhook");
24621
+ const running = worker.running === true;
24622
+ const detail = `enabled \xB7 worker ${running ? "running" : "stopped"} \xB7 tokens: ${tokens.length ? tokens.join(", ") : "none"}`;
24623
+ row(running ? "ok" : "warn", detail);
24624
+ });
24625
+ await check("skills", async (row) => {
24626
+ const s = runtime.skillsStatus?.() || {};
24627
+ const skills = Array.isArray(s.skills) ? s.skills : [];
24628
+ const broken = skills.filter((x) => x && (x.broken || x.error || x.invalid));
24629
+ const disabled = skills.filter((x) => x && x.disabled);
24630
+ let detail = `${s.count ?? skills.length} available`;
24631
+ if (disabled.length) detail += ` \xB7 ${disabled.length} disabled`;
24632
+ if (broken.length) detail += ` \xB7 broken: ${broken.map((x) => x.name || x.id).filter(Boolean).join(", ")}`;
24633
+ row(broken.length ? "warn" : "ok", detail);
24634
+ });
24635
+ await check("plugins", async (row) => {
24636
+ const p = runtime.pluginsStatus?.() || {};
24637
+ const plugins = Array.isArray(p.plugins) ? p.plugins : [];
24638
+ const broken = plugins.filter((x) => x && (x.broken || x.error));
24639
+ const disabled = plugins.filter((x) => x && x.disabled);
24640
+ let detail = `${p.count ?? plugins.length} detected`;
24641
+ if (disabled.length) detail += ` \xB7 ${disabled.length} disabled`;
24642
+ if (broken.length) detail += ` \xB7 broken: ${broken.map((x) => x.title || x.name || x.id).filter(Boolean).join(", ")}`;
24643
+ row(broken.length ? "warn" : "ok", detail);
24644
+ });
24645
+ await check("hooks", async (row) => {
24646
+ const h = runtime.hooksStatus?.() || {};
24647
+ const events = Array.isArray(h.events) ? h.events : [];
24648
+ const enabled = h.enabled === true;
24649
+ row("ok", `${enabled ? "enabled" : "disabled"} \xB7 ${events.length} event${events.length === 1 ? "" : "s"}`);
24650
+ });
24651
+ return ["mixdog doctor \u2014 installation health", ...rows].join("\n");
24652
+ }
24653
+
24444
24654
  // src/tui/engine/session-api.mjs
24445
24655
  function createEngineApi(bag) {
24446
24656
  return { ...createEngineApiA(bag), ...createEngineApiB(bag) };
@@ -24883,6 +25093,22 @@ function createEngineApiA(bag) {
24883
25093
  set({ commandBusy: false, commandStatus: null });
24884
25094
  }
24885
25095
  },
25096
+ runDoctor: async () => {
25097
+ if (getState().commandBusy) return null;
25098
+ const startedAt = Date.now();
25099
+ set({ commandBusy: true, commandStatus: { active: true, verb: "Running diagnostics", startedAt, mode: "doctor" } });
25100
+ try {
25101
+ await new Promise((resolve6) => setTimeout(resolve6, 0));
25102
+ const report = await buildDoctorReport(runtime, getState);
25103
+ pushNotice(report, "info");
25104
+ return report;
25105
+ } catch (e) {
25106
+ pushNotice(`doctor failed: ${e?.message || e}`, "error");
25107
+ return null;
25108
+ } finally {
25109
+ set({ commandBusy: false, commandStatus: null });
25110
+ }
25111
+ },
24886
25112
  compact: async () => {
24887
25113
  if (getState().commandBusy) return null;
24888
25114
  if (getState().busy) {
@@ -25512,18 +25738,18 @@ function installProcessSignalCleanup({
25512
25738
 
25513
25739
  // src/runtime/channels/lib/runtime-paths.mjs
25514
25740
  import { tmpdir as tmpdir2 } from "os";
25515
- import { basename as basename4, join as join8, resolve as resolve5 } from "path";
25741
+ import { basename as basename4, join as join10, resolve as resolve5 } from "path";
25516
25742
 
25517
25743
  // src/runtime/channels/lib/state-file.mjs
25518
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync6, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
25744
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync7, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
25519
25745
  function ensureDir2(dirPath) {
25520
25746
  mkdirSync6(dirPath, { recursive: true });
25521
25747
  }
25522
25748
 
25523
25749
  // src/runtime/channels/lib/runtime-paths.mjs
25524
- var RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT ? resolve5(process.env.MIXDOG_RUNTIME_ROOT) : join8(tmpdir2(), "mixdog");
25525
- var OWNER_DIR = join8(RUNTIME_ROOT, "owners");
25526
- var ACTIVE_INSTANCE_FILE = join8(RUNTIME_ROOT, "active-instance.json");
25750
+ var RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT ? resolve5(process.env.MIXDOG_RUNTIME_ROOT) : join10(tmpdir2(), "mixdog");
25751
+ var OWNER_DIR = join10(RUNTIME_ROOT, "owners");
25752
+ var ACTIVE_INSTANCE_FILE = join10(RUNTIME_ROOT, "active-instance.json");
25527
25753
  var RUNTIME_STALE_TTL = 24 * 60 * 60 * 1e3;
25528
25754
  var STATUS_FILE_TTL = 6 * 60 * 60 * 1e3;
25529
25755
  var TMP_FILE_TTL = 60 * 60 * 1e3;
@@ -25546,7 +25772,7 @@ function touchUiHeartbeat(instanceId) {
25546
25772
  } catch {
25547
25773
  }
25548
25774
  }
25549
- var SERVER_PID_FILE = join8(
25775
+ var SERVER_PID_FILE = join10(
25550
25776
  RUNTIME_ROOT,
25551
25777
  `server-${sanitize(resolvePluginData() ?? "default")}.pid`
25552
25778
  );
@@ -25766,7 +25992,7 @@ function scheduleHardExit(code = 0) {
25766
25992
  timer.unref?.();
25767
25993
  }
25768
25994
  function resolveTuiStderrLogPath() {
25769
- return process.env.MIXDOG_TUI_STDERR_LOG || join9(process.env.MIXDOG_RUNTIME_ROOT || join9(tmpdir3(), "mixdog"), "mixdog-tui.stderr.log");
25995
+ return process.env.MIXDOG_TUI_STDERR_LOG || join11(process.env.MIXDOG_RUNTIME_ROOT || join11(tmpdir3(), "mixdog"), "mixdog-tui.stderr.log");
25770
25996
  }
25771
25997
  function ansiFg(rgb) {
25772
25998
  const m = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/.exec(String(rgb || ""));
@@ -25848,7 +26074,7 @@ function installTuiStderrGuard() {
25848
26074
  const originalWrite = process.stderr.write.bind(process.stderr);
25849
26075
  const logPath = resolveTuiStderrLogPath();
25850
26076
  try {
25851
- mkdirSync7(dirname7(logPath), { recursive: true });
26077
+ mkdirSync7(dirname9(logPath), { recursive: true });
25852
26078
  } catch {
25853
26079
  }
25854
26080
  (0, import_mixdog_debug.rotateBoundedLog)(logPath, import_mixdog_debug.PLUGIN_LOG_MAX_BYTES, import_mixdog_debug.PLUGIN_LOG_KEEP_BYTES);
@@ -5,6 +5,7 @@ import { compactEventDetail, projectNameFromPath } from './labels.mjs';
5
5
  import { toolErrorDisplay } from './tool-result-text.mjs';
6
6
  import { isQueuedEntryEditable, promptDisplayText } from './queue-helpers.mjs';
7
7
  import { createEngineApiB } from './session-api-ext.mjs';
8
+ import { buildDoctorReport } from '../app/doctor.mjs';
8
9
 
9
10
  export function createEngineApi(bag) {
10
11
  return { ...createEngineApiA(bag), ...createEngineApiB(bag) };
@@ -444,6 +445,24 @@ export function createEngineApiA(bag) {
444
445
  set({ commandBusy: false, commandStatus: null });
445
446
  }
446
447
  },
448
+ runDoctor: async () => {
449
+ if (getState().commandBusy) return null;
450
+ const startedAt = Date.now();
451
+ set({ commandBusy: true, commandStatus: { active: true, verb: 'Running diagnostics', startedAt, mode: 'doctor' } });
452
+ try {
453
+ // Yield one event-loop turn so Ink paints the running indicator before
454
+ // the (mostly synchronous) health checks run — same pattern as compact.
455
+ await new Promise((resolve) => setTimeout(resolve, 0));
456
+ const report = await buildDoctorReport(runtime, getState);
457
+ pushNotice(report, 'info');
458
+ return report;
459
+ } catch (e) {
460
+ pushNotice(`doctor failed: ${e?.message || e}`, 'error');
461
+ return null;
462
+ } finally {
463
+ set({ commandBusy: false, commandStatus: null });
464
+ }
465
+ },
447
466
  compact: async () => {
448
467
  if (getState().commandBusy) return null;
449
468
  if (getState().busy) {