mixdog 0.9.84 → 0.9.86

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 (91) hide show
  1. package/README.md +7 -1
  2. package/package.json +3 -2
  3. package/scripts/channel-daemon-stub.mjs +2 -1
  4. package/src/defaults/agents.json +12 -0
  5. package/src/defaults/skills/setup/SKILL.md +44 -12
  6. package/src/help.mjs +40 -9
  7. package/src/lib/keychain-cjs.cjs +11 -1
  8. package/src/rules/agent/43-title-agent.md +22 -0
  9. package/src/rules/shared/01-tool.md +3 -3
  10. package/src/runtime/agent/orchestrator/mcp/client.mjs +24 -0
  11. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +84 -3
  12. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -2
  13. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +30 -3
  14. package/src/runtime/agent/orchestrator/session/context-utils.mjs +73 -45
  15. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +15 -18
  16. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +38 -4
  18. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -0
  19. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +183 -41
  20. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +31 -12
  21. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +12 -2
  22. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -0
  23. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +170 -0
  24. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +45 -6
  25. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +26 -0
  26. package/src/runtime/agent/orchestrator/session/token-bpe.mjs +42 -0
  27. package/src/runtime/agent/orchestrator/session/token-native.mjs +186 -0
  28. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +15 -5
  29. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +20 -0
  30. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +24 -0
  32. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +44 -73
  33. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  34. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +286 -0
  35. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  36. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +152 -18
  37. package/src/runtime/agent/orchestrator/tools/token-binary-fetcher.mjs +201 -0
  38. package/src/runtime/agent/orchestrator/tools/token-manifest.json +5 -0
  39. package/src/runtime/channels/lib/owned-runtime.mjs +6 -8
  40. package/src/runtime/channels/lib/parent-bridge.mjs +27 -15
  41. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +3 -3
  42. package/src/runtime/channels/lib/worker-ipc.mjs +17 -4
  43. package/src/runtime/media/adapters/gemini-video.mjs +1 -1
  44. package/src/runtime/media/renditions.mjs +1 -1
  45. package/src/runtime/media/store.mjs +1 -1
  46. package/src/runtime/memory/index.mjs +14 -5
  47. package/src/runtime/memory/lib/agent-ipc.mjs +11 -12
  48. package/src/runtime/shared/atomic-file.mjs +76 -38
  49. package/src/runtime/shared/automation-attachments.mjs +3 -3
  50. package/src/runtime/shared/child-guardian.mjs +15 -5
  51. package/src/runtime/shared/child-spawn-gate.mjs +6 -12
  52. package/src/runtime/shared/err-text.mjs +1 -0
  53. package/src/runtime/shared/resource-admission.mjs +7 -3
  54. package/src/runtime/shared/safe-ipc-send.mjs +34 -0
  55. package/src/runtime/shared/turn-snapshot.mjs +0 -3
  56. package/src/session-runtime/lifecycle-api.mjs +43 -12
  57. package/src/session-runtime/prewarm.mjs +35 -0
  58. package/src/session-runtime/remote-control.mjs +8 -47
  59. package/src/session-runtime/remote-transcript.mjs +3 -7
  60. package/src/session-runtime/route-preparation.mjs +24 -0
  61. package/src/session-runtime/runtime-core.mjs +42 -86
  62. package/src/session-runtime/session-turn-api.mjs +9 -2
  63. package/src/session-runtime/workflow-agents-api.mjs +3 -0
  64. package/src/standalone/agent-shard/shard-child.mjs +300 -0
  65. package/src/standalone/agent-shard/shard-pool.mjs +442 -0
  66. package/src/standalone/agent-tool.mjs +222 -0
  67. package/src/standalone/channel-daemon-transport.mjs +126 -185
  68. package/src/standalone/channel-daemon.mjs +11 -11
  69. package/src/standalone/channel-worker.mjs +21 -19
  70. package/src/standalone/memory-runtime-proxy.mjs +98 -23
  71. package/src/tui/App.jsx +16 -9
  72. package/src/tui/app/channel-pickers.mjs +3 -8
  73. package/src/tui/app/route-pickers.mjs +117 -152
  74. package/src/tui/app/settings-picker.mjs +90 -89
  75. package/src/tui/app/slash-commands.mjs +7 -7
  76. package/src/tui/app/slash-dispatch.mjs +23 -58
  77. package/src/tui/app/theme-effort-pickers.mjs +3 -1
  78. package/src/tui/components/StatusLine.jsx +1 -1
  79. package/src/tui/components/tool-execution/ResultBody.jsx +1 -1
  80. package/src/tui/dist/index.mjs +358 -380
  81. package/src/tui/engine/live-share.mjs +2 -2
  82. package/src/tui/engine/session-api-ext.mjs +24 -9
  83. package/src/tui/engine/session-flow.mjs +20 -10
  84. package/src/tui/engine/turn.mjs +28 -16
  85. package/src/tui/engine.mjs +24 -2
  86. package/src/tui/figures.mjs +0 -1
  87. package/src/ui/statusline-format.mjs +0 -1
  88. package/src/hooks/lib/permission-evaluator.cjs +0 -24
  89. package/src/lib/config-cjs.cjs +0 -61
  90. package/src/runtime/shared/launcher-control.mjs +0 -258
  91. package/src/runtime/shared/workspace-router.mjs +0 -259
@@ -446,7 +446,7 @@ export function createLiveShare({
446
446
  const applyViewerFrame = (frame, socket) => {
447
447
  if (frame.t === 'full') {
448
448
  viewerApply.replaceItems(Array.isArray(frame.items) ? frame.items : []);
449
- if (frame.tail) viewerApply.updateStreamingTail(frame.tail.id, frame.tail);
449
+ if (frame.tail) viewerApply.updateStreamingTail(frame.tail.id, frame.tail, {}, { resetText: true });
450
450
  viewerApply.set({ spinner: frame.spinner || null });
451
451
  applyLiveState(frame.live);
452
452
  return;
@@ -472,7 +472,7 @@ export function createLiveShare({
472
472
  requestSync(socket);
473
473
  }
474
474
  } else if ('tail' in frame) {
475
- if (frame.tail) viewerApply.updateStreamingTail(frame.tail.id, frame.tail);
475
+ if (frame.tail) viewerApply.updateStreamingTail(frame.tail.id, frame.tail, {}, { resetText: true });
476
476
  else viewerApply.clearStreamingTail();
477
477
  }
478
478
  if ('spinner' in frame) viewerApply.set({ spinner: frame.spinner || null });
@@ -432,9 +432,10 @@ export function createEngineApiB(bag) {
432
432
  return next;
433
433
  }
434
434
  if (enabled && owner && currentId && owner !== currentId) {
435
- runtime.claimRemoteForCurrentSession?.();
436
- set({ remoteEnabled: true, remoteSessionId: runtime.getRemoteSessionId?.() || null });
437
- return true;
435
+ await runtime.startRemote?.();
436
+ const next = runtime.isRemoteEnabled?.() === true;
437
+ set({ remoteEnabled: next, remoteSessionId: runtime.getRemoteSessionId?.() || null });
438
+ return next;
438
439
  }
439
440
  if (enabled) await runtime.releaseRemote?.();
440
441
  else await runtime.startRemote?.();
@@ -447,17 +448,31 @@ export function createEngineApiB(bag) {
447
448
  // superseded and flips itself OFF via onRemoteStateChange. Used by the
448
449
  // `/remote` slash command — repeated /remote just re-claims (idempotent).
449
450
  claimRemote: async () => {
450
- await runtime.startRemote?.();
451
- const next = runtime.isRemoteEnabled?.() === true;
452
- set({ remoteEnabled: next, remoteSessionId: runtime.getRemoteSessionId?.() || null });
453
- return next;
451
+ // Desired state paints before daemon cold boot. The final read in
452
+ // `finally` reconciles a skipped/failed claim to the backend truth.
453
+ set({
454
+ remoteEnabled: true,
455
+ remoteSessionId: runtime.getRemoteSessionId?.() || getState().sessionId || null,
456
+ });
457
+ try {
458
+ await runtime.startRemote?.();
459
+ } finally {
460
+ const next = runtime.isRemoteEnabled?.() === true;
461
+ set({ remoteEnabled: next, remoteSessionId: next ? runtime.getRemoteSessionId?.() || null : null });
462
+ }
463
+ return runtime.isRemoteEnabled?.() === true;
454
464
  },
455
465
  // Idempotent desired-state OFF for desktop controls. Unlike toggleRemote,
456
466
  // a delayed/replayed request can never turn remote back on.
457
467
  releaseRemote: async () => {
458
- await runtime.releaseRemote?.();
459
468
  set({ remoteEnabled: false, remoteSessionId: null });
460
- return false;
469
+ try {
470
+ await runtime.releaseRemote?.();
471
+ } finally {
472
+ const next = runtime.isRemoteEnabled?.() === true;
473
+ set({ remoteEnabled: next, remoteSessionId: next ? runtime.getRemoteSessionId?.() || null : null });
474
+ }
475
+ return runtime.isRemoteEnabled?.() === true;
461
476
  },
462
477
  isRemoteEnabled: () => runtime.isRemoteEnabled?.() === true,
463
478
  getVoiceStatus: () => getVoiceStatus(),
@@ -43,8 +43,12 @@ export function createSessionFlow(bag) {
43
43
  const mode = options.mode || 'prompt';
44
44
  const priority = options.priority || defaultQueuePriority(mode);
45
45
  const displayText = promptDisplayText(text, options);
46
+ const submittedAt = Number(options.submittedAt);
46
47
  return {
47
48
  id: options.id || nextId(),
49
+ submittedAt: Number.isFinite(submittedAt) && submittedAt > 0
50
+ ? Math.round(submittedAt)
51
+ : Date.now(),
48
52
  text: displayText,
49
53
  content: text,
50
54
  pastedImages: options.pastedImages && typeof options.pastedImages === 'object' ? options.pastedImages : null,
@@ -283,6 +287,8 @@ export function createSessionFlow(bag) {
283
287
  set({ queued: [...getState().queued, entry] });
284
288
  if (isQueuedEntryEditable(entry)) flushEmitImmediate?.();
285
289
  }
290
+ const queueAgeMs = Math.max(0, Date.now() - Number(entry.submittedAt || Date.now()));
291
+ tuiDebug(`prompt-queued id=${String(entry.id)} busy=${getState().busy ? 1 : 0} ageMs=${queueAgeMs}`);
286
292
  if (getState().busy) tuiDebug(`busy-queue enqueue mode=${entry.mode} pending=${pending.length}`);
287
293
  void drain();
288
294
  return true;
@@ -309,24 +315,28 @@ export function createSessionFlow(bag) {
309
315
  if (batch.length === 0) break;
310
316
  for (const entry of batch) {
311
317
  const content = entry.content;
318
+ const steeringMeta = {
319
+ id: entry.id,
320
+ submittedAt: entry.submittedAt,
321
+ ...(Array.isArray(entry.images) && entry.images.length ? { images: entry.images } : {}),
322
+ };
312
323
  if (entry.suppressDisplay) {
313
324
  // Model-visible twin of an already-rendered live completion: deliver
314
325
  // content to the model but flag onSteerMessage to skip the duplicate
315
326
  // transcript card (no fall-back to content-derived display text).
316
327
  if (Array.isArray(content) ? content.length > 0 : String(content ?? '').trim().length > 0) {
317
- out.push({ text: STEERING_SUPPRESSED_DISPLAY, content });
328
+ out.push({ ...steeringMeta, text: STEERING_SUPPRESSED_DISPLAY, content });
318
329
  }
319
330
  continue;
320
331
  }
321
- const value = typeof content === 'string'
322
- ? content.trim()
323
- : { text: String(entry.text || '').trim(), content };
324
- if (typeof value === 'string') {
325
- if (value.length > 0) out.push(value);
326
- } else if (Array.isArray(value.content)) {
327
- if (value.content.length > 0) out.push(value);
328
- } else if (String(value.content ?? '').trim().length > 0) {
329
- out.push(value);
332
+ const displayText = String(entry.text || '').trim();
333
+ if (typeof content === 'string') {
334
+ const value = content.trim();
335
+ if (value.length > 0) out.push({ ...steeringMeta, text: displayText || value, content: value });
336
+ } else if (Array.isArray(content)) {
337
+ if (content.length > 0) out.push({ ...steeringMeta, text: displayText, content });
338
+ } else if (String(content ?? '').trim().length > 0) {
339
+ out.push({ ...steeringMeta, text: displayText, content });
330
340
  }
331
341
  }
332
342
  commitSteeringQueueEntries(batch);
@@ -17,8 +17,11 @@ export function createRunTurn(bag) {
17
17
  } = bag;
18
18
  // Small fallbacks keep isolated createRunTurn harnesses source-compatible;
19
19
  // the real engine supplies atomic implementations that also maintain revision.
20
- const updateStreamingTail = updateStreamingTailFromStore || ((id, patch = {}) => {
21
- set({ streamingTail: { ...(getState().streamingTail || {}), ...patch, kind: 'assistant', id, streaming: true } });
20
+ const updateStreamingTail = updateStreamingTailFromStore || ((id, patch = {}, extra = {}) => {
21
+ set({
22
+ streamingTail: { ...(getState().streamingTail || {}), ...patch, kind: 'assistant', id, streaming: true },
23
+ ...extra,
24
+ });
22
25
  return true;
23
26
  });
24
27
  const settleStreamingTail = settleStreamingTailFromStore || ((id, patch = {}) => {
@@ -671,7 +674,12 @@ export function createRunTurn(bag) {
671
674
  patch.spinner = { ...getState().spinner, responseLength: responseLengthVal, thinking: false, thinkingLastEndedAt: _pendingThinkingLastEndedAt || getState().spinner.thinkingLastEndedAt, mode: compactingActive ? 'compacting' : 'responding' };
672
675
  _publishedThinkingActive = false;
673
676
  }
674
- if (Object.keys(patch).length > 0) set(patch);
677
+ if (patch.streamingTail) {
678
+ const { streamingTail, ...extra } = patch;
679
+ updateStreamingTail(streamingTail.id, streamingTail, extra);
680
+ } else if (Object.keys(patch).length > 0) {
681
+ set(patch);
682
+ }
675
683
  _pendingThinkingLastEndedAt = 0;
676
684
  }
677
685
  if (_pendingThinkFlush) {
@@ -800,7 +808,7 @@ export function createRunTurn(bag) {
800
808
  onStreamDelta: () => {
801
809
  markTurnProgress('stream-delta');
802
810
  },
803
- onSteerMessage: (text) => {
811
+ onSteerMessage: (text, steeringMeta = {}) => {
804
812
  if (!markTurnProgress('steer-message')) return;
805
813
  // A suppressed live-completion twin is model-visible only; its
806
814
  // Response card was already pushed at delivery time. Skip the
@@ -826,7 +834,17 @@ export function createRunTurn(bag) {
826
834
  // same-category tool call opens a fresh card instead of reusing
827
835
  // one whose count would then change ABOVE this steered user item.
828
836
  clearAggregateContinuation();
829
- pushUserOrSyntheticItem(value, undefined, 'injected');
837
+ const steeringIds = Array.isArray(steeringMeta?.ids)
838
+ ? steeringMeta.ids.filter((id) => id !== undefined && id !== null)
839
+ : [];
840
+ pushUserOrSyntheticItem(
841
+ value,
842
+ steeringIds[0],
843
+ 'injected',
844
+ Array.isArray(steeringMeta?.images) && steeringMeta.images.length
845
+ ? { images: steeringMeta.images }
846
+ : null,
847
+ );
830
848
  }
831
849
  },
832
850
  onToolCall: async (_iter, calls) => {
@@ -1091,17 +1109,11 @@ export function createRunTurn(bag) {
1091
1109
  _emittedVisibleText = '';
1092
1110
  if (currentAssistantId) {
1093
1111
  if (currentAssistantText) {
1094
- set({
1095
- streamingTail: {
1096
- ...(getState().streamingTail || {}),
1097
- kind: 'assistant',
1098
- id: currentAssistantId,
1099
- text: currentAssistantText,
1100
- streaming: true,
1101
- at: getState().streamingTail?.at || Date.now(),
1102
- ...turnRouteMeta,
1103
- },
1104
- });
1112
+ updateStreamingTail(currentAssistantId, {
1113
+ text: currentAssistantText,
1114
+ at: getState().streamingTail?.at || Date.now(),
1115
+ ...turnRouteMeta,
1116
+ }, {}, { resetText: true });
1105
1117
  } else {
1106
1118
  clearStreamingTail(currentAssistantId);
1107
1119
  currentAssistantId = null;
@@ -345,6 +345,12 @@ export async function createEngineSession({
345
345
  const commandBusyReleased = state.commandBusy === true
346
346
  && Object.prototype.hasOwnProperty.call(patch, 'commandBusy')
347
347
  && patch.commandBusy === false;
348
+ // Some recovery and externally-driven settlement paths release busy
349
+ // without returning through drain's own finally. Re-kick centrally so a
350
+ // prompt accepted during the old turn cannot remain queued indefinitely.
351
+ const busyReleased = state.busy === true
352
+ && Object.prototype.hasOwnProperty.call(patch, 'busy')
353
+ && patch.busy === false;
348
354
  state = { ...state, ...effectivePatch };
349
355
  if (requestsStructureChange) publisher.markStructureChange();
350
356
  emit();
@@ -353,7 +359,7 @@ export async function createEngineSession({
353
359
  if (effectivePatch.commandStatus || effectivePatch.toolApproval) {
354
360
  flushEmitImmediate();
355
361
  }
356
- if (commandBusyReleased) queueMicrotask(() => { void bag.drain?.(); });
362
+ if (commandBusyReleased || busyReleased) queueMicrotask(() => { void bag.drain?.(); });
357
363
  return true;
358
364
  };
359
365
 
@@ -537,12 +543,28 @@ export async function createEngineSession({
537
543
  ...extra,
538
544
  });
539
545
  };
540
- const updateStreamingTail = (id, patch = {}, extra = {}) => {
546
+ // Non-enumerable so the marker never enters persisted transcript items,
547
+ // live-share JSON, or renderer snapshots. The desktop host reads the shared
548
+ // Symbol.for key before cloning and uses it to prove that a growing text is
549
+ // an append, avoiding an O(total text) startsWith check on every frame.
550
+ const streamingTailTextEpochKey = Symbol.for('mixdog.streaming-tail-text-epoch');
551
+ let streamingTailTextEpoch = 0;
552
+ const updateStreamingTail = (id, patch = {}, extra = {}, { resetText = false } = {}) => {
541
553
  if (id == null) return false;
542
554
  const current = state.streamingTail?.id === id
543
555
  ? state.streamingTail
544
556
  : { kind: 'assistant', id, text: '', streaming: true };
545
557
  const next = { ...current, ...patch, kind: 'assistant', id, streaming: true };
558
+ const currentTextEpoch = current[streamingTailTextEpochKey];
559
+ const textEpoch = !resetText && Number.isSafeInteger(currentTextEpoch)
560
+ ? currentTextEpoch
561
+ : ++streamingTailTextEpoch;
562
+ Object.defineProperty(next, streamingTailTextEpochKey, {
563
+ value: textEpoch,
564
+ enumerable: false,
565
+ configurable: false,
566
+ writable: false,
567
+ });
546
568
  let changed = state.streamingTail !== current;
547
569
  if (!changed) {
548
570
  for (const [key, value] of Object.entries(next)) {
@@ -27,7 +27,6 @@ export const RESULT_GUTTER_CONT_GLYPH = '\u2502'; // │ - tool-result continuat
27
27
 
28
28
  export const BLOCKQUOTE_BAR = '\u258e'; // ▎ - blockquote line prefix
29
29
 
30
- export const THEREFORE = '\u2234'; // ∴ - collapsed-thinking prefix (∴ Thinking)
31
30
  // Turn-complete marker — kept distinct from live-thinking's therefore sign.
32
31
  export const TURN_DONE_MARKER = '\u25c8'; // ◈ - turn-complete marker
33
32
 
@@ -26,7 +26,6 @@ export const D = colorEnabled() ? rgbSgr(136, 136, 136) : '';
26
26
  export const GRN = colorEnabled() ? rgbSgr(0, 170, 75) : '';
27
27
  export const YLW = colorEnabled() ? rgbSgr(255, 193, 7) : '';
28
28
  export const RED = colorEnabled() ? rgbSgr(220, 70, 88) : '';
29
- export const GREY = colorEnabled() ? rgbSgr(136, 136, 136) : '';
30
29
 
31
30
  export function terminalColumns() {
32
31
  const cols = Number(process.stdout?.columns);
@@ -1,24 +0,0 @@
1
- 'use strict';
2
- /**
3
- * permission-evaluator.cjs
4
- *
5
- * Permission enforcement has been removed by user request: every tool call is
6
- * trusted. This module retains only the `evaluatePermission` entry point for
7
- * API compatibility with callers (PreToolUse hook / hook-pipe-server,
8
- * session manager, agent loop). It unconditionally returns an `allow`
9
- * decision — no path-based hard-deny, no secret-read deny, no settings.json
10
- * allow/deny rules, no permission-mode sandbox.
11
- *
12
- * Exported function:
13
- * evaluatePermission({ toolName }) → { decision: 'allow', reason: string }
14
- *
15
- * The 'deny'/'ask' decision types are no longer produced; the return shape is
16
- * kept as `{ decision, reason }` so existing consumers keep working.
17
- */
18
-
19
- function evaluatePermission({ toolName } = {}) {
20
- const name = typeof toolName === 'string' ? toolName : '';
21
- return { decision: 'allow', reason: `permissions disabled: trust/allow (${name})` };
22
- }
23
-
24
- module.exports.evaluatePermission = evaluatePermission;
@@ -1,61 +0,0 @@
1
- 'use strict';
2
- /**
3
- * CJS shim for shared config reading in hooks.
4
- *
5
- * Read-only mirror of src/shared/config.mjs's section accessor. Hook
6
- * processes only read mixdog-config.json — never write, never rename.
7
- */
8
-
9
- const fs = require('fs');
10
- const path = require('path');
11
- const { resolvePluginData } = require('./plugin-paths.cjs');
12
- const { getSecret: _getSecret } = require('./keychain-cjs.cjs');
13
-
14
- const DATA_DIR = resolvePluginData();
15
-
16
- const GENERATED_KEY = '_generated';
17
-
18
- function stripGeneratedMarker(data) {
19
- if (!data || typeof data !== 'object' || Array.isArray(data)) return data;
20
- if (!Object.prototype.hasOwnProperty.call(data, GENERATED_KEY)) return data;
21
- const { [GENERATED_KEY]: _unused, ...rest } = data;
22
- return rest;
23
- }
24
-
25
- function readJsonFile(filePath) {
26
- try {
27
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
28
- } catch {
29
- return null;
30
- }
31
- }
32
-
33
- function readSection(section) {
34
- const unified = readJsonFile(path.join(DATA_DIR, 'mixdog-config.json'));
35
- if (!unified || typeof unified !== 'object') return {};
36
- const raw = unified[section];
37
- if (raw == null) return {};
38
- return stripGeneratedMarker(raw) || {};
39
- }
40
-
41
- // ── Secret-aware getters (CJS, for hooks) ────────────────────────────────────
42
- // Read order: ENV MIXDOG_<UPPER_SNAKE> → OS keychain → null.
43
-
44
- function _envKey(account) {
45
- return 'MIXDOG_' + account.replace(/[.\s]+/g, '_').toUpperCase();
46
- }
47
-
48
- function _readSecret(account) {
49
- const envVal = process.env[_envKey(account)];
50
- if (envVal) return envVal;
51
- try { return _getSecret(account); } catch { return null; }
52
- }
53
-
54
- function getDiscordToken() {
55
- return _readSecret('discord.token');
56
- }
57
-
58
- module.exports = {
59
- readSection,
60
- getDiscordToken,
61
- };
@@ -1,258 +0,0 @@
1
- // Cross-platform managed-launcher control channel.
2
- //
3
- // Replaces the Windows-only AttachConsole + WriteConsoleInputW front-injection
4
- // path. A `mixdog`-launched session can be "managed": the launcher
5
- // (setup/mixdog.mjs) owns the child process and exposes a file-based
6
- // command queue under the runtime root. Internal commands (/clear,
7
- // /reload-plugins) are enqueued by the MCP server (inject_input)
8
- // and the launcher delivers them to the child's stdin — no terminal keystroke
9
- // injection, no new window. Works on Windows / macOS / Linux.
10
- //
11
- // Native backend (CANONICAL): Rust is canonical for mixdog's native
12
- // functionality. The enqueue/drain primitives and the managed terminal bridge
13
- // are owned by the Rust crate native/mixdog-launch. The pure-JS queue path below
14
- // is only a wire-compatible fallback for queue writes/drains when the binary has
15
- // not landed; managed terminal ownership never falls back to JS/node-pty.
16
- //
17
- // Wire-up:
18
- // • The launcher sets MIXDOG_LAUNCH_ID (and MIXDOG_MANAGED_LAUNCH=1) on the
19
- // child env ONLY when it has actually engaged the stdin control bridge.
20
- // • The MCP server (a descendant of the child) inherits that env, so
21
- // managedLaunchId() is the single source of truth for "am I managed?".
22
- // • enqueueLauncherCommand() publishes one command file atomically; the
23
- // launcher's watchLauncherCommands() drains + delivers them in order.
24
-
25
- import {
26
- mkdirSync,
27
- readdirSync,
28
- readFileSync,
29
- writeFileSync,
30
- rmSync,
31
- renameSync,
32
- existsSync,
33
- statSync,
34
- } from 'node:fs';
35
- import { spawnSync } from 'node:child_process';
36
- import { tmpdir } from 'node:os';
37
- import { dirname, join, resolve } from 'node:path';
38
- import { fileURLToPath } from 'node:url';
39
- import { randomUUID } from 'node:crypto';
40
-
41
- const ENV_LAUNCH_ID = 'MIXDOG_LAUNCH_ID';
42
- const ENV_MANAGED = 'MIXDOG_MANAGED_LAUNCH';
43
-
44
- // 0x1E (ASCII Record Separator): how `mixdog-launch drain` joins multiple
45
- // command bodies on stdout so embedded newlines in a command survive transport.
46
- const RECORD_SEP = '\x1e';
47
-
48
- // Names the env vars the launcher must set on the child so this module's
49
- // managedLaunchId() resolves inside the MCP server.
50
-
51
- // Mirror of channels/lib/runtime-paths.mjs RUNTIME_ROOT resolution so the
52
- // launcher and the MCP server agree on the queue location without importing
53
- // the channels layer into setup/.
54
- function launcherRuntimeRoot() {
55
- return process.env.MIXDOG_RUNTIME_ROOT
56
- ? resolve(process.env.MIXDOG_RUNTIME_ROOT)
57
- : join(tmpdir(), 'mixdog');
58
- }
59
-
60
- function sanitize(value) {
61
- return String(value).replace(/[^a-zA-Z0-9._-]/g, '_');
62
- }
63
-
64
- function launcherQueueDir(launchId) {
65
- return join(launcherRuntimeRoot(), `launch-cmds-${sanitize(launchId)}`);
66
- }
67
-
68
- // ── Native backend resolution (canonical) ──────────────────────────────────
69
- // Locate the Rust mixdog-launch binary built by dev-sync / cargo. Resolution
70
- // order: explicit override → fresh live build → local cargo release build →
71
- // shipped prebuilt.
72
- // Returns an absolute path when present, else null (JS fallback engages).
73
- const PLUGIN_ROOT = process.env.MIXDOG_ROOT
74
- || resolve(dirname(fileURLToPath(import.meta.url)), '../..');
75
- const NATIVE_LAUNCH_DEFAULT_BIN = join(
76
- PLUGIN_ROOT,
77
- 'native/mixdog-launch/target/release',
78
- process.platform === 'win32' ? 'mixdog-launch.exe' : 'mixdog-launch',
79
- );
80
- const NATIVE_LAUNCH_LIVE_BIN = join(
81
- PLUGIN_ROOT,
82
- 'native/mixdog-launch/target-live/release',
83
- process.platform === 'win32' ? 'mixdog-launch.exe' : 'mixdog-launch',
84
- );
85
-
86
- function nativeLaunchSourceMtime() {
87
- let mtime = 0;
88
- for (const rel of ['native/mixdog-launch/Cargo.toml', 'native/mixdog-launch/src/main.rs']) {
89
- try { mtime = Math.max(mtime, statSync(join(PLUGIN_ROOT, rel)).mtimeMs); } catch {}
90
- }
91
- return mtime;
92
- }
93
-
94
- function freshNativeLaunchBin(path) {
95
- try {
96
- const binMtime = statSync(path).mtimeMs;
97
- const srcMtime = nativeLaunchSourceMtime();
98
- return binMtime > 0 && binMtime >= srcMtime ? path : null;
99
- } catch {
100
- return null;
101
- }
102
- }
103
-
104
- // Shipped prebuilt path under native/prebuilt/<os-arch>/mixdog-launch(.exe)
105
- function nativeLaunchPrebuiltPath() {
106
- const arch =
107
- process.arch === 'x64' ? 'x86_64'
108
- : process.arch === 'arm64' ? 'aarch64'
109
- : process.arch === 'ia32' ? 'i686'
110
- : process.arch;
111
- const os =
112
- process.platform === 'win32' ? 'windows'
113
- : process.platform === 'darwin' ? 'macos'
114
- : 'linux';
115
- const ext = process.platform === 'win32' ? '.exe' : '';
116
- return join(PLUGIN_ROOT, 'native', 'prebuilt', `${os}-${arch}`, `mixdog-launch${ext}`);
117
- }
118
-
119
- let _nativeBinCache; // undefined = unresolved, string|null = resolved
120
- function nativeLaunchBin() {
121
- if (_nativeBinCache !== undefined) return _nativeBinCache;
122
- // Opt-out for tests / forced-JS parity checks.
123
- if (/^(0|false|no|off|js)$/i.test(String(process.env.MIXDOG_LAUNCH_NATIVE || ''))) {
124
- _nativeBinCache = null;
125
- return _nativeBinCache;
126
- }
127
- const override = process.env.MIXDOG_LAUNCH_NATIVE_BIN;
128
- if (override && existsSync(override)) { _nativeBinCache = override; return _nativeBinCache; }
129
- const live = freshNativeLaunchBin(NATIVE_LAUNCH_LIVE_BIN);
130
- if (live) { _nativeBinCache = live; return _nativeBinCache; }
131
- const release = freshNativeLaunchBin(NATIVE_LAUNCH_DEFAULT_BIN);
132
- if (release) { _nativeBinCache = release; return _nativeBinCache; }
133
- // Shipped prebuilt fallback (under native/prebuilt/<os-arch>/)
134
- const prebuilt = nativeLaunchPrebuiltPath();
135
- if (prebuilt && existsSync(prebuilt)) { _nativeBinCache = prebuilt; return _nativeBinCache; }
136
- _nativeBinCache = null;
137
- return _nativeBinCache;
138
- }
139
-
140
- // Returns true when the native mixdog-launch binary's `bridge` subcommand is
141
- // available for this platform (exit 0). The launcher uses this to gate the
142
- // managed-launch interactive path — no JS stdin pipe bridge fallback.
143
- function nativeBridgeAvailable() {
144
- const bin = nativeLaunchBin();
145
- if (!bin) return false;
146
- const r = spawnSync(bin, ['bridge', '--probe'], {
147
- encoding: 'utf8', windowsHide: true,
148
- stdio: ['ignore', 'pipe', 'pipe'],
149
- });
150
- return r.status === 0;
151
- }
152
-
153
- function newLaunchId() {
154
- return `${process.pid}-${randomUUID().slice(0, 8)}`;
155
- }
156
-
157
- // MCP-server side: returns the managed launch id when this process belongs to a
158
- // `mixdog`-launched session that engaged the control bridge, else null. The
159
- // launcher only sets MIXDOG_LAUNCH_ID once the bridge is live, so presence is
160
- // sufficient — there is no half-managed state to disambiguate.
161
- function managedLaunchId() {
162
- const id = process.env[ENV_LAUNCH_ID];
163
- return id && String(id).length > 0 ? String(id) : null;
164
- }
165
-
166
- let enqueueSequence = 0;
167
-
168
- // Writer side (inject_input): publish one command for the
169
- // launcher to deliver. Prefers the canonical Rust backend; the JS writer (temp
170
- // name + rename so the watcher never sees a partial file) is the fallback.
171
- // Returns the published path.
172
- function enqueueLauncherCommand(launchId, command) {
173
- const dir = launcherQueueDir(launchId);
174
- mkdirSync(dir, { recursive: true });
175
- const bin = nativeLaunchBin();
176
- if (bin) {
177
- const r = spawnSync(bin, ['enqueue', '--dir', dir, '--command', String(command)], {
178
- encoding: 'utf8', windowsHide: true,
179
- });
180
- if (r.status === 0) {
181
- const out = String(r.stdout || '').trim();
182
- if (out) return out;
183
- }
184
- // Native spawn failed / non-zero → fall through to the JS writer.
185
- }
186
- const seq = String(enqueueSequence++).padStart(6, '0');
187
- const base = `${Date.now()}-${seq}-${randomUUID().slice(0, 8)}.cmd`;
188
- const tmp = join(dir, `.${base}.tmp`);
189
- const dst = join(dir, base);
190
- writeFileSync(tmp, String(command), 'utf8');
191
- renameSync(tmp, dst);
192
- return dst;
193
- }
194
-
195
- // Launcher side: drain queued commands oldest-first (lexical sort on the
196
- // timestamp-prefixed name), deleting each as it is read. Prefers the canonical
197
- // Rust backend; the JS reader is the fallback.
198
- function drainLauncherCommands(launchId) {
199
- const dir = launcherQueueDir(launchId);
200
- const bin = nativeLaunchBin();
201
- if (bin) {
202
- const r = spawnSync(bin, ['drain', '--dir', dir], {
203
- encoding: 'utf8', windowsHide: true,
204
- });
205
- if (r.status === 0) {
206
- const out = String(r.stdout || '');
207
- return out.length === 0 ? [] : out.split(RECORD_SEP);
208
- }
209
- // Native spawn failed / non-zero → fall through to the JS reader.
210
- }
211
- let entries;
212
- try {
213
- entries = readdirSync(dir);
214
- } catch {
215
- return [];
216
- }
217
- const out = [];
218
- for (const name of entries.filter((f) => f.endsWith('.cmd')).sort()) {
219
- const full = join(dir, name);
220
- let text;
221
- try {
222
- text = readFileSync(full, 'utf8');
223
- } catch {
224
- continue;
225
- }
226
- try {
227
- rmSync(full, { force: true });
228
- } catch {}
229
- out.push(text);
230
- }
231
- return out;
232
- }
233
-
234
- // Launcher side: poll-based watcher. fs.watch semantics differ enough across
235
- // platforms (and miss atomic-rename publishes on some) that a short poll is the
236
- // robust choice for this low-rate control path. Returns a stop() that also
237
- // removes the queue directory.
238
- function watchLauncherCommands(launchId, onCommand, { intervalMs = 150 } = {}) {
239
- mkdirSync(launcherQueueDir(launchId), { recursive: true });
240
- let stopped = false;
241
- const timer = setInterval(() => {
242
- if (stopped) return;
243
- for (const cmd of drainLauncherCommands(launchId)) {
244
- try {
245
- onCommand(cmd);
246
- } catch {}
247
- }
248
- }, intervalMs);
249
- if (typeof timer.unref === 'function') timer.unref();
250
- return () => {
251
- if (stopped) return;
252
- stopped = true;
253
- clearInterval(timer);
254
- try {
255
- rmSync(launcherQueueDir(launchId), { recursive: true, force: true });
256
- } catch {}
257
- };
258
- }