@namzu/sdk 57.1.0 → 58.0.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/CHANGELOG.md +51 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +2 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/agents/file-definitions.d.ts +35 -4
- package/dist/agents/file-definitions.d.ts.map +1 -1
- package/dist/agents/file-definitions.js +98 -8
- package/dist/agents/file-definitions.js.map +1 -1
- package/dist/authorization/protected-paths.d.ts +9 -0
- package/dist/authorization/protected-paths.d.ts.map +1 -1
- package/dist/authorization/protected-paths.js +29 -3
- package/dist/authorization/protected-paths.js.map +1 -1
- package/dist/connector/mcp/client.d.ts +8 -0
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +75 -2
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/constants/mcp/index.d.ts +2 -0
- package/dist/constants/mcp/index.d.ts.map +1 -1
- package/dist/constants/mcp/index.js +2 -0
- package/dist/constants/mcp/index.js.map +1 -1
- package/dist/constants/plugin/index.d.ts +2 -0
- package/dist/constants/plugin/index.d.ts.map +1 -1
- package/dist/constants/plugin/index.js +2 -0
- package/dist/constants/plugin/index.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +25 -4
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +2 -1
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/persona/assembler.d.ts.map +1 -1
- package/dist/persona/assembler.js +2 -2
- package/dist/persona/assembler.js.map +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/lifecycle.d.ts +8 -1
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +9 -2
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/shell-hook-http.d.ts +39 -0
- package/dist/plugin/shell-hook-http.d.ts.map +1 -0
- package/dist/plugin/shell-hook-http.js +163 -0
- package/dist/plugin/shell-hook-http.js.map +1 -0
- package/dist/plugin/shell-hook.d.ts +93 -11
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +223 -39
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/public-runtime.d.ts +2 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +2 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +1 -0
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +2 -0
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +1 -1
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/query/executor.d.ts +1 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +34 -9
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +2 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +25 -1
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +6 -2
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +18 -8
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.js +33 -9
- package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +6 -4
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +70 -3
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/path-escalation.d.ts +2 -0
- package/dist/runtime/query/path-escalation.d.ts.map +1 -1
- package/dist/runtime/query/path-escalation.js +7 -4
- package/dist/runtime/query/path-escalation.js.map +1 -1
- package/dist/runtime/query/plugin-hooks.js +2 -2
- package/dist/runtime/query/plugin-hooks.js.map +1 -1
- package/dist/runtime/query/prepare-turn.d.ts +2 -0
- package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
- package/dist/runtime/query/prepare-turn.js +25 -0
- package/dist/runtime/query/prepare-turn.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +3 -2
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +32 -13
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/session/retention/archive.js +1 -1
- package/dist/session/retention/archive.js.map +1 -1
- package/dist/session/workspace/driver.d.ts +6 -0
- package/dist/session/workspace/driver.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.js +28 -0
- package/dist/session/workspace/git-worktree.js.map +1 -1
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/loader.d.ts +7 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +53 -0
- package/dist/skills/loader.js.map +1 -1
- package/dist/skills/registry.d.ts +1 -1
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +2 -1
- package/dist/skills/registry.js.map +1 -1
- package/dist/tools/builtins/bash-cwd.d.ts +52 -0
- package/dist/tools/builtins/bash-cwd.d.ts.map +1 -0
- package/dist/tools/builtins/bash-cwd.js +127 -0
- package/dist/tools/builtins/bash-cwd.js.map +1 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +41 -6
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/bounded-file-read.d.ts +10 -0
- package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -0
- package/dist/tools/builtins/bounded-file-read.js +67 -0
- package/dist/tools/builtins/bounded-file-read.js.map +1 -0
- package/dist/tools/builtins/grep.d.ts +6 -1
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +187 -24
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts +1 -0
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +113 -28
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/read-media.d.ts +68 -0
- package/dist/tools/builtins/read-media.d.ts.map +1 -0
- package/dist/tools/builtins/read-media.js +397 -0
- package/dist/tools/builtins/read-media.js.map +1 -0
- package/dist/tools/builtins/search-tools.js +1 -1
- package/dist/tools/builtins/search-tools.js.map +1 -1
- package/dist/tools/builtins/skill.d.ts +4 -2
- package/dist/tools/builtins/skill.d.ts.map +1 -1
- package/dist/tools/builtins/skill.js +61 -2
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/builtins/sleep-guard.d.ts +15 -0
- package/dist/tools/builtins/sleep-guard.d.ts.map +1 -0
- package/dist/tools/builtins/sleep-guard.js +33 -0
- package/dist/tools/builtins/sleep-guard.js.map +1 -0
- package/dist/tools/builtins/view-image.d.ts +23 -1
- package/dist/tools/builtins/view-image.d.ts.map +1 -1
- package/dist/tools/builtins/view-image.js +66 -58
- package/dist/tools/builtins/view-image.js.map +1 -1
- package/dist/tools/file-read-tracker.d.ts.map +1 -1
- package/dist/tools/file-read-tracker.js +31 -0
- package/dist/tools/file-read-tracker.js.map +1 -1
- package/dist/toolsets/manager.js +3 -3
- package/dist/toolsets/manager.js.map +1 -1
- package/dist/types/agent/query.d.ts +2 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/agent/task.d.ts +6 -2
- package/dist/types/agent/task.d.ts.map +1 -1
- package/dist/types/agent/task.js.map +1 -1
- package/dist/types/connector/mcp.d.ts +46 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +1 -1
- package/dist/types/plugin/index.d.ts +72 -5
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +12 -1
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/session/sub-session.d.ts +6 -2
- package/dist/types/session/sub-session.d.ts.map +1 -1
- package/dist/types/skills/index.d.ts +36 -0
- package/dist/types/skills/index.d.ts.map +1 -1
- package/dist/types/skills/index.js +9 -0
- package/dist/types/skills/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +22 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/workspace/ref.d.ts +2 -0
- package/dist/types/workspace/ref.d.ts.map +1 -1
- package/dist/utils/frontmatter.d.ts +12 -0
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +6 -0
- package/dist/utils/frontmatter.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +2 -0
- package/src/agents/file-definitions.ts +126 -6
- package/src/authorization/protected-paths.ts +30 -2
- package/src/connector/mcp/client.ts +87 -1
- package/src/constants/mcp/index.ts +3 -0
- package/src/constants/plugin/index.ts +2 -0
- package/src/manager/agent/lifecycle.ts +24 -5
- package/src/manager/session/turn-recorder.ts +2 -1
- package/src/persona/assembler.ts +2 -2
- package/src/plugin/index.ts +5 -0
- package/src/plugin/lifecycle.ts +16 -2
- package/src/plugin/shell-hook-http.ts +181 -0
- package/src/plugin/shell-hook.ts +386 -89
- package/src/public-runtime.ts +2 -0
- package/src/public-tools.ts +2 -0
- package/src/public-types.ts +5 -0
- package/src/runtime/query/executor.ts +40 -10
- package/src/runtime/query/index.ts +33 -0
- package/src/runtime/query/iteration/index.ts +11 -3
- package/src/runtime/query/iteration/phases/compaction-breaker.ts +38 -9
- package/src/runtime/query/iteration/phases/compaction.ts +21 -4
- package/src/runtime/query/iteration/phases/context.ts +2 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +74 -3
- package/src/runtime/query/path-escalation.ts +11 -5
- package/src/runtime/query/plugin-hooks.ts +2 -2
- package/src/runtime/query/prepare-turn.ts +33 -0
- package/src/runtime/query/review-policy.ts +45 -16
- package/src/session/retention/archive.ts +1 -1
- package/src/session/workspace/driver.ts +6 -0
- package/src/session/workspace/git-worktree.ts +31 -0
- package/src/skills/index.ts +6 -1
- package/src/skills/loader.ts +54 -0
- package/src/skills/registry.ts +7 -6
- package/src/tools/builtins/bash-cwd.ts +156 -0
- package/src/tools/builtins/bash.ts +79 -34
- package/src/tools/builtins/bounded-file-read.ts +80 -0
- package/src/tools/builtins/grep.ts +198 -28
- package/src/tools/builtins/read-file.ts +151 -30
- package/src/tools/builtins/read-media.ts +483 -0
- package/src/tools/builtins/search-tools.ts +1 -1
- package/src/tools/builtins/skill.ts +91 -2
- package/src/tools/builtins/sleep-guard.ts +31 -0
- package/src/tools/builtins/view-image.ts +74 -62
- package/src/tools/file-read-tracker.ts +30 -0
- package/src/toolsets/manager.ts +3 -3
- package/src/types/agent/query.ts +2 -0
- package/src/types/agent/task.ts +6 -2
- package/src/types/connector/mcp.ts +48 -0
- package/src/types/hitl/index.ts +1 -1
- package/src/types/plugin/index.ts +102 -16
- package/src/types/session/sub-session.ts +6 -2
- package/src/types/skills/index.ts +48 -0
- package/src/types/tool/index.ts +22 -0
- package/src/types/workspace/ref.ts +2 -0
- package/src/utils/frontmatter.ts +21 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rm } from 'node:fs/promises'
|
|
1
2
|
import { z } from 'zod'
|
|
2
3
|
import { isReadOnlyCommandLine } from '../../authorization/readonly-commands.js'
|
|
3
4
|
import { SANDBOX_KILL_GRACE_MS } from '../../constants/sandbox/index.js'
|
|
@@ -24,8 +25,15 @@ import { defineTool } from '../defineTool.js'
|
|
|
24
25
|
import { scrubInheritedEnv, withheldEnvNoticeText } from '../env-scrub.js'
|
|
25
26
|
import { createHostOutputDecoder } from '../host-output.js'
|
|
26
27
|
import { looksBinary, windowText } from '../text-window.js'
|
|
28
|
+
import {
|
|
29
|
+
bashWorkingDirectoryForExecution,
|
|
30
|
+
newCwdFile,
|
|
31
|
+
recordDirectory,
|
|
32
|
+
withCwdCapture,
|
|
33
|
+
} from './bash-cwd.js'
|
|
27
34
|
import { judgeExit } from './exit-semantics.js'
|
|
28
35
|
import { jsonStringEscapes } from './json-string-hint.js'
|
|
36
|
+
import { leadingLongSleep, longSleepRefusal } from './sleep-guard.js'
|
|
29
37
|
|
|
30
38
|
// Namzu owns its own bash timeout knob — `NAMZU_BASH_TIMEOUT_MS` — and
|
|
31
39
|
// reads no consumer's env name. A consumer that wants one knob aliases
|
|
@@ -526,7 +534,7 @@ export const SANDBOX_ESCAPE_NOT_APPROVED =
|
|
|
526
534
|
'dangerously_disable_sandbox was not approved for this call, so the command did not run. Leaving the sandbox needs the user to confirm it each time, and this turn either cannot ask or does not allow it. Run the command inside the sandbox, or tell the user what it needs and let them run it.'
|
|
527
535
|
|
|
528
536
|
const BASH_DESCRIPTION =
|
|
529
|
-
'Executes a command line using the execution shell disclosed in the system context and returns stdout/stderr output. Command timeout is configurable. The `command` parameter is required, so every call carries one. Long file content goes through `write`, not a heredoc, so it cannot hit the output token limit mid-stream.'
|
|
537
|
+
'Executes a command line using the execution shell disclosed in the system context and returns stdout/stderr output. The working directory persists between calls, but shell state does not. Command timeout is configurable. The `command` parameter is required, so every call carries one. Long file content goes through `write`, not a heredoc, so it cannot hit the output token limit mid-stream.'
|
|
530
538
|
|
|
531
539
|
const BashToolBase = defineTool({
|
|
532
540
|
name: 'bash',
|
|
@@ -579,6 +587,7 @@ const BashToolBase = defineTool({
|
|
|
579
587
|
},
|
|
580
588
|
|
|
581
589
|
async execute(input, context) {
|
|
590
|
+
const startDirectory = bashWorkingDirectoryForExecution(context)
|
|
582
591
|
if (isDangerousCommand(input.command)) {
|
|
583
592
|
return {
|
|
584
593
|
success: false,
|
|
@@ -588,6 +597,15 @@ const BashToolBase = defineTool({
|
|
|
588
597
|
}
|
|
589
598
|
}
|
|
590
599
|
|
|
600
|
+
// Where a job registry exists to wait on, a long leading sleep only
|
|
601
|
+
// holds the turn; it is refused with the better ways to wait.
|
|
602
|
+
if (!input.run_in_background && context.backgroundJobs && !backgroundTasksDisabled()) {
|
|
603
|
+
const longSleep = leadingLongSleep(input.command)
|
|
604
|
+
if (longSleep !== null) {
|
|
605
|
+
return { success: false, output: '', error: longSleepRefusal(longSleep) }
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
591
609
|
// Asked to leave the sandbox. Honoured only on the executor's word that
|
|
592
610
|
// a reviewer confirmed THIS call (`sandboxEscapeApproved`); the input
|
|
593
611
|
// alone is the model's request, never the permission. Without a
|
|
@@ -648,7 +666,7 @@ const BashToolBase = defineTool({
|
|
|
648
666
|
try {
|
|
649
667
|
const job = context.backgroundJobs.start({
|
|
650
668
|
command: input.command,
|
|
651
|
-
workingDirectory:
|
|
669
|
+
workingDirectory: startDirectory,
|
|
652
670
|
})
|
|
653
671
|
// `start` returns before the process exists, and a spawn that
|
|
654
672
|
// fails (no such shell, bad directory) reports a tick later.
|
|
@@ -799,38 +817,57 @@ const BashToolBase = defineTool({
|
|
|
799
817
|
let withdrawOffer: (() => void) | undefined
|
|
800
818
|
const offer = context.backgroundJobs?.registerForeground?.bind(context.backgroundJobs)
|
|
801
819
|
|
|
820
|
+
// The directory carries over between calls of a session on a POSIX host
|
|
821
|
+
// shell; the shell records where it ended, read back in `finally`.
|
|
822
|
+
const hostShell = hostCommandShell()
|
|
823
|
+
const carryDirectory =
|
|
824
|
+
context.sandbox === undefined &&
|
|
825
|
+
typeof context.sessionId === 'string' &&
|
|
826
|
+
hostShell.path !== undefined &&
|
|
827
|
+
hostShell.windows !== true
|
|
828
|
+
const cwdFile = carryDirectory ? newCwdFile() : undefined
|
|
829
|
+
let settled = false
|
|
830
|
+
const settle = async (): Promise<string | undefined> => {
|
|
831
|
+
if (cwdFile === undefined) return undefined
|
|
832
|
+
settled = true
|
|
833
|
+
return recordDirectory(context.sessionId, cwdFile, context)
|
|
834
|
+
}
|
|
835
|
+
|
|
802
836
|
try {
|
|
803
837
|
const { stdout, stderr, stdoutBytes, stderrBytes, backgroundedJobId, backgroundedBy } =
|
|
804
|
-
await execHostShell(
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
? {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
838
|
+
await execHostShell(
|
|
839
|
+
cwdFile === undefined ? input.command : withCwdCapture(input.command, cwdFile),
|
|
840
|
+
{
|
|
841
|
+
cwd: startDirectory,
|
|
842
|
+
timeout: input.timeout,
|
|
843
|
+
env: { ...inherited.env, ...context.env },
|
|
844
|
+
maxBuffer: DEFAULT_BASH_MAX_BUFFER_BYTES,
|
|
845
|
+
signal: context.abortSignal,
|
|
846
|
+
onOutput,
|
|
847
|
+
...(canMoveOnTimeout ? { onTimeout: adoptRunning } : {}),
|
|
848
|
+
...(canHandOff
|
|
849
|
+
? {
|
|
850
|
+
adopt: adoptRunning,
|
|
851
|
+
adoptOnInterrupt: true,
|
|
852
|
+
...(offer
|
|
853
|
+
? {
|
|
854
|
+
onRunning: (control: {
|
|
855
|
+
background(reason: 'user' | 'message'): boolean
|
|
856
|
+
}) => {
|
|
857
|
+
offerTimer = setTimeout(() => {
|
|
858
|
+
withdrawOffer = offer({
|
|
859
|
+
command: input.command,
|
|
860
|
+
background: (reason) => control.background(reason),
|
|
861
|
+
})
|
|
862
|
+
}, FOREGROUND_HANDOFF_AFTER_MS)
|
|
863
|
+
offerTimer.unref?.()
|
|
864
|
+
},
|
|
865
|
+
}
|
|
866
|
+
: {}),
|
|
867
|
+
}
|
|
868
|
+
: {}),
|
|
869
|
+
},
|
|
870
|
+
)
|
|
834
871
|
if (backgroundedJobId !== undefined) {
|
|
835
872
|
const timedOut = backgroundedBy === 'timeout'
|
|
836
873
|
return {
|
|
@@ -853,9 +890,10 @@ const BashToolBase = defineTool({
|
|
|
853
890
|
facts: { kind: 'command', command: input.command, background: true, continued: true },
|
|
854
891
|
}
|
|
855
892
|
}
|
|
893
|
+
const reset = await settle()
|
|
856
894
|
return {
|
|
857
895
|
success: true,
|
|
858
|
-
output: formatShellOutput(stdout, stderr) || '(no output)',
|
|
896
|
+
output: withResetNotice(formatShellOutput(stdout, stderr) || '(no output)', reset),
|
|
859
897
|
data: {
|
|
860
898
|
exitCode: 0,
|
|
861
899
|
...(leaveSandbox ? { sandboxed: false, sandboxEscape: true } : {}),
|
|
@@ -903,6 +941,7 @@ const BashToolBase = defineTool({
|
|
|
903
941
|
const judged =
|
|
904
942
|
!timedOut && exitCode !== undefined ? judgeExit(input.command, exitCode) : undefined
|
|
905
943
|
const answered = judged !== undefined && !judged.isError
|
|
944
|
+
const reset = await settle()
|
|
906
945
|
const output = [
|
|
907
946
|
formatShellOutput(failure.stdout, failure.stderr),
|
|
908
947
|
clipped.length > 0
|
|
@@ -914,7 +953,7 @@ const BashToolBase = defineTool({
|
|
|
914
953
|
|
|
915
954
|
return {
|
|
916
955
|
success: answered,
|
|
917
|
-
output: output || '(no output)',
|
|
956
|
+
output: withResetNotice(output || '(no output)', reset),
|
|
918
957
|
data: {
|
|
919
958
|
...(exitCode !== undefined ? { exitCode } : {}),
|
|
920
959
|
...(judged?.note ? { exitNote: judged.note } : {}),
|
|
@@ -949,6 +988,7 @@ const BashToolBase = defineTool({
|
|
|
949
988
|
}),
|
|
950
989
|
}
|
|
951
990
|
} finally {
|
|
991
|
+
if (cwdFile !== undefined && !settled) await rm(cwdFile, { force: true })
|
|
952
992
|
if (offerTimer !== undefined) clearTimeout(offerTimer)
|
|
953
993
|
withdrawOffer?.()
|
|
954
994
|
}
|
|
@@ -1002,6 +1042,11 @@ function formatShellOutput(stdout: string | undefined, stderr: string | undefine
|
|
|
1002
1042
|
.join('\n\n')
|
|
1003
1043
|
}
|
|
1004
1044
|
|
|
1045
|
+
/** The result text with the directory-reset line, when there is one, on its own last line. */
|
|
1046
|
+
function withResetNotice(output: string, notice: string | undefined): string {
|
|
1047
|
+
return notice === undefined ? output : `${output}\n${notice}`
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1005
1050
|
/**
|
|
1006
1051
|
* The notice text for the credential-shaped variables left out of a command's
|
|
1007
1052
|
* environment (see {@link withheldEnvNoticeText}). It is sent as a notice after
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { constants } from 'node:fs'
|
|
2
|
+
import { open } from 'node:fs/promises'
|
|
3
|
+
|
|
4
|
+
const CHUNK_BYTES = 64 * 1024
|
|
5
|
+
|
|
6
|
+
/** A hard input ceiling, independent of output windows and allow_large. */
|
|
7
|
+
export const TEXT_MAX_INPUT_BYTES = 32 * 1024 * 1024
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read at most the limit plus one sentinel byte, even if the file grows after
|
|
11
|
+
* stat. No allocation or request is derived from an untrusted file size.
|
|
12
|
+
* The first bounded chunk can choose a stricter limit from the file signature.
|
|
13
|
+
*/
|
|
14
|
+
export async function readBoundedChunks(
|
|
15
|
+
read: (offset: number, length: number) => Promise<Buffer>,
|
|
16
|
+
maximum: number,
|
|
17
|
+
signal?: AbortSignal,
|
|
18
|
+
selectLimit?: (prefix: Buffer) => number,
|
|
19
|
+
): Promise<Buffer> {
|
|
20
|
+
const chunks: Buffer[] = []
|
|
21
|
+
let offset = 0
|
|
22
|
+
let limit = maximum
|
|
23
|
+
for (;;) {
|
|
24
|
+
signal?.throwIfAborted()
|
|
25
|
+
const length = Math.min(CHUNK_BYTES, limit + 1 - offset)
|
|
26
|
+
const chunk = await read(offset, length)
|
|
27
|
+
signal?.throwIfAborted()
|
|
28
|
+
if (chunk.length > length)
|
|
29
|
+
throw new Error('The file backend returned more bytes than the requested bounded range.')
|
|
30
|
+
if (offset === 0 && selectLimit) limit = Math.min(maximum, selectLimit(chunk))
|
|
31
|
+
offset += chunk.length
|
|
32
|
+
if (offset > limit) throw inputTooLarge(limit)
|
|
33
|
+
chunks.push(chunk)
|
|
34
|
+
if (chunk.length < length) return Buffer.concat(chunks, offset)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function readBoundedFile(
|
|
39
|
+
path: string,
|
|
40
|
+
maximum: number,
|
|
41
|
+
signal?: AbortSignal,
|
|
42
|
+
selectLimit?: (prefix: Buffer) => number,
|
|
43
|
+
): Promise<Buffer> {
|
|
44
|
+
// O_NONBLOCK prevents a substituted FIFO from blocking open before fstat.
|
|
45
|
+
signal?.throwIfAborted()
|
|
46
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NONBLOCK)
|
|
47
|
+
try {
|
|
48
|
+
const info = await file.stat()
|
|
49
|
+
if (!info.isFile()) throw new Error('Read requires a regular file.')
|
|
50
|
+
if (info.size > maximum) throw inputTooLarge(maximum)
|
|
51
|
+
return await readBoundedChunks(
|
|
52
|
+
async (offset, length) => {
|
|
53
|
+
const buffer = Buffer.allocUnsafe(length)
|
|
54
|
+
let used = 0
|
|
55
|
+
while (used < length) {
|
|
56
|
+
signal?.throwIfAborted()
|
|
57
|
+
const { bytesRead } = await file.read(buffer, used, length - used, offset + used)
|
|
58
|
+
if (bytesRead === 0) break
|
|
59
|
+
used += bytesRead
|
|
60
|
+
}
|
|
61
|
+
return buffer.subarray(0, used)
|
|
62
|
+
},
|
|
63
|
+
maximum,
|
|
64
|
+
signal,
|
|
65
|
+
(prefix) => {
|
|
66
|
+
const limit = selectLimit?.(prefix) ?? maximum
|
|
67
|
+
if (info.size > limit) throw inputTooLarge(limit)
|
|
68
|
+
return limit
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
} finally {
|
|
72
|
+
await file.close()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function inputTooLarge(limit: number): Error {
|
|
77
|
+
return new Error(
|
|
78
|
+
`File exceeds the bounded read limit (${limit} bytes). Extract a smaller file with a shell command.`,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stat } from 'node:fs/promises'
|
|
2
2
|
import { isAbsolute, join, relative, sep } from 'node:path'
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
import { walkFilesLocally } from '../../sandbox/file-walk.js'
|
|
5
5
|
import type { SandboxWalkFilesOptions } from '../../types/sandbox/index.js'
|
|
6
|
+
import type { ToolContext } from '../../types/tool/index.js'
|
|
6
7
|
import { subscribeToAbort } from '../../utils/abort.js'
|
|
7
8
|
import { defineTool } from '../defineTool.js'
|
|
8
9
|
import {
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
toolRoots,
|
|
14
15
|
} from '../paths.js'
|
|
15
16
|
import { relativePosix, resolveWithinPosix } from '../posix-path.js'
|
|
17
|
+
import { readBoundedChunks, readBoundedFile } from './bounded-file-read.js'
|
|
16
18
|
|
|
17
19
|
/** A remote read has no signal parameter; stop consuming it when the turn ends. */
|
|
18
20
|
async function readWithSignal(read: () => Promise<Buffer>, signal?: AbortSignal): Promise<Buffer> {
|
|
@@ -57,6 +59,46 @@ export const DEFAULT_HEAD_LIMIT = 250
|
|
|
57
59
|
/** A matching or context line longer than this is replaced by a notice, not printed. */
|
|
58
60
|
export const MAX_LINE_COLUMNS = 500
|
|
59
61
|
|
|
62
|
+
/** File kinds for the `type` argument: extensions (without the dot) and exact file names. */
|
|
63
|
+
const FILE_TYPES: Record<string, { ext: string[]; names?: string[] }> = {
|
|
64
|
+
js: { ext: ['js', 'jsx', 'mjs', 'cjs'] },
|
|
65
|
+
ts: { ext: ['ts', 'tsx', 'mts', 'cts'] },
|
|
66
|
+
py: { ext: ['py', 'pyi'] },
|
|
67
|
+
rust: { ext: ['rs'] },
|
|
68
|
+
go: { ext: ['go'] },
|
|
69
|
+
java: { ext: ['java'] },
|
|
70
|
+
kotlin: { ext: ['kt', 'kts'] },
|
|
71
|
+
swift: { ext: ['swift'] },
|
|
72
|
+
c: { ext: ['c', 'h'] },
|
|
73
|
+
cpp: { ext: ['cc', 'cpp', 'cxx', 'hpp', 'hh', 'hxx', 'h'] },
|
|
74
|
+
cs: { ext: ['cs'] },
|
|
75
|
+
rb: { ext: ['rb', 'rake', 'gemspec'] },
|
|
76
|
+
php: { ext: ['php'] },
|
|
77
|
+
sh: { ext: ['sh', 'bash', 'zsh'] },
|
|
78
|
+
md: { ext: ['md', 'markdown', 'mdx'] },
|
|
79
|
+
json: { ext: ['json', 'jsonc'] },
|
|
80
|
+
yaml: { ext: ['yml', 'yaml'] },
|
|
81
|
+
toml: { ext: ['toml'] },
|
|
82
|
+
html: { ext: ['html', 'htm'] },
|
|
83
|
+
css: { ext: ['css', 'scss', 'sass', 'less'] },
|
|
84
|
+
sql: { ext: ['sql'] },
|
|
85
|
+
xml: { ext: ['xml'] },
|
|
86
|
+
make: { ext: ['mk', 'mak'], names: ['Makefile', 'makefile', 'GNUmakefile'] },
|
|
87
|
+
docker: { ext: [], names: ['Dockerfile'] },
|
|
88
|
+
}
|
|
89
|
+
FILE_TYPES.javascript = FILE_TYPES.js as { ext: string[] }
|
|
90
|
+
FILE_TYPES.typescript = FILE_TYPES.ts as { ext: string[] }
|
|
91
|
+
FILE_TYPES.python = FILE_TYPES.py as { ext: string[] }
|
|
92
|
+
FILE_TYPES.rs = FILE_TYPES.rust as { ext: string[] }
|
|
93
|
+
FILE_TYPES.markdown = FILE_TYPES.md as { ext: string[] }
|
|
94
|
+
|
|
95
|
+
function matchesType(path: string, kind: { ext: string[]; names?: string[] }): boolean {
|
|
96
|
+
const name = path.slice(path.lastIndexOf('/') + 1)
|
|
97
|
+
if (kind.names?.includes(name)) return true
|
|
98
|
+
const dot = name.lastIndexOf('.')
|
|
99
|
+
return dot > 0 && kind.ext.includes(name.slice(dot + 1))
|
|
100
|
+
}
|
|
101
|
+
|
|
60
102
|
const inputSchema = z.object({
|
|
61
103
|
pattern: z.string().describe('Regular expression pattern to search for in file contents'),
|
|
62
104
|
path: z
|
|
@@ -74,6 +116,24 @@ const inputSchema = z.object({
|
|
|
74
116
|
.min(0)
|
|
75
117
|
.default(0)
|
|
76
118
|
.describe('Number of lines to show before and after each match'),
|
|
119
|
+
before_context: z.coerce
|
|
120
|
+
.number()
|
|
121
|
+
.int()
|
|
122
|
+
.min(0)
|
|
123
|
+
.optional()
|
|
124
|
+
.describe('Lines before each content match; overrides context_lines for this side.'),
|
|
125
|
+
after_context: z.coerce
|
|
126
|
+
.number()
|
|
127
|
+
.int()
|
|
128
|
+
.min(0)
|
|
129
|
+
.optional()
|
|
130
|
+
.describe('Lines after each content match; overrides context_lines for this side.'),
|
|
131
|
+
only_matching: z
|
|
132
|
+
.boolean()
|
|
133
|
+
.optional()
|
|
134
|
+
.describe(
|
|
135
|
+
'In content mode, print each matched substring instead of the whole line. Ignores context.',
|
|
136
|
+
),
|
|
77
137
|
output_mode: z
|
|
78
138
|
.enum(['files_with_matches', 'content', 'count'])
|
|
79
139
|
.default('files_with_matches')
|
|
@@ -94,6 +154,18 @@ const inputSchema = z.object({
|
|
|
94
154
|
.min(0)
|
|
95
155
|
.default(0)
|
|
96
156
|
.describe('Skip this many entries before applying head_limit, to page through results.'),
|
|
157
|
+
multiline: z
|
|
158
|
+
.boolean()
|
|
159
|
+
.optional()
|
|
160
|
+
.describe(
|
|
161
|
+
'Let a pattern span lines: "." also matches a newline, and "^" and "$" match at line starts and ends. Content mode prints every line a match covers.',
|
|
162
|
+
),
|
|
163
|
+
type: z
|
|
164
|
+
.string()
|
|
165
|
+
.optional()
|
|
166
|
+
.describe(
|
|
167
|
+
'Only search files of this kind, e.g. "js", "ts", "py", "rust", "go", "java", "md". Narrows what include selects.',
|
|
168
|
+
),
|
|
97
169
|
max_results: z.coerce
|
|
98
170
|
.number()
|
|
99
171
|
.int()
|
|
@@ -117,7 +189,7 @@ function isBinaryContent(buffer: Buffer): boolean {
|
|
|
117
189
|
export const GrepTool = defineTool({
|
|
118
190
|
name: 'grep',
|
|
119
191
|
description:
|
|
120
|
-
'Searches file contents using a regular expression. By default returns the paths of matching files, newest first; output_mode "content" returns matching lines with file paths, line numbers and optional context lines, and "count" returns the number of matching lines per file. Results are paged: head_limit (default 250, 0 for none) and offset. A line over 500 characters is replaced by a notice. Searches with a bounded traversal and identifies incomplete searches. Skips binary files, files over 5 MB, and symlinks. Choose the narrowest relevant directory.',
|
|
192
|
+
'Searches file contents using a regular expression. By default returns the paths of matching files, newest first; output_mode "content" returns matching lines with file paths, line numbers and optional context lines, and "count" returns the number of matching lines per file. Set multiline for a pattern that spans lines and type (js, py, rust, ...) to search one kind of file. In content mode, before_context and after_context override symmetric context_lines; only_matching returns matched substrings without context. Results are paged: head_limit (default 250, 0 for none) and offset. A line over 500 characters is replaced by a notice. Searches with a bounded traversal and identifies incomplete searches. Skips binary files, files over 5 MB, and symlinks. Choose the narrowest relevant directory.',
|
|
121
193
|
inputSchema,
|
|
122
194
|
category: 'analysis',
|
|
123
195
|
// Declared so a path outside the turn's roots can be reviewed before the
|
|
@@ -146,7 +218,18 @@ export const GrepTool = defineTool({
|
|
|
146
218
|
|
|
147
219
|
async execute(input, context) {
|
|
148
220
|
context.abortSignal?.throwIfAborted()
|
|
149
|
-
const
|
|
221
|
+
const multiline = input.multiline === true
|
|
222
|
+
const flags = `${(input.case_sensitive ?? true) ? 'g' : 'gi'}${multiline ? 'ms' : ''}`
|
|
223
|
+
const fileType = input.type
|
|
224
|
+
? FILE_TYPES[input.type.toLowerCase().replace(/^\./, '')]
|
|
225
|
+
: undefined
|
|
226
|
+
if (input.type && !fileType) {
|
|
227
|
+
return {
|
|
228
|
+
success: false,
|
|
229
|
+
output: '',
|
|
230
|
+
error: `Unknown file type "${input.type}". Known types: ${Object.keys(FILE_TYPES).sort().join(', ')}. Use include for another pattern.`,
|
|
231
|
+
}
|
|
232
|
+
}
|
|
150
233
|
let regex: RegExp
|
|
151
234
|
try {
|
|
152
235
|
regex = new RegExp(input.pattern, flags)
|
|
@@ -189,6 +272,8 @@ export const GrepTool = defineTool({
|
|
|
189
272
|
// `max_results` predates `head_limit`; it keeps meaning "at most this many".
|
|
190
273
|
const headLimit = input.head_limit ?? input.max_results ?? DEFAULT_HEAD_LIMIT
|
|
191
274
|
const offset = input.offset ?? 0
|
|
275
|
+
const beforeContext = input.before_context ?? input.context_lines ?? 0
|
|
276
|
+
const afterContext = input.after_context ?? input.context_lines ?? 0
|
|
192
277
|
// Content lines in output order. Only content mode uses them as the
|
|
193
278
|
// result; the other modes build from `matchedFiles`.
|
|
194
279
|
const results: string[] = []
|
|
@@ -213,6 +298,7 @@ export const GrepTool = defineTool({
|
|
|
213
298
|
)
|
|
214
299
|
}
|
|
215
300
|
filesSearched++
|
|
301
|
+
if (fileType && !matchesType(entry.path, fileType)) continue
|
|
216
302
|
if (entry.size > MAX_FILE_SIZE) continue
|
|
217
303
|
|
|
218
304
|
let content: string
|
|
@@ -221,10 +307,15 @@ export const GrepTool = defineTool({
|
|
|
221
307
|
const buffer = await readWithSignal(
|
|
222
308
|
() =>
|
|
223
309
|
sandbox
|
|
224
|
-
?
|
|
310
|
+
? readBoundedChunks(
|
|
311
|
+
(offset, length) =>
|
|
312
|
+
sandbox.readFile(filePath, { offset, length, signal: context.abortSignal }),
|
|
313
|
+
MAX_FILE_SIZE,
|
|
314
|
+
context.abortSignal,
|
|
315
|
+
)
|
|
225
316
|
: resolveWithinReal(root, filePath).then((path) => {
|
|
226
317
|
contained = path
|
|
227
|
-
return
|
|
318
|
+
return readBoundedFile(path, MAX_FILE_SIZE, context.abortSignal)
|
|
228
319
|
}),
|
|
229
320
|
context.abortSignal,
|
|
230
321
|
)
|
|
@@ -245,36 +336,47 @@ export const GrepTool = defineTool({
|
|
|
245
336
|
.split(sep)
|
|
246
337
|
.join('/')}`
|
|
247
338
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
339
|
+
if (mode === 'content' && input.only_matching) {
|
|
340
|
+
for (const fragment of matchingFragments(content, regex, multiline)) {
|
|
341
|
+
context.abortSignal?.throwIfAborted()
|
|
342
|
+
if (fileMatches++ === 0) filesMatched++
|
|
343
|
+
totalMatches++
|
|
344
|
+
results.push(`${relPath}:${fragment.line}:${clipLine(fragment.text, 'matching')}`)
|
|
345
|
+
if (headLimit > 0 && results.length >= offset + headLimit) {
|
|
346
|
+
pageHasMore = true
|
|
347
|
+
break
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (pageHasMore) break
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
253
353
|
|
|
354
|
+
// Each hit is the first and last line (0-based) a match covers.
|
|
355
|
+
const hits = multiline ? multilineHits(content, regex) : lineHits(lines, regex, context)
|
|
356
|
+
// Count mode reports matching lines, so a line covered twice counts once.
|
|
357
|
+
const countedLines = new Set<number>()
|
|
358
|
+
for (const [first, last] of hits) {
|
|
359
|
+
context.abortSignal?.throwIfAborted()
|
|
254
360
|
if (fileMatches === 0) filesMatched++
|
|
255
361
|
fileMatches++
|
|
256
362
|
totalMatches++
|
|
257
363
|
// A file list only needs to know that the file matches.
|
|
258
364
|
if (mode === 'files_with_matches') break
|
|
259
|
-
if (mode === 'count')
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const end = Math.min(lines.length - 1, i + input.context_lines)
|
|
264
|
-
|
|
265
|
-
if (results.length > 0) {
|
|
266
|
-
results.push('--')
|
|
267
|
-
}
|
|
365
|
+
if (mode === 'count') {
|
|
366
|
+
for (let j = first; j <= last; j++) countedLines.add(j)
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
268
369
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
370
|
+
const before = Math.max(0, first - beforeContext)
|
|
371
|
+
const after = Math.min(lines.length - 1, last + afterContext)
|
|
372
|
+
if ((beforeContext > 0 || afterContext > 0) && results.length > 0) results.push('--')
|
|
373
|
+
for (let j = before; j <= after; j++) {
|
|
374
|
+
if (headLimit > 0 && results.length >= offset + headLimit) break
|
|
375
|
+
const hit = j >= first && j <= last
|
|
376
|
+
const prefix = hit ? ':' : '-'
|
|
377
|
+
results.push(
|
|
378
|
+
`${relPath}${prefix}${j + 1}${prefix}${clipLine(lines[j] ?? '', hit ? 'matching' : 'context')}`,
|
|
379
|
+
)
|
|
278
380
|
}
|
|
279
381
|
|
|
280
382
|
// A full page ends the search: another file is not read just to
|
|
@@ -285,6 +387,7 @@ export const GrepTool = defineTool({
|
|
|
285
387
|
break
|
|
286
388
|
}
|
|
287
389
|
}
|
|
390
|
+
if (mode === 'count' && fileMatches > 0) fileMatches = countedLines.size
|
|
288
391
|
|
|
289
392
|
if (fileMatches > 0 && mode !== 'content') {
|
|
290
393
|
matchedFiles.push({
|
|
@@ -404,3 +507,70 @@ async function sortByNewest<T extends { relPath: string; host?: string }>(
|
|
|
404
507
|
.sort((a, b) => b.mtime - a.mtime || a.file.relPath.localeCompare(b.file.relPath))
|
|
405
508
|
.map((entry) => entry.file)
|
|
406
509
|
}
|
|
510
|
+
|
|
511
|
+
/** One hit per matching line. */
|
|
512
|
+
function lineHits(
|
|
513
|
+
lines: readonly string[],
|
|
514
|
+
regex: RegExp,
|
|
515
|
+
context: ToolContext,
|
|
516
|
+
): [number, number][] {
|
|
517
|
+
const hits: [number, number][] = []
|
|
518
|
+
for (let i = 0; i < lines.length; i++) {
|
|
519
|
+
context.abortSignal?.throwIfAborted()
|
|
520
|
+
regex.lastIndex = 0
|
|
521
|
+
if (regex.test(lines[i] ?? '')) hits.push([i, i])
|
|
522
|
+
}
|
|
523
|
+
return hits
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** One hit per match over the whole text, from the line it starts on to the line it ends on. */
|
|
527
|
+
function multilineHits(content: string, regex: RegExp): [number, number][] {
|
|
528
|
+
const hits: [number, number][] = []
|
|
529
|
+
const starts = [0]
|
|
530
|
+
for (let i = content.indexOf('\n'); i !== -1; i = content.indexOf('\n', i + 1)) starts.push(i + 1)
|
|
531
|
+
const lineOf = (offset: number): number => {
|
|
532
|
+
let lo = 0
|
|
533
|
+
let hi = starts.length - 1
|
|
534
|
+
while (lo < hi) {
|
|
535
|
+
const mid = (lo + hi + 1) >> 1
|
|
536
|
+
if ((starts[mid] ?? 0) <= offset) lo = mid
|
|
537
|
+
else hi = mid - 1
|
|
538
|
+
}
|
|
539
|
+
return lo
|
|
540
|
+
}
|
|
541
|
+
regex.lastIndex = 0
|
|
542
|
+
for (let m = regex.exec(content); m !== null; m = regex.exec(content)) {
|
|
543
|
+
const text = m[0]
|
|
544
|
+
if (text.length === 0) {
|
|
545
|
+
regex.lastIndex++
|
|
546
|
+
continue
|
|
547
|
+
}
|
|
548
|
+
// A match that ends on a newline covers the lines before it, not the next one.
|
|
549
|
+
const end = m.index + text.length - (text.endsWith('\n') ? 1 : 0)
|
|
550
|
+
hits.push([lineOf(m.index), lineOf(Math.max(m.index, end))])
|
|
551
|
+
}
|
|
552
|
+
return hits
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Lazy fragments: a dense file never allocates every match before paging. */
|
|
556
|
+
function* matchingFragments(content: string, regex: RegExp, multiline: boolean) {
|
|
557
|
+
const subjects = multiline ? [content] : content.split('\n')
|
|
558
|
+
let line = 1
|
|
559
|
+
for (const subject of subjects) {
|
|
560
|
+
regex.lastIndex = 0
|
|
561
|
+
let previous = 0
|
|
562
|
+
for (let match = regex.exec(subject); match !== null; match = regex.exec(subject)) {
|
|
563
|
+
if (multiline) {
|
|
564
|
+
for (let i = previous; i < match.index; i++) if (subject[i] === '\n') line++
|
|
565
|
+
previous = match.index
|
|
566
|
+
}
|
|
567
|
+
if (match[0].length === 0) {
|
|
568
|
+
regex.lastIndex++
|
|
569
|
+
continue
|
|
570
|
+
}
|
|
571
|
+
// Keep a multiline substring on one output row, never forge path rows.
|
|
572
|
+
yield { line, text: match[0].replace(/\n/g, '\\n') }
|
|
573
|
+
}
|
|
574
|
+
if (!multiline) line++
|
|
575
|
+
}
|
|
576
|
+
}
|