march-cli 0.1.21 → 0.1.22
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/bin/march.mjs +13 -13
- package/package.json +43 -43
- package/src/agent/command-exec-tool.mjs +172 -168
- package/src/agent/context-stats-tool.mjs +57 -57
- package/src/agent/editing/diff-apply.mjs +28 -28
- package/src/agent/editing/diff-format.mjs +57 -57
- package/src/agent/editing/lsp-report.mjs +69 -69
- package/src/agent/file-edit-tool.mjs +262 -262
- package/src/agent/file-tools/read-file-tool.mjs +112 -112
- package/src/agent/file-tools/read-image-tool.mjs +76 -76
- package/src/agent/model-payload-dumper.mjs +208 -208
- package/src/agent/pi-session/pi-session-sidecar-failure.mjs +10 -10
- package/src/agent/provider/payload-messages.mjs +138 -138
- package/src/agent/runner/codex-large-context-guard.mjs +87 -87
- package/src/agent/runner/codex-transport-compression.mjs +180 -180
- package/src/agent/runner/codex-transport-debug.mjs +113 -113
- package/src/agent/runner/codex-websocket-event-debug.mjs +130 -130
- package/src/agent/runner/fast-model.mjs +36 -36
- package/src/agent/runner/runner-cleanup.mjs +12 -12
- package/src/agent/runner/runner-init.mjs +15 -15
- package/src/agent/runner/runner-session-state.mjs +40 -40
- package/src/agent/runner/runner-utils.mjs +24 -24
- package/src/agent/runner.mjs +299 -299
- package/src/agent/runtime/ipc/ipc-peer.mjs +99 -99
- package/src/agent/runtime/ipc/process-ipc-transport.mjs +16 -16
- package/src/agent/runtime/remote-runner-client.mjs +73 -73
- package/src/agent/runtime/remote-ui-client.mjs +20 -20
- package/src/agent/runtime/runner-ipc-target.mjs +125 -125
- package/src/agent/runtime/runner-process-client.mjs +47 -47
- package/src/agent/runtime/runner-process-entry.mjs +11 -11
- package/src/agent/runtime/runner-process-factory.mjs +108 -108
- package/src/agent/runtime/runner-runtime-host.mjs +79 -79
- package/src/agent/runtime/runtime-factory.mjs +42 -42
- package/src/agent/runtime/runtime-host.mjs +34 -34
- package/src/agent/runtime/ui-event-bridge.mjs +95 -95
- package/src/agent/screen-tools/list-windows-tool.mjs +39 -39
- package/src/agent/screen-tools/screen-tool.mjs +49 -49
- package/src/agent/screen-tools/windows-screen.mjs +133 -133
- package/src/agent/session/session-auto-name.mjs +41 -41
- package/src/agent/session/session-binding.mjs +12 -12
- package/src/agent/session/session-options.mjs +47 -47
- package/src/agent/tool-names.mjs +1 -1
- package/src/agent/tool-result.mjs +3 -3
- package/src/agent/tool-summary.mjs +112 -112
- package/src/agent/tools.mjs +58 -58
- package/src/agent/turn/turn-events.mjs +111 -111
- package/src/agent/turn/turn-logging.mjs +30 -30
- package/src/agent/turn/turn-runner.mjs +196 -196
- package/src/agent/vision-capability.mjs +14 -14
- package/src/auth/login-command.mjs +90 -90
- package/src/auth/storage.mjs +34 -34
- package/src/cli/args.mjs +79 -79
- package/src/cli/commands/copy-command.mjs +87 -87
- package/src/cli/commands/export-command.mjs +206 -206
- package/src/cli/commands/extensions-command.mjs +53 -53
- package/src/cli/commands/help-command.mjs +7 -7
- package/src/cli/commands/model-command.mjs +141 -141
- package/src/cli/commands/paste-image-command.mjs +43 -43
- package/src/cli/commands/provider-command.mjs +59 -59
- package/src/cli/commands/status-command.mjs +194 -194
- package/src/cli/commands/thinking-command.mjs +87 -87
- package/src/cli/fallback-ui.mjs +156 -156
- package/src/cli/input/attachment-tokens.mjs +20 -20
- package/src/cli/input/autocomplete.mjs +74 -106
- package/src/cli/input/external-editor.mjs +39 -39
- package/src/cli/input/file-search/index.mjs +160 -0
- package/src/cli/input/history-store.mjs +35 -35
- package/src/cli/input/image-clipboard.mjs +55 -55
- package/src/cli/input/keybinding-dispatch.mjs +76 -76
- package/src/cli/input/keybindings.mjs +96 -96
- package/src/cli/input/mode-state.mjs +43 -43
- package/src/cli/input/prompt-templates.mjs +84 -84
- package/src/cli/input/select-with-keyboard.mjs +86 -86
- package/src/cli/permissions.mjs +103 -103
- package/src/cli/repl-commands.mjs +86 -86
- package/src/cli/repl-loop.mjs +183 -183
- package/src/cli/selector-list.mjs +21 -21
- package/src/cli/session/pi-session-switch-command.mjs +41 -41
- package/src/cli/session/session-command.mjs +23 -23
- package/src/cli/session/session-list-command.mjs +68 -68
- package/src/cli/session/session-name-command.mjs +26 -26
- package/src/cli/session/session-source-command.mjs +89 -89
- package/src/cli/session/session-switch-command.mjs +1 -1
- package/src/cli/shell/shell-command.mjs +55 -55
- package/src/cli/shell/shell-drawer-controls.mjs +33 -33
- package/src/cli/shell/shell-drawer.mjs +192 -192
- package/src/cli/shell/shell-split-layout.mjs +70 -70
- package/src/cli/slash-commands.mjs +192 -192
- package/src/cli/startup/create-runtime-runner.mjs +61 -61
- package/src/cli/startup/runtime-close.mjs +23 -23
- package/src/cli/startup/startup-banner.mjs +71 -71
- package/src/cli/startup/startup-session.mjs +51 -51
- package/src/cli/status-line-updater.mjs +75 -75
- package/src/cli/tool-output.mjs +9 -9
- package/src/cli/tui/editor/external-editor-runner.mjs +24 -24
- package/src/cli/tui/input/mouse-selection-controller.mjs +91 -91
- package/src/cli/tui/input/mouse-tracking.mjs +20 -20
- package/src/cli/tui/layout/main-pane-layout.mjs +47 -47
- package/src/cli/tui/layout/safe-render-boundary.mjs +46 -46
- package/src/cli/tui/markdown-renderer.mjs +285 -285
- package/src/cli/tui/output/scroll-state.mjs +79 -79
- package/src/cli/tui/output/text-line-renderer.mjs +50 -50
- package/src/cli/tui/output/tool-card-renderer.mjs +59 -59
- package/src/cli/tui/output/visible-lines.mjs +8 -8
- package/src/cli/tui/output-buffer.mjs +293 -293
- package/src/cli/tui/permission-request-ui.mjs +18 -18
- package/src/cli/tui/recall-rendering.mjs +25 -25
- package/src/cli/tui/render/render-scheduler.mjs +26 -26
- package/src/cli/tui/render/stream-delta-buffer.mjs +46 -46
- package/src/cli/tui/select/editor-select-list.mjs +111 -111
- package/src/cli/tui/selection-screen.mjs +269 -269
- package/src/cli/tui/status/retry-status.mjs +72 -72
- package/src/cli/tui/status/spinner-status.mjs +42 -42
- package/src/cli/tui/status/status-bar.mjs +225 -225
- package/src/cli/tui/syntax/highlighting.mjs +260 -260
- package/src/cli/tui/syntax/languages.mjs +91 -91
- package/src/cli/tui/syntax/tree-sitter/bash.highlights.scm +261 -261
- package/src/cli/tui/syntax/tree-sitter/c.highlights.scm +341 -341
- package/src/cli/tui/syntax/tree-sitter/cpp.highlights.scm +268 -268
- package/src/cli/tui/syntax/tree-sitter/csharp.highlights.scm +577 -577
- package/src/cli/tui/syntax/tree-sitter/css.highlights.scm +109 -109
- package/src/cli/tui/syntax/tree-sitter/diff.highlights.scm +49 -49
- package/src/cli/tui/syntax/tree-sitter/go.highlights.scm +254 -254
- package/src/cli/tui/syntax/tree-sitter/html.highlights.scm +13 -13
- package/src/cli/tui/syntax/tree-sitter/java.highlights.scm +330 -330
- package/src/cli/tui/syntax/tree-sitter/json.highlights.scm +38 -38
- package/src/cli/tui/syntax/tree-sitter/php.highlights.scm +203 -203
- package/src/cli/tui/syntax/tree-sitter/python.highlights.scm +137 -137
- package/src/cli/tui/syntax/tree-sitter/ruby.highlights.scm +309 -309
- package/src/cli/tui/syntax/tree-sitter/rust.highlights.scm +531 -531
- package/src/cli/tui/syntax/tree-sitter/toml.highlights.scm +39 -39
- package/src/cli/tui/syntax/tree-sitter/tsx.highlights.scm +35 -35
- package/src/cli/tui/syntax/tree-sitter/typescript.highlights.scm +35 -35
- package/src/cli/tui/syntax/tree-sitter/yaml.highlights.scm +99 -99
- package/src/cli/tui/tool-rendering.mjs +87 -87
- package/src/cli/tui/tui-diff-rendering.mjs +157 -157
- package/src/cli/tui/tui-handlers.mjs +111 -111
- package/src/cli/tui/tui-input-controller.mjs +61 -61
- package/src/cli/tui/ui-theme.mjs +157 -157
- package/src/cli/ui.mjs +297 -297
- package/src/config/config-json.mjs +84 -84
- package/src/config/dotenv.mjs +20 -20
- package/src/config/features.mjs +75 -75
- package/src/config/loader.mjs +143 -143
- package/src/config/settings-command.mjs +97 -97
- package/src/context/engine.mjs +198 -198
- package/src/context/injections.mjs +26 -26
- package/src/context/profiles.mjs +39 -39
- package/src/context/project-context.mjs +20 -20
- package/src/context/session-status.mjs +17 -17
- package/src/context/shell-layers.mjs +23 -23
- package/src/context/system-core/base.md +65 -65
- package/src/context/system-core/prompts/deepseek-v4-pro.md +3 -3
- package/src/context/system-core/prompts/default.md +3 -3
- package/src/context/system-core.mjs +35 -35
- package/src/debug/logger.mjs +141 -141
- package/src/debug/model-context-dumper.mjs +52 -52
- package/src/extensions/discovery.mjs +40 -40
- package/src/extensions/lifecycle-adapter.mjs +210 -210
- package/src/extensions/lifecycle-manifest.mjs +69 -69
- package/src/image-gen/index.mjs +7 -7
- package/src/image-gen/provider.mjs +231 -231
- package/src/image-gen/tool.mjs +84 -84
- package/src/lsp/client.mjs +257 -257
- package/src/lsp/diagnostic-store.mjs +42 -42
- package/src/lsp/diagnostics-format.mjs +72 -72
- package/src/lsp/managed-node-server.mjs +99 -99
- package/src/lsp/path-match.mjs +10 -10
- package/src/lsp/server-definitions.mjs +188 -188
- package/src/lsp/servers.mjs +165 -165
- package/src/lsp/service.mjs +110 -110
- package/src/lsp/status-message.mjs +9 -9
- package/src/lsp/typescript-project-resolver.mjs +186 -186
- package/src/main.mjs +299 -299
- package/src/mcp/client.mjs +195 -195
- package/src/mcp/config.mjs +130 -130
- package/src/mcp/index.mjs +48 -48
- package/src/mcp/tools.mjs +98 -98
- package/src/memory/database.mjs +219 -219
- package/src/memory/glossary.mjs +124 -124
- package/src/memory/graph/graph-cascades.mjs +109 -109
- package/src/memory/graph/graph-diagnostics.mjs +73 -73
- package/src/memory/graph/graph-path-removal.mjs +50 -50
- package/src/memory/graph/graph-path-utils.mjs +17 -17
- package/src/memory/graph/graph-primitives.mjs +103 -103
- package/src/memory/graph/graph-read.mjs +159 -159
- package/src/memory/graph.mjs +282 -282
- package/src/memory/markdown/markdown-delete.mjs +23 -23
- package/src/memory/markdown/markdown-format.mjs +128 -128
- package/src/memory/markdown/markdown-recall.mjs +28 -28
- package/src/memory/markdown/ripgrep.mjs +16 -16
- package/src/memory/markdown/sqlite-index.mjs +87 -87
- package/src/memory/markdown-store.mjs +286 -286
- package/src/memory/markdown-tools.mjs +103 -103
- package/src/memory/search.mjs +142 -142
- package/src/memory/snapshot.mjs +86 -86
- package/src/memory/system-views.mjs +120 -120
- package/src/memory/tools.mjs +282 -282
- package/src/network/environment.mjs +131 -131
- package/src/notification/desktop-notifier.mjs +262 -262
- package/src/platform/open-file.mjs +28 -28
- package/src/platform/spawn-command.mjs +27 -27
- package/src/provider/accept-command.mjs +89 -89
- package/src/provider/command.mjs +21 -21
- package/src/provider/config-command.mjs +129 -129
- package/src/provider/custom-provider.mjs +113 -113
- package/src/provider/hosted-tools.mjs +111 -111
- package/src/provider/presets.mjs +72 -72
- package/src/provider/share-command.mjs +79 -79
- package/src/provider/share-payload.mjs +52 -52
- package/src/session/attachment-display.mjs +16 -16
- package/src/session/attachment-references.mjs +65 -65
- package/src/session/attachments.mjs +140 -140
- package/src/session/persist.mjs +1 -1
- package/src/session/pi-manager.mjs +34 -34
- package/src/session/session-utils.mjs +16 -16
- package/src/session/sidecar-sync.mjs +19 -19
- package/src/session/sidecar.mjs +69 -69
- package/src/session/transcript.mjs +83 -83
- package/src/session/tree.mjs +42 -42
- package/src/shell/cli-runtime.mjs +11 -11
- package/src/shell/hints.mjs +12 -12
- package/src/shell/node-pty-adapter.mjs +81 -81
- package/src/shell/runtime-state.mjs +126 -126
- package/src/shell/runtime.mjs +252 -252
- package/src/shell/screen-buffer.mjs +136 -136
- package/src/shell/tool-read.mjs +74 -74
- package/src/shell/tools.mjs +299 -299
- package/src/supergrok/actions/image-generate.mjs +60 -60
- package/src/supergrok/actions/search.mjs +78 -78
- package/src/supergrok/auth.mjs +36 -36
- package/src/supergrok/constants.mjs +18 -18
- package/src/supergrok/oauth-provider.mjs +278 -278
- package/src/supergrok/provider.mjs +35 -35
- package/src/supergrok/response.mjs +76 -76
- package/src/supergrok/tool.mjs +61 -61
- package/src/text/ansi.mjs +3 -3
- package/src/web/config-command.mjs +43 -43
- package/src/web/fetch.mjs +78 -78
- package/src/web/presets.mjs +16 -16
- package/src/web/search.mjs +83 -83
- package/src/web/tools.mjs +107 -107
package/bin/march.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const emitWarning = process.emitWarning;
|
|
4
|
-
process.emitWarning = function filteredWarning(warning, ...args) {
|
|
5
|
-
const message = typeof warning === "string" ? warning : warning?.message;
|
|
6
|
-
const type = typeof warning === "string" ? args[0] : warning?.name;
|
|
7
|
-
if (type === "ExperimentalWarning" && String(message).includes("SQLite")) return;
|
|
8
|
-
return emitWarning.call(this, warning, ...args);
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const { run } = await import("../src/main.mjs");
|
|
12
|
-
const code = await run(process.argv.slice(2));
|
|
13
|
-
process.exitCode = code;
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const emitWarning = process.emitWarning;
|
|
4
|
+
process.emitWarning = function filteredWarning(warning, ...args) {
|
|
5
|
+
const message = typeof warning === "string" ? warning : warning?.message;
|
|
6
|
+
const type = typeof warning === "string" ? args[0] : warning?.name;
|
|
7
|
+
if (type === "ExperimentalWarning" && String(message).includes("SQLite")) return;
|
|
8
|
+
return emitWarning.call(this, warning, ...args);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const { run } = await import("../src/main.mjs");
|
|
12
|
+
const code = await run(process.argv.slice(2));
|
|
13
|
+
process.exitCode = code;
|
package/package.json
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "march-cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "March CLI — terminal-native coding agent with context reconstruction",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./src/main.mjs",
|
|
7
|
-
"bin": {
|
|
8
|
-
"march": "bin/march.mjs"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"bin/",
|
|
12
|
-
"src/"
|
|
13
|
-
],
|
|
14
|
-
"scripts": {
|
|
15
|
-
"dev": "cd .. && node march-cli/bin/march.mjs",
|
|
16
|
-
"test": "node test/smoke.test.mjs",
|
|
17
|
-
"test:fast": "node test/fast.test.mjs",
|
|
18
|
-
"test:real": "npm run test:shell-runtime-real && npm run test:shell-tui-real && npm run test:tui-key-real",
|
|
19
|
-
"test:shell-runtime-real": "node test/shell-real-runtime.acceptance.mjs",
|
|
20
|
-
"test:shell-tui-real": "node test/shell-tui-real.acceptance.mjs",
|
|
21
|
-
"test:tui-key-real": "node test/tui-key-real.acceptance.mjs",
|
|
22
|
-
"context": "cd .. && node march-cli/bin/march.mjs --dump-context",
|
|
23
|
-
"notify:experiment": "node scripts/notify-experiment.mjs",
|
|
24
|
-
"publish:env": "node scripts/npm-publish-from-env.mjs"
|
|
25
|
-
},
|
|
26
|
-
"dependencies": {
|
|
27
|
-
"@earendil-works/pi-ai": "^0.74.0",
|
|
28
|
-
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
29
|
-
"@earendil-works/pi-tui": "^0.74.0",
|
|
30
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
31
|
-
"@xterm/headless": "^5.5.0",
|
|
32
|
-
"marked": "^18.0.3",
|
|
33
|
-
"node-notifier": "^10.0.1",
|
|
34
|
-
"node-pty": "^1.1.0",
|
|
35
|
-
"typebox": "^1.0.58",
|
|
36
|
-
"undici": "^7.25.0",
|
|
37
|
-
"web-tree-sitter": "^0.26.8",
|
|
38
|
-
"ws": "^8.20.1"
|
|
39
|
-
},
|
|
40
|
-
"optionalDependencies": {
|
|
41
|
-
"@vscode/ripgrep": "^1.18.0"
|
|
42
|
-
}
|
|
43
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "march-cli",
|
|
3
|
+
"version": "0.1.22",
|
|
4
|
+
"description": "March CLI — terminal-native coding agent with context reconstruction",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/main.mjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"march": "bin/march.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"src/"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "cd .. && node march-cli/bin/march.mjs",
|
|
16
|
+
"test": "node test/smoke.test.mjs",
|
|
17
|
+
"test:fast": "node test/fast.test.mjs",
|
|
18
|
+
"test:real": "npm run test:shell-runtime-real && npm run test:shell-tui-real && npm run test:tui-key-real",
|
|
19
|
+
"test:shell-runtime-real": "node test/shell-real-runtime.acceptance.mjs",
|
|
20
|
+
"test:shell-tui-real": "node test/shell-tui-real.acceptance.mjs",
|
|
21
|
+
"test:tui-key-real": "node test/tui-key-real.acceptance.mjs",
|
|
22
|
+
"context": "cd .. && node march-cli/bin/march.mjs --dump-context",
|
|
23
|
+
"notify:experiment": "node scripts/notify-experiment.mjs",
|
|
24
|
+
"publish:env": "node scripts/npm-publish-from-env.mjs"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@earendil-works/pi-ai": "^0.74.0",
|
|
28
|
+
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
29
|
+
"@earendil-works/pi-tui": "^0.74.0",
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
31
|
+
"@xterm/headless": "^5.5.0",
|
|
32
|
+
"marked": "^18.0.3",
|
|
33
|
+
"node-notifier": "^10.0.1",
|
|
34
|
+
"node-pty": "^1.1.0",
|
|
35
|
+
"typebox": "^1.0.58",
|
|
36
|
+
"undici": "^7.25.0",
|
|
37
|
+
"web-tree-sitter": "^0.26.8",
|
|
38
|
+
"ws": "^8.20.1"
|
|
39
|
+
},
|
|
40
|
+
"optionalDependencies": {
|
|
41
|
+
"@vscode/ripgrep": "^1.18.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,168 +1,172 @@
|
|
|
1
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { Type } from "typebox";
|
|
5
|
-
import { toolText } from "./tool-result.mjs";
|
|
6
|
-
import { stripAnsi } from "../text/ansi.mjs";
|
|
7
|
-
|
|
8
|
-
const OUTPUT_LIMIT = 64 * 1024;
|
|
9
|
-
const DEFAULT_COMMAND_TIMEOUT_SECONDS = 60;
|
|
10
|
-
const COMMAND_KILL_GRACE_MS = 1000;
|
|
11
|
-
|
|
12
|
-
export function createCommandExecTool({ cwd }) {
|
|
13
|
-
return defineTool({
|
|
14
|
-
name: "command_exec",
|
|
15
|
-
label: "Command Exec",
|
|
16
|
-
description: "Run a one-shot command in the project directory. Use terminal_* for interactive or long-running processes.",
|
|
17
|
-
parameters: Type.Object({
|
|
18
|
-
command: Type.String({ description: "Command to execute" }),
|
|
19
|
-
shell: Type.Optional(Type.String({ description: "auto (default), bash, or powershell" })),
|
|
20
|
-
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds; default 60", default: DEFAULT_COMMAND_TIMEOUT_SECONDS })),
|
|
21
|
-
}),
|
|
22
|
-
execute: async (_toolCallId, params, signal) => executeCommand({ cwd, signal, ...params }),
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function executeCommand({ cwd, command, shell = "auto", timeout = DEFAULT_COMMAND_TIMEOUT_SECONDS, signal, spawnImpl = spawn, killProcessTreeImpl = killProcessTree, forceSettleMs = COMMAND_KILL_GRACE_MS }) {
|
|
27
|
-
let resolved;
|
|
28
|
-
try {
|
|
29
|
-
resolved = resolveCommandShell(shell);
|
|
30
|
-
} catch (err) {
|
|
31
|
-
return toolText(`Error: ${err.message}`, { error: true });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const timeoutSeconds = Math.max(0.001, Number(timeout) || DEFAULT_COMMAND_TIMEOUT_SECONDS);
|
|
35
|
-
const timeoutMs = timeoutSeconds * 1000;
|
|
36
|
-
const result = await spawnCommand(spawnImpl, resolved.bin, [...resolved.args, String(command ?? "")], {
|
|
37
|
-
cwd,
|
|
38
|
-
timeoutMs,
|
|
39
|
-
signal,
|
|
40
|
-
windowsHide: true,
|
|
41
|
-
killProcessTreeImpl,
|
|
42
|
-
forceSettleMs,
|
|
43
|
-
});
|
|
44
|
-
if (result.error) {
|
|
45
|
-
const detail = result.timedOut ? ` (timed out after ${timeoutSeconds}s)` : "";
|
|
46
|
-
return toolText(`Error: ${result.error.message}${detail}`, { error: true });
|
|
47
|
-
}
|
|
48
|
-
const stdout = stripAnsi(result.stdout ?? "");
|
|
49
|
-
const stderr = stripAnsi(result.stderr ?? "");
|
|
50
|
-
const output = formatCommandOutput({ stdout, stderr, status: result.status });
|
|
51
|
-
return toolText(output, {
|
|
52
|
-
status: result.status,
|
|
53
|
-
stdout,
|
|
54
|
-
stderr,
|
|
55
|
-
shell: resolved.name,
|
|
56
|
-
error: result.status !== 0,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function spawnCommand(spawnImpl, bin, args, options) {
|
|
61
|
-
return new Promise((resolve) => {
|
|
62
|
-
let stdout = "";
|
|
63
|
-
let stderr = "";
|
|
64
|
-
let settled = false;
|
|
65
|
-
let timedOut = false;
|
|
66
|
-
let aborted = false;
|
|
67
|
-
let forceTimer = null;
|
|
68
|
-
const child = spawnImpl(bin, args, {
|
|
69
|
-
cwd: options.cwd,
|
|
70
|
-
windowsHide: options.windowsHide,
|
|
71
|
-
detached: process.platform !== "win32",
|
|
72
|
-
});
|
|
73
|
-
const timer = setTimeout(() => {
|
|
74
|
-
timedOut = true;
|
|
75
|
-
terminateChild(new Error("Command timed out"));
|
|
76
|
-
}, options.timeoutMs);
|
|
77
|
-
const onAbort = () => {
|
|
78
|
-
aborted = true;
|
|
79
|
-
terminateChild(new Error("Command aborted"));
|
|
80
|
-
};
|
|
81
|
-
if (options.signal?.aborted) queueMicrotask(onAbort);
|
|
82
|
-
else options.signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
83
|
-
|
|
84
|
-
child.stdout?.setEncoding?.("utf8");
|
|
85
|
-
child.stderr?.setEncoding?.("utf8");
|
|
86
|
-
child.stdout?.on?.("data", (chunk) => { stdout = appendLimited(stdout, chunk); });
|
|
87
|
-
child.stderr?.on?.("data", (chunk) => { stderr = appendLimited(stderr, chunk); });
|
|
88
|
-
child.once?.("error", (error) => finish({ error }));
|
|
89
|
-
child.once?.("close", (status, signal) => finish({ status, signal }));
|
|
90
|
-
|
|
91
|
-
function terminateChild(error) {
|
|
92
|
-
if (settled) return;
|
|
93
|
-
options.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import { toolText } from "./tool-result.mjs";
|
|
6
|
+
import { stripAnsi } from "../text/ansi.mjs";
|
|
7
|
+
|
|
8
|
+
const OUTPUT_LIMIT = 64 * 1024;
|
|
9
|
+
const DEFAULT_COMMAND_TIMEOUT_SECONDS = 60;
|
|
10
|
+
const COMMAND_KILL_GRACE_MS = 1000;
|
|
11
|
+
|
|
12
|
+
export function createCommandExecTool({ cwd }) {
|
|
13
|
+
return defineTool({
|
|
14
|
+
name: "command_exec",
|
|
15
|
+
label: "Command Exec",
|
|
16
|
+
description: "Run a one-shot command in the project directory. Use terminal_* for interactive or long-running processes.",
|
|
17
|
+
parameters: Type.Object({
|
|
18
|
+
command: Type.String({ description: "Command to execute" }),
|
|
19
|
+
shell: Type.Optional(Type.String({ description: "auto (default), bash, or powershell" })),
|
|
20
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds; default 60", default: DEFAULT_COMMAND_TIMEOUT_SECONDS })),
|
|
21
|
+
}),
|
|
22
|
+
execute: async (_toolCallId, params, signal) => executeCommand({ cwd, signal, ...params }),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function executeCommand({ cwd, command, shell = "auto", timeout = DEFAULT_COMMAND_TIMEOUT_SECONDS, signal, spawnImpl = spawn, killProcessTreeImpl = killProcessTree, forceSettleMs = COMMAND_KILL_GRACE_MS }) {
|
|
27
|
+
let resolved;
|
|
28
|
+
try {
|
|
29
|
+
resolved = resolveCommandShell(shell);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
return toolText(`Error: ${err.message}`, { error: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const timeoutSeconds = Math.max(0.001, Number(timeout) || DEFAULT_COMMAND_TIMEOUT_SECONDS);
|
|
35
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
36
|
+
const result = await spawnCommand(spawnImpl, resolved.bin, [...resolved.args, String(command ?? "")], {
|
|
37
|
+
cwd,
|
|
38
|
+
timeoutMs,
|
|
39
|
+
signal,
|
|
40
|
+
windowsHide: true,
|
|
41
|
+
killProcessTreeImpl,
|
|
42
|
+
forceSettleMs,
|
|
43
|
+
});
|
|
44
|
+
if (result.error) {
|
|
45
|
+
const detail = result.timedOut ? ` (timed out after ${timeoutSeconds}s)` : "";
|
|
46
|
+
return toolText(`Error: ${result.error.message}${detail}`, { error: true });
|
|
47
|
+
}
|
|
48
|
+
const stdout = stripAnsi(result.stdout ?? "");
|
|
49
|
+
const stderr = stripAnsi(result.stderr ?? "");
|
|
50
|
+
const output = formatCommandOutput({ stdout, stderr, status: result.status });
|
|
51
|
+
return toolText(output, {
|
|
52
|
+
status: result.status,
|
|
53
|
+
stdout,
|
|
54
|
+
stderr,
|
|
55
|
+
shell: resolved.name,
|
|
56
|
+
error: result.status !== 0,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function spawnCommand(spawnImpl, bin, args, options) {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
let stdout = "";
|
|
63
|
+
let stderr = "";
|
|
64
|
+
let settled = false;
|
|
65
|
+
let timedOut = false;
|
|
66
|
+
let aborted = false;
|
|
67
|
+
let forceTimer = null;
|
|
68
|
+
const child = spawnImpl(bin, args, {
|
|
69
|
+
cwd: options.cwd,
|
|
70
|
+
windowsHide: options.windowsHide,
|
|
71
|
+
detached: process.platform !== "win32",
|
|
72
|
+
});
|
|
73
|
+
const timer = setTimeout(() => {
|
|
74
|
+
timedOut = true;
|
|
75
|
+
terminateChild(new Error("Command timed out"));
|
|
76
|
+
}, options.timeoutMs);
|
|
77
|
+
const onAbort = () => {
|
|
78
|
+
aborted = true;
|
|
79
|
+
terminateChild(new Error("Command aborted"));
|
|
80
|
+
};
|
|
81
|
+
if (options.signal?.aborted) queueMicrotask(onAbort);
|
|
82
|
+
else options.signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
83
|
+
|
|
84
|
+
child.stdout?.setEncoding?.("utf8");
|
|
85
|
+
child.stderr?.setEncoding?.("utf8");
|
|
86
|
+
child.stdout?.on?.("data", (chunk) => { stdout = appendLimited(stdout, chunk); });
|
|
87
|
+
child.stderr?.on?.("data", (chunk) => { stderr = appendLimited(stderr, chunk); });
|
|
88
|
+
child.once?.("error", (error) => finish({ error }));
|
|
89
|
+
child.once?.("close", (status, signal) => finish({ status, signal }));
|
|
90
|
+
|
|
91
|
+
function terminateChild(error) {
|
|
92
|
+
if (settled) return;
|
|
93
|
+
forceTimer ??= setTimeout(() => finish({ error }), options.forceSettleMs);
|
|
94
|
+
try {
|
|
95
|
+
options.killProcessTreeImpl?.(child);
|
|
96
|
+
} catch {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function finish(result) {
|
|
100
|
+
if (settled) return;
|
|
101
|
+
settled = true;
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
clearTimeout(forceTimer);
|
|
104
|
+
options.signal?.removeEventListener?.("abort", onAbort);
|
|
105
|
+
const error = result.error ?? (timedOut ? Object.assign(new Error("Command timed out"), { code: "ETIMEDOUT" }) : null) ?? (aborted ? Object.assign(new Error("Command aborted"), { code: "ABORT_ERR" }) : null);
|
|
106
|
+
resolve({ ...result, error, stdout, stderr, timedOut, aborted });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function killProcessTree(child, platform = process.platform, spawnImpl = spawn) {
|
|
112
|
+
if (!child?.pid) {
|
|
113
|
+
child?.kill?.("SIGTERM");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (platform === "win32") {
|
|
117
|
+
try {
|
|
118
|
+
const killer = spawnImpl("taskkill", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
|
|
119
|
+
killer?.on?.("error", () => child.kill?.("SIGTERM"));
|
|
120
|
+
killer?.unref?.();
|
|
121
|
+
return;
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
process.kill(-child.pid, "SIGTERM");
|
|
126
|
+
return;
|
|
127
|
+
} catch {}
|
|
128
|
+
child.kill?.("SIGTERM");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function appendLimited(current, chunk) {
|
|
132
|
+
const next = current + String(chunk ?? "");
|
|
133
|
+
return next.length <= OUTPUT_LIMIT ? next : next.slice(-OUTPUT_LIMIT);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function resolveCommandShell(shell = "auto", platform = process.platform) {
|
|
137
|
+
const normalized = String(shell ?? "auto").trim().toLowerCase();
|
|
138
|
+
if (normalized === "powershell" || (normalized === "auto" && platform === "win32")) {
|
|
139
|
+
return { name: "powershell", bin: findPowerShell() ?? "powershell.exe", args: ["-NoProfile", "-Command"] };
|
|
140
|
+
}
|
|
141
|
+
if (normalized === "bash" || normalized === "auto") {
|
|
142
|
+
return { name: "bash", bin: "bash", args: ["-lc"] };
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`unsupported shell: ${shell}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function findPowerShell() {
|
|
148
|
+
for (const name of ["pwsh.exe", "powershell.exe"]) {
|
|
149
|
+
try {
|
|
150
|
+
const result = spawnSync("where", [name], { encoding: "utf-8", timeout: 5000, windowsHide: true });
|
|
151
|
+
if (result.status === 0 && result.stdout) {
|
|
152
|
+
const first = result.stdout.trim().split(/\r?\n/)[0];
|
|
153
|
+
if (first && existsSync(first)) return first;
|
|
154
|
+
}
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function formatCommandOutput({ stdout, stderr, status }) {
|
|
161
|
+
const parts = [];
|
|
162
|
+
if (stdout) parts.push(stdout.trimEnd());
|
|
163
|
+
if (stderr) parts.push(stderr.trimEnd());
|
|
164
|
+
if (status && status !== 0) parts.push(`exit ${status}`);
|
|
165
|
+
const output = parts.filter(Boolean).join("\n");
|
|
166
|
+
return output ? truncateOutput(output) : "(no output)";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function truncateOutput(text) {
|
|
170
|
+
if (text.length <= OUTPUT_LIMIT) return text;
|
|
171
|
+
return `${text.slice(-OUTPUT_LIMIT)}\n... (truncated to last ${OUTPUT_LIMIT} chars)`;
|
|
172
|
+
}
|
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Type } from "typebox";
|
|
3
|
-
import { toolText } from "./tool-result.mjs";
|
|
4
|
-
|
|
5
|
-
export function createContextStatsTool({ engine }) {
|
|
6
|
-
return defineTool({
|
|
7
|
-
name: "context_stats",
|
|
8
|
-
label: "Context Stats",
|
|
9
|
-
description: "Show size statistics for the current March context layers without returning the full prompt text.",
|
|
10
|
-
parameters: Type.Object({}),
|
|
11
|
-
execute: async () => toolText(formatContextStats(engine), buildContextStats(engine)),
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function buildContextStats(engine) {
|
|
16
|
-
const layers = engine.buildContextLayers("");
|
|
17
|
-
const contextText = layers.map((layer) => layer.text).join("\n\n");
|
|
18
|
-
const layerStats = layers.map((layer) => ({
|
|
19
|
-
name: layer.name,
|
|
20
|
-
chars: layer.text.length,
|
|
21
|
-
estimatedTokens: estimateTokens(layer.text.length),
|
|
22
|
-
}));
|
|
23
|
-
return {
|
|
24
|
-
totalChars: contextText.length,
|
|
25
|
-
estimatedTokens: estimateTokens(contextText.length),
|
|
26
|
-
layers: layerStats,
|
|
27
|
-
runtime: {
|
|
28
|
-
turns: engine.turns?.length ?? 0,
|
|
29
|
-
toolDefs: engine.toolDefs?.length ?? 0,
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function formatContextStats(engine) {
|
|
35
|
-
const stats = buildContextStats(engine);
|
|
36
|
-
const lines = [
|
|
37
|
-
"Context stats:",
|
|
38
|
-
`total_chars: ${stats.totalChars}`,
|
|
39
|
-
`estimated_tokens: ${stats.estimatedTokens}`,
|
|
40
|
-
"",
|
|
41
|
-
"Layers:",
|
|
42
|
-
];
|
|
43
|
-
for (const layer of stats.layers) {
|
|
44
|
-
lines.push(`- ${layer.name}: ${layer.chars} chars, ~${layer.estimatedTokens} tokens`);
|
|
45
|
-
}
|
|
46
|
-
lines.push(
|
|
47
|
-
"",
|
|
48
|
-
"Runtime:",
|
|
49
|
-
`- turns: ${stats.runtime.turns}`,
|
|
50
|
-
`- tool_defs: ${stats.runtime.toolDefs}`,
|
|
51
|
-
);
|
|
52
|
-
return lines.join("\n");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function estimateTokens(chars) {
|
|
56
|
-
return Math.ceil(chars / 4);
|
|
57
|
-
}
|
|
1
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { toolText } from "./tool-result.mjs";
|
|
4
|
+
|
|
5
|
+
export function createContextStatsTool({ engine }) {
|
|
6
|
+
return defineTool({
|
|
7
|
+
name: "context_stats",
|
|
8
|
+
label: "Context Stats",
|
|
9
|
+
description: "Show size statistics for the current March context layers without returning the full prompt text.",
|
|
10
|
+
parameters: Type.Object({}),
|
|
11
|
+
execute: async () => toolText(formatContextStats(engine), buildContextStats(engine)),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildContextStats(engine) {
|
|
16
|
+
const layers = engine.buildContextLayers("");
|
|
17
|
+
const contextText = layers.map((layer) => layer.text).join("\n\n");
|
|
18
|
+
const layerStats = layers.map((layer) => ({
|
|
19
|
+
name: layer.name,
|
|
20
|
+
chars: layer.text.length,
|
|
21
|
+
estimatedTokens: estimateTokens(layer.text.length),
|
|
22
|
+
}));
|
|
23
|
+
return {
|
|
24
|
+
totalChars: contextText.length,
|
|
25
|
+
estimatedTokens: estimateTokens(contextText.length),
|
|
26
|
+
layers: layerStats,
|
|
27
|
+
runtime: {
|
|
28
|
+
turns: engine.turns?.length ?? 0,
|
|
29
|
+
toolDefs: engine.toolDefs?.length ?? 0,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatContextStats(engine) {
|
|
35
|
+
const stats = buildContextStats(engine);
|
|
36
|
+
const lines = [
|
|
37
|
+
"Context stats:",
|
|
38
|
+
`total_chars: ${stats.totalChars}`,
|
|
39
|
+
`estimated_tokens: ${stats.estimatedTokens}`,
|
|
40
|
+
"",
|
|
41
|
+
"Layers:",
|
|
42
|
+
];
|
|
43
|
+
for (const layer of stats.layers) {
|
|
44
|
+
lines.push(`- ${layer.name}: ${layer.chars} chars, ~${layer.estimatedTokens} tokens`);
|
|
45
|
+
}
|
|
46
|
+
lines.push(
|
|
47
|
+
"",
|
|
48
|
+
"Runtime:",
|
|
49
|
+
`- turns: ${stats.runtime.turns}`,
|
|
50
|
+
`- tool_defs: ${stats.runtime.toolDefs}`,
|
|
51
|
+
);
|
|
52
|
+
return lines.join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function estimateTokens(chars) {
|
|
56
|
+
return Math.ceil(chars / 4);
|
|
57
|
+
}
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Apply unified-diff-like patches to in-memory text content.
|
|
3
|
-
* Used by file-edit-tool for dry-run validation and safe application.
|
|
4
|
-
*/
|
|
5
|
-
export function applyReplaceTextPatch(text, oldText, newText) {
|
|
6
|
-
const idx = text.indexOf(oldText);
|
|
7
|
-
if (idx === -1) {
|
|
8
|
-
return { ok: false, error: `Text not found in content` };
|
|
9
|
-
}
|
|
10
|
-
const before = text.slice(0, idx);
|
|
11
|
-
const after = text.slice(idx + oldText.length);
|
|
12
|
-
return { ok: true, result: before + newText + after };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function applyReplaceRangePatch(text, startLine, endLine, newText) {
|
|
16
|
-
const lines = text.split("\n");
|
|
17
|
-
if (startLine < 1 || startLine > lines.length) {
|
|
18
|
-
return { ok: false, error: `startLine ${startLine} out of range (1-${lines.length})` };
|
|
19
|
-
}
|
|
20
|
-
if (endLine < startLine || endLine > lines.length) {
|
|
21
|
-
return { ok: false, error: `endLine ${endLine} out of range (${startLine}-${lines.length})` };
|
|
22
|
-
}
|
|
23
|
-
const zeroBasedStart = startLine - 1;
|
|
24
|
-
const before = lines.slice(0, zeroBasedStart);
|
|
25
|
-
const after = lines.slice(endLine);
|
|
26
|
-
const newLines = newText === "" ? [] : newText.split("\n");
|
|
27
|
-
return { ok: true, result: [...before, ...newLines, ...after].join("\n") };
|
|
28
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Apply unified-diff-like patches to in-memory text content.
|
|
3
|
+
* Used by file-edit-tool for dry-run validation and safe application.
|
|
4
|
+
*/
|
|
5
|
+
export function applyReplaceTextPatch(text, oldText, newText) {
|
|
6
|
+
const idx = text.indexOf(oldText);
|
|
7
|
+
if (idx === -1) {
|
|
8
|
+
return { ok: false, error: `Text not found in content` };
|
|
9
|
+
}
|
|
10
|
+
const before = text.slice(0, idx);
|
|
11
|
+
const after = text.slice(idx + oldText.length);
|
|
12
|
+
return { ok: true, result: before + newText + after };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function applyReplaceRangePatch(text, startLine, endLine, newText) {
|
|
16
|
+
const lines = text.split("\n");
|
|
17
|
+
if (startLine < 1 || startLine > lines.length) {
|
|
18
|
+
return { ok: false, error: `startLine ${startLine} out of range (1-${lines.length})` };
|
|
19
|
+
}
|
|
20
|
+
if (endLine < startLine || endLine > lines.length) {
|
|
21
|
+
return { ok: false, error: `endLine ${endLine} out of range (${startLine}-${lines.length})` };
|
|
22
|
+
}
|
|
23
|
+
const zeroBasedStart = startLine - 1;
|
|
24
|
+
const before = lines.slice(0, zeroBasedStart);
|
|
25
|
+
const after = lines.slice(endLine);
|
|
26
|
+
const newLines = newText === "" ? [] : newText.split("\n");
|
|
27
|
+
return { ok: true, result: [...before, ...newLines, ...after].join("\n") };
|
|
28
|
+
}
|