mixdog 0.9.51 → 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 +5 -3
- package/scripts/abort-recovery-test.mjs +17 -1
- package/scripts/agent-model-liveness-test.mjs +79 -2
- package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
- package/scripts/anthropic-transport-policy-test.mjs +466 -0
- package/scripts/atomic-lock-tryonce-test.mjs +60 -1
- package/scripts/bench-run.mjs +2 -2
- package/scripts/build-tui.mjs +13 -1
- package/scripts/channel-daemon-smoke.mjs +630 -10
- package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
- package/scripts/code-graph-disk-hit-test.mjs +11 -0
- package/scripts/code-graph-root-federation-test.mjs +273 -0
- package/scripts/compact-pressure-test.mjs +159 -1
- package/scripts/compact-smoke.mjs +80 -0
- package/scripts/context-mcp-metering-test.mjs +1350 -19
- package/scripts/deferred-tool-loading-test.mjs +17 -0
- package/scripts/desktop-session-bridge-test.mjs +704 -0
- package/scripts/freevar-smoke.mjs +7 -4
- package/scripts/gemini-provider-test.mjs +1053 -0
- package/scripts/interrupted-turn-history-test.mjs +371 -0
- package/scripts/lifecycle-api-test.mjs +137 -0
- package/scripts/max-output-recovery-test.mjs +86 -0
- package/scripts/mcp-grace-deferred-test.mjs +89 -13
- package/scripts/memory-core-input-test.mjs +10 -0
- package/scripts/memory-pg-recovery-test.mjs +59 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +387 -6
- 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 +447 -0
- package/scripts/provider-admission-scheduler-test.mjs +582 -0
- package/scripts/provider-contract-test.mjs +525 -0
- package/scripts/provider-toolcall-test.mjs +492 -11
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- package/scripts/session-orphan-sweep-test.mjs +27 -1
- package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
- package/scripts/steering-drain-buckets-test.mjs +18 -0
- package/scripts/toolcall-args-test.mjs +14 -6
- package/scripts/tui-transcript-perf-test.mjs +5 -7
- package/src/cli.mjs +15 -2
- package/src/lib/keychain-cjs.cjs +36 -23
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +27 -13
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
- 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/admission-scheduler.mjs +331 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +114 -308
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +31 -21
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -286
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +42 -5
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +84 -150
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -119
- 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/model-catalog.mjs +54 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +28 -74
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +136 -20
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +94 -33
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +201 -123
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +37 -28
- 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-pool.mjs +36 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +50 -45
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +229 -111
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +130 -63
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +37 -224
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +81 -42
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +30 -7
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +20 -4
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +12 -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-summary-index.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +353 -108
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
- 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-job-process.mjs +223 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +317 -250
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +125 -85
- 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 +46 -202
- 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/adapter.mjs +84 -10
- package/src/runtime/memory/lib/pg/process.mjs +91 -47
- package/src/runtime/memory/lib/pg/supervisor.mjs +50 -17
- 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/atomic-file.mjs +28 -150
- 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 +436 -0
- package/src/runtime/shared/process-shutdown.mjs +33 -4
- package/src/runtime/shared/resource-admission.mjs +364 -0
- package/src/runtime/shared/staged-child-result.mjs +19 -0
- package/src/session-runtime/channel-config-api.mjs +7 -7
- package/src/session-runtime/config-lifecycle.mjs +20 -17
- package/src/session-runtime/context-status.mjs +38 -27
- 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 +60 -13
- package/src/session-runtime/mcp-glue.mjs +6 -11
- package/src/session-runtime/provider-init-key.mjs +17 -0
- package/src/session-runtime/provider-request-tools.mjs +72 -0
- package/src/session-runtime/runtime-core.mjs +49 -105
- package/src/session-runtime/runtime-paths.mjs +20 -0
- package/src/session-runtime/runtime-tool-routing.mjs +55 -0
- package/src/session-runtime/session-turn-api.mjs +5 -4
- package/src/session-runtime/tool-catalog-data.mjs +51 -0
- package/src/session-runtime/tool-catalog.mjs +390 -104
- 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 +29 -200
- package/src/standalone/channel-admin.mjs +29 -0
- package/src/standalone/channel-daemon-client.mjs +200 -38
- package/src/standalone/channel-daemon-transport.mjs +136 -9
- package/src/standalone/channel-worker.mjs +79 -12
- package/src/tui/App.jsx +11 -5
- package/src/tui/dist/index.mjs +275 -343
- package/src/tui/engine/session-api-ext.mjs +50 -6
- package/src/tui/engine/session-api.mjs +1 -1
- package/src/tui/engine/turn.mjs +10 -6
- package/src/tui/engine.mjs +13 -4
- package/src/tui/index.jsx +5 -1
- package/src/tui/lib/voice-setup.mjs +21 -5
- package/scripts/_devtools-stub.mjs +0 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -288
|
@@ -68,17 +68,7 @@ export const DEFAULT_ACTIVITY_HEARTBEAT_MS = resolveTimeoutMs(
|
|
|
68
68
|
|
|
69
69
|
export const PROVIDER_FIRST_BYTE_TIMEOUT_MS = resolveTimeoutMs(
|
|
70
70
|
'MIXDOG_PROVIDER_FIRST_BYTE_TIMEOUT_MS',
|
|
71
|
-
|
|
72
|
-
// p90 43s / p99 164s / max 171s — the distribution hugs the previous 180s
|
|
73
|
-
// window, and 16/178 fetches "died" in repeated back-to-back 183s
|
|
74
|
-
// fetch→fetch cycles (3+ consecutive in the worst session). That pattern
|
|
75
|
-
// is queued-but-alive first bytes being axed AT the window and re-queued
|
|
76
|
-
// from scratch, not dead sockets: each trip doubled the wait (one worker
|
|
77
|
-
// session burned ~40min purely on 180s-abort→retry loops). Raise to the
|
|
78
|
-
// policy ceiling (STALL_WARN - tick, ~285s): models with fast first bytes
|
|
79
|
-
// never reach the timer, and a truly wedged socket is still bounded by
|
|
80
|
-
// the agent stall first-byte abort (300s, DEFAULT_STALL_FIRST_BYTE_ABORT_S).
|
|
81
|
-
PROVIDER_MAX_BEFORE_WARN_MS,
|
|
71
|
+
60_000,
|
|
82
72
|
{ minMs: MIN_PROVIDER_TIMEOUT_MS, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
|
|
83
73
|
);
|
|
84
74
|
|
|
@@ -252,7 +242,7 @@ export const PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS = resolveTimeoutMs(
|
|
|
252
242
|
// provider layer catches the wedge before the agent watchdog does. Env-tunable.
|
|
253
243
|
export const PROVIDER_WS_FIRST_MEANINGFUL_TIMEOUT_MS = resolveTimeoutMs(
|
|
254
244
|
'MIXDOG_PROVIDER_WS_FIRST_MEANINGFUL_TIMEOUT_MS',
|
|
255
|
-
|
|
245
|
+
60_000,
|
|
256
246
|
{ minMs: 10_000, maxMs: STALL_WARN_MS },
|
|
257
247
|
);
|
|
258
248
|
|
|
@@ -48,6 +48,7 @@ import { _captureTrackedMtimes, _trackedDriftNoteAfter, getDedupedDestructiveWar
|
|
|
48
48
|
import { scrubLoaderVars, scrubProviderSecrets } from './env-scrub.mjs';
|
|
49
49
|
import { checkExecPolicyMessage } from './bash-policy-scan.mjs';
|
|
50
50
|
import { startChildGuardian } from '../../../shared/child-guardian.mjs';
|
|
51
|
+
import { resourceAdmission } from '../../../shared/resource-admission.mjs';
|
|
51
52
|
|
|
52
53
|
globalThis.__mixdogBashSessionRuntimeLoaded = true;
|
|
53
54
|
|
|
@@ -344,7 +345,7 @@ function buildBashEnv() {
|
|
|
344
345
|
return env;
|
|
345
346
|
}
|
|
346
347
|
|
|
347
|
-
function _spawnSession(id, initialCwd = process.cwd()) {
|
|
348
|
+
function _spawnSession(id, initialCwd = process.cwd(), resourceLease = null) {
|
|
348
349
|
_installParentExitHook();
|
|
349
350
|
_evictOldestIfFull();
|
|
350
351
|
const shell = resolveBash();
|
|
@@ -376,6 +377,12 @@ function _spawnSession(id, initialCwd = process.cwd()) {
|
|
|
376
377
|
busy: false,
|
|
377
378
|
dead: false,
|
|
378
379
|
exitInfo: null,
|
|
380
|
+
resourceLease,
|
|
381
|
+
};
|
|
382
|
+
const releaseResourceLease = () => {
|
|
383
|
+
if (!entry.resourceLease) return;
|
|
384
|
+
try { entry.resourceLease.release(); } catch {}
|
|
385
|
+
entry.resourceLease = null;
|
|
379
386
|
};
|
|
380
387
|
// Hard-capped concat: past STREAM_BUF_BYTE_CAP we drop further
|
|
381
388
|
// chunks and stamp a truncation marker once. Without this a runaway
|
|
@@ -402,18 +409,27 @@ function _spawnSession(id, initialCwd = process.cwd()) {
|
|
|
402
409
|
proc.on('error', (err) => {
|
|
403
410
|
entry.dead = true;
|
|
404
411
|
entry.exitInfo = { error: err?.message || String(err) };
|
|
412
|
+
// An error event is not proof of process exit. Keep admission until
|
|
413
|
+
// exit/close confirms termination, and force the failed tree down.
|
|
414
|
+
_killProcessTree(proc);
|
|
405
415
|
});
|
|
406
416
|
proc.on('exit', (code, signal) => {
|
|
407
417
|
entry.dead = true;
|
|
408
418
|
entry.exitInfo = { code, signal };
|
|
409
419
|
_sessions.delete(id);
|
|
420
|
+
releaseResourceLease();
|
|
421
|
+
});
|
|
422
|
+
proc.on('close', () => {
|
|
423
|
+
entry.dead = true;
|
|
424
|
+
_sessions.delete(id);
|
|
425
|
+
releaseResourceLease();
|
|
410
426
|
});
|
|
411
427
|
_sessions.set(id, entry);
|
|
412
428
|
_startReaper();
|
|
413
429
|
return entry;
|
|
414
430
|
}
|
|
415
431
|
|
|
416
|
-
function _getOrCreate(sessionId, initialCwd = process.cwd(), opts = {}) {
|
|
432
|
+
async function _getOrCreate(sessionId, initialCwd = process.cwd(), opts = {}) {
|
|
417
433
|
const explicit = typeof sessionId === 'string' && sessionId.length > 0;
|
|
418
434
|
const id = explicit ? sessionId : `sess_${randomUUID()}`;
|
|
419
435
|
let entry = _sessions.get(id);
|
|
@@ -425,7 +441,20 @@ function _getOrCreate(sessionId, initialCwd = process.cwd(), opts = {}) {
|
|
|
425
441
|
if (explicit && opts.create !== true) {
|
|
426
442
|
return { error: `Error: unknown session_id "${id}" (pass create:true to start a new persistent session)` };
|
|
427
443
|
}
|
|
428
|
-
|
|
444
|
+
const admission = opts.resourceAdmission || resourceAdmission;
|
|
445
|
+
const lease = await admission.acquire('shell', {
|
|
446
|
+
signal: opts.signal || null,
|
|
447
|
+
label: `persistent:${id}`,
|
|
448
|
+
dependency: 'detached',
|
|
449
|
+
});
|
|
450
|
+
try {
|
|
451
|
+
entry = _sessions.get(id);
|
|
452
|
+
if (!entry || entry.dead) entry = _spawnSession(id, initialCwd, lease);
|
|
453
|
+
else await lease.release();
|
|
454
|
+
} catch (error) {
|
|
455
|
+
await lease.release();
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
429
458
|
}
|
|
430
459
|
return { id, entry };
|
|
431
460
|
}
|
|
@@ -656,7 +685,16 @@ async function bash_session(args, cwd = process.cwd(), opts = {}) {
|
|
|
656
685
|
const effectiveTimeout = hasExplicitTimeout
|
|
657
686
|
? Math.min(Math.max(timeoutMs, 1), wmicRewrite?.timeoutMs || TIMER_MAX_MS)
|
|
658
687
|
: Math.min(Math.max(timeoutMs, 1), wmicRewrite?.timeoutMs || MAX_TIMEOUT_MS);
|
|
659
|
-
|
|
688
|
+
let resolved;
|
|
689
|
+
try {
|
|
690
|
+
resolved = await _getOrCreate(requestedSessionId || args?.session_id, baseCwd, {
|
|
691
|
+
create: args?.create === true,
|
|
692
|
+
resourceAdmission: opts?.resourceAdmission || resourceAdmission,
|
|
693
|
+
signal: abortSignal,
|
|
694
|
+
});
|
|
695
|
+
} catch (error) {
|
|
696
|
+
return `Error: ${error?.message || String(error)}`;
|
|
697
|
+
}
|
|
660
698
|
if (resolved.error) return resolved.error;
|
|
661
699
|
const { id, entry } = resolved;
|
|
662
700
|
if (entry.syncError) {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
endShellJobWait,
|
|
18
18
|
clearShellJobNotifyCtx,
|
|
19
19
|
shellJobPublicTaskResult,
|
|
20
|
+
attachShellJobResourceLease,
|
|
20
21
|
} from './shell-jobs.mjs';
|
|
21
22
|
import {
|
|
22
23
|
analyzeShellCommandEffects,
|
|
@@ -45,6 +46,7 @@ import { invalidateBuiltinResultCache } from './cache-layers.mjs';
|
|
|
45
46
|
import { resolveOptionalCwd } from './cwd-utils.mjs';
|
|
46
47
|
import { scrubLoaderVars, scrubProviderSecrets } from '../env-scrub.mjs';
|
|
47
48
|
import { resolveSessionCwd, stateFilePath, wrapPowerShellWithCwdProbe, wrapBashWithCwdProbe } from '../shell-state.mjs';
|
|
49
|
+
import { resourceAdmission } from '../../../../shared/resource-admission.mjs';
|
|
48
50
|
|
|
49
51
|
// Post-exec drift detection. After a foreground shell command, compare the
|
|
50
52
|
// live mtime+size of files mixdog has already read this session against their
|
|
@@ -263,7 +265,11 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
263
265
|
const shouldCreate = args.create === true || !userProvidedSession;
|
|
264
266
|
effectiveArgs = { ...effectiveArgs, create: shouldCreate };
|
|
265
267
|
try {
|
|
266
|
-
return await executeBashSessionTool('bash_session', effectiveArgs, bashWorkDir, {
|
|
268
|
+
return await executeBashSessionTool('bash_session', effectiveArgs, bashWorkDir, {
|
|
269
|
+
abortSignal: combinedPersistAbort.signal,
|
|
270
|
+
sessionId: options?.sessionId,
|
|
271
|
+
resourceAdmission: options?.resourceAdmission || resourceAdmission,
|
|
272
|
+
});
|
|
267
273
|
} finally {
|
|
268
274
|
combinedPersistAbort.cleanup();
|
|
269
275
|
}
|
|
@@ -406,24 +412,50 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
406
412
|
wrappedCommand = command;
|
|
407
413
|
}
|
|
408
414
|
if (runInBackground) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
shell,
|
|
416
|
-
shellArg,
|
|
417
|
-
shellArgs,
|
|
418
|
-
shellType,
|
|
419
|
-
// Per-terminal session stamp: the dispatching terminal's
|
|
420
|
-
// claude.exe pid (server-main threads callerSession.clientHostPid).
|
|
421
|
-
clientHostPid: options?.clientHostPid,
|
|
422
|
-
});
|
|
423
|
-
if (job && job.error) return formatShellToolFailure(job.error);
|
|
424
|
-
let task;
|
|
415
|
+
let asyncAbortSignal = null;
|
|
416
|
+
try { asyncAbortSignal = (await getAbortSignalForSession(options?.sessionId)) || null; }
|
|
417
|
+
catch { asyncAbortSignal = null; }
|
|
418
|
+
const combinedAsyncAbort = _combineAbortSignals(asyncAbortSignal, options?.abortSignal || null);
|
|
419
|
+
let asyncLease = null;
|
|
420
|
+
let job;
|
|
425
421
|
try {
|
|
426
|
-
|
|
422
|
+
asyncLease = await (options?.resourceAdmission || resourceAdmission).acquire('shell', {
|
|
423
|
+
signal: combinedAsyncAbort.signal,
|
|
424
|
+
label: String(command).replace(/\s+/g, ' ').slice(0, 120),
|
|
425
|
+
dependency: 'detached',
|
|
426
|
+
});
|
|
427
|
+
if (combinedAsyncAbort.signal?.aborted) {
|
|
428
|
+
throw combinedAsyncAbort.signal.reason || new Error('shell background task cancelled before spawn');
|
|
429
|
+
}
|
|
430
|
+
job = await startBackgroundShellJob({
|
|
431
|
+
command: wrappedCommand,
|
|
432
|
+
timeoutMs: timeout,
|
|
433
|
+
workDir: bashWorkDir,
|
|
434
|
+
mergeStderr,
|
|
435
|
+
spawnEnv,
|
|
436
|
+
shell,
|
|
437
|
+
shellArg,
|
|
438
|
+
shellArgs,
|
|
439
|
+
shellType,
|
|
440
|
+
// Per-terminal session stamp: the dispatching terminal's
|
|
441
|
+
// claude.exe pid (server-main threads callerSession.clientHostPid).
|
|
442
|
+
clientHostPid: options?.clientHostPid,
|
|
443
|
+
...(options?.shellJobRuntime || {}),
|
|
444
|
+
});
|
|
445
|
+
if (job && job.error) {
|
|
446
|
+
if (job.rollbackPending && attachShellJobResourceLease(job.jobId, asyncLease, { allowUnpersisted: true })) {
|
|
447
|
+
asyncLease = null;
|
|
448
|
+
}
|
|
449
|
+
return formatShellToolFailure(job.error);
|
|
450
|
+
}
|
|
451
|
+
if (combinedAsyncAbort.signal?.aborted) {
|
|
452
|
+
try { killShellJob(job.jobId); } catch {}
|
|
453
|
+
throw combinedAsyncAbort.signal.reason || new Error('shell background task cancelled before registration');
|
|
454
|
+
}
|
|
455
|
+
if (job && !job.error && attachShellJobResourceLease(job.jobId, asyncLease)) {
|
|
456
|
+
asyncLease = null;
|
|
457
|
+
}
|
|
458
|
+
const task = registerBackgroundTask({
|
|
427
459
|
taskId: job.jobId,
|
|
428
460
|
surface: 'shell',
|
|
429
461
|
operation: 'shell',
|
|
@@ -446,26 +478,31 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
446
478
|
resultType: 'shell_task_result',
|
|
447
479
|
cancel: () => killShellJob(job.jobId),
|
|
448
480
|
});
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
481
|
+
if (combinedAsyncAbort.signal?.aborted) {
|
|
482
|
+
try { killShellJob(job.jobId); } catch {}
|
|
483
|
+
cancelBackgroundTask(job.jobId, 'cancelled before background registration completed');
|
|
484
|
+
throw combinedAsyncAbort.signal.reason || new Error('shell background task cancelled during registration');
|
|
485
|
+
}
|
|
486
|
+
// Wire a one-shot completion push so the dispatching session learns
|
|
487
|
+
// the background task finished (no polling tool is auto-driven).
|
|
488
|
+
try {
|
|
489
|
+
watchBackgroundShellJob(job.jobId, {
|
|
490
|
+
notifyFn: typeof options?.notifyFn === 'function' ? options.notifyFn : null,
|
|
491
|
+
callerSessionId: options?.callerSessionId || options?.sessionId,
|
|
492
|
+
routingSessionId: options?.routingSessionId,
|
|
493
|
+
clientHostPid: options?.clientHostPid,
|
|
494
|
+
});
|
|
495
|
+
} catch { /* watcher arm is best-effort; never blocks the spawn */ }
|
|
496
|
+
return _prependDestructiveWarning(command, renderBackgroundTask(task));
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (job?.jobId && !job.error) {
|
|
499
|
+
try { killShellJob(job.jobId); } catch {}
|
|
500
|
+
}
|
|
501
|
+
return formatShellToolFailure(normalizeErrorMessage(error instanceof Error ? error.message : String(error)));
|
|
502
|
+
} finally {
|
|
503
|
+
combinedAsyncAbort.cleanup();
|
|
504
|
+
try { await asyncLease?.release(); } catch {}
|
|
452
505
|
}
|
|
453
|
-
// Wire a one-shot completion push so the dispatching session learns
|
|
454
|
-
// the background task finished (no polling tool is auto-driven). The
|
|
455
|
-
// notify ctx is threaded down from the MCP dispatch frame
|
|
456
|
-
// (server-main agentContext / _dispatchByModule) the same way the
|
|
457
|
-
// agent/explore-style tools receive notifyFn/routingSessionId/clientHostPid.
|
|
458
|
-
// Missing notifyFn (e.g. a non-MCP caller) degrades to a stderr
|
|
459
|
-
// diagnostic inside watchBackgroundShellJob — never fails the spawn.
|
|
460
|
-
try {
|
|
461
|
-
watchBackgroundShellJob(job.jobId, {
|
|
462
|
-
notifyFn: typeof options?.notifyFn === 'function' ? options.notifyFn : null,
|
|
463
|
-
callerSessionId: options?.callerSessionId || options?.sessionId,
|
|
464
|
-
routingSessionId: options?.routingSessionId,
|
|
465
|
-
clientHostPid: options?.clientHostPid,
|
|
466
|
-
});
|
|
467
|
-
} catch { /* watcher arm is best-effort; never blocks the spawn */ }
|
|
468
|
-
return _prependDestructiveWarning(command, renderBackgroundTask(task));
|
|
469
506
|
}
|
|
470
507
|
|
|
471
508
|
let bashAbortSignal = null;
|
|
@@ -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
|
+
}
|