@shawnstack/quickforge 1.3.17 → 1.3.18
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 +10 -10
- package/package.json +1 -1
- package/server/tools/definitions.mjs +2 -0
- package/server/tools/index.mjs +211 -83
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# 速构 QuickForge
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<img alt="Version" src="https://img.shields.io/badge/version-1.3.
|
|
4
|
+
<img alt="Version" src="https://img.shields.io/badge/version-1.3.18-blue" />
|
|
5
5
|
<img alt="License" src="https://img.shields.io/badge/license-MIT-green" />
|
|
6
6
|
<img alt="Node" src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" />
|
|
7
7
|
<img alt="React" src="https://img.shields.io/badge/react-19-61DAFB?logo=react" />
|
|
@@ -65,7 +65,7 @@ QuickForge 的工具能力很直接,因此也需要谨慎使用:
|
|
|
65
65
|
#### 从 npm 安装
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
|
-
npm install -g @shawnstack/quickforge@1.3.
|
|
68
|
+
npm install -g @shawnstack/quickforge@1.3.18
|
|
69
69
|
qf
|
|
70
70
|
|
|
71
71
|
# CLI 工具
|
|
@@ -79,17 +79,17 @@ qf update
|
|
|
79
79
|
当前版本的离线包:
|
|
80
80
|
|
|
81
81
|
```text
|
|
82
|
-
package-offline/shawnstack-quickforge-1.3.
|
|
82
|
+
package-offline/shawnstack-quickforge-1.3.18.tgz
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
在安装了 Node.js 20+ 和 npm 的机器上执行:
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
-
npm install -g ./package-offline/shawnstack-quickforge-1.3.
|
|
88
|
+
npm install -g ./package-offline/shawnstack-quickforge-1.3.18.tgz
|
|
89
89
|
qf
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
该包由 `v1.3.
|
|
92
|
+
该包由 `v1.3.18` 标签生成,包含离线安装所需的运行时依赖。
|
|
93
93
|
|
|
94
94
|
### 本地开发
|
|
95
95
|
|
|
@@ -228,7 +228,7 @@ QuickForge intentionally exposes powerful local capabilities, so the boundaries
|
|
|
228
228
|
#### npm
|
|
229
229
|
|
|
230
230
|
```bash
|
|
231
|
-
npm install -g @shawnstack/quickforge@1.3.
|
|
231
|
+
npm install -g @shawnstack/quickforge@1.3.18
|
|
232
232
|
qf
|
|
233
233
|
|
|
234
234
|
# CLI utilities
|
|
@@ -239,20 +239,20 @@ qf update
|
|
|
239
239
|
|
|
240
240
|
#### Offline tarball
|
|
241
241
|
|
|
242
|
-
The offline release package for `v1.3.
|
|
242
|
+
The offline release package for `v1.3.18` is:
|
|
243
243
|
|
|
244
244
|
```text
|
|
245
|
-
package-offline/shawnstack-quickforge-1.3.
|
|
245
|
+
package-offline/shawnstack-quickforge-1.3.18.tgz
|
|
246
246
|
```
|
|
247
247
|
|
|
248
248
|
Install it on a machine with Node.js 20+ and npm:
|
|
249
249
|
|
|
250
250
|
```bash
|
|
251
|
-
npm install -g ./package-offline/shawnstack-quickforge-1.3.
|
|
251
|
+
npm install -g ./package-offline/shawnstack-quickforge-1.3.18.tgz
|
|
252
252
|
qf
|
|
253
253
|
```
|
|
254
254
|
|
|
255
|
-
The package was generated from tag `v1.3.
|
|
255
|
+
The package was generated from tag `v1.3.18` and includes bundled runtime dependencies for offline installation.
|
|
256
256
|
|
|
257
257
|
### Local development
|
|
258
258
|
|
package/package.json
CHANGED
|
@@ -68,6 +68,8 @@ export const workspaceTools = [
|
|
|
68
68
|
description: 'Run a shell command in the project bound to this chat. Use this for lint, build, tests, git status, and diagnostics.',
|
|
69
69
|
parameters: Type.Object({
|
|
70
70
|
command: Type.String({ description: 'Command to execute in the workspace.' }),
|
|
71
|
+
timeoutMs: Type.Optional(Type.Number({ description: 'Command timeout in milliseconds. Defaults to 30 minutes and is clamped to the supported range.', default: 1800000 })),
|
|
72
|
+
description: Type.Optional(Type.String({ description: 'Short explanation of why this command is being run.' })),
|
|
71
73
|
}),
|
|
72
74
|
executionMode: 'sequential',
|
|
73
75
|
},
|
package/server/tools/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs'
|
|
1
|
+
import { createWriteStream, promises as fs } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { spawn } from 'node:child_process'
|
|
4
4
|
import { createRequire } from 'node:module'
|
|
5
5
|
import { resolveWorkspacePath, toWorkspaceRelative, assertSafeWorkspacePath, truncateText, splitLines, walkFiles } from '../utils/workspace.mjs'
|
|
6
|
+
import { logsDir } from '../storage.mjs'
|
|
6
7
|
import { createTextDiff } from '../utils/text-diff.mjs'
|
|
7
8
|
import {
|
|
8
9
|
formatSkillActivation,
|
|
@@ -476,63 +477,63 @@ export async function toolWriteFile(params, context) {
|
|
|
476
477
|
}
|
|
477
478
|
}
|
|
478
479
|
|
|
479
|
-
// --- edit_file ---
|
|
480
|
-
function countOccurrences(text, needle) {
|
|
481
|
-
if (!needle) return 0
|
|
482
|
-
let count = 0
|
|
483
|
-
let index = 0
|
|
484
|
-
while ((index = text.indexOf(needle, index)) !== -1) {
|
|
485
|
-
count++
|
|
486
|
-
index += needle.length
|
|
487
|
-
}
|
|
488
|
-
return count
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function detectLineEnding(text) {
|
|
492
|
-
return text.includes('\r\n') ? '\r\n' : '\n'
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function normalizeLineEndings(text) {
|
|
496
|
-
return text.replaceAll('\r\n', '\n').replaceAll('\r', '\n')
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function convertToLineEnding(text, ending) {
|
|
500
|
-
const normalized = normalizeLineEndings(text)
|
|
501
|
-
return ending === '\r\n' ? normalized.replaceAll('\n', '\r\n') : normalized
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
export async function toolEditFile(params, context) {
|
|
505
|
-
const file = resolveWorkspacePath(params?.path, context)
|
|
506
|
-
await assertSafeWorkspacePath(file, context)
|
|
507
|
-
|
|
508
|
-
const rawOldText = String(params?.oldText ?? '')
|
|
509
|
-
const rawNewText = String(params?.newText ?? '')
|
|
510
|
-
const text = await fs.readFile(file, 'utf8')
|
|
511
|
-
const lineEnding = detectLineEnding(text)
|
|
512
|
-
const oldText = convertToLineEnding(rawOldText, lineEnding)
|
|
513
|
-
const newText = convertToLineEnding(rawNewText, lineEnding)
|
|
514
|
-
const count = countOccurrences(text, oldText)
|
|
515
|
-
|
|
516
|
-
if (count !== 1) {
|
|
517
|
-
const rawCount = oldText === rawOldText ? count : countOccurrences(text, rawOldText)
|
|
518
|
-
const suffix = rawCount !== count ? ` after normalizing line endings; raw match count was ${rawCount}` : ''
|
|
519
|
-
const error = new Error(`oldText must match exactly once; found ${count} matches${suffix}`)
|
|
520
|
-
error.statusCode = 400
|
|
521
|
-
throw error
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const nextText = text.replace(oldText, newText)
|
|
525
|
-
const relativePath = toWorkspaceRelative(file, context)
|
|
526
|
-
const diff = createTextDiff(text, nextText, relativePath)
|
|
527
|
-
|
|
528
|
-
await fs.writeFile(file, nextText, 'utf8')
|
|
529
|
-
|
|
530
|
-
return {
|
|
531
|
-
content: `Edited ${relativePath} (+${diff.addedLines} -${diff.removedLines})`,
|
|
532
|
-
details: { path: relativePath, project: context?.project, replaced: count, diff },
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
480
|
+
// --- edit_file ---
|
|
481
|
+
function countOccurrences(text, needle) {
|
|
482
|
+
if (!needle) return 0
|
|
483
|
+
let count = 0
|
|
484
|
+
let index = 0
|
|
485
|
+
while ((index = text.indexOf(needle, index)) !== -1) {
|
|
486
|
+
count++
|
|
487
|
+
index += needle.length
|
|
488
|
+
}
|
|
489
|
+
return count
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function detectLineEnding(text) {
|
|
493
|
+
return text.includes('\r\n') ? '\r\n' : '\n'
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function normalizeLineEndings(text) {
|
|
497
|
+
return text.replaceAll('\r\n', '\n').replaceAll('\r', '\n')
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function convertToLineEnding(text, ending) {
|
|
501
|
+
const normalized = normalizeLineEndings(text)
|
|
502
|
+
return ending === '\r\n' ? normalized.replaceAll('\n', '\r\n') : normalized
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export async function toolEditFile(params, context) {
|
|
506
|
+
const file = resolveWorkspacePath(params?.path, context)
|
|
507
|
+
await assertSafeWorkspacePath(file, context)
|
|
508
|
+
|
|
509
|
+
const rawOldText = String(params?.oldText ?? '')
|
|
510
|
+
const rawNewText = String(params?.newText ?? '')
|
|
511
|
+
const text = await fs.readFile(file, 'utf8')
|
|
512
|
+
const lineEnding = detectLineEnding(text)
|
|
513
|
+
const oldText = convertToLineEnding(rawOldText, lineEnding)
|
|
514
|
+
const newText = convertToLineEnding(rawNewText, lineEnding)
|
|
515
|
+
const count = countOccurrences(text, oldText)
|
|
516
|
+
|
|
517
|
+
if (count !== 1) {
|
|
518
|
+
const rawCount = oldText === rawOldText ? count : countOccurrences(text, rawOldText)
|
|
519
|
+
const suffix = rawCount !== count ? ` after normalizing line endings; raw match count was ${rawCount}` : ''
|
|
520
|
+
const error = new Error(`oldText must match exactly once; found ${count} matches${suffix}`)
|
|
521
|
+
error.statusCode = 400
|
|
522
|
+
throw error
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const nextText = text.replace(oldText, newText)
|
|
526
|
+
const relativePath = toWorkspaceRelative(file, context)
|
|
527
|
+
const diff = createTextDiff(text, nextText, relativePath)
|
|
528
|
+
|
|
529
|
+
await fs.writeFile(file, nextText, 'utf8')
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
content: `Edited ${relativePath} (+${diff.addedLines} -${diff.removedLines})`,
|
|
533
|
+
details: { path: relativePath, project: context?.project, replaced: count, diff },
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
536
537
|
// --- run_command ---
|
|
537
538
|
function activeSkillsForContext(context) {
|
|
538
539
|
return mergeSkills(context?.globalSkills, context?.projectSkills)
|
|
@@ -587,6 +588,29 @@ export async function toolReadSkillResource(params, context) {
|
|
|
587
588
|
}
|
|
588
589
|
|
|
589
590
|
// --- run_command ---
|
|
591
|
+
const DEFAULT_RUN_COMMAND_TIMEOUT_MS = 30 * 60 * 1000
|
|
592
|
+
const MIN_RUN_COMMAND_TIMEOUT_MS = 1000
|
|
593
|
+
const MAX_RUN_COMMAND_TIMEOUT_MS = 30 * 60 * 1000
|
|
594
|
+
const COMMAND_TAIL_LINES = 100
|
|
595
|
+
|
|
596
|
+
function formatDurationMs(durationMs = 0) {
|
|
597
|
+
const totalSeconds = Math.max(0, Math.floor(durationMs / 1000))
|
|
598
|
+
const minutes = Math.floor(totalSeconds / 60)
|
|
599
|
+
const seconds = totalSeconds % 60
|
|
600
|
+
if (minutes > 0) return `${minutes}m ${seconds}s (${durationMs}ms)`
|
|
601
|
+
return `${seconds}s (${durationMs}ms)`
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function tailText(current, chunk, maxLines = COMMAND_TAIL_LINES) {
|
|
605
|
+
const lines = (current + chunk).split(/\r?\n/)
|
|
606
|
+
if (lines.length <= maxLines) return lines.join('\n')
|
|
607
|
+
return lines.slice(lines.length - maxLines).join('\n')
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function tailLabel(name, truncated) {
|
|
611
|
+
return truncated ? `${name} (last ${COMMAND_TAIL_LINES} lines):` : `${name}:`
|
|
612
|
+
}
|
|
613
|
+
|
|
590
614
|
function commandStatus(meta = {}) {
|
|
591
615
|
if (meta.running) return 'Status: running'
|
|
592
616
|
const flags = [
|
|
@@ -598,16 +622,46 @@ function commandStatus(meta = {}) {
|
|
|
598
622
|
}
|
|
599
623
|
|
|
600
624
|
function formatCommandOutput(command, stdout, stderr, meta = {}) {
|
|
601
|
-
|
|
625
|
+
const lines = [
|
|
602
626
|
`Command: ${command}`,
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
627
|
+
]
|
|
628
|
+
if (meta.description) lines.push(`Description: ${meta.description}`)
|
|
629
|
+
lines.push(commandStatus(meta))
|
|
630
|
+
if (typeof meta.durationMs === 'number') lines.push(`Duration: ${formatDurationMs(meta.durationMs)}`)
|
|
631
|
+
if (typeof meta.timeoutMs === 'number') lines.push(`Timeout: ${formatDurationMs(meta.timeoutMs)}`)
|
|
632
|
+
if (meta.cwd) lines.push(`CWD: ${meta.cwd}`)
|
|
633
|
+
if (meta.outputFile) lines.push(`Full output: ${meta.outputFile}`)
|
|
634
|
+
if (meta.outputTruncated) lines.push(`Output mode: showing the last ${COMMAND_TAIL_LINES} lines; full output is saved to the log file.`)
|
|
635
|
+
if (meta.logError) lines.push(`Log warning: ${meta.logError}`)
|
|
636
|
+
lines.push('', tailLabel('STDOUT', meta.stdoutTruncated), stdout || '(empty)', '', tailLabel('STDERR', meta.stderrTruncated), stderr || '(empty)')
|
|
637
|
+
return lines.join('\n')
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function safeLogFilePart(value, fallback) {
|
|
641
|
+
const text = String(value || '').replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 80)
|
|
642
|
+
return text || fallback
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async function createCommandLogStream(command, { cwd, description, toolCallId } = {}) {
|
|
646
|
+
const dir = path.join(logsDir, 'commands')
|
|
647
|
+
await fs.mkdir(dir, { recursive: true })
|
|
648
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
649
|
+
const fileName = `${timestamp}_${safeLogFilePart(toolCallId, 'command')}.log`
|
|
650
|
+
const outputFile = path.join(dir, fileName)
|
|
651
|
+
const stream = createWriteStream(outputFile, { flags: 'wx' })
|
|
652
|
+
stream.write([
|
|
653
|
+
`Command: ${command}`,
|
|
654
|
+
description ? `Description: ${description}` : null,
|
|
655
|
+
`CWD: ${cwd}`,
|
|
656
|
+
`Started at: ${new Date().toISOString()}`,
|
|
607
657
|
'',
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
658
|
+
].filter(Boolean).join('\n'))
|
|
659
|
+
return { stream, outputFile }
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function writeCommandLog(stream, source, chunk) {
|
|
663
|
+
stream.write(`\n[${source} ${new Date().toISOString()}]\n`)
|
|
664
|
+
stream.write(chunk)
|
|
611
665
|
}
|
|
612
666
|
|
|
613
667
|
function killProcessTree(child, signal = 'SIGTERM') {
|
|
@@ -641,8 +695,6 @@ export function abortRunningCommand(toolCallId) {
|
|
|
641
695
|
return true
|
|
642
696
|
}
|
|
643
697
|
|
|
644
|
-
const RUN_COMMAND_TIMEOUT_MS = 30 * 60 * 1000
|
|
645
|
-
|
|
646
698
|
export async function toolRunCommand(params, context, runtime = {}) {
|
|
647
699
|
const command = String(params?.command || '')
|
|
648
700
|
if (!command.trim()) {
|
|
@@ -651,12 +703,34 @@ export async function toolRunCommand(params, context, runtime = {}) {
|
|
|
651
703
|
throw error
|
|
652
704
|
}
|
|
653
705
|
|
|
654
|
-
const
|
|
706
|
+
const description = String(params?.description || '').trim().slice(0, 500)
|
|
707
|
+
const timeoutMs = clampNumber(params?.timeoutMs, DEFAULT_RUN_COMMAND_TIMEOUT_MS, MIN_RUN_COMMAND_TIMEOUT_MS, MAX_RUN_COMMAND_TIMEOUT_MS)
|
|
655
708
|
const cwd = getToolWorkspaceRoot(context)
|
|
709
|
+
const startedAt = Date.now()
|
|
656
710
|
|
|
657
711
|
if (runtime.signal?.aborted) {
|
|
658
|
-
const content = formatCommandOutput(command, '', 'Command aborted before start.', {
|
|
659
|
-
|
|
712
|
+
const content = formatCommandOutput(command, '', 'Command aborted before start.', {
|
|
713
|
+
aborted: true,
|
|
714
|
+
cwd,
|
|
715
|
+
description,
|
|
716
|
+
timeoutMs,
|
|
717
|
+
durationMs: 0,
|
|
718
|
+
})
|
|
719
|
+
return { content: truncateText(content), details: { command, description, project: context?.project, cwd, timeoutMs, durationMs: 0, aborted: true } }
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
let logStream = null
|
|
723
|
+
let outputFile = null
|
|
724
|
+
let logError = null
|
|
725
|
+
try {
|
|
726
|
+
const log = await createCommandLogStream(command, { cwd, description, toolCallId: runtime.toolCallId })
|
|
727
|
+
logStream = log.stream
|
|
728
|
+
outputFile = log.outputFile
|
|
729
|
+
logStream.on('error', (error) => {
|
|
730
|
+
logError = error?.message || 'Failed to write command log.'
|
|
731
|
+
})
|
|
732
|
+
} catch (error) {
|
|
733
|
+
logError = error?.message || 'Failed to create command log.'
|
|
660
734
|
}
|
|
661
735
|
|
|
662
736
|
return new Promise((resolve) => {
|
|
@@ -670,6 +744,8 @@ export async function toolRunCommand(params, context, runtime = {}) {
|
|
|
670
744
|
|
|
671
745
|
let stdout = ''
|
|
672
746
|
let stderr = ''
|
|
747
|
+
let stdoutTruncated = false
|
|
748
|
+
let stderrTruncated = false
|
|
673
749
|
let timedOut = false
|
|
674
750
|
let aborted = false
|
|
675
751
|
let settled = false
|
|
@@ -685,21 +761,65 @@ export async function toolRunCommand(params, context, runtime = {}) {
|
|
|
685
761
|
runtime.signal?.removeEventListener?.('abort', onAbort)
|
|
686
762
|
}
|
|
687
763
|
|
|
764
|
+
const commonDetails = (extra = {}) => {
|
|
765
|
+
const now = Date.now()
|
|
766
|
+
return {
|
|
767
|
+
command,
|
|
768
|
+
description,
|
|
769
|
+
project: context?.project,
|
|
770
|
+
cwd,
|
|
771
|
+
timeoutMs,
|
|
772
|
+
outputFile,
|
|
773
|
+
stdout,
|
|
774
|
+
stderr,
|
|
775
|
+
stdoutTruncated,
|
|
776
|
+
stderrTruncated,
|
|
777
|
+
outputTruncated: stdoutTruncated || stderrTruncated,
|
|
778
|
+
durationMs: now - startedAt,
|
|
779
|
+
toolCallId: runtime.toolCallId,
|
|
780
|
+
logError,
|
|
781
|
+
...extra,
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const resolveAfterLogClose = (result) => {
|
|
786
|
+
if (!logStream) {
|
|
787
|
+
resolve(result)
|
|
788
|
+
return
|
|
789
|
+
}
|
|
790
|
+
const details = result.details || {}
|
|
791
|
+
logStream.write([
|
|
792
|
+
'',
|
|
793
|
+
'',
|
|
794
|
+
`[quickforge ${new Date().toISOString()}]`,
|
|
795
|
+
`Exit code: ${details.code ?? 'unknown'}${details.signal ? `, signal: ${details.signal}` : ''}`,
|
|
796
|
+
`Duration: ${formatDurationMs(details.durationMs)}`,
|
|
797
|
+
`Timed out: ${Boolean(details.timedOut)}`,
|
|
798
|
+
`Aborted: ${Boolean(details.aborted)}`,
|
|
799
|
+
'Command finished.',
|
|
800
|
+
'',
|
|
801
|
+
].join('\n'))
|
|
802
|
+
logStream.end(() => resolve(result))
|
|
803
|
+
}
|
|
804
|
+
|
|
688
805
|
const finish = ({ code = null, signal = null, error = null } = {}) => {
|
|
689
806
|
if (settled) return
|
|
690
807
|
flushUpdate()
|
|
691
808
|
settled = true
|
|
692
809
|
cleanup()
|
|
810
|
+
const durationMs = Date.now() - startedAt
|
|
693
811
|
if (error) {
|
|
694
|
-
|
|
812
|
+
const details = commonDetails({ error: error.message, aborted, timedOut, durationMs })
|
|
813
|
+
resolveAfterLogClose({
|
|
695
814
|
isError: true,
|
|
696
|
-
content: truncateText(`Error running command: ${error.message}
|
|
697
|
-
details
|
|
815
|
+
content: truncateText(formatCommandOutput(command, stdout, `Error running command: ${error.message}\n${stderr}`.trim(), details)),
|
|
816
|
+
details,
|
|
698
817
|
})
|
|
699
818
|
return
|
|
700
819
|
}
|
|
701
|
-
const
|
|
702
|
-
|
|
820
|
+
const details = commonDetails({ code, signal, timedOut, aborted, durationMs })
|
|
821
|
+
const content = formatCommandOutput(command, stdout, stderr, details)
|
|
822
|
+
resolveAfterLogClose({ content: truncateText(content), details })
|
|
703
823
|
}
|
|
704
824
|
|
|
705
825
|
const stopChild = (reason) => {
|
|
@@ -718,15 +838,16 @@ export async function toolRunCommand(params, context, runtime = {}) {
|
|
|
718
838
|
finish({ signal: 'SIGTERM' })
|
|
719
839
|
}
|
|
720
840
|
|
|
721
|
-
const runningDetails = () => ({
|
|
841
|
+
const runningDetails = () => commonDetails({ running: true })
|
|
722
842
|
|
|
723
843
|
const emitUpdate = () => {
|
|
724
844
|
updateTimer = null
|
|
725
845
|
if (settled || !updatePending) return
|
|
726
846
|
updatePending = false
|
|
847
|
+
const details = runningDetails()
|
|
727
848
|
runtime.onUpdate?.({
|
|
728
|
-
content: [{ type: 'text', text: truncateText(formatCommandOutput(command, stdout, stderr,
|
|
729
|
-
details
|
|
849
|
+
content: [{ type: 'text', text: truncateText(formatCommandOutput(command, stdout, stderr, details)) }],
|
|
850
|
+
details,
|
|
730
851
|
})
|
|
731
852
|
}
|
|
732
853
|
const flushUpdate = () => {
|
|
@@ -736,9 +857,10 @@ export async function toolRunCommand(params, context, runtime = {}) {
|
|
|
736
857
|
}
|
|
737
858
|
if (!updatePending) return
|
|
738
859
|
updatePending = false
|
|
860
|
+
const details = runningDetails()
|
|
739
861
|
runtime.onUpdate?.({
|
|
740
|
-
content: [{ type: 'text', text: truncateText(formatCommandOutput(command, stdout, stderr,
|
|
741
|
-
details
|
|
862
|
+
content: [{ type: 'text', text: truncateText(formatCommandOutput(command, stdout, stderr, details)) }],
|
|
863
|
+
details,
|
|
742
864
|
})
|
|
743
865
|
}
|
|
744
866
|
const scheduleUpdate = () => {
|
|
@@ -755,12 +877,18 @@ export async function toolRunCommand(params, context, runtime = {}) {
|
|
|
755
877
|
|
|
756
878
|
child.stdout.on('data', (chunk) => {
|
|
757
879
|
if (settled) return
|
|
758
|
-
|
|
880
|
+
const text = chunk.toString()
|
|
881
|
+
stdoutTruncated = stdoutTruncated || (stdout + text).split(/\r?\n/).length > COMMAND_TAIL_LINES
|
|
882
|
+
stdout = tailText(stdout, text)
|
|
883
|
+
if (logStream && !logError) writeCommandLog(logStream, 'stdout', text)
|
|
759
884
|
scheduleUpdate()
|
|
760
885
|
})
|
|
761
886
|
child.stderr.on('data', (chunk) => {
|
|
762
887
|
if (settled) return
|
|
763
|
-
|
|
888
|
+
const text = chunk.toString()
|
|
889
|
+
stderrTruncated = stderrTruncated || (stderr + text).split(/\r?\n/).length > COMMAND_TAIL_LINES
|
|
890
|
+
stderr = tailText(stderr, text)
|
|
891
|
+
if (logStream && !logError) writeCommandLog(logStream, 'stderr', text)
|
|
764
892
|
scheduleUpdate()
|
|
765
893
|
})
|
|
766
894
|
child.on('close', (code, signal) => {
|