mixdog 0.9.20 → 0.9.22

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 (35) hide show
  1. package/README.md +14 -12
  2. package/package.json +1 -1
  3. package/src/defaults/skills/setup/SKILL.md +327 -0
  4. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -2
  5. package/src/runtime/agent/orchestrator/providers/gemini.mjs +3 -0
  6. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -10
  7. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -0
  8. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +40 -3
  9. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +73 -3
  10. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +10 -10
  11. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +8 -6
  12. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +14 -1
  13. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +23 -13
  14. package/src/runtime/channels/lib/owned-runtime.mjs +95 -15
  15. package/src/runtime/channels/lib/runtime-paths.mjs +20 -9
  16. package/src/runtime/channels/lib/worker-main.mjs +7 -1
  17. package/src/runtime/memory/index.mjs +90 -0
  18. package/src/runtime/memory/lib/http-router.mjs +39 -0
  19. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +6 -0
  20. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +46 -9
  21. package/src/runtime/memory/lib/memory-cycle2.mjs +87 -11
  22. package/src/runtime/memory/lib/memory.mjs +39 -0
  23. package/src/runtime/shared/atomic-file.mjs +161 -85
  24. package/src/runtime/shared/child-guardian.mjs +61 -3
  25. package/src/session-runtime/runtime-core.mjs +75 -17
  26. package/src/standalone/channel-worker.mjs +63 -7
  27. package/src/standalone/folder-dialog.mjs +4 -1
  28. package/src/standalone/memory-runtime-proxy.mjs +98 -11
  29. package/src/standalone/seeds.mjs +28 -1
  30. package/src/tui/App.jsx +1 -0
  31. package/src/tui/app/doctor.mjs +175 -0
  32. package/src/tui/app/slash-commands.mjs +1 -0
  33. package/src/tui/app/slash-dispatch.mjs +9 -0
  34. package/src/tui/dist/index.mjs +314 -74
  35. 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;
@@ -13880,8 +13927,21 @@ function _reclaimGuardMatchesToken(guardPath, token) {
13880
13927
  return false;
13881
13928
  }
13882
13929
  }
13883
- function _unlinkReclaimGuardIfPidMatches(guardPath, pid) {
13884
- if (_readLockOwnerPid(guardPath) !== pid) return false;
13930
+ function _readGuardContent(guardPath) {
13931
+ try {
13932
+ return readFileSync2(guardPath, "utf8");
13933
+ } catch {
13934
+ return null;
13935
+ }
13936
+ }
13937
+ function _parseStampPid(raw) {
13938
+ if (raw === null) return null;
13939
+ const tok = String(raw).trim().split(/\s+/)[0];
13940
+ const pid = Number.parseInt(tok, 10);
13941
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
13942
+ }
13943
+ function _unlinkReclaimGuardIfContentMatches(guardPath, content) {
13944
+ if (_readGuardContent(guardPath) !== content) return false;
13885
13945
  try {
13886
13946
  unlinkSync2(guardPath);
13887
13947
  return true;
@@ -13899,9 +13959,17 @@ function _unlinkUnreadableStaleReclaimGuard(guardPath, staleMs) {
13899
13959
  }
13900
13960
  }
13901
13961
  function _tryClearStaleReclaimGuard(guardPath, staleMs) {
13902
- const guardPid = _readLockOwnerPid(guardPath);
13962
+ const guardContent = _readGuardContent(guardPath);
13963
+ const guardPid = _parseStampPid(guardContent);
13903
13964
  if (guardPid !== null) {
13904
- if (_pidIsDead(guardPid)) _unlinkReclaimGuardIfPidMatches(guardPath, guardPid);
13965
+ let guardStale = false;
13966
+ try {
13967
+ guardStale = Date.now() - statSync2(guardPath).mtimeMs > staleMs;
13968
+ } catch {
13969
+ }
13970
+ if (_pidIsDead(guardPid) || guardStale) {
13971
+ _unlinkReclaimGuardIfContentMatches(guardPath, guardContent);
13972
+ }
13905
13973
  return;
13906
13974
  }
13907
13975
  _unlinkUnreadableStaleReclaimGuard(guardPath, staleMs);
@@ -13963,32 +14031,7 @@ async function withFileLock(lockPath, fn, opts = {}) {
13963
14031
  throw contErr;
13964
14032
  }
13965
14033
  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
- }
14034
+ if (_tryReclaimStaleLock(lockPath, staleMs)) continue;
13992
14035
  } catch {
13993
14036
  }
13994
14037
  if (Date.now() >= deadline) break;
@@ -13999,7 +14042,7 @@ async function withFileLock(lockPath, fn, opts = {}) {
13999
14042
  continue;
14000
14043
  }
14001
14044
  try {
14002
- writeFileSync2(fd, `${process.pid} ${Date.now()}
14045
+ writeFileSync2(fd, `${process.pid} ${Date.now()} ${_OWNER_TOKEN}
14003
14046
  `, "utf8");
14004
14047
  } catch {
14005
14048
  }
@@ -14012,7 +14055,7 @@ async function withFileLock(lockPath, fn, opts = {}) {
14012
14055
  } catch {
14013
14056
  }
14014
14057
  try {
14015
- if (_lockOwnedByPid(lockPath, process.pid)) unlinkSync2(lockPath);
14058
+ if (_lockOwnedBySelf(lockPath)) unlinkSync2(lockPath);
14016
14059
  } catch {
14017
14060
  }
14018
14061
  }
@@ -16808,6 +16851,7 @@ function createSlashDispatch({
16808
16851
  openContextPicker,
16809
16852
  openProfilePicker,
16810
16853
  openUpdatePicker,
16854
+ runDoctor,
16811
16855
  requestExit
16812
16856
  }) {
16813
16857
  const runSlashCommand = (cmd, arg = "") => {
@@ -17123,6 +17167,13 @@ function createSlashDispatch({
17123
17167
  case "update":
17124
17168
  openUpdatePicker();
17125
17169
  return true;
17170
+ case "doctor":
17171
+ if (state.commandBusy) {
17172
+ store.pushNotice("wait for the current command to finish before /doctor", "warn");
17173
+ return false;
17174
+ }
17175
+ void Promise.resolve(runDoctor?.()).catch((e) => store.pushNotice(`doctor failed: ${e?.message || e}`, "error"));
17176
+ return true;
17126
17177
  case "quit":
17127
17178
  requestExit();
17128
17179
  return true;
@@ -18980,6 +19031,7 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
18980
19031
  openContextPicker: (...a) => openContextPicker(...a),
18981
19032
  openProfilePicker: (...a) => openProfilePicker(...a),
18982
19033
  openUpdatePicker: (...a) => openUpdatePicker(...a),
19034
+ runDoctor: (...a) => store.runDoctor?.(...a),
18983
19035
  requestExit: (...a) => requestExit(...a)
18984
19036
  });
18985
19037
  const promptHintTimerRef = useRef9(null);
@@ -24441,6 +24493,178 @@ function createEngineApiB(bag) {
24441
24493
  };
24442
24494
  }
24443
24495
 
24496
+ // src/runtime/shared/update-checker.mjs
24497
+ import { dirname as dirname7, join as join8 } from "node:path";
24498
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
24499
+ var PACKAGE_NAME = "mixdog";
24500
+ var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
24501
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
24502
+ var _MODULE_DIR = dirname7(fileURLToPath2(import.meta.url));
24503
+ var _PACKAGE_JSON_PATH = join8(_MODULE_DIR, "..", "..", "..", "package.json");
24504
+ function parseSemver(value) {
24505
+ const text = String(value || "").trim().replace(/^v/i, "");
24506
+ const [core, ...preParts] = text.split("-");
24507
+ const parts = core.split(".").map((n) => {
24508
+ const num2 = Number.parseInt(n, 10);
24509
+ return Number.isFinite(num2) ? num2 : 0;
24510
+ });
24511
+ while (parts.length < 3) parts.push(0);
24512
+ return { parts, prerelease: preParts.join("-") };
24513
+ }
24514
+ function compareSemver(a, b) {
24515
+ const pa = parseSemver(a);
24516
+ const pb = parseSemver(b);
24517
+ for (let i = 0; i < 3; i++) {
24518
+ const diff = (pa.parts[i] || 0) - (pb.parts[i] || 0);
24519
+ if (diff !== 0) return diff;
24520
+ }
24521
+ if (pa.prerelease && !pb.prerelease) return -1;
24522
+ if (!pa.prerelease && pb.prerelease) return 1;
24523
+ if (pa.prerelease !== pb.prerelease) return pa.prerelease < pb.prerelease ? -1 : 1;
24524
+ return 0;
24525
+ }
24526
+
24527
+ // src/tui/app/doctor.mjs
24528
+ import { readFileSync as readFileSync6 } from "node:fs";
24529
+ import { dirname as dirname8, join as join9 } from "node:path";
24530
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
24531
+ var GLYPH = { ok: "\u2713", warn: "\u26A0", fail: "\u2717" };
24532
+ function readPackageJson() {
24533
+ try {
24534
+ const dir = dirname8(fileURLToPath3(import.meta.url));
24535
+ const raw = JSON.parse(readFileSync6(join9(dir, "..", "..", "..", "package.json"), "utf8"));
24536
+ return raw && typeof raw === "object" ? raw : null;
24537
+ } catch {
24538
+ return null;
24539
+ }
24540
+ }
24541
+ async function buildDoctorReport(runtime = {}, getState = () => ({})) {
24542
+ const rows = [];
24543
+ const check = async (label, fn) => {
24544
+ const row = (level, detail) => {
24545
+ rows.push(`${GLYPH[level] || GLYPH.warn} ${label}: ${detail}`);
24546
+ };
24547
+ try {
24548
+ await fn(row);
24549
+ } catch (e) {
24550
+ row("fail", `check failed: ${e?.message || e}`);
24551
+ }
24552
+ };
24553
+ const pkg = readPackageJson();
24554
+ await check("mixdog", async (row) => {
24555
+ const upd = await runtime.checkForUpdate?.({}) || {};
24556
+ const current = upd.currentVersion || pkg?.version || "unknown";
24557
+ const latest = upd.latestVersion;
24558
+ if (latest == null) {
24559
+ row("warn", `v${current} \xB7 update check skipped (registry unreachable)`);
24560
+ return;
24561
+ }
24562
+ if (upd.updateAvailable) row("warn", `v${current} \xB7 update available \u2192 v${latest}`);
24563
+ else row("ok", `v${current} \xB7 up to date`);
24564
+ });
24565
+ await check("node", async (row) => {
24566
+ const nodeVer = process.versions?.node || "0.0.0";
24567
+ const engines = pkg?.engines?.node;
24568
+ if (!engines) {
24569
+ row("ok", `v${nodeVer}`);
24570
+ return;
24571
+ }
24572
+ const m = String(engines).match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
24573
+ if (!m) {
24574
+ row("warn", `v${nodeVer} \xB7 engines "${engines}" unparsed`);
24575
+ return;
24576
+ }
24577
+ const required = `${m[1]}.${m[2] || 0}.${m[3] || 0}`;
24578
+ const ok = compareSemver(nodeVer, required) >= 0;
24579
+ row(ok ? "ok" : "fail", `v${nodeVer} \xB7 requires node ${engines}`);
24580
+ });
24581
+ await check("providers", async (row) => {
24582
+ const setup = await runtime.getProviderSetup?.() || {};
24583
+ const lists = [...setup.api || [], ...setup.oauth || [], ...setup.local || []];
24584
+ const isAuthed = (p) => Boolean(p && (p.authenticated || p.enabled || p.detected));
24585
+ const authed = lists.filter(isAuthed);
24586
+ const active = getState()?.provider || "";
24587
+ const activeEntry = active ? lists.find((p) => p.id === active) : null;
24588
+ if (activeEntry && !isAuthed(activeEntry)) {
24589
+ row("fail", `route ${active} has no auth \xB7 ${authed.length} configured`);
24590
+ return;
24591
+ }
24592
+ if (active && !activeEntry) {
24593
+ row("warn", `${authed.length} authed \xB7 route ${active} (not listed)`);
24594
+ return;
24595
+ }
24596
+ row("ok", `${authed.length} authed \xB7 route ${active || "unknown"}`);
24597
+ });
24598
+ await check("mcp", async (row) => {
24599
+ const m = runtime.mcpStatus?.() || {};
24600
+ const servers = Array.isArray(m.servers) ? m.servers : [];
24601
+ const conn = Number(m.connectedCount || 0);
24602
+ const conf = Number(m.configuredCount || 0);
24603
+ if (conf === 0) {
24604
+ row("ok", "no servers configured");
24605
+ return;
24606
+ }
24607
+ const failed = servers.filter((s) => s && (s.error || s.status === "failed")).map((s) => s.name || s.id).filter(Boolean);
24608
+ const detail = `${conn}/${conf} connected${failed.length ? ` \xB7 failed: ${failed.join(", ")}` : ""}`;
24609
+ row(failed.length || conn < conf ? "warn" : "ok", detail);
24610
+ });
24611
+ await check("memory", async (row) => {
24612
+ const mem = runtime.getMemorySettings?.() || {};
24613
+ const enabled = mem.enabled !== false;
24614
+ let detail = enabled ? "enabled" : "disabled";
24615
+ if (mem.backend) detail += ` \xB7 backend ${mem.backend}`;
24616
+ const health = mem.backendHealth || mem.health;
24617
+ if (health != null) {
24618
+ detail += ` \xB7 ${typeof health === "string" ? health : health.ok ? "healthy" : "unhealthy"}`;
24619
+ }
24620
+ row(enabled ? "ok" : "warn", detail);
24621
+ });
24622
+ await check("channels", async (row) => {
24623
+ const settings = runtime.getChannelSettings?.({ includeStatus: true }) || {};
24624
+ const enabled = settings.enabled !== false;
24625
+ if (!enabled) {
24626
+ row("ok", "disabled");
24627
+ return;
24628
+ }
24629
+ const worker = settings.status || runtime.getChannelWorkerStatus?.() || {};
24630
+ const setup = runtime.getChannelSetup?.() || {};
24631
+ const tokens = [];
24632
+ if (setup.discord?.authenticated) tokens.push("discord");
24633
+ if (setup.telegram?.authenticated) tokens.push("telegram");
24634
+ if (setup.webhook?.authenticated) tokens.push("webhook");
24635
+ const running = worker.running === true;
24636
+ const detail = `enabled \xB7 worker ${running ? "running" : "stopped"} \xB7 tokens: ${tokens.length ? tokens.join(", ") : "none"}`;
24637
+ row(running ? "ok" : "warn", detail);
24638
+ });
24639
+ await check("skills", async (row) => {
24640
+ const s = runtime.skillsStatus?.() || {};
24641
+ const skills = Array.isArray(s.skills) ? s.skills : [];
24642
+ const broken = skills.filter((x) => x && (x.broken || x.error || x.invalid));
24643
+ const disabled = skills.filter((x) => x && x.disabled);
24644
+ let detail = `${s.count ?? skills.length} available`;
24645
+ if (disabled.length) detail += ` \xB7 ${disabled.length} disabled`;
24646
+ if (broken.length) detail += ` \xB7 broken: ${broken.map((x) => x.name || x.id).filter(Boolean).join(", ")}`;
24647
+ row(broken.length ? "warn" : "ok", detail);
24648
+ });
24649
+ await check("plugins", async (row) => {
24650
+ const p = runtime.pluginsStatus?.() || {};
24651
+ const plugins = Array.isArray(p.plugins) ? p.plugins : [];
24652
+ const broken = plugins.filter((x) => x && (x.broken || x.error));
24653
+ const disabled = plugins.filter((x) => x && x.disabled);
24654
+ let detail = `${p.count ?? plugins.length} detected`;
24655
+ if (disabled.length) detail += ` \xB7 ${disabled.length} disabled`;
24656
+ if (broken.length) detail += ` \xB7 broken: ${broken.map((x) => x.title || x.name || x.id).filter(Boolean).join(", ")}`;
24657
+ row(broken.length ? "warn" : "ok", detail);
24658
+ });
24659
+ await check("hooks", async (row) => {
24660
+ const h = runtime.hooksStatus?.() || {};
24661
+ const events = Array.isArray(h.events) ? h.events : [];
24662
+ const enabled = h.enabled === true;
24663
+ row("ok", `${enabled ? "enabled" : "disabled"} \xB7 ${events.length} event${events.length === 1 ? "" : "s"}`);
24664
+ });
24665
+ return ["mixdog doctor \u2014 installation health", ...rows].join("\n");
24666
+ }
24667
+
24444
24668
  // src/tui/engine/session-api.mjs
24445
24669
  function createEngineApi(bag) {
24446
24670
  return { ...createEngineApiA(bag), ...createEngineApiB(bag) };
@@ -24883,6 +25107,22 @@ function createEngineApiA(bag) {
24883
25107
  set({ commandBusy: false, commandStatus: null });
24884
25108
  }
24885
25109
  },
25110
+ runDoctor: async () => {
25111
+ if (getState().commandBusy) return null;
25112
+ const startedAt = Date.now();
25113
+ set({ commandBusy: true, commandStatus: { active: true, verb: "Running diagnostics", startedAt, mode: "doctor" } });
25114
+ try {
25115
+ await new Promise((resolve6) => setTimeout(resolve6, 0));
25116
+ const report = await buildDoctorReport(runtime, getState);
25117
+ pushNotice(report, "info");
25118
+ return report;
25119
+ } catch (e) {
25120
+ pushNotice(`doctor failed: ${e?.message || e}`, "error");
25121
+ return null;
25122
+ } finally {
25123
+ set({ commandBusy: false, commandStatus: null });
25124
+ }
25125
+ },
24886
25126
  compact: async () => {
24887
25127
  if (getState().commandBusy) return null;
24888
25128
  if (getState().busy) {
@@ -25512,18 +25752,18 @@ function installProcessSignalCleanup({
25512
25752
 
25513
25753
  // src/runtime/channels/lib/runtime-paths.mjs
25514
25754
  import { tmpdir as tmpdir2 } from "os";
25515
- import { basename as basename4, join as join8, resolve as resolve5 } from "path";
25755
+ import { basename as basename4, join as join10, resolve as resolve5 } from "path";
25516
25756
 
25517
25757
  // src/runtime/channels/lib/state-file.mjs
25518
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync6, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
25758
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync7, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
25519
25759
  function ensureDir2(dirPath) {
25520
25760
  mkdirSync6(dirPath, { recursive: true });
25521
25761
  }
25522
25762
 
25523
25763
  // 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");
25764
+ var RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT ? resolve5(process.env.MIXDOG_RUNTIME_ROOT) : join10(tmpdir2(), "mixdog");
25765
+ var OWNER_DIR = join10(RUNTIME_ROOT, "owners");
25766
+ var ACTIVE_INSTANCE_FILE = join10(RUNTIME_ROOT, "active-instance.json");
25527
25767
  var RUNTIME_STALE_TTL = 24 * 60 * 60 * 1e3;
25528
25768
  var STATUS_FILE_TTL = 6 * 60 * 60 * 1e3;
25529
25769
  var TMP_FILE_TTL = 60 * 60 * 1e3;
@@ -25546,7 +25786,7 @@ function touchUiHeartbeat(instanceId) {
25546
25786
  } catch {
25547
25787
  }
25548
25788
  }
25549
- var SERVER_PID_FILE = join8(
25789
+ var SERVER_PID_FILE = join10(
25550
25790
  RUNTIME_ROOT,
25551
25791
  `server-${sanitize(resolvePluginData() ?? "default")}.pid`
25552
25792
  );
@@ -25766,7 +26006,7 @@ function scheduleHardExit(code = 0) {
25766
26006
  timer.unref?.();
25767
26007
  }
25768
26008
  function resolveTuiStderrLogPath() {
25769
- return process.env.MIXDOG_TUI_STDERR_LOG || join9(process.env.MIXDOG_RUNTIME_ROOT || join9(tmpdir3(), "mixdog"), "mixdog-tui.stderr.log");
26009
+ return process.env.MIXDOG_TUI_STDERR_LOG || join11(process.env.MIXDOG_RUNTIME_ROOT || join11(tmpdir3(), "mixdog"), "mixdog-tui.stderr.log");
25770
26010
  }
25771
26011
  function ansiFg(rgb) {
25772
26012
  const m = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/.exec(String(rgb || ""));
@@ -25848,7 +26088,7 @@ function installTuiStderrGuard() {
25848
26088
  const originalWrite = process.stderr.write.bind(process.stderr);
25849
26089
  const logPath = resolveTuiStderrLogPath();
25850
26090
  try {
25851
- mkdirSync7(dirname7(logPath), { recursive: true });
26091
+ mkdirSync7(dirname9(logPath), { recursive: true });
25852
26092
  } catch {
25853
26093
  }
25854
26094
  (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) {