mixdog 0.9.52 → 0.9.53
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/package.json +1 -1
- package/scripts/bench-run.mjs +2 -2
- package/scripts/compact-pressure-test.mjs +104 -0
- package/scripts/desktop-session-bridge-test.mjs +704 -0
- package/scripts/freevar-smoke.mjs +7 -4
- package/scripts/lifecycle-api-test.mjs +65 -4
- package/scripts/max-output-recovery-test.mjs +31 -0
- package/scripts/memory-core-input-test.mjs +10 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +63 -3
- package/scripts/openai-ws-early-settle-test.mjs +40 -0
- package/scripts/parent-abort-link-test.mjs +24 -0
- package/scripts/process-lifecycle-test.mjs +80 -22
- package/scripts/provider-contract-test.mjs +257 -0
- package/scripts/provider-toolcall-test.mjs +172 -10
- package/scripts/session-orphan-sweep-test.mjs +27 -1
- package/src/lib/keychain-cjs.cjs +36 -23
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -13
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
- package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -300
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -4
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +18 -266
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +18 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +15 -130
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +5 -115
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
- package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
- package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -71
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +47 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +72 -77
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -20
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +37 -28
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -7
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +20 -223
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -5
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +24 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +305 -127
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +22 -298
- package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -71
- package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
- package/src/runtime/channels/backends/discord.mjs +6 -6
- package/src/runtime/channels/lib/config.mjs +15 -2
- package/src/runtime/channels/lib/memory-client.mjs +20 -3
- package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
- package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
- package/src/runtime/channels/lib/worker-main.mjs +16 -5
- package/src/runtime/memory/index.mjs +24 -198
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
- package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +0 -4
- package/src/runtime/memory/lib/query-handlers.mjs +2 -122
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
- package/src/runtime/search/lib/http-fetch.mjs +1 -1
- package/src/runtime/shared/config.mjs +58 -13
- package/src/runtime/shared/llm/cost.mjs +14 -4
- package/src/runtime/shared/memory-snapshot.mjs +236 -0
- package/src/runtime/shared/process-lifecycle.mjs +92 -19
- package/src/runtime/shared/process-shutdown.mjs +6 -0
- package/src/runtime/shared/resource-admission.mjs +7 -2
- package/src/session-runtime/channel-config-api.mjs +7 -7
- package/src/session-runtime/config-lifecycle.mjs +20 -17
- package/src/session-runtime/cwd-plugins.mjs +9 -7
- package/src/session-runtime/env.mjs +1 -2
- package/src/session-runtime/hitch-profile.mjs +45 -0
- package/src/session-runtime/lifecycle-api.mjs +36 -9
- package/src/session-runtime/mcp-glue.mjs +6 -11
- package/src/session-runtime/provider-init-key.mjs +17 -0
- package/src/session-runtime/runtime-core.mjs +44 -103
- package/src/session-runtime/runtime-paths.mjs +20 -0
- package/src/session-runtime/runtime-tool-routing.mjs +55 -0
- package/src/session-runtime/tool-catalog-data.mjs +51 -0
- package/src/session-runtime/tool-catalog.mjs +15 -89
- package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
- package/src/standalone/agent-tool/worker-rows.mjs +93 -0
- package/src/standalone/agent-tool.mjs +12 -162
- package/src/standalone/channel-admin.mjs +29 -0
- package/src/tui/App.jsx +11 -5
- package/src/tui/dist/index.mjs +202 -65
- package/src/tui/engine/session-api-ext.mjs +37 -4
- package/src/tui/index.jsx +1 -0
- package/src/tui/lib/voice-setup.mjs +5 -5
- package/scripts/_devtools-stub.mjs +0 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -288
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildNotFoundHint,
|
|
3
|
+
finalizeReadFamilyEnoentTail,
|
|
4
|
+
tryReadFamilyEnoentRedirect,
|
|
5
|
+
} from '../search-path-diagnostics.mjs';
|
|
6
|
+
import { normalizeErrorMessage } from '../path-diagnostics.mjs';
|
|
7
|
+
import { isUncPath, isWindowsDevicePath, hasUnsafeWin32Component } from '../device-paths.mjs';
|
|
8
|
+
import { normalizeOutputPath } from '../path-utils.mjs';
|
|
9
|
+
|
|
10
|
+
/** undefined / invalid / negative → defaultCap; 0 = no page cap (absolute caps still apply). */
|
|
11
|
+
export async function readFamilyPathEnoentOrError(workDir, fullPath, inputPath, args, options, err, rerunTool) {
|
|
12
|
+
const redirected = await tryReadFamilyEnoentRedirect({
|
|
13
|
+
workDir,
|
|
14
|
+
resolvedPath: fullPath,
|
|
15
|
+
requestedPath: inputPath,
|
|
16
|
+
errCode: err?.code,
|
|
17
|
+
options,
|
|
18
|
+
rerun: (target, opts) => rerunTool({ ...args, path: target }, workDir, opts),
|
|
19
|
+
});
|
|
20
|
+
if (redirected) return redirected;
|
|
21
|
+
const msg = `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
22
|
+
const hint = buildNotFoundHint(workDir, fullPath, 'List', err?.code);
|
|
23
|
+
return msg + finalizeReadFamilyEnoentTail(hint, inputPath, err?.code);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function normalizeListHeadLimit(raw, defaultCap) {
|
|
27
|
+
if (raw === undefined || raw === null || raw === '') return defaultCap;
|
|
28
|
+
const n = Number(raw);
|
|
29
|
+
if (!Number.isFinite(n) || n < 0) return defaultCap;
|
|
30
|
+
return Math.floor(n);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// UNC / Windows-device / NTFS-ADS guard for directory-walking modes
|
|
34
|
+
// (list / tree / find). Walking a UNC share auto-authenticates to the
|
|
35
|
+
// remote host (NTLM hash leak); a raw-device / reserved-name path can
|
|
36
|
+
// hang or grant raw access. Mirrors the read path's string-based checks.
|
|
37
|
+
// Returns an Error string when the path is blocked, else null.
|
|
38
|
+
export function listGuardPath(p) {
|
|
39
|
+
if (typeof isUncPath === 'function' && isUncPath(p))
|
|
40
|
+
return `Error: cannot walk UNC / SMB path (network credential leak risk): ${normalizeOutputPath(p)}`;
|
|
41
|
+
if (typeof isWindowsDevicePath === 'function' && isWindowsDevicePath(p))
|
|
42
|
+
return `Error: cannot walk Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(p)}`;
|
|
43
|
+
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(p))
|
|
44
|
+
return `Error: cannot walk Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(p)}`;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { splitGrepLinePrefix } from '../grep-formatting.mjs';
|
|
2
|
+
|
|
3
|
+
const GREP_RESULT_LINE_SKIP = /^\[(?:Showing|total|pattern set|capped|warning|redirected|regex parse)/;
|
|
4
|
+
const GREP_CHUNK_AGGREGATE_FLOOR = 200;
|
|
5
|
+
const GREP_CHUNK_AGGREGATE_DEFAULT = 800;
|
|
6
|
+
const GREP_CHUNK_AGGREGATE_MAX = 4000;
|
|
7
|
+
|
|
8
|
+
export function chunkPatternList(patterns, cap) {
|
|
9
|
+
const out = [];
|
|
10
|
+
for (let i = 0; i < patterns.length; i += cap) out.push(patterns.slice(i, i + cap));
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function computeGrepChunkAggregateBudget(offset, headLimit, headLimitCoerced) {
|
|
15
|
+
if (headLimitCoerced === 0 && headLimit === Infinity) return GREP_CHUNK_AGGREGATE_MAX;
|
|
16
|
+
if (headLimit === Infinity) return GREP_CHUNK_AGGREGATE_DEFAULT;
|
|
17
|
+
const need = offset + headLimit;
|
|
18
|
+
return Math.min(GREP_CHUNK_AGGREGATE_MAX, Math.max(GREP_CHUNK_AGGREGATE_FLOOR, need * 2));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function compareGrepLinesByPathLine(a, b) {
|
|
22
|
+
const pa = splitGrepLinePrefix(a);
|
|
23
|
+
const pb = splitGrepLinePrefix(b);
|
|
24
|
+
if (!pa && !pb) return String(a).localeCompare(String(b));
|
|
25
|
+
if (!pa) return 1;
|
|
26
|
+
if (!pb) return -1;
|
|
27
|
+
const byPath = pa.path.localeCompare(pb.path);
|
|
28
|
+
if (byPath !== 0) return byPath;
|
|
29
|
+
return pa.lineNo - pb.lineNo;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function deriveGrepCountLinesFromMatchContent(lines) {
|
|
33
|
+
const byPath = new Map();
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
const split = splitGrepLinePrefix(line);
|
|
36
|
+
if (!split || split.delimiter !== ':') continue;
|
|
37
|
+
if (!byPath.has(split.path)) byPath.set(split.path, new Set());
|
|
38
|
+
byPath.get(split.path).add(split.lineNo);
|
|
39
|
+
}
|
|
40
|
+
return [...byPath.entries()]
|
|
41
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
42
|
+
.map(([path, lineNos]) => `${path}:${lineNos.size}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isGrepMatchLine(line) {
|
|
46
|
+
const split = splitGrepLinePrefix(line);
|
|
47
|
+
return !!(split && split.delimiter === ':');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function grepMatchAnchorKey(line) {
|
|
51
|
+
const split = splitGrepLinePrefix(line);
|
|
52
|
+
if (!split || split.delimiter !== ':') return '';
|
|
53
|
+
return `${split.path}\0${split.lineNo}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseGrepContextBlocksInSegment(segmentLines) {
|
|
57
|
+
const blocks = [];
|
|
58
|
+
let pending = [];
|
|
59
|
+
let i = 0;
|
|
60
|
+
while (i < segmentLines.length) {
|
|
61
|
+
const line = segmentLines[i];
|
|
62
|
+
if (isGrepMatchLine(line)) {
|
|
63
|
+
const blockLines = pending.concat([line]);
|
|
64
|
+
pending = [];
|
|
65
|
+
i += 1;
|
|
66
|
+
while (i < segmentLines.length) {
|
|
67
|
+
const next = segmentLines[i];
|
|
68
|
+
if (next === '--' || isGrepMatchLine(next)) break;
|
|
69
|
+
blockLines.push(next);
|
|
70
|
+
i += 1;
|
|
71
|
+
}
|
|
72
|
+
const anchor = grepMatchAnchorKey(line);
|
|
73
|
+
if (anchor) blocks.push({ anchor, lines: blockLines });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
pending.push(line);
|
|
77
|
+
i += 1;
|
|
78
|
+
}
|
|
79
|
+
return blocks;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function compareGrepAnchorKeys(a, b) {
|
|
83
|
+
const [pa, la] = String(a || '').split('\0');
|
|
84
|
+
const [pb, lb] = String(b || '').split('\0');
|
|
85
|
+
const byPath = pa.localeCompare(pb);
|
|
86
|
+
if (byPath !== 0) return byPath;
|
|
87
|
+
return Number(la) - Number(lb);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function mergeGrepContextChunkLines(lines) {
|
|
91
|
+
const segments = [];
|
|
92
|
+
let current = [];
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
if (line === '--') {
|
|
95
|
+
segments.push(current);
|
|
96
|
+
current = [];
|
|
97
|
+
} else {
|
|
98
|
+
current.push(line);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
segments.push(current);
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
const blocks = [];
|
|
104
|
+
for (const segment of segments) {
|
|
105
|
+
if (!segment.length) continue;
|
|
106
|
+
for (const block of parseGrepContextBlocksInSegment(segment)) {
|
|
107
|
+
if (!block.anchor || seen.has(block.anchor)) continue;
|
|
108
|
+
seen.add(block.anchor);
|
|
109
|
+
blocks.push(block);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
blocks.sort((a, b) => compareGrepAnchorKeys(a.anchor, b.anchor));
|
|
113
|
+
const out = [];
|
|
114
|
+
for (const block of blocks) {
|
|
115
|
+
if (out.length) out.push('--');
|
|
116
|
+
out.push(...block.lines);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function mergeGrepChunkLines(lines, { outputMode, beforeN, afterN, contextN }) {
|
|
122
|
+
const hasContext = (beforeN > 0 || afterN > 0 || contextN > 0);
|
|
123
|
+
if (outputMode === 'count') {
|
|
124
|
+
return deriveGrepCountLinesFromMatchContent(lines);
|
|
125
|
+
}
|
|
126
|
+
if (outputMode === 'files_with_matches') {
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
const out = [];
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
const path = String(line || '').trim();
|
|
131
|
+
if (!path || seen.has(path)) continue;
|
|
132
|
+
seen.add(path);
|
|
133
|
+
out.push(path);
|
|
134
|
+
}
|
|
135
|
+
out.sort((a, b) => a.localeCompare(b));
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
if (outputMode === 'content' && hasContext) {
|
|
139
|
+
return mergeGrepContextChunkLines(lines);
|
|
140
|
+
}
|
|
141
|
+
const matches = new Map();
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
const split = splitGrepLinePrefix(line);
|
|
144
|
+
if (split && split.delimiter === ':') {
|
|
145
|
+
const key = `${split.path}\0${split.lineNo}`;
|
|
146
|
+
if (!matches.has(key)) matches.set(key, line);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return [...matches.values()].sort(compareGrepLinesByPathLine);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function extractGrepChunkResultLines(body, room = Infinity) {
|
|
153
|
+
const text = String(body || '').trim();
|
|
154
|
+
if (!text || /^Error:/i.test(text)) return { error: text || 'Error: empty grep chunk result' };
|
|
155
|
+
if (/^\(no matches\)/i.test(text)) return { lines: [], truncated: false };
|
|
156
|
+
const rawLines = text.split('\n');
|
|
157
|
+
const childShowingTruncated = rawLines.some((line) => /^\[Showing /i.test(String(line || '').trim()));
|
|
158
|
+
const lines = rawLines.filter((line) => line && !GREP_RESULT_LINE_SKIP.test(line));
|
|
159
|
+
const truncated = childShowingTruncated
|
|
160
|
+
|| (Number.isFinite(room) && room >= 0 && lines.length >= room);
|
|
161
|
+
return { lines, truncated };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function buildGrepChunkMergePrefix(patternChunkCount, truncated, aggregateBudget, outputMode = 'content') {
|
|
165
|
+
if (patternChunkCount <= 1 && !truncated) return '';
|
|
166
|
+
const parts = [];
|
|
167
|
+
if (patternChunkCount > 1) parts.push(`pattern set split into ${patternChunkCount} chunks`);
|
|
168
|
+
if (truncated) {
|
|
169
|
+
parts.push(`chunk results truncated at aggregate budget ${aggregateBudget} lines; results are partial`);
|
|
170
|
+
if (outputMode === 'count') parts.push('counts are lower bounds (>=)');
|
|
171
|
+
}
|
|
172
|
+
return `[${parts.join('; ')}]\n`;
|
|
173
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { isAbsolute } from 'path';
|
|
2
|
+
import { canonicalizeGlobSlashes, normalizeOutputPath } from '../path-utils.mjs';
|
|
3
|
+
import {
|
|
4
|
+
relativePathPrefix,
|
|
5
|
+
} from '../search-path-diagnostics.mjs';
|
|
6
|
+
import {
|
|
7
|
+
normalizeGrepLine,
|
|
8
|
+
splitGrepCountPrefix,
|
|
9
|
+
splitGrepLinePrefix,
|
|
10
|
+
} from '../grep-formatting.mjs';
|
|
11
|
+
|
|
12
|
+
export function expandLegacyEscapedAlternationPattern(rawPattern) {
|
|
13
|
+
if (typeof rawPattern !== 'string' || !rawPattern.includes('\\|')) return null;
|
|
14
|
+
const parts = rawPattern.split('\\|').map((part) => part.trim()).filter(Boolean);
|
|
15
|
+
return parts.length > 1 ? parts : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function relativeGrepLine(line, workDir, pathOnly = false, outputMode = 'content', filenameOmitted = false) {
|
|
19
|
+
const normalized = normalizeGrepLine(line, pathOnly, outputMode, filenameOmitted);
|
|
20
|
+
if (!workDir) return normalized;
|
|
21
|
+
if (pathOnly) return relativePathPrefix(normalized, workDir);
|
|
22
|
+
if (filenameOmitted) return normalized;
|
|
23
|
+
const split = splitGrepLinePrefix(normalized);
|
|
24
|
+
if (split) {
|
|
25
|
+
return relativePathPrefix(normalized.slice(0, split.pathEnd), workDir) + normalized.slice(split.pathEnd);
|
|
26
|
+
}
|
|
27
|
+
if (outputMode === 'count') {
|
|
28
|
+
const countSplit = splitGrepCountPrefix(normalized);
|
|
29
|
+
if (countSplit) {
|
|
30
|
+
return relativePathPrefix(normalized.slice(0, countSplit.pathEnd), workDir) + normalized.slice(countSplit.pathEnd);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return normalized;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function uniqueStrings(values) {
|
|
37
|
+
return Array.from(new Set(values.filter((value) => typeof value === 'string' && value)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isRgRegexParseError(err) {
|
|
41
|
+
const msg = `${err?.stderr || ''}\n${err?.message || err || ''}`;
|
|
42
|
+
return /regex parse error/i.test(msg);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function regexPatternToFixedTerms(pattern) {
|
|
46
|
+
const raw = String(pattern || '');
|
|
47
|
+
if (!raw) return [];
|
|
48
|
+
return raw
|
|
49
|
+
.split(/\\?\|/g)
|
|
50
|
+
.map((part) => part.trim())
|
|
51
|
+
.map((part) => part
|
|
52
|
+
.replace(/\\[bB]/g, '')
|
|
53
|
+
.replace(/^\^/, '')
|
|
54
|
+
.replace(/\$$/, '')
|
|
55
|
+
.replace(/\\([\\.^$*+?()[\]{}|/-])/g, '$1')
|
|
56
|
+
.trim())
|
|
57
|
+
.filter((part) => part.length > 0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function coerceNonNegInt(value) {
|
|
61
|
+
if (value === undefined || value === null || value === '') return null;
|
|
62
|
+
const n = Number(value);
|
|
63
|
+
if (!Number.isFinite(n) || n < 0) return NaN;
|
|
64
|
+
return Math.floor(n);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function globMtimeTiePath(entry) {
|
|
68
|
+
const p = String(entry?.path ?? entry?.full ?? '');
|
|
69
|
+
return process.platform === 'win32' ? p.toLocaleLowerCase() : p;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function splitGlobString(value) {
|
|
73
|
+
const out = [];
|
|
74
|
+
const str = String(value);
|
|
75
|
+
let depth = 0;
|
|
76
|
+
let token = '';
|
|
77
|
+
const flush = () => {
|
|
78
|
+
const trimmed = token.trim();
|
|
79
|
+
if (trimmed) out.push(trimmed);
|
|
80
|
+
token = '';
|
|
81
|
+
};
|
|
82
|
+
for (const ch of str) {
|
|
83
|
+
if (ch === '{') {
|
|
84
|
+
depth++;
|
|
85
|
+
token += ch;
|
|
86
|
+
} else if (ch === '}') {
|
|
87
|
+
if (depth > 0) depth--;
|
|
88
|
+
token += ch;
|
|
89
|
+
} else if (depth === 0 && (ch === ',' || /\s/.test(ch))) {
|
|
90
|
+
flush();
|
|
91
|
+
} else {
|
|
92
|
+
token += ch;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
flush();
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function isRedundantAllFilesGlob(value) {
|
|
100
|
+
const g = canonicalizeGlobSlashes(String(value || '').trim())
|
|
101
|
+
.replace(/^\.\//, '')
|
|
102
|
+
.replace(/^\/+/, '')
|
|
103
|
+
.replace(/\/+$/, '');
|
|
104
|
+
return g === '**/*' || g === '**';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function parseGrepCountLine(line) {
|
|
108
|
+
const text = String(line || '');
|
|
109
|
+
const searchFrom = /^[A-Za-z]:/.test(text) ? 2 : 0;
|
|
110
|
+
const idx = text.lastIndexOf(':');
|
|
111
|
+
if (idx <= searchFrom) return null;
|
|
112
|
+
const count = Number(text.slice(idx + 1));
|
|
113
|
+
if (!Number.isFinite(count) || count <= 0) return null;
|
|
114
|
+
const path = text.slice(0, idx);
|
|
115
|
+
if (!path) return null;
|
|
116
|
+
return { path, count };
|
|
117
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, readSync, statSync } from 'fs';
|
|
2
|
+
import { stripAnsi } from '../../shell-command.mjs';
|
|
3
|
+
|
|
4
|
+
const JOB_STATUS_PREVIEW_MAX_BYTES = 4096;
|
|
5
|
+
const JOB_STATUS_PREVIEW_MAX_LINES = 20;
|
|
6
|
+
const JOB_STATUS_PREVIEW_MAX_CHARS = 1200;
|
|
7
|
+
// Hard ceiling on a background job's on-disk stdout+stderr. Mirrors the
|
|
8
|
+
// foreground SHELL_OUTPUT_DISK_CAP (shell-command.mjs) so a runaway
|
|
9
|
+
// background loop is killed and flagged instead of filling the filesystem.
|
|
10
|
+
export const SHELL_JOB_OUTPUT_DISK_CAP = 100 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
// Combined byte size of a job's spilled stdout/stderr files, or 0 if
|
|
13
|
+
// unreadable. mergeStderr collapses both onto stdoutPath, so count it once.
|
|
14
|
+
export function shellJobOutputBytes(detail) {
|
|
15
|
+
let total = 0;
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
for (const p of [detail?.stdoutPath, detail?.stderrPath]) {
|
|
18
|
+
if (!p || seen.has(p)) continue;
|
|
19
|
+
seen.add(p);
|
|
20
|
+
try {
|
|
21
|
+
if (existsSync(p)) total += statSync(p).size;
|
|
22
|
+
} catch { /* ignore */ }
|
|
23
|
+
}
|
|
24
|
+
return total;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readTailPreviewSync(filePath, { maxBytes = JOB_STATUS_PREVIEW_MAX_BYTES, maxLines = JOB_STATUS_PREVIEW_MAX_LINES, maxChars = JOB_STATUS_PREVIEW_MAX_CHARS } = {}) {
|
|
28
|
+
try {
|
|
29
|
+
if (!filePath || !existsSync(filePath)) return null;
|
|
30
|
+
const st = statSync(filePath);
|
|
31
|
+
if (!st.isFile()) return null;
|
|
32
|
+
const size = st.size;
|
|
33
|
+
if (size <= 0) return { bytes: 0, preview: '' };
|
|
34
|
+
const readBytes = Math.min(size, maxBytes);
|
|
35
|
+
const fd = openSync(filePath, 'r');
|
|
36
|
+
try {
|
|
37
|
+
const buf = Buffer.alloc(readBytes);
|
|
38
|
+
readSync(fd, buf, 0, readBytes, size - readBytes);
|
|
39
|
+
let text = buf.toString('utf8');
|
|
40
|
+
if (size > readBytes) {
|
|
41
|
+
const nl = text.indexOf('\n');
|
|
42
|
+
if (nl !== -1) text = text.slice(nl + 1);
|
|
43
|
+
}
|
|
44
|
+
let lines = text.split(/\r?\n/);
|
|
45
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
|
46
|
+
let truncated = size > readBytes;
|
|
47
|
+
if (lines.length > maxLines) {
|
|
48
|
+
lines = lines.slice(-maxLines);
|
|
49
|
+
truncated = true;
|
|
50
|
+
}
|
|
51
|
+
let preview = lines.join('\n');
|
|
52
|
+
if (preview.length > maxChars) {
|
|
53
|
+
preview = preview.slice(preview.length - maxChars);
|
|
54
|
+
const nl = preview.indexOf('\n');
|
|
55
|
+
if (nl !== -1) preview = preview.slice(nl + 1);
|
|
56
|
+
truncated = true;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
bytes: size,
|
|
60
|
+
preview,
|
|
61
|
+
truncated,
|
|
62
|
+
};
|
|
63
|
+
} finally {
|
|
64
|
+
try { closeSync(fd); } catch { /* ignore */ }
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function attachJobPreview(detail) {
|
|
72
|
+
if (!detail || typeof detail !== 'object') return detail;
|
|
73
|
+
const withPreview = { ...detail };
|
|
74
|
+
const stdoutInfo = readTailPreviewSync(detail.stdoutPath);
|
|
75
|
+
if (stdoutInfo) {
|
|
76
|
+
withPreview.stdoutBytes = stdoutInfo.bytes;
|
|
77
|
+
if (stdoutInfo.preview) withPreview.stdoutPreview = stdoutInfo.preview;
|
|
78
|
+
if (stdoutInfo.truncated) withPreview.stdoutPreviewTruncated = true;
|
|
79
|
+
}
|
|
80
|
+
if (detail.mergeStderr !== true) {
|
|
81
|
+
const stderrInfo = readTailPreviewSync(detail.stderrPath);
|
|
82
|
+
if (stderrInfo) {
|
|
83
|
+
withPreview.stderrBytes = stderrInfo.bytes;
|
|
84
|
+
if (stderrInfo.preview) withPreview.stderrPreview = stderrInfo.preview;
|
|
85
|
+
if (stderrInfo.truncated) withPreview.stderrPreviewTruncated = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return withPreview;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function summarizeJobPreviewText(text, maxChars = 160) {
|
|
92
|
+
if (typeof text !== 'string' || !text.trim()) return '';
|
|
93
|
+
const lines = text
|
|
94
|
+
.split(/\r?\n/)
|
|
95
|
+
.map((line) => stripAnsi(line).replace(/\s+/g, ' ').trim())
|
|
96
|
+
.filter(Boolean);
|
|
97
|
+
if (lines.length === 0) return '';
|
|
98
|
+
let summary = lines[lines.length - 1];
|
|
99
|
+
if (summary.length > maxChars) summary = `${summary.slice(0, maxChars - 1)}…`;
|
|
100
|
+
return summary;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const SHELL_JOB_PROMPT_TAIL_BYTES = 1024;
|
|
104
|
+
const SHELL_JOB_PROMPT_TAIL_LINES = 16;
|
|
105
|
+
const SHELL_JOB_PROMPT_TAIL_CHARS = 1024;
|
|
106
|
+
const SHELL_JOB_PROMPT_PATTERNS = [
|
|
107
|
+
/\((?:y|yes)\/(?:n|no)\)\s*[:?]?\s*$/i,
|
|
108
|
+
/\[(?:y|yes)\/(?:n|no)\]\s*[:?]?\s*$/i,
|
|
109
|
+
/\b(?:continue|proceed|confirm|overwrite|replace)\b[^\n]*[?:]\s*$/i,
|
|
110
|
+
/\bpress\s+(?:enter|return)\b[^\n]*$/i,
|
|
111
|
+
/\bdo you (?:want|wish|agree|accept)\b[^\n]*\?\s*$/i,
|
|
112
|
+
/\b(?:password|passphrase|otp|verification code)\b[^\n]*[:?]\s*$/i,
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
export function looksLikeInteractivePrompt(text) {
|
|
116
|
+
const tail = stripAnsi(String(text || '')).trim();
|
|
117
|
+
if (!tail) return false;
|
|
118
|
+
const last = tail.split(/\r?\n/).slice(-4).join('\n').trim();
|
|
119
|
+
return SHELL_JOB_PROMPT_PATTERNS.some((pattern) => pattern.test(last));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function readPromptTail(detail) {
|
|
123
|
+
if (!detail || typeof detail !== 'object') return { bytes: 0, text: '' };
|
|
124
|
+
const stdoutInfo = readTailPreviewSync(detail.stdoutPath, {
|
|
125
|
+
maxBytes: SHELL_JOB_PROMPT_TAIL_BYTES,
|
|
126
|
+
maxLines: SHELL_JOB_PROMPT_TAIL_LINES,
|
|
127
|
+
maxChars: SHELL_JOB_PROMPT_TAIL_CHARS,
|
|
128
|
+
});
|
|
129
|
+
const stderrInfo = detail.mergeStderr === true ? null : readTailPreviewSync(detail.stderrPath, {
|
|
130
|
+
maxBytes: SHELL_JOB_PROMPT_TAIL_BYTES,
|
|
131
|
+
maxLines: SHELL_JOB_PROMPT_TAIL_LINES,
|
|
132
|
+
maxChars: SHELL_JOB_PROMPT_TAIL_CHARS,
|
|
133
|
+
});
|
|
134
|
+
const bytes = (stdoutInfo?.bytes || 0) + (stderrInfo?.bytes || 0);
|
|
135
|
+
const parts = [
|
|
136
|
+
stdoutInfo?.preview ? `[stdout tail]\n${stdoutInfo.preview}` : '',
|
|
137
|
+
stderrInfo?.preview ? `[stderr tail]\n${stderrInfo.preview}` : '',
|
|
138
|
+
].filter(Boolean);
|
|
139
|
+
return { bytes, text: parts.join('\n\n') };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function attachJobInsights(detail) {
|
|
143
|
+
const withPreview = attachJobPreview(detail);
|
|
144
|
+
if (!withPreview || typeof withPreview !== 'object') return withPreview;
|
|
145
|
+
let summary = '';
|
|
146
|
+
let summarySource = '';
|
|
147
|
+
if (withPreview.status === 'completed') {
|
|
148
|
+
summary = summarizeJobPreviewText(withPreview.stdoutPreview)
|
|
149
|
+
|| summarizeJobPreviewText(withPreview.stderrPreview);
|
|
150
|
+
summarySource = summary ? (withPreview.stdoutPreview ? 'stdout' : 'stderr') : '';
|
|
151
|
+
} else if (withPreview.status === 'failed') {
|
|
152
|
+
summary = summarizeJobPreviewText(withPreview.stderrPreview)
|
|
153
|
+
|| summarizeJobPreviewText(withPreview.stdoutPreview)
|
|
154
|
+
|| String(withPreview.error || '').trim();
|
|
155
|
+
summarySource = summary ? (withPreview.stderrPreview ? 'stderr' : (withPreview.stdoutPreview ? 'stdout' : 'status')) : '';
|
|
156
|
+
} else if (withPreview.status === 'cancelled') {
|
|
157
|
+
summary = 'cancelled before completion';
|
|
158
|
+
summarySource = 'status';
|
|
159
|
+
} else if (withPreview.status === 'running') {
|
|
160
|
+
summary = summarizeJobPreviewText(withPreview.stdoutPreview)
|
|
161
|
+
|| summarizeJobPreviewText(withPreview.stderrPreview);
|
|
162
|
+
summarySource = summary ? (withPreview.stdoutPreview ? 'stdout' : 'stderr') : '';
|
|
163
|
+
}
|
|
164
|
+
if (summary) {
|
|
165
|
+
withPreview.summary = summary;
|
|
166
|
+
withPreview.summarySource = summarySource;
|
|
167
|
+
}
|
|
168
|
+
return withPreview;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function shellJobPublicTaskResult(detail) {
|
|
172
|
+
if (!detail || typeof detail !== 'object') return detail;
|
|
173
|
+
const result = {
|
|
174
|
+
task_id: detail.jobId || detail.task_id || null,
|
|
175
|
+
shell: detail.shellType || null,
|
|
176
|
+
status: detail.status || null,
|
|
177
|
+
cwd: detail.cwd || null,
|
|
178
|
+
pid: detail.pid || null,
|
|
179
|
+
exit_code: (typeof detail.exitCode === 'number') ? detail.exitCode : null,
|
|
180
|
+
signal: detail.signal || null,
|
|
181
|
+
timed_out: detail.timedOut === true ? true : null,
|
|
182
|
+
killed: detail.killed === true ? true : null,
|
|
183
|
+
stdout_bytes: (typeof detail.stdoutBytes === 'number') ? detail.stdoutBytes : null,
|
|
184
|
+
stderr_bytes: (typeof detail.stderrBytes === 'number') ? detail.stderrBytes : null,
|
|
185
|
+
stdout_preview: detail.stdoutPreview || null,
|
|
186
|
+
stderr_preview: detail.stderrPreview || null,
|
|
187
|
+
summary: detail.summary || null,
|
|
188
|
+
summary_source: detail.summarySource || null,
|
|
189
|
+
waited_ms: (typeof detail.waitedMs === 'number') ? detail.waitedMs : null,
|
|
190
|
+
wait_timed_out: detail.waitTimedOut === true ? true : null,
|
|
191
|
+
started_at: detail.startedAt || null,
|
|
192
|
+
finished_at: detail.finishedAt || null,
|
|
193
|
+
error: detail.error || null,
|
|
194
|
+
};
|
|
195
|
+
for (const [key, value] of Object.entries(result)) {
|
|
196
|
+
if (value == null || value === '') delete result[key];
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { basename } from 'path';
|
|
2
|
+
import { killProcessTree } from '../shell-job-process.mjs';
|
|
3
|
+
|
|
4
|
+
const SPAWN_ERROR_GUARD = Symbol('mixdog.spawnErrorGuard');
|
|
5
|
+
|
|
6
|
+
export function sleep(ms) {
|
|
7
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function awaitSpawnReady(child, label) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
if (!child || typeof child.once !== 'function') {
|
|
13
|
+
reject(new Error(`${label} spawn returned no child process`));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
let spawned = false;
|
|
17
|
+
let bufferedError = null;
|
|
18
|
+
const cleanup = () => child.removeListener('spawn', onSpawn);
|
|
19
|
+
const onError = (error) => {
|
|
20
|
+
if (spawned) {
|
|
21
|
+
bufferedError = bufferedError || error;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
cleanup();
|
|
25
|
+
child.removeListener('error', onError);
|
|
26
|
+
reject(error);
|
|
27
|
+
};
|
|
28
|
+
const onSpawn = () => {
|
|
29
|
+
cleanup();
|
|
30
|
+
spawned = true;
|
|
31
|
+
const pid = Number(child.pid);
|
|
32
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
33
|
+
child.removeListener('error', onError);
|
|
34
|
+
reject(new Error(`${label} spawn returned no pid`));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
child[SPAWN_ERROR_GUARD] = {
|
|
38
|
+
adopt(handler) {
|
|
39
|
+
child.on('error', handler);
|
|
40
|
+
child.removeListener('error', onError);
|
|
41
|
+
if (bufferedError) handler(bufferedError);
|
|
42
|
+
delete child[SPAWN_ERROR_GUARD];
|
|
43
|
+
},
|
|
44
|
+
discard() {
|
|
45
|
+
child.removeListener('error', onError);
|
|
46
|
+
delete child[SPAWN_ERROR_GUARD];
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
resolve(child);
|
|
50
|
+
};
|
|
51
|
+
child.once('error', onError);
|
|
52
|
+
child.once('spawn', onSpawn);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function adoptSpawnErrorHandler(child, handler) {
|
|
57
|
+
const guard = child?.[SPAWN_ERROR_GUARD];
|
|
58
|
+
if (guard) guard.adopt(handler);
|
|
59
|
+
else child?.on?.('error', handler);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function discardSpawnErrorGuard(child) {
|
|
63
|
+
child?.[SPAWN_ERROR_GUARD]?.discard?.();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function rollbackSpawnedChild(child, { timeoutMs = 5000 } = {}) {
|
|
67
|
+
if (!child || child.exitCode != null || child.signalCode != null) {
|
|
68
|
+
return { confirmed: true, errors: [] };
|
|
69
|
+
}
|
|
70
|
+
const errors = [];
|
|
71
|
+
let timer = null;
|
|
72
|
+
let settled = false;
|
|
73
|
+
const outcome = await new Promise((resolve) => {
|
|
74
|
+
const finish = (confirmed) => {
|
|
75
|
+
if (settled) return;
|
|
76
|
+
settled = true;
|
|
77
|
+
if (timer) clearTimeout(timer);
|
|
78
|
+
child.removeListener('exit', onExit);
|
|
79
|
+
child.removeListener('close', onExit);
|
|
80
|
+
child.removeListener('error', onError);
|
|
81
|
+
resolve({ confirmed, errors });
|
|
82
|
+
};
|
|
83
|
+
const onExit = () => finish(true);
|
|
84
|
+
const onError = (error) => { errors.push(error); };
|
|
85
|
+
child.on('error', onError);
|
|
86
|
+
child.once('exit', onExit);
|
|
87
|
+
child.once('close', onExit);
|
|
88
|
+
timer = setTimeout(() => finish(false), Math.max(1, Number(timeoutMs) || 5000));
|
|
89
|
+
try { killProcessTree(child.pid, 'SIGKILL'); } catch (error) { errors.push(error); }
|
|
90
|
+
try { child.kill?.('SIGKILL'); } catch (error) { errors.push(error); }
|
|
91
|
+
});
|
|
92
|
+
return outcome;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function shellQuoteSingle(s) {
|
|
96
|
+
return `'${String(s).replace(/'/g, `'\"'\"'`)}'`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function psSingleQuote(s) {
|
|
100
|
+
return `'${String(s).replace(/'/g, "''")}'`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isPowerShellShell(shell, shellType) {
|
|
104
|
+
if (shellType === 'powershell') return true;
|
|
105
|
+
const stem = basename(String(shell || '')).toLowerCase().replace(/\.exe$/, '');
|
|
106
|
+
return stem === 'pwsh' || stem === 'powershell';
|
|
107
|
+
}
|