@nxuss/lemma 1.8.0 → 1.9.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/README.md +126 -176
- package/dist/cjs/mcp/prompts.d.ts +10 -1
- package/dist/cjs/mcp/prompts.d.ts.map +1 -1
- package/dist/cjs/mcp/prompts.js +25 -73
- package/dist/cjs/mcp/prompts.js.map +1 -1
- package/dist/cjs/mcp/tool-profiles.d.ts +10 -1
- package/dist/cjs/mcp/tool-profiles.d.ts.map +1 -1
- package/dist/cjs/mcp/tool-profiles.js +12 -3
- package/dist/cjs/mcp/tool-profiles.js.map +1 -1
- package/dist/cjs/mcp/tools.d.ts.map +1 -1
- package/dist/cjs/mcp/tools.js +122 -28
- package/dist/cjs/mcp/tools.js.map +1 -1
- package/dist/cjs/mcp/utils.d.ts.map +1 -1
- package/dist/cjs/mcp/utils.js +19 -6
- package/dist/cjs/mcp/utils.js.map +1 -1
- package/dist/cjs/security/SemanticScrubber.d.ts +2 -0
- package/dist/cjs/security/SemanticScrubber.d.ts.map +1 -1
- package/dist/cjs/security/SemanticScrubber.js +53 -13
- package/dist/cjs/security/SemanticScrubber.js.map +1 -1
- package/dist/cjs/utils/ContextSqueezer.d.ts.map +1 -1
- package/dist/cjs/utils/ContextSqueezer.js +55 -22
- package/dist/cjs/utils/ContextSqueezer.js.map +1 -1
- package/dist/cjs/utils/FileOutline.d.ts +22 -0
- package/dist/cjs/utils/FileOutline.d.ts.map +1 -0
- package/dist/cjs/utils/FileOutline.js +55 -0
- package/dist/cjs/utils/FileOutline.js.map +1 -0
- package/dist/cjs/utils/LineNumberMap.d.ts +29 -0
- package/dist/cjs/utils/LineNumberMap.d.ts.map +1 -0
- package/dist/cjs/utils/LineNumberMap.js +98 -0
- package/dist/cjs/utils/LineNumberMap.js.map +1 -0
- package/dist/cjs/utils/PatchMatcher.d.ts +2 -0
- package/dist/cjs/utils/PatchMatcher.d.ts.map +1 -1
- package/dist/cjs/utils/PatchMatcher.js +9 -1
- package/dist/cjs/utils/PatchMatcher.js.map +1 -1
- package/dist/cjs/utils/ReadWorkspaceCache.d.ts +16 -3
- package/dist/cjs/utils/ReadWorkspaceCache.d.ts.map +1 -1
- package/dist/cjs/utils/ReadWorkspaceCache.js +20 -8
- package/dist/cjs/utils/ReadWorkspaceCache.js.map +1 -1
- package/dist/esm/mcp/prompts.d.ts +10 -1
- package/dist/esm/mcp/prompts.d.ts.map +1 -1
- package/dist/esm/mcp/prompts.js +25 -73
- package/dist/esm/mcp/prompts.js.map +1 -1
- package/dist/esm/mcp/tool-profiles.d.ts +10 -1
- package/dist/esm/mcp/tool-profiles.d.ts.map +1 -1
- package/dist/esm/mcp/tool-profiles.js +12 -3
- package/dist/esm/mcp/tool-profiles.js.map +1 -1
- package/dist/esm/mcp/tools.d.ts.map +1 -1
- package/dist/esm/mcp/tools.js +122 -28
- package/dist/esm/mcp/tools.js.map +1 -1
- package/dist/esm/mcp/utils.d.ts.map +1 -1
- package/dist/esm/mcp/utils.js +19 -6
- package/dist/esm/mcp/utils.js.map +1 -1
- package/dist/esm/security/SemanticScrubber.d.ts +2 -0
- package/dist/esm/security/SemanticScrubber.d.ts.map +1 -1
- package/dist/esm/security/SemanticScrubber.js +53 -13
- package/dist/esm/security/SemanticScrubber.js.map +1 -1
- package/dist/esm/utils/ContextSqueezer.d.ts.map +1 -1
- package/dist/esm/utils/ContextSqueezer.js +55 -22
- package/dist/esm/utils/ContextSqueezer.js.map +1 -1
- package/dist/esm/utils/FileOutline.d.ts +22 -0
- package/dist/esm/utils/FileOutline.d.ts.map +1 -0
- package/dist/esm/utils/FileOutline.js +51 -0
- package/dist/esm/utils/FileOutline.js.map +1 -0
- package/dist/esm/utils/LineNumberMap.d.ts +29 -0
- package/dist/esm/utils/LineNumberMap.d.ts.map +1 -0
- package/dist/esm/utils/LineNumberMap.js +93 -0
- package/dist/esm/utils/LineNumberMap.js.map +1 -0
- package/dist/esm/utils/PatchMatcher.d.ts +2 -0
- package/dist/esm/utils/PatchMatcher.d.ts.map +1 -1
- package/dist/esm/utils/PatchMatcher.js +9 -1
- package/dist/esm/utils/PatchMatcher.js.map +1 -1
- package/dist/esm/utils/ReadWorkspaceCache.d.ts +16 -3
- package/dist/esm/utils/ReadWorkspaceCache.d.ts.map +1 -1
- package/dist/esm/utils/ReadWorkspaceCache.js +19 -8
- package/dist/esm/utils/ReadWorkspaceCache.js.map +1 -1
- package/package.json +1 -1
package/dist/esm/mcp/tools.js
CHANGED
|
@@ -38,6 +38,8 @@ import { distillCommandOutput, buildDistillFooter, readRegion } from "../utils/C
|
|
|
38
38
|
import { findMatch, reindentReplacement } from "../utils/PatchMatcher.js";
|
|
39
39
|
import { searchWorkspace, groupSearchResults, parseExtensionFilter } from "../utils/WorkspaceSearch.js";
|
|
40
40
|
import { checkAlreadySent, recordSent } from "../utils/ReadWorkspaceCache.js";
|
|
41
|
+
import { mapSqueezedToOriginal, withLineNumbers, sequentialNumbers } from "../utils/LineNumberMap.js";
|
|
42
|
+
import { fileOutline, formatOutline } from "../utils/FileOutline.js";
|
|
41
43
|
import { getCachedSearch, setCachedSearch } from "../utils/SearchCache.js";
|
|
42
44
|
// ── Pro License Gate ──────────────────────────────────────────────────────────
|
|
43
45
|
const LICENSE_FILE = path.join(os.homedir(), '.lemma-cache', 'license.json');
|
|
@@ -163,13 +165,16 @@ const toolDefinitions = [
|
|
|
163
165
|
},
|
|
164
166
|
{
|
|
165
167
|
name: "read_workspace_file",
|
|
166
|
-
description: "Read a file inside the workspace.
|
|
168
|
+
description: "Read a file inside the workspace. Compresses comments/whitespace and scrubs API keys. Every line carries its REAL line number in the original file (compression removes lines, it never renumbers them), so you can cite file:line and patch precisely without re-reading. The header lists which imports were compressed away. Use offset/limit to read a line range of a large file. If this exact view was already read this session and hasn't changed on disk, returns an UNCHANGED marker plus a symbol outline — reuse your copy, or pass force:true if it's no longer in your context.",
|
|
167
169
|
inputSchema: {
|
|
168
170
|
type: "object",
|
|
169
171
|
properties: {
|
|
170
172
|
filePath: { type: "string", description: "Path relative to the project root" },
|
|
171
173
|
compact: { type: "boolean", description: "Whether to compress comments and whitespace", default: true },
|
|
172
174
|
force: { type: "boolean", description: "Bypass the unchanged-file short-circuit and always return full content", default: false },
|
|
175
|
+
lineNumbers: { type: "boolean", description: "Prefix each line with its real line number in the original file", default: true },
|
|
176
|
+
offset: { type: "number", description: "First line to read (1-indexed). Omit to start at the beginning." },
|
|
177
|
+
limit: { type: "number", description: "How many lines to read from offset. Omit to read to the end." },
|
|
173
178
|
},
|
|
174
179
|
required: ["filePath"],
|
|
175
180
|
},
|
|
@@ -213,7 +218,7 @@ const toolDefinitions = [
|
|
|
213
218
|
},
|
|
214
219
|
{
|
|
215
220
|
name: "run_workspace_command",
|
|
216
|
-
description: "Execute a bash command in the workspace root (
|
|
221
|
+
description: "Execute a bash command in the workspace root. Default timeout 120s — raise it with timeoutMs for a full test or build run (max 600s). Captures both stdout and stderr, and returns whatever was produced even if the command times out. Long test/build/lint output is distilled deterministically — for jest, vitest and tsc it returns the failing tests or diagnostics with their locations and reasons, dropping code frames and node_modules stack frames; anything else falls back to head/tail plus error-matching lines. The complete output is always stored first and the reply carries a handle: nothing is lost, use output_region to retrieve any part verbatim. Pass raw:true to skip distillation.",
|
|
217
222
|
inputSchema: {
|
|
218
223
|
type: "object",
|
|
219
224
|
properties: {
|
|
@@ -223,6 +228,11 @@ const toolDefinitions = [
|
|
|
223
228
|
description: "Return the complete untouched output instead of the distilled view",
|
|
224
229
|
default: false,
|
|
225
230
|
},
|
|
231
|
+
timeoutMs: {
|
|
232
|
+
type: "number",
|
|
233
|
+
description: "Milliseconds before the command is killed (default 120000, max 600000)",
|
|
234
|
+
default: 120000,
|
|
235
|
+
},
|
|
226
236
|
},
|
|
227
237
|
required: ["command"],
|
|
228
238
|
},
|
|
@@ -1044,7 +1054,15 @@ const allToolDefinitions = [
|
|
|
1044
1054
|
];
|
|
1045
1055
|
// Narrow what gets advertised to what this project can actually use. Hidden tools stay
|
|
1046
1056
|
// registered in `toolHandlers` and callable — both directly and through `lemma_toolbox`.
|
|
1047
|
-
|
|
1057
|
+
//
|
|
1058
|
+
// The entitlement filter matters as much as the project filter: without it a free session
|
|
1059
|
+
// advertised the full core surface — read, search, patch, run, AST — and refused fourteen of
|
|
1060
|
+
// those twenty tools at call time. That is the worst of both, paying the per-turn schema cost
|
|
1061
|
+
// for capabilities the session cannot use. They remain listed in `lemma_toolbox`, so the
|
|
1062
|
+
// capability is still discoverable and the upgrade path is still visible.
|
|
1063
|
+
const _toolSurface = resolveToolSurface(allToolDefinitions, process.cwd(), {
|
|
1064
|
+
isEntitled: (name) => isProUser() || FREE_TOOLS.has(name),
|
|
1065
|
+
});
|
|
1048
1066
|
const toolDefinitionsArray = _toolSurface.advertised;
|
|
1049
1067
|
const hiddenToolDefinitions = _toolSurface.hidden;
|
|
1050
1068
|
/** Serialized size of every tool schema sent to the client, for cost accounting. */
|
|
@@ -1437,14 +1455,28 @@ async function handleReadWorkspaceFile(args) {
|
|
|
1437
1455
|
const filePath = args?.filePath;
|
|
1438
1456
|
const compact = args?.compact !== false;
|
|
1439
1457
|
const force = !!args?.force;
|
|
1458
|
+
const lineNumbers = args?.lineNumbers !== false;
|
|
1459
|
+
const offset = typeof args?.offset === "number" && args.offset > 0 ? Math.floor(args.offset) : undefined;
|
|
1460
|
+
const limit = typeof args?.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : undefined;
|
|
1440
1461
|
if (!filePath)
|
|
1441
1462
|
throw new Error("filePath is required");
|
|
1442
1463
|
const workspaceRoot = process.cwd();
|
|
1443
1464
|
try {
|
|
1444
1465
|
const { resolved } = safeResolvePath(workspaceRoot, filePath);
|
|
1445
|
-
|
|
1466
|
+
const fullContent = fs.readFileSync(resolved, "utf8");
|
|
1467
|
+
const view = { compact, lineNumbers, offset, limit };
|
|
1468
|
+
// The range is applied to the original file, before any compression: `offset` means the
|
|
1469
|
+
// line the user would see in an editor, not a position in the squeezed output.
|
|
1470
|
+
const allLines = fullContent.split("\n");
|
|
1471
|
+
const startIndex = offset ? Math.min(offset - 1, allLines.length) : 0;
|
|
1472
|
+
const endIndex = limit ? Math.min(startIndex + limit, allLines.length) : allLines.length;
|
|
1473
|
+
const ranged = offset || limit ? allLines.slice(startIndex, endIndex).join("\n") : fullContent;
|
|
1474
|
+
const rangeHeader = offset || limit
|
|
1475
|
+
? `[lines ${startIndex + 1}-${endIndex} of ${allLines.length} in ${filePath}]\n`
|
|
1476
|
+
: "";
|
|
1477
|
+
let content = ranged;
|
|
1446
1478
|
const originalSize = content.length;
|
|
1447
|
-
const dedup = checkAlreadySent(resolved,
|
|
1479
|
+
const dedup = checkAlreadySent(resolved, view, content);
|
|
1448
1480
|
if (!force && dedup.unchanged) {
|
|
1449
1481
|
reportSavings({
|
|
1450
1482
|
source: "contextSqueeze",
|
|
@@ -1453,17 +1485,32 @@ async function handleReadWorkspaceFile(args) {
|
|
|
1453
1485
|
toolName: "read_workspace_file",
|
|
1454
1486
|
filePath,
|
|
1455
1487
|
});
|
|
1488
|
+
// A hash alone is not actionable: the model's context may have been compacted since,
|
|
1489
|
+
// in which case it no longer holds the copy this marker tells it to reuse. The outline
|
|
1490
|
+
// costs ~50 tokens and is enough to either locate what it needs or decide to re-fetch.
|
|
1491
|
+
const outline = formatOutline(fileOutline(content), allLines.length);
|
|
1456
1492
|
return {
|
|
1457
1493
|
content: [
|
|
1458
1494
|
{
|
|
1459
1495
|
type: "text",
|
|
1460
|
-
text:
|
|
1496
|
+
text: [
|
|
1497
|
+
`UNCHANGED: ${filePath} is identical to what you already read this session (hash ${dedup.hash.slice(0, 12)}) — reuse your existing copy.`,
|
|
1498
|
+
`If it is no longer in your context, re-fetch with force:true. Structure:`,
|
|
1499
|
+
outline,
|
|
1500
|
+
].join("\n"),
|
|
1461
1501
|
},
|
|
1462
1502
|
],
|
|
1463
1503
|
};
|
|
1464
1504
|
}
|
|
1505
|
+
// Line numbers are computed against the pre-squeeze text and carried through, so a
|
|
1506
|
+
// compressed read still reports the position the code actually occupies on disk.
|
|
1507
|
+
let numbers = null;
|
|
1465
1508
|
if (compact) {
|
|
1466
|
-
|
|
1509
|
+
const squeezed = squeezeCode(content);
|
|
1510
|
+
if (lineNumbers) {
|
|
1511
|
+
numbers = mapSqueezedToOriginal(content, squeezed).map((n) => (n === null ? null : n + startIndex));
|
|
1512
|
+
}
|
|
1513
|
+
content = squeezed;
|
|
1467
1514
|
if (content.length < originalSize) {
|
|
1468
1515
|
reportSavings({
|
|
1469
1516
|
source: "contextSqueeze",
|
|
@@ -1474,16 +1521,26 @@ async function handleReadWorkspaceFile(args) {
|
|
|
1474
1521
|
});
|
|
1475
1522
|
}
|
|
1476
1523
|
}
|
|
1524
|
+
else if (lineNumbers) {
|
|
1525
|
+
numbers = sequentialNumbers(content, startIndex + 1);
|
|
1526
|
+
}
|
|
1477
1527
|
try {
|
|
1478
1528
|
const scrubber = new SemanticScrubber();
|
|
1479
1529
|
const { maskedPrompt } = scrubber.mask(content);
|
|
1530
|
+
// Masking must not change the line count, or every number after a masked secret would
|
|
1531
|
+
// be off. If it ever does, drop the gutter rather than print numbers that lie.
|
|
1532
|
+
if (numbers && maskedPrompt.split("\n").length !== content.split("\n").length) {
|
|
1533
|
+
numbers = null;
|
|
1534
|
+
}
|
|
1480
1535
|
content = maskedPrompt;
|
|
1481
1536
|
}
|
|
1482
1537
|
catch (err) {
|
|
1483
1538
|
logWarn("read_workspace_file", "SemanticScrubber failed, returning raw content");
|
|
1484
1539
|
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1540
|
+
if (numbers)
|
|
1541
|
+
content = withLineNumbers(content, numbers);
|
|
1542
|
+
recordSent(resolved, view, dedup.hash);
|
|
1543
|
+
return { content: [{ type: "text", text: rangeHeader + content }] };
|
|
1487
1544
|
}
|
|
1488
1545
|
catch (err) {
|
|
1489
1546
|
logError("read_workspace_file", err);
|
|
@@ -1535,13 +1592,35 @@ async function handleCreateWorkspaceFile(args) {
|
|
|
1535
1592
|
return { content: [{ type: "text", text: `Error creating file: ${err.message}` }] };
|
|
1536
1593
|
}
|
|
1537
1594
|
}
|
|
1595
|
+
/**
|
|
1596
|
+
* `read_workspace_file` now returns a "123| code" gutter, which makes it easy to paste a block
|
|
1597
|
+
* back into a patch with the numbers still attached. That block matches nothing on disk, so the
|
|
1598
|
+
* patch fails for a reason that has nothing to do with the edit. Strip the gutter when *every*
|
|
1599
|
+
* non-empty line carries one — a uniform prefix is a gutter; a lone "12| " inside real code is
|
|
1600
|
+
* not, and must be left alone.
|
|
1601
|
+
*/
|
|
1602
|
+
function stripLineNumberGutter(text) {
|
|
1603
|
+
const lines = text.split("\n");
|
|
1604
|
+
const nonEmpty = lines.filter((l) => l.trim().length > 0);
|
|
1605
|
+
if (nonEmpty.length === 0)
|
|
1606
|
+
return text;
|
|
1607
|
+
const GUTTER = /^\s*\d+\|\s?/;
|
|
1608
|
+
if (!nonEmpty.every((l) => GUTTER.test(l)))
|
|
1609
|
+
return text;
|
|
1610
|
+
return lines.map((l) => l.replace(GUTTER, "")).join("\n");
|
|
1611
|
+
}
|
|
1538
1612
|
async function handleApplyWorkspacePatch(args) {
|
|
1539
1613
|
const filePath = args?.filePath;
|
|
1540
|
-
const
|
|
1541
|
-
const
|
|
1542
|
-
if (!filePath || !
|
|
1614
|
+
const rawSearch = args?.searchContent;
|
|
1615
|
+
const rawReplace = args?.replaceContent;
|
|
1616
|
+
if (!filePath || !rawSearch || rawReplace === undefined) {
|
|
1543
1617
|
throw new Error("filePath, searchContent, and replaceContent are required");
|
|
1544
1618
|
}
|
|
1619
|
+
const searchContent = stripLineNumberGutter(rawSearch);
|
|
1620
|
+
// Only strip the replacement's gutter when the search block had one too: that pairing means
|
|
1621
|
+
// the model was working from a numbered read. Stripping unconditionally would mangle a
|
|
1622
|
+
// replacement that legitimately contains "1| " (a table, a doc block).
|
|
1623
|
+
const replaceContent = searchContent !== rawSearch ? stripLineNumberGutter(rawReplace) : rawReplace;
|
|
1545
1624
|
const workspaceRoot = process.cwd();
|
|
1546
1625
|
try {
|
|
1547
1626
|
const { resolved } = safeResolvePath(workspaceRoot, filePath);
|
|
@@ -1581,10 +1660,19 @@ async function handleApplyWorkspacePatch(args) {
|
|
|
1581
1660
|
};
|
|
1582
1661
|
}
|
|
1583
1662
|
const match = matchResult[0];
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1663
|
+
let updatedContent;
|
|
1664
|
+
if (match.strategy === "exact" && match.charStart !== undefined && match.charEnd !== undefined) {
|
|
1665
|
+
// Splice by character offset — the search block may be a mid-line fragment, and
|
|
1666
|
+
// a whole-line splice would discard everything else sharing that line.
|
|
1667
|
+
updatedContent = originalContent.slice(0, match.charStart) + replaceContent + originalContent.slice(match.charEnd);
|
|
1668
|
+
}
|
|
1669
|
+
else {
|
|
1670
|
+
const contentLines = originalContent.split("\n");
|
|
1671
|
+
const finalReplace = reindentReplacement(replaceContent, searchContent, contentLines[match.startLine]);
|
|
1672
|
+
const updatedLines = [...contentLines.slice(0, match.startLine), ...finalReplace.split("\n"), ...contentLines.slice(match.endLine + 1)];
|
|
1673
|
+
updatedContent = updatedLines.join("\n");
|
|
1674
|
+
}
|
|
1675
|
+
fs.writeFileSync(resolved, updatedContent, "utf8");
|
|
1588
1676
|
const note = match.strategy === "exact" ? "" : ` (matched via ${match.strategy}, score ${match.score.toFixed(2)})`;
|
|
1589
1677
|
return { content: [{ type: "text", text: `Success: Patch successfully applied to ${filePath}${note}` }] };
|
|
1590
1678
|
}
|
|
@@ -1626,32 +1714,38 @@ async function handleRunWorkspaceCommand(args) {
|
|
|
1626
1714
|
// stdio is overridden, which silently dropped the output of every tool that reports on
|
|
1627
1715
|
// stderr (jest and tsc both do) on a successful run. It also gives us stdout, stderr and
|
|
1628
1716
|
// the exit code through one code path instead of a throw-on-failure split.
|
|
1717
|
+
// 15s was below the runtime of the commands this tool exists for: a real `npm test` or
|
|
1718
|
+
// `tsc --noEmit` always tripped it, so the distillation pipeline never ran on the output
|
|
1719
|
+
// it was built for and the call was pure overhead before falling back to a plain shell.
|
|
1720
|
+
const DEFAULT_TIMEOUT_MS = 120000;
|
|
1721
|
+
const MAX_TIMEOUT_MS = 600000;
|
|
1722
|
+
const requested = typeof args?.timeoutMs === "number" && args.timeoutMs > 0 ? args.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
1723
|
+
const timeoutMs = Math.min(Math.floor(requested), MAX_TIMEOUT_MS);
|
|
1629
1724
|
const result = spawnSync(command, {
|
|
1630
1725
|
cwd: workspaceRoot,
|
|
1631
1726
|
shell: true,
|
|
1632
1727
|
encoding: "utf8",
|
|
1633
|
-
timeout:
|
|
1728
|
+
timeout: timeoutMs,
|
|
1634
1729
|
maxBuffer: 20 * 1024 * 1024,
|
|
1635
1730
|
env: { ...process.env },
|
|
1636
1731
|
});
|
|
1637
|
-
|
|
1638
|
-
|
|
1732
|
+
const timedOut = !!result.error && result.error.code === "ETIMEDOUT";
|
|
1733
|
+
if (result.error && !timedOut) {
|
|
1639
1734
|
return {
|
|
1640
|
-
content: [
|
|
1641
|
-
{
|
|
1642
|
-
type: "text",
|
|
1643
|
-
text: timedOut
|
|
1644
|
-
? `Command timed out after 15s: ${command}`
|
|
1645
|
-
: `Command could not be executed: ${result.error.message}`,
|
|
1646
|
-
},
|
|
1647
|
-
],
|
|
1735
|
+
content: [{ type: "text", text: `Command could not be executed: ${result.error.message}` }],
|
|
1648
1736
|
};
|
|
1649
1737
|
}
|
|
1650
1738
|
const stdout = result.stdout || "";
|
|
1651
1739
|
const stderr = result.stderr || "";
|
|
1652
1740
|
const combined = [stdout, stderr].filter((s) => s.trim().length > 0).join("\n");
|
|
1653
1741
|
const status = result.status ?? 0;
|
|
1654
|
-
|
|
1742
|
+
// A timed-out run still produced output, and that partial output is normally where the
|
|
1743
|
+
// failure is. Discarding it forced the whole command to be re-run somewhere else.
|
|
1744
|
+
const header = timedOut
|
|
1745
|
+
? `Command timed out after ${Math.round(timeoutMs / 1000)}s (killed): ${command}\nOutput produced before the kill is below — it may be incomplete. Re-run with a higher timeoutMs if it was still making progress.`
|
|
1746
|
+
: status === 0
|
|
1747
|
+
? "Command completed successfully."
|
|
1748
|
+
: `Command failed with exit code ${status}.`;
|
|
1655
1749
|
if (!combined.trim()) {
|
|
1656
1750
|
return { content: [{ type: "text", text: `${header}\n\n(no output)` }] };
|
|
1657
1751
|
}
|