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.
- package/README.md +7 -1
- package/package.json +3 -2
- package/scripts/channel-daemon-stub.mjs +2 -1
- package/src/defaults/agents.json +12 -0
- package/src/defaults/skills/setup/SKILL.md +44 -12
- package/src/help.mjs +40 -9
- package/src/lib/keychain-cjs.cjs +11 -1
- package/src/rules/agent/43-title-agent.md +22 -0
- package/src/rules/shared/01-tool.md +3 -3
- package/src/runtime/agent/orchestrator/mcp/client.mjs +24 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +84 -3
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +30 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +73 -45
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +15 -18
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +38 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +183 -41
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +31 -12
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +12 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +45 -6
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/token-bpe.mjs +42 -0
- package/src/runtime/agent/orchestrator/session/token-native.mjs +186 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +15 -5
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +44 -73
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +286 -0
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +152 -18
- package/src/runtime/agent/orchestrator/tools/token-binary-fetcher.mjs +201 -0
- package/src/runtime/agent/orchestrator/tools/token-manifest.json +5 -0
- package/src/runtime/channels/lib/owned-runtime.mjs +6 -8
- package/src/runtime/channels/lib/parent-bridge.mjs +27 -15
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +3 -3
- package/src/runtime/channels/lib/worker-ipc.mjs +17 -4
- package/src/runtime/media/adapters/gemini-video.mjs +1 -1
- package/src/runtime/media/renditions.mjs +1 -1
- package/src/runtime/media/store.mjs +1 -1
- package/src/runtime/memory/index.mjs +14 -5
- package/src/runtime/memory/lib/agent-ipc.mjs +11 -12
- package/src/runtime/shared/atomic-file.mjs +76 -38
- package/src/runtime/shared/automation-attachments.mjs +3 -3
- package/src/runtime/shared/child-guardian.mjs +15 -5
- package/src/runtime/shared/child-spawn-gate.mjs +6 -12
- package/src/runtime/shared/err-text.mjs +1 -0
- package/src/runtime/shared/resource-admission.mjs +7 -3
- package/src/runtime/shared/safe-ipc-send.mjs +34 -0
- package/src/runtime/shared/turn-snapshot.mjs +0 -3
- package/src/session-runtime/lifecycle-api.mjs +43 -12
- package/src/session-runtime/prewarm.mjs +35 -0
- package/src/session-runtime/remote-control.mjs +8 -47
- package/src/session-runtime/remote-transcript.mjs +3 -7
- package/src/session-runtime/route-preparation.mjs +24 -0
- package/src/session-runtime/runtime-core.mjs +42 -86
- package/src/session-runtime/session-turn-api.mjs +9 -2
- package/src/session-runtime/workflow-agents-api.mjs +3 -0
- package/src/standalone/agent-shard/shard-child.mjs +300 -0
- package/src/standalone/agent-shard/shard-pool.mjs +442 -0
- package/src/standalone/agent-tool.mjs +222 -0
- package/src/standalone/channel-daemon-transport.mjs +126 -185
- package/src/standalone/channel-daemon.mjs +11 -11
- package/src/standalone/channel-worker.mjs +21 -19
- package/src/standalone/memory-runtime-proxy.mjs +98 -23
- package/src/tui/App.jsx +16 -9
- package/src/tui/app/channel-pickers.mjs +3 -8
- package/src/tui/app/route-pickers.mjs +117 -152
- package/src/tui/app/settings-picker.mjs +90 -89
- package/src/tui/app/slash-commands.mjs +7 -7
- package/src/tui/app/slash-dispatch.mjs +23 -58
- package/src/tui/app/theme-effort-pickers.mjs +3 -1
- package/src/tui/components/StatusLine.jsx +1 -1
- package/src/tui/components/tool-execution/ResultBody.jsx +1 -1
- package/src/tui/dist/index.mjs +358 -380
- package/src/tui/engine/live-share.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +24 -9
- package/src/tui/engine/session-flow.mjs +20 -10
- package/src/tui/engine/turn.mjs +28 -16
- package/src/tui/engine.mjs +24 -2
- package/src/tui/figures.mjs +0 -1
- package/src/ui/statusline-format.mjs +0 -1
- package/src/hooks/lib/permission-evaluator.cjs +0 -24
- package/src/lib/config-cjs.cjs +0 -61
- package/src/runtime/shared/launcher-control.mjs +0 -258
- 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.
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
set({
|
|
453
|
-
|
|
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
|
-
|
|
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
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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);
|
package/src/tui/engine/turn.mjs
CHANGED
|
@@ -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({
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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;
|
package/src/tui/engine.mjs
CHANGED
|
@@ -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
|
-
|
|
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)) {
|
package/src/tui/figures.mjs
CHANGED
|
@@ -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;
|
package/src/lib/config-cjs.cjs
DELETED
|
@@ -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
|
-
}
|