mixdog 0.9.0 → 0.9.2

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 (240) hide show
  1. package/package.json +10 -3
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/background-task-meta-smoke.mjs +1 -1
  6. package/scripts/bench-run.mjs +262 -0
  7. package/scripts/compact-smoke.mjs +12 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  9. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  10. package/scripts/internal-comms-bench.mjs +727 -0
  11. package/scripts/internal-comms-smoke.mjs +75 -0
  12. package/scripts/lead-workflow-smoke.mjs +4 -4
  13. package/scripts/live-worker-smoke.mjs +9 -9
  14. package/scripts/output-style-bench.mjs +285 -0
  15. package/scripts/output-style-smoke.mjs +13 -10
  16. package/scripts/patch-replay.mjs +90 -0
  17. package/scripts/provider-stream-stall-test.mjs +276 -0
  18. package/scripts/provider-toolcall-test.mjs +599 -1
  19. package/scripts/routing-corpus.mjs +281 -0
  20. package/scripts/session-bench.mjs +1526 -0
  21. package/scripts/session-diag.mjs +595 -0
  22. package/scripts/session-ingest-smoke.mjs +2 -2
  23. package/scripts/task-bench.mjs +207 -0
  24. package/scripts/tool-failures.mjs +6 -6
  25. package/scripts/tool-smoke.mjs +306 -66
  26. package/scripts/toolcall-args-test.mjs +81 -0
  27. package/src/agents/debugger/AGENT.md +4 -4
  28. package/src/agents/heavy-worker/AGENT.md +4 -2
  29. package/src/agents/reviewer/AGENT.md +4 -4
  30. package/src/agents/worker/AGENT.md +4 -2
  31. package/src/app.mjs +10 -6
  32. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  33. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  34. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  35. package/src/headless-role.mjs +14 -14
  36. package/src/help.mjs +1 -0
  37. package/src/lib/mixdog-debug.cjs +0 -22
  38. package/src/lib/plugin-paths.cjs +1 -7
  39. package/src/lib/rules-builder.cjs +34 -56
  40. package/src/mixdog-session-runtime.mjs +710 -319
  41. package/src/output-styles/default.md +12 -7
  42. package/src/output-styles/minimal.md +25 -0
  43. package/src/output-styles/oneline.md +21 -0
  44. package/src/output-styles/simple.md +10 -9
  45. package/src/repl.mjs +12 -4
  46. package/src/rules/agent/00-common.md +7 -5
  47. package/src/rules/agent/30-explorer.md +7 -8
  48. package/src/rules/lead/01-general.md +3 -1
  49. package/src/rules/lead/lead-tool.md +7 -0
  50. package/src/rules/shared/01-tool.md +17 -12
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  52. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  54. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  55. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  56. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  57. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  59. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  60. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  61. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  62. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  65. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
  66. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
  67. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
  75. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  76. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  77. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  78. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
  79. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  80. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  81. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  82. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  83. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  84. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  85. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  86. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  87. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  92. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  99. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  104. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  105. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  106. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  107. package/src/runtime/channels/backends/discord.mjs +99 -9
  108. package/src/runtime/channels/backends/telegram.mjs +501 -0
  109. package/src/runtime/channels/index.mjs +441 -1254
  110. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  111. package/src/runtime/channels/lib/config.mjs +54 -3
  112. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  113. package/src/runtime/channels/lib/executor.mjs +0 -3
  114. package/src/runtime/channels/lib/format.mjs +4 -2
  115. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  116. package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
  117. package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
  118. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  119. package/src/runtime/channels/lib/session-discovery.mjs +0 -4
  120. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  121. package/src/runtime/channels/lib/tool-format.mjs +1 -2
  122. package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
  123. package/src/runtime/channels/lib/webhook.mjs +59 -31
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  126. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  127. package/src/runtime/memory/index.mjs +187 -43
  128. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  129. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  130. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  131. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  132. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  133. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  134. package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
  135. package/src/runtime/memory/lib/memory.mjs +101 -4
  136. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  137. package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
  138. package/src/runtime/memory/lib/session-ingest.mjs +116 -7
  139. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  140. package/src/runtime/memory/tool-defs.mjs +6 -3
  141. package/src/runtime/search/index.mjs +2 -7
  142. package/src/runtime/search/lib/config.mjs +0 -4
  143. package/src/runtime/search/lib/state.mjs +1 -15
  144. package/src/runtime/search/lib/web-tools.mjs +0 -1
  145. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  146. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  147. package/src/runtime/shared/child-spawn-gate.mjs +0 -6
  148. package/src/runtime/shared/config.mjs +9 -0
  149. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  150. package/src/runtime/shared/schedules-store.mjs +21 -19
  151. package/src/runtime/shared/tool-surface.mjs +98 -13
  152. package/src/runtime/shared/transcript-writer.mjs +129 -0
  153. package/src/runtime/shared/update-checker.mjs +214 -0
  154. package/src/standalone/agent-tool.mjs +255 -109
  155. package/src/standalone/channel-admin.mjs +133 -40
  156. package/src/standalone/channel-worker.mjs +8 -291
  157. package/src/standalone/explore-tool.mjs +2 -2
  158. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  159. package/src/standalone/provider-admin.mjs +11 -0
  160. package/src/standalone/seeds.mjs +1 -11
  161. package/src/standalone/usage-dashboard.mjs +1 -1
  162. package/src/tui/App.jsx +2137 -750
  163. package/src/tui/components/ConfirmBar.jsx +47 -0
  164. package/src/tui/components/ContextPanel.jsx +5 -3
  165. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  166. package/src/tui/components/Markdown.jsx +22 -98
  167. package/src/tui/components/Message.jsx +14 -35
  168. package/src/tui/components/Picker.jsx +87 -12
  169. package/src/tui/components/PromptInput.jsx +146 -9
  170. package/src/tui/components/QueuedCommands.jsx +1 -1
  171. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  172. package/src/tui/components/Spinner.jsx +7 -7
  173. package/src/tui/components/StatusLine.jsx +40 -21
  174. package/src/tui/components/TextEntryPanel.jsx +51 -7
  175. package/src/tui/components/ToolExecution.jsx +177 -100
  176. package/src/tui/components/TurnDone.jsx +4 -4
  177. package/src/tui/components/UsagePanel.jsx +1 -1
  178. package/src/tui/components/tool-output-format.mjs +312 -40
  179. package/src/tui/components/tool-output-format.test.mjs +180 -1
  180. package/src/tui/display-width.mjs +69 -0
  181. package/src/tui/display-width.test.mjs +35 -0
  182. package/src/tui/dist/index.mjs +7324 -2393
  183. package/src/tui/engine.mjs +287 -126
  184. package/src/tui/index.jsx +117 -7
  185. package/src/tui/keyboard-protocol.mjs +42 -0
  186. package/src/tui/lib/voice-recorder.mjs +453 -0
  187. package/src/tui/markdown/format-token.mjs +354 -142
  188. package/src/tui/markdown/format-token.test.mjs +155 -17
  189. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  190. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  191. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  192. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  193. package/src/tui/markdown/table-layout.mjs +9 -9
  194. package/src/tui/paste-attachments.mjs +0 -11
  195. package/src/tui/prompt-history-store.mjs +129 -0
  196. package/src/tui/prompt-history-store.test.mjs +52 -0
  197. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  198. package/src/tui/theme.mjs +41 -647
  199. package/src/tui/themes/base.mjs +86 -0
  200. package/src/tui/themes/basic.mjs +85 -0
  201. package/src/tui/themes/catppuccin.mjs +72 -0
  202. package/src/tui/themes/dracula.mjs +70 -0
  203. package/src/tui/themes/everforest.mjs +71 -0
  204. package/src/tui/themes/gruvbox.mjs +71 -0
  205. package/src/tui/themes/index.mjs +71 -0
  206. package/src/tui/themes/indigo.mjs +78 -0
  207. package/src/tui/themes/kanagawa.mjs +80 -0
  208. package/src/tui/themes/light.mjs +81 -0
  209. package/src/tui/themes/nord.mjs +72 -0
  210. package/src/tui/themes/onedark.mjs +16 -0
  211. package/src/tui/themes/rosepine.mjs +70 -0
  212. package/src/tui/themes/teal.mjs +81 -0
  213. package/src/tui/themes/tokyonight.mjs +79 -0
  214. package/src/tui/themes/utils.mjs +106 -0
  215. package/src/tui/themes/warm.mjs +79 -0
  216. package/src/tui/transcript-tool-failures.mjs +13 -2
  217. package/src/ui/markdown.mjs +1 -1
  218. package/src/ui/model-display.mjs +2 -2
  219. package/src/ui/statusline.mjs +26 -27
  220. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
  221. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  222. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  223. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  224. package/src/workflows/default/WORKFLOW.md +39 -12
  225. package/src/workflows/sequential/WORKFLOW.md +46 -0
  226. package/src/workflows/solo/WORKFLOW.md +7 -0
  227. package/vendor/ink/build/display-width.js +62 -0
  228. package/vendor/ink/build/ink.js +154 -20
  229. package/vendor/ink/build/measure-text.js +4 -1
  230. package/vendor/ink/build/output.js +115 -9
  231. package/vendor/ink/build/render-node-to-output.js +4 -1
  232. package/vendor/ink/build/render.js +4 -0
  233. package/src/hooks/lib/permission-rules.cjs +0 -170
  234. package/src/hooks/lib/settings-loader.cjs +0 -112
  235. package/src/lib/hook-pipe-path.cjs +0 -10
  236. package/src/output-styles/extreme-simple.md +0 -20
  237. package/src/rules/lead/04-workflow.md +0 -51
  238. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  239. package/src/workflows/default/workflow.json +0 -13
  240. package/src/workflows/solo/workflow.json +0 -7
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Transcript writer for remote (Discord) mode.
3
+ *
4
+ * The channel worker's OutputForwarder tails a newline-delimited JSON
5
+ * transcript and forwards the "surface" view (assistant text + one-line tool
6
+ * summaries) to Discord. Nothing wrote that file for standalone sessions, so
7
+ * remote outbound never worked. This module writes the JSONL in the exact
8
+ * schema the forwarder parses (see channels/lib/output-forwarder.mjs
9
+ * extractNewText) plus a session record the forwarder's discovery reads
10
+ * (channels/lib/session-discovery.mjs readSessionRecord).
11
+ *
12
+ * All writes are best-effort: a failure must never break the ask() turn. We
13
+ * log the FIRST occurrence of each distinct failure string to stderr (prefix
14
+ * `mixdog: transcript-writer: `) and suppress duplicates so a broken path
15
+ * cannot spam the terminal.
16
+ */
17
+ import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs';
18
+ import { dirname, join, resolve } from 'node:path';
19
+
20
+ // Byte-identical to cwdToProjectSlug() in
21
+ // src/runtime/channels/lib/session-discovery.mjs. Inlined to avoid a
22
+ // shared -> channels import coupling; keep the two in sync if either changes.
23
+ function cwdToProjectSlug(cwd) {
24
+ return resolve(cwd).replace(/\\/g, '/').replace(/^([A-Za-z]):/, '$1-').replace(/\//g, '-');
25
+ }
26
+
27
+ export function createTranscriptWriter({ mixdogHome, sessionId, cwd, pid } = {}) {
28
+ if (!mixdogHome) throw new Error('transcript-writer: mixdogHome is required');
29
+ if (!sessionId) throw new Error('transcript-writer: sessionId is required');
30
+ if (!cwd) throw new Error('transcript-writer: cwd is required');
31
+
32
+ const resolvedCwd = resolve(cwd);
33
+ const projectDir = join(mixdogHome, 'projects', cwdToProjectSlug(resolvedCwd));
34
+ const transcriptPath = join(projectDir, `${sessionId}.jsonl`);
35
+ const sessionRecordPath = join(mixdogHome, 'sessions', `${pid ?? process.pid}.json`);
36
+ const startedAt = Date.now();
37
+
38
+ // Repeated-failure guard: log the first time each distinct message is seen,
39
+ // then stay quiet so a persistently-broken path does not flood stderr.
40
+ const seenErrors = new Set();
41
+ function logOnce(err) {
42
+ const msg = err && err.message ? err.message : String(err);
43
+ if (seenErrors.has(msg)) return;
44
+ seenErrors.add(msg);
45
+ try { process.stderr.write(`mixdog: transcript-writer: ${msg}\n`); } catch { /* stderr broken */ }
46
+ }
47
+
48
+ let projectDirReady = false;
49
+ function ensureProjectDir() {
50
+ if (projectDirReady) return;
51
+ try {
52
+ mkdirSync(projectDir, { recursive: true });
53
+ projectDirReady = true;
54
+ } catch (err) {
55
+ logOnce(err);
56
+ }
57
+ }
58
+
59
+ function appendLine(entry) {
60
+ ensureProjectDir();
61
+ try {
62
+ appendFileSync(transcriptPath, `${JSON.stringify(entry)}\n`);
63
+ } catch (err) {
64
+ logOnce(err);
65
+ }
66
+ }
67
+
68
+ function writeSessionRecord() {
69
+ try {
70
+ mkdirSync(dirname(sessionRecordPath), { recursive: true });
71
+ writeFileSync(sessionRecordPath, JSON.stringify({
72
+ sessionId,
73
+ cwd: resolvedCwd,
74
+ transcriptPath,
75
+ startedAt,
76
+ updatedAt: Date.now(),
77
+ kind: 'interactive',
78
+ entrypoint: 'cli',
79
+ }));
80
+ } catch (err) {
81
+ logOnce(err);
82
+ }
83
+ }
84
+
85
+ // Refresh only the session record's updatedAt so discovery keeps ranking
86
+ // this session as live across long-lived remote sessions.
87
+ function refresh() {
88
+ writeSessionRecord();
89
+ }
90
+
91
+ function appendAssistant(text) {
92
+ const value = typeof text === 'string' ? text : (text == null ? '' : String(text));
93
+ if (!value.trim()) return;
94
+ appendLine({
95
+ type: 'assistant',
96
+ sessionId,
97
+ message: { content: [{ type: 'text', text: value }] },
98
+ });
99
+ }
100
+
101
+ function appendToolUse(name, input) {
102
+ if (!name) return;
103
+ appendLine({
104
+ type: 'assistant',
105
+ sessionId,
106
+ message: { content: [{ type: 'tool_use', name, input: input || {} }] },
107
+ });
108
+ }
109
+
110
+ function appendToolResult(toolUseResult) {
111
+ if (!toolUseResult) return;
112
+ appendLine({
113
+ type: 'user',
114
+ sessionId,
115
+ message: { content: [{ type: 'tool_result' }] },
116
+ toolUseResult,
117
+ });
118
+ }
119
+
120
+ return {
121
+ transcriptPath,
122
+ sessionRecordPath,
123
+ writeSessionRecord,
124
+ refresh,
125
+ appendAssistant,
126
+ appendToolUse,
127
+ appendToolResult,
128
+ };
129
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * update-checker.mjs — npm registry version check + self-update for the
3
+ * globally-installed `mixdog` CLI package.
4
+ *
5
+ * checkLatestVersion({force}) hits the npm registry for the `latest`
6
+ * dist-tag, compares against the local package.json version, and caches
7
+ * the result on disk (TTL 24h) so repeated calls (e.g. on every TUI mount)
8
+ * don't hammer the registry. Network/parse failures are silent — this is a
9
+ * best-effort convenience check, never a boot-blocking dependency.
10
+ *
11
+ * runGlobalUpdate() shells out to `npm install -g mixdog@latest` in the
12
+ * background (windowsHide so no console flash on Windows) and resolves once
13
+ * the child exits, reporting the resolved version on success.
14
+ */
15
+
16
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
17
+ import { dirname, join } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { spawn } from 'node:child_process';
20
+ import { resolvePluginData } from './plugin-paths.mjs';
21
+
22
+ const PACKAGE_NAME = 'mixdog';
23
+ const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
24
+ const FETCH_TIMEOUT_MS = 5_000;
25
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
26
+ const CACHE_FILE_NAME = 'update-check-cache.json';
27
+
28
+ // Resolve the local package.json shipped alongside src/ (two levels up from
29
+ // src/runtime/shared/).
30
+ const _MODULE_DIR = dirname(fileURLToPath(import.meta.url));
31
+ const _PACKAGE_JSON_PATH = join(_MODULE_DIR, '..', '..', '..', 'package.json');
32
+
33
+ let _localVersionCache = null;
34
+ export function localPackageVersion() {
35
+ if (_localVersionCache) return _localVersionCache;
36
+ try {
37
+ const raw = JSON.parse(readFileSync(_PACKAGE_JSON_PATH, 'utf8'));
38
+ _localVersionCache = String(raw?.version || '0.0.0');
39
+ } catch {
40
+ _localVersionCache = '0.0.0';
41
+ }
42
+ return _localVersionCache;
43
+ }
44
+
45
+ function cacheFilePath(dataDir) {
46
+ const dir = dataDir || resolvePluginData();
47
+ return join(dir, CACHE_FILE_NAME);
48
+ }
49
+
50
+ function readCache(dataDir) {
51
+ try {
52
+ const raw = JSON.parse(readFileSync(cacheFilePath(dataDir), 'utf8'));
53
+ if (!raw || typeof raw !== 'object') return null;
54
+ return raw;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ function writeCache(dataDir, payload) {
61
+ try {
62
+ const dir = dataDir || resolvePluginData();
63
+ mkdirSync(dir, { recursive: true });
64
+ writeFileSync(cacheFilePath(dataDir), JSON.stringify(payload), 'utf8');
65
+ } catch {
66
+ // Best-effort — cache miss just means the next call re-fetches.
67
+ }
68
+ }
69
+
70
+ /** Parse a semver-ish string into { parts:[maj,min,patch], prerelease }. */
71
+ function parseSemver(value) {
72
+ const text = String(value || '').trim().replace(/^v/i, '');
73
+ const [core, ...preParts] = text.split('-');
74
+ const parts = core.split('.').map((n) => {
75
+ const num = Number.parseInt(n, 10);
76
+ return Number.isFinite(num) ? num : 0;
77
+ });
78
+ while (parts.length < 3) parts.push(0);
79
+ return { parts, prerelease: preParts.join('-') };
80
+ }
81
+
82
+ /**
83
+ * Compare two semver strings. Returns positive if `a` > `b`, negative if
84
+ * `a` < `b`, 0 if equal. A version with a prerelease tag is considered
85
+ * lower than the same core version without one (standard semver rule).
86
+ */
87
+ export function compareSemver(a, b) {
88
+ const pa = parseSemver(a);
89
+ const pb = parseSemver(b);
90
+ for (let i = 0; i < 3; i++) {
91
+ const diff = (pa.parts[i] || 0) - (pb.parts[i] || 0);
92
+ if (diff !== 0) return diff;
93
+ }
94
+ if (pa.prerelease && !pb.prerelease) return -1;
95
+ if (!pa.prerelease && pb.prerelease) return 1;
96
+ if (pa.prerelease !== pb.prerelease) return pa.prerelease < pb.prerelease ? -1 : 1;
97
+ return 0;
98
+ }
99
+
100
+ export function isNewerVersion(latest, current) {
101
+ if (!latest || !current) return false;
102
+ return compareSemver(latest, current) > 0;
103
+ }
104
+
105
+ async function fetchLatestFromRegistry() {
106
+ const controller = new AbortController();
107
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
108
+ try {
109
+ const res = await fetch(REGISTRY_URL, { signal: controller.signal });
110
+ if (!res.ok) return null;
111
+ const body = await res.json();
112
+ const version = body && typeof body === 'object' ? String(body.version || '').trim() : '';
113
+ return version || null;
114
+ } catch {
115
+ return null;
116
+ } finally {
117
+ clearTimeout(timer);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * checkLatestVersion({force, dataDir}) — resolve the latest published
123
+ * version of the `mixdog` npm package.
124
+ *
125
+ * Returns:
126
+ * { currentVersion, latestVersion, updateAvailable, lastCheckedAt, fromCache }
127
+ * `latestVersion` is null when the registry could not be reached and no
128
+ * usable cache exists — this call never throws.
129
+ */
130
+ export async function checkLatestVersion({ force = false, dataDir } = {}) {
131
+ const currentVersion = localPackageVersion();
132
+ const cache = force ? null : readCache(dataDir);
133
+ const now = Date.now();
134
+ if (cache && typeof cache.lastCheckedAt === 'number' && now - cache.lastCheckedAt < CACHE_TTL_MS) {
135
+ return {
136
+ currentVersion,
137
+ latestVersion: cache.latestVersion || null,
138
+ updateAvailable: isNewerVersion(cache.latestVersion, currentVersion),
139
+ lastCheckedAt: cache.lastCheckedAt,
140
+ fromCache: true,
141
+ };
142
+ }
143
+ const latestVersion = await fetchLatestFromRegistry();
144
+ if (latestVersion) {
145
+ const payload = { latestVersion, lastCheckedAt: now };
146
+ writeCache(dataDir, payload);
147
+ return {
148
+ currentVersion,
149
+ latestVersion,
150
+ updateAvailable: isNewerVersion(latestVersion, currentVersion),
151
+ lastCheckedAt: now,
152
+ fromCache: false,
153
+ };
154
+ }
155
+ // Registry unreachable: fall back to a stale cache (if any) rather than
156
+ // reporting "no update info" every time the network hiccups.
157
+ if (cache && cache.latestVersion) {
158
+ return {
159
+ currentVersion,
160
+ latestVersion: cache.latestVersion,
161
+ updateAvailable: isNewerVersion(cache.latestVersion, currentVersion),
162
+ lastCheckedAt: cache.lastCheckedAt || now,
163
+ fromCache: true,
164
+ };
165
+ }
166
+ return {
167
+ currentVersion,
168
+ latestVersion: null,
169
+ updateAvailable: false,
170
+ lastCheckedAt: now,
171
+ fromCache: false,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * runGlobalUpdate() — `npm install -g mixdog@latest` in a background child
177
+ * process (windowsHide so no console flash on Windows). Resolves once the
178
+ * child exits; never throws — failures come back as {ok:false, error}.
179
+ */
180
+ export function runGlobalUpdate() {
181
+ return new Promise((resolvePromise) => {
182
+ let child;
183
+ try {
184
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
185
+ child = spawn(npmCmd, ['install', '-g', `${PACKAGE_NAME}@latest`], {
186
+ stdio: 'ignore',
187
+ windowsHide: true,
188
+ shell: process.platform === 'win32',
189
+ });
190
+ } catch (err) {
191
+ resolvePromise({ ok: false, error: err?.message || String(err) });
192
+ return;
193
+ }
194
+ child.once('error', (err) => {
195
+ resolvePromise({ ok: false, error: err?.message || String(err) });
196
+ });
197
+ // Detach from the event-loop keep-alive so a boot-time auto-update can
198
+ // never hold the TUI/node process open after the user quits; the exit
199
+ // listener still fires as long as the process is otherwise alive.
200
+ child.unref?.();
201
+ child.once('exit', async (code) => {
202
+ if (code === 0) {
203
+ // Best-effort: report whichever version the registry now reports as
204
+ // latest (the just-installed one), falling back to 'unknown'.
205
+ const version = await fetchLatestFromRegistry();
206
+ resolvePromise({ ok: true, version: version || 'unknown' });
207
+ } else {
208
+ resolvePromise({ ok: false, error: `npm install exited with code ${code}` });
209
+ }
210
+ });
211
+ });
212
+ }
213
+
214
+ export { PACKAGE_NAME as UPDATE_PACKAGE_NAME };