@animus-labs/cortex 0.2.0
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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/budget-guard.d.ts +75 -0
- package/dist/budget-guard.d.ts.map +1 -0
- package/dist/budget-guard.js +142 -0
- package/dist/budget-guard.js.map +1 -0
- package/dist/compaction/compaction.d.ts +99 -0
- package/dist/compaction/compaction.d.ts.map +1 -0
- package/dist/compaction/compaction.js +302 -0
- package/dist/compaction/compaction.js.map +1 -0
- package/dist/compaction/failsafe.d.ts +57 -0
- package/dist/compaction/failsafe.d.ts.map +1 -0
- package/dist/compaction/failsafe.js +135 -0
- package/dist/compaction/failsafe.js.map +1 -0
- package/dist/compaction/index.d.ts +381 -0
- package/dist/compaction/index.d.ts.map +1 -0
- package/dist/compaction/index.js +979 -0
- package/dist/compaction/index.js.map +1 -0
- package/dist/compaction/microcompaction.d.ts +219 -0
- package/dist/compaction/microcompaction.d.ts.map +1 -0
- package/dist/compaction/microcompaction.js +536 -0
- package/dist/compaction/microcompaction.js.map +1 -0
- package/dist/compaction/observational/buffering.d.ts +225 -0
- package/dist/compaction/observational/buffering.d.ts.map +1 -0
- package/dist/compaction/observational/buffering.js +354 -0
- package/dist/compaction/observational/buffering.js.map +1 -0
- package/dist/compaction/observational/constants.d.ts +70 -0
- package/dist/compaction/observational/constants.d.ts.map +1 -0
- package/dist/compaction/observational/constants.js +507 -0
- package/dist/compaction/observational/constants.js.map +1 -0
- package/dist/compaction/observational/index.d.ts +219 -0
- package/dist/compaction/observational/index.d.ts.map +1 -0
- package/dist/compaction/observational/index.js +641 -0
- package/dist/compaction/observational/index.js.map +1 -0
- package/dist/compaction/observational/observer.d.ts +97 -0
- package/dist/compaction/observational/observer.d.ts.map +1 -0
- package/dist/compaction/observational/observer.js +424 -0
- package/dist/compaction/observational/observer.js.map +1 -0
- package/dist/compaction/observational/recall-tool.d.ts +27 -0
- package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
- package/dist/compaction/observational/recall-tool.js +93 -0
- package/dist/compaction/observational/recall-tool.js.map +1 -0
- package/dist/compaction/observational/reflector.d.ts +94 -0
- package/dist/compaction/observational/reflector.d.ts.map +1 -0
- package/dist/compaction/observational/reflector.js +167 -0
- package/dist/compaction/observational/reflector.js.map +1 -0
- package/dist/compaction/observational/types.d.ts +271 -0
- package/dist/compaction/observational/types.d.ts.map +1 -0
- package/dist/compaction/observational/types.js +15 -0
- package/dist/compaction/observational/types.js.map +1 -0
- package/dist/context-manager.d.ts +134 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +170 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/cortex-agent.d.ts +1020 -0
- package/dist/cortex-agent.d.ts.map +1 -0
- package/dist/cortex-agent.js +3589 -0
- package/dist/cortex-agent.js.map +1 -0
- package/dist/error-classifier.d.ts +48 -0
- package/dist/error-classifier.d.ts.map +1 -0
- package/dist/error-classifier.js +152 -0
- package/dist/error-classifier.js.map +1 -0
- package/dist/event-bridge.d.ts +166 -0
- package/dist/event-bridge.d.ts.map +1 -0
- package/dist/event-bridge.js +381 -0
- package/dist/event-bridge.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +119 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +474 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/model-wrapper.d.ts +58 -0
- package/dist/model-wrapper.d.ts.map +1 -0
- package/dist/model-wrapper.js +86 -0
- package/dist/model-wrapper.js.map +1 -0
- package/dist/noop-logger.d.ts +4 -0
- package/dist/noop-logger.d.ts.map +1 -0
- package/dist/noop-logger.js +8 -0
- package/dist/noop-logger.js.map +1 -0
- package/dist/prompt-diagnostics.d.ts +47 -0
- package/dist/prompt-diagnostics.d.ts.map +1 -0
- package/dist/prompt-diagnostics.js +230 -0
- package/dist/prompt-diagnostics.js.map +1 -0
- package/dist/provider-manager.d.ts +224 -0
- package/dist/provider-manager.d.ts.map +1 -0
- package/dist/provider-manager.js +563 -0
- package/dist/provider-manager.js.map +1 -0
- package/dist/provider-registry.d.ts +115 -0
- package/dist/provider-registry.d.ts.map +1 -0
- package/dist/provider-registry.js +305 -0
- package/dist/provider-registry.js.map +1 -0
- package/dist/schema-converter.d.ts +20 -0
- package/dist/schema-converter.d.ts.map +1 -0
- package/dist/schema-converter.js +48 -0
- package/dist/schema-converter.js.map +1 -0
- package/dist/skill-preprocessor.d.ts +46 -0
- package/dist/skill-preprocessor.d.ts.map +1 -0
- package/dist/skill-preprocessor.js +237 -0
- package/dist/skill-preprocessor.js.map +1 -0
- package/dist/skill-registry.d.ts +107 -0
- package/dist/skill-registry.d.ts.map +1 -0
- package/dist/skill-registry.js +330 -0
- package/dist/skill-registry.js.map +1 -0
- package/dist/skill-tool.d.ts +54 -0
- package/dist/skill-tool.d.ts.map +1 -0
- package/dist/skill-tool.js +88 -0
- package/dist/skill-tool.js.map +1 -0
- package/dist/sub-agent-manager.d.ts +90 -0
- package/dist/sub-agent-manager.d.ts.map +1 -0
- package/dist/sub-agent-manager.js +192 -0
- package/dist/sub-agent-manager.js.map +1 -0
- package/dist/token-estimator.d.ts +23 -0
- package/dist/token-estimator.d.ts.map +1 -0
- package/dist/token-estimator.js +27 -0
- package/dist/token-estimator.js.map +1 -0
- package/dist/tool-contract.d.ts +68 -0
- package/dist/tool-contract.d.ts.map +1 -0
- package/dist/tool-contract.js +35 -0
- package/dist/tool-contract.js.map +1 -0
- package/dist/tool-result-persistence.d.ts +89 -0
- package/dist/tool-result-persistence.d.ts.map +1 -0
- package/dist/tool-result-persistence.js +152 -0
- package/dist/tool-result-persistence.js.map +1 -0
- package/dist/tools/bash/index.d.ts +71 -0
- package/dist/tools/bash/index.d.ts.map +1 -0
- package/dist/tools/bash/index.js +485 -0
- package/dist/tools/bash/index.js.map +1 -0
- package/dist/tools/bash/interactive.d.ts +47 -0
- package/dist/tools/bash/interactive.d.ts.map +1 -0
- package/dist/tools/bash/interactive.js +262 -0
- package/dist/tools/bash/interactive.js.map +1 -0
- package/dist/tools/bash/safety.d.ts +149 -0
- package/dist/tools/bash/safety.d.ts.map +1 -0
- package/dist/tools/bash/safety.js +1116 -0
- package/dist/tools/bash/safety.js.map +1 -0
- package/dist/tools/edit.d.ts +57 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +310 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/glob.d.ts +34 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +268 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/grep.d.ts +53 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +673 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +43 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +459 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/runtime.d.ts +62 -0
- package/dist/tools/runtime.d.ts.map +1 -0
- package/dist/tools/runtime.js +116 -0
- package/dist/tools/runtime.js.map +1 -0
- package/dist/tools/shared/cwd-tracker.d.ts +32 -0
- package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
- package/dist/tools/shared/cwd-tracker.js +44 -0
- package/dist/tools/shared/cwd-tracker.js.map +1 -0
- package/dist/tools/shared/edit-history.d.ts +55 -0
- package/dist/tools/shared/edit-history.d.ts.map +1 -0
- package/dist/tools/shared/edit-history.js +72 -0
- package/dist/tools/shared/edit-history.js.map +1 -0
- package/dist/tools/shared/edit-matcher.d.ts +83 -0
- package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
- package/dist/tools/shared/edit-matcher.js +359 -0
- package/dist/tools/shared/edit-matcher.js.map +1 -0
- package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
- package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
- package/dist/tools/shared/file-mutation-lock.js +35 -0
- package/dist/tools/shared/file-mutation-lock.js.map +1 -0
- package/dist/tools/shared/gitignore.d.ts +17 -0
- package/dist/tools/shared/gitignore.d.ts.map +1 -0
- package/dist/tools/shared/gitignore.js +59 -0
- package/dist/tools/shared/gitignore.js.map +1 -0
- package/dist/tools/shared/pdf-extractor.d.ts +96 -0
- package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
- package/dist/tools/shared/pdf-extractor.js +196 -0
- package/dist/tools/shared/pdf-extractor.js.map +1 -0
- package/dist/tools/shared/read-registry.d.ts +66 -0
- package/dist/tools/shared/read-registry.d.ts.map +1 -0
- package/dist/tools/shared/read-registry.js +65 -0
- package/dist/tools/shared/read-registry.js.map +1 -0
- package/dist/tools/shared/safe-env.d.ts +18 -0
- package/dist/tools/shared/safe-env.d.ts.map +1 -0
- package/dist/tools/shared/safe-env.js +70 -0
- package/dist/tools/shared/safe-env.js.map +1 -0
- package/dist/tools/sub-agent.d.ts +91 -0
- package/dist/tools/sub-agent.d.ts.map +1 -0
- package/dist/tools/sub-agent.js +89 -0
- package/dist/tools/sub-agent.js.map +1 -0
- package/dist/tools/task-output.d.ts +38 -0
- package/dist/tools/task-output.d.ts.map +1 -0
- package/dist/tools/task-output.js +186 -0
- package/dist/tools/task-output.js.map +1 -0
- package/dist/tools/tool-search/index.d.ts +40 -0
- package/dist/tools/tool-search/index.d.ts.map +1 -0
- package/dist/tools/tool-search/index.js +110 -0
- package/dist/tools/tool-search/index.js.map +1 -0
- package/dist/tools/tool-search/registry.d.ts +82 -0
- package/dist/tools/tool-search/registry.d.ts.map +1 -0
- package/dist/tools/tool-search/registry.js +238 -0
- package/dist/tools/tool-search/registry.js.map +1 -0
- package/dist/tools/undo-edit.d.ts +51 -0
- package/dist/tools/undo-edit.d.ts.map +1 -0
- package/dist/tools/undo-edit.js +231 -0
- package/dist/tools/undo-edit.js.map +1 -0
- package/dist/tools/web-fetch/cache.d.ts +49 -0
- package/dist/tools/web-fetch/cache.d.ts.map +1 -0
- package/dist/tools/web-fetch/cache.js +89 -0
- package/dist/tools/web-fetch/cache.js.map +1 -0
- package/dist/tools/web-fetch/index.d.ts +53 -0
- package/dist/tools/web-fetch/index.d.ts.map +1 -0
- package/dist/tools/web-fetch/index.js +513 -0
- package/dist/tools/web-fetch/index.js.map +1 -0
- package/dist/tools/write.d.ts +59 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +316 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/types.d.ts +881 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/working-tags.d.ts +44 -0
- package/dist/working-tags.d.ts.map +1 -0
- package/dist/working-tags.js +103 -0
- package/dist/working-tags.js.map +1 -0
- package/package.json +87 -0
- package/src/budget-guard.ts +170 -0
- package/src/compaction/compaction.ts +386 -0
- package/src/compaction/failsafe.ts +185 -0
- package/src/compaction/index.ts +1199 -0
- package/src/compaction/microcompaction.ts +709 -0
- package/src/compaction/observational/buffering.ts +430 -0
- package/src/compaction/observational/constants.ts +532 -0
- package/src/compaction/observational/index.ts +837 -0
- package/src/compaction/observational/observer.ts +510 -0
- package/src/compaction/observational/recall-tool.ts +130 -0
- package/src/compaction/observational/reflector.ts +221 -0
- package/src/compaction/observational/types.ts +343 -0
- package/src/context-manager.ts +237 -0
- package/src/cortex-agent.ts +4297 -0
- package/src/error-classifier.ts +199 -0
- package/src/event-bridge.ts +508 -0
- package/src/index.ts +292 -0
- package/src/mcp-client.ts +582 -0
- package/src/model-wrapper.ts +128 -0
- package/src/noop-logger.ts +9 -0
- package/src/prompt-diagnostics.ts +296 -0
- package/src/provider-manager.ts +823 -0
- package/src/provider-registry.ts +386 -0
- package/src/schema-converter.ts +51 -0
- package/src/skill-preprocessor.ts +314 -0
- package/src/skill-registry.ts +378 -0
- package/src/skill-tool.ts +130 -0
- package/src/sub-agent-manager.ts +236 -0
- package/src/token-estimator.ts +26 -0
- package/src/tool-contract.ts +113 -0
- package/src/tool-result-persistence.ts +197 -0
- package/src/tools/bash/index.ts +633 -0
- package/src/tools/bash/interactive.ts +302 -0
- package/src/tools/bash/safety.ts +1297 -0
- package/src/tools/edit.ts +422 -0
- package/src/tools/glob.ts +330 -0
- package/src/tools/grep.ts +819 -0
- package/src/tools/index.ts +110 -0
- package/src/tools/read.ts +580 -0
- package/src/tools/runtime.ts +173 -0
- package/src/tools/shared/cwd-tracker.ts +50 -0
- package/src/tools/shared/edit-history.ts +96 -0
- package/src/tools/shared/edit-matcher.ts +457 -0
- package/src/tools/shared/file-mutation-lock.ts +40 -0
- package/src/tools/shared/gitignore.ts +61 -0
- package/src/tools/shared/pdf-extractor.ts +290 -0
- package/src/tools/shared/read-registry.ts +93 -0
- package/src/tools/shared/safe-env.ts +82 -0
- package/src/tools/sub-agent.ts +171 -0
- package/src/tools/task-output.ts +236 -0
- package/src/tools/tool-search/index.ts +167 -0
- package/src/tools/tool-search/registry.ts +278 -0
- package/src/tools/undo-edit.ts +314 -0
- package/src/tools/web-fetch/cache.ts +112 -0
- package/src/tools/web-fetch/index.ts +604 -0
- package/src/tools/write.ts +385 -0
- package/src/types.ts +1057 -0
- package/src/working-tags.ts +118 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash tool: execute shell commands in the host environment.
|
|
3
|
+
*
|
|
4
|
+
* Cross-platform: bash/zsh on macOS and Linux, PowerShell on Windows.
|
|
5
|
+
* Tracks working directory across calls within an agentic loop.
|
|
6
|
+
* Supports background execution with auto-yield.
|
|
7
|
+
*
|
|
8
|
+
* Reference: docs/cortex/tools/bash.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as child_process from 'node:child_process';
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import { Type, type Static } from 'typebox';
|
|
14
|
+
import type { CwdTracker } from '../shared/cwd-tracker.js';
|
|
15
|
+
import type { ToolContentDetails, ToolExecuteContext } from '../../types.js';
|
|
16
|
+
import { buildSafeEnv, runSafetyChecks } from './safety.js';
|
|
17
|
+
import {
|
|
18
|
+
type BackgroundTask,
|
|
19
|
+
type CortexToolRuntime,
|
|
20
|
+
attachRuntimeAwareTool,
|
|
21
|
+
globalBackgroundTaskStore,
|
|
22
|
+
} from '../runtime.js';
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Schema
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
export const BashParams = Type.Object({
|
|
29
|
+
command: Type.String({ description: 'The shell command to execute' }),
|
|
30
|
+
timeout: Type.Optional(
|
|
31
|
+
Type.Number({ description: 'Timeout in milliseconds. Default: 120000 (2 min). Max: 600000 (10 min).' }),
|
|
32
|
+
),
|
|
33
|
+
description: Type.Optional(
|
|
34
|
+
Type.String({ description: 'Human-readable explanation of the command.' }),
|
|
35
|
+
),
|
|
36
|
+
background: Type.Optional(
|
|
37
|
+
Type.Boolean({ description: 'Run the command in the background immediately. Default: false.' }),
|
|
38
|
+
),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export type BashParamsType = Static<typeof BashParams>;
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Details type
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
export interface BashDetails {
|
|
48
|
+
stdout: string;
|
|
49
|
+
stderr: string;
|
|
50
|
+
exitCode: number | null;
|
|
51
|
+
duration: number;
|
|
52
|
+
interrupted: boolean;
|
|
53
|
+
timedOut: boolean;
|
|
54
|
+
backgrounded: boolean;
|
|
55
|
+
taskId: string | null;
|
|
56
|
+
finalCwd: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Partial result details emitted during bash streaming via onUpdate.
|
|
61
|
+
*/
|
|
62
|
+
export interface BashStreamUpdate {
|
|
63
|
+
/** New stdout chunk since last update (complete lines only). */
|
|
64
|
+
stdout: string;
|
|
65
|
+
/** New stderr chunk since last update. */
|
|
66
|
+
stderr: string;
|
|
67
|
+
/** Total stdout lines emitted so far. */
|
|
68
|
+
totalLines: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Constants
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
const DEFAULT_TIMEOUT = 120_000;
|
|
76
|
+
const MAX_TIMEOUT = 600_000;
|
|
77
|
+
const AUTO_YIELD_THRESHOLD = 10_000; // 10 seconds
|
|
78
|
+
const CWD_MARKER = '___CWD___';
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Config
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
export interface BashToolConfig {
|
|
85
|
+
runtime?: CortexToolRuntime | undefined;
|
|
86
|
+
cwdTracker?: CwdTracker | undefined;
|
|
87
|
+
/** Custom shell path override. */
|
|
88
|
+
shellPath?: string | undefined;
|
|
89
|
+
/** Auto-yield threshold in ms. Default: 10000. */
|
|
90
|
+
autoYieldThreshold?: number | undefined;
|
|
91
|
+
/** Callback for tracking subprocess PIDs (for cleanup on exit). */
|
|
92
|
+
onProcessSpawned?: ((pid: number) => void) | undefined;
|
|
93
|
+
/** Callback for removing tracked PIDs when process exits. */
|
|
94
|
+
onProcessExited?: ((pid: number) => void) | undefined;
|
|
95
|
+
/** Utility model completion function for Layer 7 safety classifier. */
|
|
96
|
+
utilityComplete?: ((context: unknown) => Promise<unknown>) | undefined;
|
|
97
|
+
/**
|
|
98
|
+
* Consumer-set environment variable overrides that bypass the security blocklist.
|
|
99
|
+
* Merged ON TOP of the sanitized environment for shell subprocesses.
|
|
100
|
+
* Used for macOS dock icon suppression vars (DYLD_INSERT_LIBRARIES, etc.).
|
|
101
|
+
*/
|
|
102
|
+
envOverrides?: Record<string, string> | undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getBackgroundTask(id: string): BackgroundTask | undefined {
|
|
106
|
+
return globalBackgroundTaskStore.get(id);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getAllBackgroundTasks(): Map<string, BackgroundTask> {
|
|
110
|
+
return globalBackgroundTaskStore.getAll();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Shell Selection
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
interface ShellConfig {
|
|
118
|
+
shell: string;
|
|
119
|
+
args: string[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read /etc/shells and return the set of trusted shell paths.
|
|
124
|
+
*/
|
|
125
|
+
function readTrustedShells(): Set<string> {
|
|
126
|
+
const trusted = new Set<string>();
|
|
127
|
+
try {
|
|
128
|
+
const content = fs.readFileSync('/etc/shells', 'utf8');
|
|
129
|
+
for (const line of content.split('\n')) {
|
|
130
|
+
const trimmed = line.trim();
|
|
131
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
132
|
+
trusted.add(trimmed);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// /etc/shells not available; empty set means we fall back
|
|
137
|
+
}
|
|
138
|
+
return trusted;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Select the appropriate shell for the current platform.
|
|
143
|
+
*/
|
|
144
|
+
function selectShell(customShellPath?: string): ShellConfig {
|
|
145
|
+
// Custom override
|
|
146
|
+
if (customShellPath) {
|
|
147
|
+
if (process.platform === 'win32') {
|
|
148
|
+
return { shell: customShellPath, args: ['-NoProfile', '-NonInteractive', '-Command'] };
|
|
149
|
+
}
|
|
150
|
+
return { shell: customShellPath, args: ['-c'] };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (process.platform === 'win32') {
|
|
154
|
+
return selectWindowsShell();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return selectUnixShell();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function selectUnixShell(): ShellConfig {
|
|
161
|
+
const userShell = process.env['SHELL'];
|
|
162
|
+
|
|
163
|
+
if (userShell) {
|
|
164
|
+
// Reject fish (incompatible with common bashisms)
|
|
165
|
+
if (userShell.endsWith('/fish')) {
|
|
166
|
+
return findUnixFallback();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Validate against /etc/shells
|
|
170
|
+
const trusted = readTrustedShells();
|
|
171
|
+
if (trusted.size === 0 || trusted.has(userShell)) {
|
|
172
|
+
return { shell: userShell, args: ['-c'] };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return findUnixFallback();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function findUnixFallback(): ShellConfig {
|
|
180
|
+
// Try /bin/bash first, then /bin/sh
|
|
181
|
+
for (const shell of ['/bin/bash', '/bin/sh']) {
|
|
182
|
+
try {
|
|
183
|
+
fs.accessSync(shell, fs.constants.X_OK);
|
|
184
|
+
return { shell, args: ['-c'] };
|
|
185
|
+
} catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { shell: '/bin/sh', args: ['-c'] };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function selectWindowsShell(): ShellConfig {
|
|
194
|
+
// Try PowerShell 7 first
|
|
195
|
+
const ps7Paths = [
|
|
196
|
+
'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
|
|
197
|
+
`${process.env['ProgramW6432']}\\PowerShell\\7\\pwsh.exe`,
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
for (const ps7 of ps7Paths) {
|
|
201
|
+
try {
|
|
202
|
+
fs.accessSync(ps7, fs.constants.X_OK);
|
|
203
|
+
return { shell: ps7, args: ['-NoProfile', '-NonInteractive', '-Command'] };
|
|
204
|
+
} catch {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Fall back to Windows PowerShell 5.1
|
|
210
|
+
const ps5 = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
|
|
211
|
+
return { shell: ps5, args: ['-NoProfile', '-NonInteractive', '-Command'] };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Output handling
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Sanitize output by stripping binary control characters.
|
|
220
|
+
* Preserves tab, newline, and carriage return.
|
|
221
|
+
*/
|
|
222
|
+
function sanitizeOutput(output: string): string {
|
|
223
|
+
// eslint-disable-next-line no-control-regex
|
|
224
|
+
return output.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Extract CWD from output using the CWD_MARKER.
|
|
229
|
+
* Returns [cleanedOutput, extractedCwd].
|
|
230
|
+
*/
|
|
231
|
+
function extractCwd(output: string): [string, string | null] {
|
|
232
|
+
const markerIdx = output.lastIndexOf(CWD_MARKER);
|
|
233
|
+
if (markerIdx === -1) return [output, null];
|
|
234
|
+
|
|
235
|
+
const beforeMarker = output.slice(0, markerIdx);
|
|
236
|
+
const afterMarker = output.slice(markerIdx + CWD_MARKER.length).trim();
|
|
237
|
+
|
|
238
|
+
// The CWD is on the line after the marker
|
|
239
|
+
const lines = afterMarker.split('\n');
|
|
240
|
+
const cwd = (lines[0] ?? '').trim();
|
|
241
|
+
|
|
242
|
+
return [beforeMarker.trimEnd(), cwd || null];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Tool factory
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
export function createBashTool(config: BashToolConfig): {
|
|
250
|
+
name: string;
|
|
251
|
+
description: string;
|
|
252
|
+
parameters: typeof BashParams;
|
|
253
|
+
execute: (params: BashParamsType, context?: ToolExecuteContext) => Promise<ToolContentDetails<BashDetails>>;
|
|
254
|
+
} {
|
|
255
|
+
const cwdTracker = config.runtime?.cwdTracker ?? config.cwdTracker;
|
|
256
|
+
if (!cwdTracker) {
|
|
257
|
+
throw new Error('createBashTool requires either runtime or cwdTracker');
|
|
258
|
+
}
|
|
259
|
+
const backgroundTasks = config.runtime?.backgroundTasks ?? globalBackgroundTaskStore;
|
|
260
|
+
const autoYieldThreshold = config.autoYieldThreshold ?? AUTO_YIELD_THRESHOLD;
|
|
261
|
+
|
|
262
|
+
const tool = {
|
|
263
|
+
name: 'Bash',
|
|
264
|
+
description: 'Execute a shell command in the host environment.',
|
|
265
|
+
parameters: BashParams,
|
|
266
|
+
|
|
267
|
+
async execute(params: BashParamsType, context?: ToolExecuteContext): Promise<ToolContentDetails<BashDetails>> {
|
|
268
|
+
backgroundTasks.cleanupCompletedTasks();
|
|
269
|
+
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
|
|
270
|
+
const background = params.background ?? false;
|
|
271
|
+
const startTime = Date.now();
|
|
272
|
+
|
|
273
|
+
// Run safety checks (Layers 2-7)
|
|
274
|
+
const safetyResult = await runSafetyChecks(
|
|
275
|
+
params.command,
|
|
276
|
+
cwdTracker.getDefaultDir(),
|
|
277
|
+
cwdTracker.getCwd(),
|
|
278
|
+
{
|
|
279
|
+
utilityComplete: config.utilityComplete,
|
|
280
|
+
description: params.description,
|
|
281
|
+
},
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
if (!safetyResult.allowed) {
|
|
285
|
+
return {
|
|
286
|
+
content: [{ type: 'text', text: safetyResult.reason ?? 'Command blocked by safety check.' }],
|
|
287
|
+
details: {
|
|
288
|
+
stdout: '',
|
|
289
|
+
stderr: '',
|
|
290
|
+
exitCode: null,
|
|
291
|
+
duration: Date.now() - startTime,
|
|
292
|
+
interrupted: false,
|
|
293
|
+
timedOut: false,
|
|
294
|
+
backgrounded: false,
|
|
295
|
+
taskId: null,
|
|
296
|
+
finalCwd: cwdTracker.getCwd(),
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Select shell
|
|
302
|
+
const shellConfig = selectShell(config.shellPath);
|
|
303
|
+
|
|
304
|
+
// Verify shell exists
|
|
305
|
+
try {
|
|
306
|
+
fs.accessSync(shellConfig.shell, fs.constants.X_OK);
|
|
307
|
+
} catch {
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: 'text', text: `Shell not found: ${shellConfig.shell}. Configure a custom shell in settings.` }],
|
|
310
|
+
details: {
|
|
311
|
+
stdout: '',
|
|
312
|
+
stderr: '',
|
|
313
|
+
exitCode: null,
|
|
314
|
+
duration: Date.now() - startTime,
|
|
315
|
+
interrupted: false,
|
|
316
|
+
timedOut: false,
|
|
317
|
+
backgrounded: false,
|
|
318
|
+
taskId: null,
|
|
319
|
+
finalCwd: cwdTracker.getCwd(),
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Build safe environment (Layer 1), with consumer overrides merged on top
|
|
325
|
+
const safeEnv = buildSafeEnv(process.env, config.envOverrides);
|
|
326
|
+
|
|
327
|
+
// Append CWD capture suffix
|
|
328
|
+
const isWindows = process.platform === 'win32';
|
|
329
|
+
// Capture exit code before CWD suffix so pwd/Get-Location don't mask it
|
|
330
|
+
const cwdSuffix = isWindows
|
|
331
|
+
? `; $__ec=$LASTEXITCODE; Write-Host "${CWD_MARKER}"; Get-Location; exit $__ec`
|
|
332
|
+
: `; __ec=$?; echo "${CWD_MARKER}"; pwd; exit $__ec`;
|
|
333
|
+
|
|
334
|
+
// UTF-8 prefix for Windows PowerShell
|
|
335
|
+
const utf8Prefix = isWindows
|
|
336
|
+
? '$OutputEncoding = [System.Text.Encoding]::UTF8; [Console]::OutputEncoding = [System.Text.Encoding]::UTF8; '
|
|
337
|
+
: '';
|
|
338
|
+
|
|
339
|
+
const fullCommand = `${utf8Prefix}${params.command}${cwdSuffix}`;
|
|
340
|
+
|
|
341
|
+
// Spawn the process
|
|
342
|
+
const proc = child_process.spawn(
|
|
343
|
+
shellConfig.shell,
|
|
344
|
+
[...shellConfig.args, fullCommand],
|
|
345
|
+
{
|
|
346
|
+
cwd: cwdTracker.getCwd(),
|
|
347
|
+
env: safeEnv,
|
|
348
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
349
|
+
detached: !isWindows, // Process group for Unix cleanup
|
|
350
|
+
},
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// Track PID
|
|
354
|
+
if (proc.pid && config.onProcessSpawned) {
|
|
355
|
+
config.onProcessSpawned(proc.pid);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Background execution
|
|
359
|
+
if (background) {
|
|
360
|
+
const taskId = backgroundTasks.nextTaskId();
|
|
361
|
+
const task: BackgroundTask = {
|
|
362
|
+
id: taskId,
|
|
363
|
+
command: params.command.slice(0, 120),
|
|
364
|
+
process: proc,
|
|
365
|
+
stdout: '',
|
|
366
|
+
stderr: '',
|
|
367
|
+
exitCode: null,
|
|
368
|
+
completed: false,
|
|
369
|
+
startTime: Date.now(),
|
|
370
|
+
};
|
|
371
|
+
backgroundTasks.set(task);
|
|
372
|
+
|
|
373
|
+
proc.stdout?.setEncoding('utf8');
|
|
374
|
+
proc.stderr?.setEncoding('utf8');
|
|
375
|
+
proc.stdout?.on('data', (data: string) => { task.stdout += data; });
|
|
376
|
+
proc.stderr?.on('data', (data: string) => { task.stderr += data; });
|
|
377
|
+
proc.on('close', (code) => {
|
|
378
|
+
task.exitCode = code;
|
|
379
|
+
task.completed = true;
|
|
380
|
+
if (proc.pid && config.onProcessExited) {
|
|
381
|
+
config.onProcessExited(proc.pid);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
content: [{ type: 'text', text: `Command running in background. Task ID: ${taskId}\nUse TaskOutput to poll, send input, or kill.` }],
|
|
387
|
+
details: {
|
|
388
|
+
stdout: '',
|
|
389
|
+
stderr: '',
|
|
390
|
+
exitCode: null,
|
|
391
|
+
duration: 0,
|
|
392
|
+
interrupted: false,
|
|
393
|
+
timedOut: false,
|
|
394
|
+
backgrounded: true,
|
|
395
|
+
taskId,
|
|
396
|
+
finalCwd: cwdTracker.getCwd(),
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Foreground execution
|
|
402
|
+
return new Promise<ToolContentDetails<BashDetails>>((resolve) => {
|
|
403
|
+
let stdout = '';
|
|
404
|
+
let stderr = '';
|
|
405
|
+
let timedOut = false;
|
|
406
|
+
let autoYielded = false;
|
|
407
|
+
let taskId: string | null = null;
|
|
408
|
+
|
|
409
|
+
// Streaming state: buffer chunks and emit complete lines every 100ms
|
|
410
|
+
const onUpdate = context?.onUpdate;
|
|
411
|
+
let pendingStdout = '';
|
|
412
|
+
let pendingStderr = '';
|
|
413
|
+
let totalLinesEmitted = 0;
|
|
414
|
+
|
|
415
|
+
const flushStreamUpdate = (): void => {
|
|
416
|
+
if (!onUpdate) return;
|
|
417
|
+
// Only emit complete lines (hold partial lines in the buffer)
|
|
418
|
+
const lastNewline = pendingStdout.lastIndexOf('\n');
|
|
419
|
+
const lastStderrNewline = pendingStderr.lastIndexOf('\n');
|
|
420
|
+
if (lastNewline === -1 && lastStderrNewline === -1) return;
|
|
421
|
+
|
|
422
|
+
let stdoutChunk = '';
|
|
423
|
+
if (lastNewline >= 0) {
|
|
424
|
+
stdoutChunk = pendingStdout.slice(0, lastNewline + 1);
|
|
425
|
+
pendingStdout = pendingStdout.slice(lastNewline + 1);
|
|
426
|
+
totalLinesEmitted += stdoutChunk.split('\n').length - 1;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let stderrChunk = '';
|
|
430
|
+
if (lastStderrNewline >= 0) {
|
|
431
|
+
stderrChunk = pendingStderr.slice(0, lastStderrNewline + 1);
|
|
432
|
+
pendingStderr = pendingStderr.slice(lastStderrNewline + 1);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
onUpdate({
|
|
436
|
+
content: [{ type: 'text', text: stdoutChunk + stderrChunk }],
|
|
437
|
+
details: { stdout: stdoutChunk, stderr: stderrChunk, totalLines: totalLinesEmitted },
|
|
438
|
+
});
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const streamInterval = onUpdate ? setInterval(flushStreamUpdate, 100) : null;
|
|
442
|
+
|
|
443
|
+
proc.stdout?.setEncoding('utf8');
|
|
444
|
+
proc.stderr?.setEncoding('utf8');
|
|
445
|
+
proc.stdout?.on('data', (data: string) => {
|
|
446
|
+
stdout += data;
|
|
447
|
+
pendingStdout += data;
|
|
448
|
+
});
|
|
449
|
+
proc.stderr?.on('data', (data: string) => {
|
|
450
|
+
stderr += data;
|
|
451
|
+
pendingStderr += data;
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Timeout handler
|
|
455
|
+
const timeoutTimer = setTimeout(() => {
|
|
456
|
+
timedOut = true;
|
|
457
|
+
killProcessTree(proc);
|
|
458
|
+
}, timeout);
|
|
459
|
+
|
|
460
|
+
// Auto-yield handler
|
|
461
|
+
const autoYieldTimer = setTimeout(() => {
|
|
462
|
+
if (!proc.exitCode && proc.pid) {
|
|
463
|
+
autoYielded = true;
|
|
464
|
+
taskId = backgroundTasks.nextTaskId();
|
|
465
|
+
const task: BackgroundTask = {
|
|
466
|
+
id: taskId,
|
|
467
|
+
command: params.command.slice(0, 120),
|
|
468
|
+
process: proc,
|
|
469
|
+
stdout,
|
|
470
|
+
stderr,
|
|
471
|
+
exitCode: null,
|
|
472
|
+
completed: false,
|
|
473
|
+
startTime: Date.now(),
|
|
474
|
+
};
|
|
475
|
+
backgroundTasks.set(task);
|
|
476
|
+
|
|
477
|
+
// Remove original foreground listeners to prevent memory leak
|
|
478
|
+
proc.stdout?.removeAllListeners('data');
|
|
479
|
+
proc.stderr?.removeAllListeners('data');
|
|
480
|
+
// Continue collecting output for the background task
|
|
481
|
+
proc.stdout?.on('data', (data: string) => { task.stdout += data; });
|
|
482
|
+
proc.stderr?.on('data', (data: string) => { task.stderr += data; });
|
|
483
|
+
proc.on('close', (code) => {
|
|
484
|
+
task.exitCode = code;
|
|
485
|
+
task.completed = true;
|
|
486
|
+
if (proc.pid && config.onProcessExited) {
|
|
487
|
+
config.onProcessExited(proc.pid);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
clearTimeout(timeoutTimer);
|
|
492
|
+
if (streamInterval) {
|
|
493
|
+
clearInterval(streamInterval);
|
|
494
|
+
flushStreamUpdate();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const [cleanedOutput] = extractCwd(sanitizeOutput(stdout));
|
|
498
|
+
resolve({
|
|
499
|
+
content: [{ type: 'text', text: `${cleanedOutput}\n\n[Command auto-yielded after ${autoYieldThreshold}ms. Task ID: ${taskId}]` }],
|
|
500
|
+
details: {
|
|
501
|
+
stdout: cleanedOutput,
|
|
502
|
+
stderr,
|
|
503
|
+
exitCode: null,
|
|
504
|
+
duration: Date.now() - startTime,
|
|
505
|
+
interrupted: false,
|
|
506
|
+
timedOut: false,
|
|
507
|
+
backgrounded: true,
|
|
508
|
+
taskId,
|
|
509
|
+
finalCwd: cwdTracker.getCwd(),
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}, autoYieldThreshold);
|
|
514
|
+
|
|
515
|
+
proc.on('close', (code) => {
|
|
516
|
+
clearTimeout(timeoutTimer);
|
|
517
|
+
clearTimeout(autoYieldTimer);
|
|
518
|
+
if (streamInterval) {
|
|
519
|
+
clearInterval(streamInterval);
|
|
520
|
+
flushStreamUpdate();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (proc.pid && config.onProcessExited) {
|
|
524
|
+
config.onProcessExited(proc.pid);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// If already auto-yielded, don't resolve again
|
|
528
|
+
if (autoYielded) return;
|
|
529
|
+
|
|
530
|
+
const rawOutput = sanitizeOutput(stdout);
|
|
531
|
+
const [cleanedOutput, newCwd] = extractCwd(rawOutput);
|
|
532
|
+
|
|
533
|
+
// Update CWD tracker
|
|
534
|
+
if (newCwd) {
|
|
535
|
+
cwdTracker.updateCwd(newCwd);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const duration = Date.now() - startTime;
|
|
539
|
+
|
|
540
|
+
let text = cleanedOutput;
|
|
541
|
+
if (stderr) {
|
|
542
|
+
text += `\nstderr: ${stderr}`;
|
|
543
|
+
}
|
|
544
|
+
if (timedOut) {
|
|
545
|
+
text += `\nCommand timed out after ${timeout}ms.`;
|
|
546
|
+
}
|
|
547
|
+
if (code !== null && code !== 0) {
|
|
548
|
+
text += `\nExit code: ${code}`;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
resolve({
|
|
552
|
+
content: [{ type: 'text', text: text || '(no output)' }],
|
|
553
|
+
details: {
|
|
554
|
+
stdout: cleanedOutput,
|
|
555
|
+
stderr,
|
|
556
|
+
exitCode: code,
|
|
557
|
+
duration,
|
|
558
|
+
interrupted: false,
|
|
559
|
+
timedOut,
|
|
560
|
+
backgrounded: false,
|
|
561
|
+
taskId: null,
|
|
562
|
+
finalCwd: newCwd ?? cwdTracker.getCwd(),
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
proc.on('error', (err) => {
|
|
568
|
+
clearTimeout(timeoutTimer);
|
|
569
|
+
clearTimeout(autoYieldTimer);
|
|
570
|
+
if (streamInterval) {
|
|
571
|
+
clearInterval(streamInterval);
|
|
572
|
+
flushStreamUpdate();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (autoYielded) return;
|
|
576
|
+
|
|
577
|
+
resolve({
|
|
578
|
+
content: [{ type: 'text', text: `Failed to execute command: ${err.message}` }],
|
|
579
|
+
details: {
|
|
580
|
+
stdout,
|
|
581
|
+
stderr,
|
|
582
|
+
exitCode: null,
|
|
583
|
+
duration: Date.now() - startTime,
|
|
584
|
+
interrupted: false,
|
|
585
|
+
timedOut: false,
|
|
586
|
+
backgrounded: false,
|
|
587
|
+
taskId: null,
|
|
588
|
+
finalCwd: cwdTracker.getCwd(),
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
},
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
return attachRuntimeAwareTool(tool, {
|
|
597
|
+
toolKind: 'Bash',
|
|
598
|
+
cloneForRuntime: (runtime) => createBashTool({
|
|
599
|
+
...config,
|
|
600
|
+
runtime,
|
|
601
|
+
cwdTracker: runtime.cwdTracker,
|
|
602
|
+
}),
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
// Process tree cleanup
|
|
608
|
+
// ---------------------------------------------------------------------------
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Kill the entire process tree.
|
|
612
|
+
* Unix: send SIGKILL to the process group.
|
|
613
|
+
* Windows: use taskkill /F /T.
|
|
614
|
+
*/
|
|
615
|
+
function killProcessTree(proc: child_process.ChildProcess): void {
|
|
616
|
+
if (!proc.pid) return;
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
if (process.platform === 'win32') {
|
|
620
|
+
child_process.execFileSync('taskkill', ['/F', '/T', '/PID', String(proc.pid)], { stdio: 'ignore' });
|
|
621
|
+
} else {
|
|
622
|
+
// Kill the entire process group
|
|
623
|
+
process.kill(-proc.pid, 'SIGKILL');
|
|
624
|
+
}
|
|
625
|
+
} catch {
|
|
626
|
+
// Process may have already exited
|
|
627
|
+
try {
|
|
628
|
+
proc.kill('SIGKILL');
|
|
629
|
+
} catch {
|
|
630
|
+
// Ignore
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|