@caupulican/pi-adaptative 0.81.32 → 0.81.35
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 +22 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/README.md +69 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/index.d.ts +53 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/index.d.ts.map +1 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/index.js +1282 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/index.js.map +1 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/index.ts +1453 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/templates/builder-validator.json +14 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/templates/claude-trio.json +32 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/templates/full-provider-review.json +15 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/templates/provider-prompt-smoke.json +14 -0
- package/dist/bundled-resources/prompts/extensionify.md +5 -4
- package/dist/bundled-resources/prompts/skillify.md +5 -5
- package/dist/bundled-resources/skills/pi-harness-learning/SKILL.md +6 -3
- package/dist/config.d.ts +5 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -2
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +7 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +25 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/background-lane-controller.d.ts +12 -3
- package/dist/core/background-lane-controller.d.ts.map +1 -1
- package/dist/core/background-lane-controller.js +67 -26
- package/dist/core/background-lane-controller.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +7 -3
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/context/context-store-retention.d.ts +4 -1
- package/dist/core/context/context-store-retention.d.ts.map +1 -1
- package/dist/core/context/context-store-retention.js +42 -151
- package/dist/core/context/context-store-retention.js.map +1 -1
- package/dist/core/context-pipeline.d.ts.map +1 -1
- package/dist/core/context-pipeline.js +6 -6
- package/dist/core/context-pipeline.js.map +1 -1
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +25 -39
- package/dist/core/exec.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +9 -2
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +105 -21
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +2 -0
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/goals/goal-state.d.ts +10 -0
- package/dist/core/goals/goal-state.d.ts.map +1 -1
- package/dist/core/goals/goal-state.js +37 -0
- package/dist/core/goals/goal-state.js.map +1 -1
- package/dist/core/goals/goal-tool-core.d.ts +11 -0
- package/dist/core/goals/goal-tool-core.d.ts.map +1 -1
- package/dist/core/goals/goal-tool-core.js +51 -1
- package/dist/core/goals/goal-tool-core.js.map +1 -1
- package/dist/core/improvement-loop.d.ts.map +1 -1
- package/dist/core/improvement-loop.js +26 -5
- package/dist/core/improvement-loop.js.map +1 -1
- package/dist/core/models/fitness-store.d.ts.map +1 -1
- package/dist/core/models/fitness-store.js +1 -1
- package/dist/core/models/fitness-store.js.map +1 -1
- package/dist/core/models/local-runtime.d.ts +6 -4
- package/dist/core/models/local-runtime.d.ts.map +1 -1
- package/dist/core/models/local-runtime.js +108 -36
- package/dist/core/models/local-runtime.js.map +1 -1
- package/dist/core/output-guard.d.ts.map +1 -1
- package/dist/core/output-guard.js +23 -8
- package/dist/core/output-guard.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +44 -59
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/profile-resource-selection.d.ts +16 -0
- package/dist/core/profile-resource-selection.d.ts.map +1 -1
- package/dist/core/profile-resource-selection.js +41 -0
- package/dist/core/profile-resource-selection.js.map +1 -1
- package/dist/core/reflection-controller.d.ts.map +1 -1
- package/dist/core/reflection-controller.js +18 -1
- package/dist/core/reflection-controller.js.map +1 -1
- package/dist/core/reload-blockers.d.ts +1 -0
- package/dist/core/reload-blockers.d.ts.map +1 -1
- package/dist/core/reload-blockers.js +6 -2
- package/dist/core/reload-blockers.js.map +1 -1
- package/dist/core/resource-loader.d.ts +2 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +50 -28
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/toolkit/script-runner.d.ts.map +1 -1
- package/dist/core/toolkit/script-runner.js +58 -24
- package/dist/core/toolkit/script-runner.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +13 -26
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/extensionify.d.ts +2 -1
- package/dist/core/tools/extensionify.d.ts.map +1 -1
- package/dist/core/tools/extensionify.js +11 -8
- package/dist/core/tools/extensionify.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +54 -48
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/git-filter.d.ts.map +1 -1
- package/dist/core/tools/git-filter.js +20 -32
- package/dist/core/tools/git-filter.js.map +1 -1
- package/dist/core/tools/goal.d.ts +1 -1
- package/dist/core/tools/goal.d.ts.map +1 -1
- package/dist/core/tools/goal.js +10 -3
- package/dist/core/tools/goal.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +89 -80
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/output-accumulator.d.ts +2 -0
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +7 -4
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +4 -3
- package/dist/core/tools/read.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +23 -4
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +18 -4
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/auth-dialogs-controller.d.ts +10 -0
- package/dist/modes/interactive/auth-dialogs-controller.d.ts.map +1 -1
- package/dist/modes/interactive/auth-dialogs-controller.js +94 -27
- package/dist/modes/interactive/auth-dialogs-controller.js.map +1 -1
- package/dist/modes/interactive/auto-learn-controller.d.ts.map +1 -1
- package/dist/modes/interactive/auto-learn-controller.js +2 -1
- package/dist/modes/interactive/auto-learn-controller.js.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.d.ts +1 -0
- package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.js +8 -0
- package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +6 -15
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +3 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +20 -8
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/profile-resource-editor.d.ts +4 -1
- package/dist/modes/interactive/components/profile-resource-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/profile-resource-editor.js +22 -39
- package/dist/modes/interactive/components/profile-resource-editor.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +1 -1
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/config-backup.d.ts +1 -0
- package/dist/modes/interactive/config-backup.d.ts.map +1 -1
- package/dist/modes/interactive/config-backup.js +14 -3
- package/dist/modes/interactive/config-backup.js.map +1 -1
- package/dist/modes/interactive/editor-overlay-host.d.ts +12 -0
- package/dist/modes/interactive/editor-overlay-host.d.ts.map +1 -1
- package/dist/modes/interactive/editor-overlay-host.js +55 -0
- package/dist/modes/interactive/editor-overlay-host.js.map +1 -1
- package/dist/modes/interactive/extension-ui-host.d.ts +2 -1
- package/dist/modes/interactive/extension-ui-host.d.ts.map +1 -1
- package/dist/modes/interactive/extension-ui-host.js +161 -96
- package/dist/modes/interactive/extension-ui-host.js.map +1 -1
- package/dist/modes/interactive/external-editor.d.ts.map +1 -1
- package/dist/modes/interactive/external-editor.js +23 -28
- package/dist/modes/interactive/external-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +14 -8
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/profile-menu-controller.d.ts +5 -1
- package/dist/modes/interactive/profile-menu-controller.d.ts.map +1 -1
- package/dist/modes/interactive/profile-menu-controller.js +118 -24
- package/dist/modes/interactive/profile-menu-controller.js.map +1 -1
- package/dist/modes/interactive/resource-shell-commands.d.ts +1 -0
- package/dist/modes/interactive/resource-shell-commands.d.ts.map +1 -1
- package/dist/modes/interactive/resource-shell-commands.js +5 -1
- package/dist/modes/interactive/resource-shell-commands.js.map +1 -1
- package/dist/modes/interactive/session-flow-commands.d.ts.map +1 -1
- package/dist/modes/interactive/session-flow-commands.js +100 -7
- package/dist/modes/interactive/session-flow-commands.js.map +1 -1
- package/dist/modes/interactive/session-io-commands.d.ts.map +1 -1
- package/dist/modes/interactive/session-io-commands.js +80 -50
- package/dist/modes/interactive/session-io-commands.js.map +1 -1
- package/dist/modes/interactive/startup-checks.d.ts.map +1 -1
- package/dist/modes/interactive/startup-checks.js +14 -17
- package/dist/modes/interactive/startup-checks.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +20 -19
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +10 -19
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/child-process.d.ts +13 -7
- package/dist/utils/child-process.d.ts.map +1 -1
- package/dist/utils/child-process.js +56 -3
- package/dist/utils/child-process.js.map +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +3 -2
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +17 -10
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/external-editor-command.d.ts +8 -0
- package/dist/utils/external-editor-command.d.ts.map +1 -0
- package/dist/utils/external-editor-command.js +60 -0
- package/dist/utils/external-editor-command.js.map +1 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +10 -2
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +23 -10
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/work-directory.d.ts +36 -0
- package/dist/utils/work-directory.d.ts.map +1 -0
- package/dist/utils/work-directory.js +462 -0
- package/dist/utils/work-directory.js.map +1 -0
- package/docs/index.md +2 -0
- package/docs/resources.md +2 -2
- package/docs/rpc.md +1 -1
- package/docs/tmux-agent-manager.md +48 -0
- package/docs/windows.md +100 -7
- package/docs/work-directory.md +41 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"goal.js","sourceRoot":"","sources":["../../../src/core/tools/goal.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Static, Type } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport type { GoalEvidenceKind, GoalState } from \"../goals/goal-state.ts\";\nimport { applyGoalAction, type GoalAction, type GoalActionName, summarizeGoalState } from \"../goals/goal-tool-core.ts\";\n\nconst goalSchema = Type.Object(\n\t{\n\t\taction: Type.Union(\n\t\t\t[\n\t\t\t\tType.Literal(\"start\"),\n\t\t\t\tType.Literal(\"add_requirement\"),\n\t\t\t\tType.Literal(\"satisfy_requirement\"),\n\t\t\t\tType.Literal(\"block_requirement\"),\n\t\t\t\tType.Literal(\"add_evidence\"),\n\t\t\t\tType.Literal(\"progress\"),\n\t\t\t\tType.Literal(\"no_progress\"),\n\t\t\t\tType.Literal(\"complete\"),\n\t\t\t\tType.Literal(\"block_goal\"),\n\t\t\t\tType.Literal(\"cancel\"),\n\t\t\t],\n\t\t\t{ description: \"Ledger action to record.\" },\n\t\t),\n\t\tgoalId: Type.Optional(Type.String({ description: \"Stable goal id. Required for action 'start'.\" })),\n\t\tuserGoal: Type.Optional(Type.String({ description: \"The goal statement. Required for action 'start'.\" })),\n\t\trequirementId: Type.Optional(\n\t\t\tType.String({\n\t\t\t\tdescription: \"Requirement id for add_requirement/satisfy_requirement/block_requirement.\",\n\t\t\t}),\n\t\t),\n\t\ttext: Type.Optional(Type.String({ description: \"Requirement text. Required for add_requirement.\" })),\n\t\tevidenceId: Type.Optional(Type.String({ description: \"Evidence id. Required for add_evidence.\" })),\n\t\tevidenceIds: Type.Optional(\n\t\t\tType.Array(Type.String(), {\n\t\t\t\tdescription: \"Evidence ids supporting a satisfy_requirement action. Each must already be recorded.\",\n\t\t\t}),\n\t\t),\n\t\tkind: Type.Optional(\n\t\t\tType.Union(\n\t\t\t\t[\n\t\t\t\t\tType.Literal(\"file\"),\n\t\t\t\t\tType.Literal(\"test\"),\n\t\t\t\t\tType.Literal(\"tool\"),\n\t\t\t\t\tType.Literal(\"user\"),\n\t\t\t\t\tType.Literal(\"finding\"),\n\t\t\t\t],\n\t\t\t\t{ description: \"Evidence kind. Required for add_evidence.\" },\n\t\t\t),\n\t\t),\n\t\tsummary: Type.Optional(Type.String({ description: \"Evidence summary. Required for add_evidence.\" })),\n\t\turi: Type.Optional(Type.String({ description: \"Optional evidence locator (path/URL).\" })),\n\t\treason: Type.Optional(Type.String({ description: \"Reason for block_requirement or block_goal.\" })),\n\t},\n\t{ additionalProperties: false },\n);\n\nexport type GoalToolInput = Static<typeof goalSchema>;\n\nexport interface GoalToolDetails {\n\taction: GoalActionName;\n\tapplied: boolean;\n\terror?: string;\n\tstate?: GoalState;\n}\n\nexport interface GoalToolDependencies {\n\t/** Read the latest persisted goal state for the active session. */\n\tgetGoalState: () => GoalState | undefined;\n\t/** Persist a new goal state snapshot to the active session. */\n\tsaveGoalState: (state: GoalState) => void;\n\t/** Clock injection for deterministic tests. */\n\tnow?: () => string;\n}\n\nfunction toGoalAction(input: GoalToolInput): GoalAction | { error: string } {\n\tswitch (input.action) {\n\t\tcase \"start\":\n\t\t\treturn { action: \"start\", goalId: input.goalId ?? \"\", userGoal: input.userGoal ?? \"\" };\n\t\tcase \"add_requirement\":\n\t\t\treturn { action: \"add_requirement\", requirementId: input.requirementId ?? \"\", text: input.text ?? \"\" };\n\t\tcase \"satisfy_requirement\":\n\t\t\treturn {\n\t\t\t\taction: \"satisfy_requirement\",\n\t\t\t\trequirementId: input.requirementId ?? \"\",\n\t\t\t\tevidenceIds: input.evidenceIds,\n\t\t\t};\n\t\tcase \"block_requirement\":\n\t\t\treturn {\n\t\t\t\taction: \"block_requirement\",\n\t\t\t\trequirementId: input.requirementId ?? \"\",\n\t\t\t\treason: input.reason ?? \"\",\n\t\t\t};\n\t\tcase \"add_evidence\": {\n\t\t\tif (input.kind === undefined) {\n\t\t\t\treturn { error: \"add_evidence requires a kind.\" };\n\t\t\t}\n\t\t\tconst kind: GoalEvidenceKind = input.kind;\n\t\t\treturn {\n\t\t\t\taction: \"add_evidence\",\n\t\t\t\tevidenceId: input.evidenceId ?? \"\",\n\t\t\t\tkind,\n\t\t\t\tsummary: input.summary ?? \"\",\n\t\t\t\turi: input.uri,\n\t\t\t};\n\t\t}\n\t\tcase \"progress\":\n\t\t\treturn { action: \"progress\" };\n\t\tcase \"no_progress\":\n\t\t\treturn { action: \"no_progress\" };\n\t\tcase \"complete\":\n\t\t\treturn { action: \"complete\" };\n\t\tcase \"block_goal\":\n\t\t\treturn { action: \"block_goal\", reason: input.reason ?? \"\" };\n\t\tcase \"cancel\":\n\t\t\treturn { action: \"cancel\" };\n\t\tdefault:\n\t\t\treturn { error: \"Unknown goal action.\" };\n\t}\n}\n\nexport function createGoalToolDefinition(deps: GoalToolDependencies): ToolDefinition {\n\tconst now = deps.now ?? (() => new Date().toISOString());\n\treturn {\n\t\tname: \"goal\",\n\t\tlabel: \"goal\",\n\t\tdescription:\n\t\t\t\"Record and update the durable goal ledger for the current task. Maintains a structured goal with requirements, evidence, and progress so long tasks can be resumed and continued. Start a goal, add requirements, attach evidence, mark requirements satisfied or blocked, and mark progress. This is the producer that drives /goal-continue; without recorded goal state, continuation has nothing to act on.\",\n\t\tpromptSnippet: \"Record goal, requirements, evidence, and progress in the durable goal ledger.\",\n\t\tpromptGuidelines: [\n\t\t\t\"At the start of a multi-step task, call goal with action 'start' to record the user goal, then add the concrete requirements with 'add_requirement'.\",\n\t\t\t\"As you make progress, record evidence with 'add_evidence' and mark requirements satisfied with 'satisfy_requirement', citing the evidence ids.\",\n\t\t\t\"Use 'progress' when you advance without satisfying a specific requirement, and 'no_progress' when a turn yields nothing, so stall detection works.\",\n\t\t\t\"Mark the goal 'complete' only when every requirement is satisfied; use 'block_goal' or 'block_requirement' with a reason when you are stuck and need the user.\",\n\t\t],\n\t\tparameters: goalSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\tinput: GoalToolInput,\n\t\t): Promise<{\n\t\t\tcontent: Array<{ type: \"text\"; text: string }>;\n\t\t\tdetails: GoalToolDetails;\n\t\t}> {\n\t\t\tconst mapped = toGoalAction(input);\n\t\t\tif (\"error\" in mapped) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\" as const, text: `goal ${input.action} failed: ${mapped.error}` }],\n\t\t\t\t\tdetails: { action: input.action, applied: false, error: mapped.error },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst current = deps.getGoalState();\n\t\t\tconst result = applyGoalAction(current, mapped, now());\n\t\t\tif (!result.ok) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\" as const, text: `goal ${input.action} failed: ${result.error}` }],\n\t\t\t\t\tdetails: { action: input.action, applied: false, error: result.error, state: current },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tdeps.saveGoalState(result.state);\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{ type: \"text\" as const, text: `goal ${input.action} recorded.\\n${summarizeGoalState(result.state)}` },\n\t\t\t\t],\n\t\t\t\tdetails: { action: input.action, applied: true, state: result.state },\n\t\t\t};\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"goal.js","sourceRoot":"","sources":["../../../src/core/tools/goal.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Static, Type } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport type { GoalEvidenceKind, GoalState } from \"../goals/goal-state.ts\";\nimport { applyGoalAction, type GoalAction, type GoalActionName, summarizeGoalState } from \"../goals/goal-tool-core.ts\";\n\nconst goalSchema = Type.Object(\n\t{\n\t\taction: Type.Union(\n\t\t\t[\n\t\t\t\tType.Literal(\"start\"),\n\t\t\t\tType.Literal(\"add_requirement\"),\n\t\t\t\tType.Literal(\"satisfy_requirement\"),\n\t\t\t\tType.Literal(\"block_requirement\"),\n\t\t\t\tType.Literal(\"reopen_requirement\"),\n\t\t\t\tType.Literal(\"add_evidence\"),\n\t\t\t\tType.Literal(\"progress\"),\n\t\t\t\tType.Literal(\"no_progress\"),\n\t\t\t\tType.Literal(\"complete\"),\n\t\t\t\tType.Literal(\"block_goal\"),\n\t\t\t\tType.Literal(\"resume_goal\"),\n\t\t\t\tType.Literal(\"cancel\"),\n\t\t\t],\n\t\t\t{ description: \"Ledger action to record.\" },\n\t\t),\n\t\tgoalId: Type.Optional(Type.String({ description: \"Stable goal id. Required for action 'start'.\" })),\n\t\tuserGoal: Type.Optional(Type.String({ description: \"The goal statement. Required for action 'start'.\" })),\n\t\trequirementId: Type.Optional(\n\t\t\tType.String({\n\t\t\t\tdescription: \"Requirement id for add_requirement/satisfy_requirement/block_requirement/reopen_requirement.\",\n\t\t\t}),\n\t\t),\n\t\ttext: Type.Optional(Type.String({ description: \"Requirement text. Required for add_requirement.\" })),\n\t\tevidenceId: Type.Optional(Type.String({ description: \"Evidence id. Required for add_evidence.\" })),\n\t\tevidenceIds: Type.Optional(\n\t\t\tType.Array(Type.String(), {\n\t\t\t\tdescription: \"Evidence ids supporting a satisfy_requirement action. Each must already be recorded.\",\n\t\t\t}),\n\t\t),\n\t\tkind: Type.Optional(\n\t\t\tType.Union(\n\t\t\t\t[\n\t\t\t\t\tType.Literal(\"file\"),\n\t\t\t\t\tType.Literal(\"test\"),\n\t\t\t\t\tType.Literal(\"tool\"),\n\t\t\t\t\tType.Literal(\"user\"),\n\t\t\t\t\tType.Literal(\"finding\"),\n\t\t\t\t],\n\t\t\t\t{ description: \"Evidence kind. Required for add_evidence.\" },\n\t\t\t),\n\t\t),\n\t\tsummary: Type.Optional(Type.String({ description: \"Evidence summary. Required for add_evidence.\" })),\n\t\turi: Type.Optional(Type.String({ description: \"Optional evidence locator (path/URL).\" })),\n\t\treason: Type.Optional(Type.String({ description: \"Reason for block_requirement or block_goal.\" })),\n\t},\n\t{ additionalProperties: false },\n);\n\nexport type GoalToolInput = Static<typeof goalSchema>;\n\nexport interface GoalToolDetails {\n\taction: GoalActionName;\n\tapplied: boolean;\n\terror?: string;\n\tstate?: GoalState;\n}\n\nexport interface GoalToolDependencies {\n\t/** Read the latest persisted goal state for the active session. */\n\tgetGoalState: () => GoalState | undefined;\n\t/** Persist a new goal state snapshot to the active session. */\n\tsaveGoalState: (state: GoalState) => void;\n\t/** Clock injection for deterministic tests. */\n\tnow?: () => string;\n}\n\nfunction toGoalAction(input: GoalToolInput): GoalAction | { error: string } {\n\tswitch (input.action) {\n\t\tcase \"start\":\n\t\t\treturn { action: \"start\", goalId: input.goalId ?? \"\", userGoal: input.userGoal ?? \"\" };\n\t\tcase \"add_requirement\":\n\t\t\treturn { action: \"add_requirement\", requirementId: input.requirementId ?? \"\", text: input.text ?? \"\" };\n\t\tcase \"satisfy_requirement\":\n\t\t\treturn {\n\t\t\t\taction: \"satisfy_requirement\",\n\t\t\t\trequirementId: input.requirementId ?? \"\",\n\t\t\t\tevidenceIds: input.evidenceIds,\n\t\t\t};\n\t\tcase \"block_requirement\":\n\t\t\treturn {\n\t\t\t\taction: \"block_requirement\",\n\t\t\t\trequirementId: input.requirementId ?? \"\",\n\t\t\t\treason: input.reason ?? \"\",\n\t\t\t};\n\t\tcase \"reopen_requirement\":\n\t\t\treturn { action: \"reopen_requirement\", requirementId: input.requirementId ?? \"\" };\n\t\tcase \"add_evidence\": {\n\t\t\tif (input.kind === undefined) {\n\t\t\t\treturn { error: \"add_evidence requires a kind.\" };\n\t\t\t}\n\t\t\tconst kind: GoalEvidenceKind = input.kind;\n\t\t\treturn {\n\t\t\t\taction: \"add_evidence\",\n\t\t\t\tevidenceId: input.evidenceId ?? \"\",\n\t\t\t\tkind,\n\t\t\t\tsummary: input.summary ?? \"\",\n\t\t\t\turi: input.uri,\n\t\t\t};\n\t\t}\n\t\tcase \"progress\":\n\t\t\treturn { action: \"progress\" };\n\t\tcase \"no_progress\":\n\t\t\treturn { action: \"no_progress\" };\n\t\tcase \"complete\":\n\t\t\treturn { action: \"complete\" };\n\t\tcase \"block_goal\":\n\t\t\treturn { action: \"block_goal\", reason: input.reason ?? \"\" };\n\t\tcase \"resume_goal\":\n\t\t\treturn { action: \"resume_goal\" };\n\t\tcase \"cancel\":\n\t\t\treturn { action: \"cancel\" };\n\t\tdefault:\n\t\t\treturn { error: \"Unknown goal action.\" };\n\t}\n}\n\nexport function createGoalToolDefinition(deps: GoalToolDependencies): ToolDefinition {\n\tconst now = deps.now ?? (() => new Date().toISOString());\n\treturn {\n\t\tname: \"goal\",\n\t\tlabel: \"goal\",\n\t\tdescription:\n\t\t\t\"Record and update the durable goal ledger for the current task. Maintains a structured goal with requirements, evidence, and progress so long tasks can be resumed and continued. Start a goal, add requirements, attach evidence, mark requirements satisfied or blocked, reopen resolved blockers, resume blocked goals, and mark progress. This is the producer that drives /goal-continue; without recorded goal state, continuation has nothing to act on.\",\n\t\tpromptSnippet: \"Record goal, requirements, evidence, and progress in the durable goal ledger.\",\n\t\tpromptGuidelines: [\n\t\t\t\"At the start of a multi-step task, call goal with action 'start' to record the user goal, then add the concrete requirements with 'add_requirement'.\",\n\t\t\t\"As you make progress, record evidence with 'add_evidence' and mark requirements satisfied with 'satisfy_requirement', citing the evidence ids.\",\n\t\t\t\"Use 'progress' when you advance without satisfying a specific requirement, and 'no_progress' when a turn yields nothing, so stall detection works.\",\n\t\t\t\"When the user resolves a blocker, use 'resume_goal' and 'reopen_requirement' as needed; do not strand the old ledger or start a duplicate goal.\",\n\t\t\t\"Mark the goal 'complete' only when every requirement is satisfied; use 'block_goal' or 'block_requirement' with a reason when you are stuck and need the user. A blocked goal can still be resumed or cancelled.\",\n\t\t],\n\t\tparameters: goalSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\tinput: GoalToolInput,\n\t\t): Promise<{\n\t\t\tcontent: Array<{ type: \"text\"; text: string }>;\n\t\t\tdetails: GoalToolDetails;\n\t\t}> {\n\t\t\tconst mapped = toGoalAction(input);\n\t\t\tif (\"error\" in mapped) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\" as const, text: `goal ${input.action} failed: ${mapped.error}` }],\n\t\t\t\t\tdetails: { action: input.action, applied: false, error: mapped.error },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst current = deps.getGoalState();\n\t\t\tconst result = applyGoalAction(current, mapped, now());\n\t\t\tif (!result.ok) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\" as const, text: `goal ${input.action} failed: ${result.error}` }],\n\t\t\t\t\tdetails: { action: input.action, applied: false, error: result.error, state: current },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tdeps.saveGoalState(result.state);\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{ type: \"text\" as const, text: `goal ${input.action} recorded.\\n${summarizeGoalState(result.state)}` },\n\t\t\t\t],\n\t\t\t\tdetails: { action: input.action, applied: true, state: result.state },\n\t\t\t};\n\t\t},\n\t};\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../../../src/core/tools/grep.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAIN,KAAK,gBAAgB,EAErB,MAAM,gCAAgC,CAAC;AAIxC,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAI5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EACN,KAAK,iBAAiB,EAKtB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AACtF,OAAO,EAIN,KAAK,gBAAgB,EAIrB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAG5E,QAAA,MAAM,UAAU;;;;;;;;EAYd,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAGtD,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,qBAAqB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,mEAAmE;IACnE,WAAW,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAClE,2CAA2C;IAC3C,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;CAC7D;AAOD,MAAM,WAAW,eAAe;IAC/B,sFAAsF;IACtF,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,oFAAoF;IACpF,GAAG,CAAC,EAAE,gBAAgB,GAAG,KAAK,CAAC;IAC/B,qFAAqF;IACrF,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,yFAAyF;IACzF,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACtC;AA+PD,wBAAsB,UAAU,CAAC,OAAO,EAAE;IACzC,OAAO,EAAE,gBAAgB,CAAC;IAC1B,MAAM,EAAE,YAAY,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,GAAG,SAAS,CAAC,CAkFlE;AAED,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC,CAgThE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG","sourcesContent":["import { readFile as fsReadFile, stat as fsStat } from \"node:fs/promises\";\nimport { createInterface } from \"node:readline\";\nimport type { AgentTool } from \"@caupulican/pi-agent-core\";\nimport {\n\tDEFAULT_MAX_BYTES,\n\tformatSize,\n\tGREP_MAX_LINE_LENGTH,\n\ttype TruncationResult,\n\ttruncateLine,\n} from \"@caupulican/pi-agent-core/node\";\nimport { Text } from \"@caupulican/pi-tui\";\nimport { spawn } from \"child_process\";\nimport path from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport { ensureTool } from \"../../utils/tools-manager.ts\";\nimport type { ArtifactStore } from \"../context/context-artifacts.ts\";\nimport {\n\ttype BroadQueryTracker,\n\tbroadQueryInvalidationNote,\n\tformatArtifactNotice,\n\tnormalizeBroadQueryKey,\n\tpackToolOutput,\n} from \"../context/tool-output-packer.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport {\n\tdefaultFffSearchBackend,\n\ttype FffGrepMatch,\n\ttype FffGrepResult,\n\ttype FffSearchBackend,\n\thasGitignoreInTree,\n\trelativePathInside,\n\tsafeGetFinder,\n} from \"./fff-search-backend.ts\";\nimport { resolveToCwd } from \"./path-utils.ts\";\nimport { getTextOutput, invalidArgText, shortenPath, str } from \"./render-utils.ts\";\nimport { defaultSearchRouter, type SearchRouter } from \"./search-router.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\nconst grepSchema = Type.Object({\n\tpattern: Type.String({ description: \"Search pattern (regex or literal string)\" }),\n\tpath: Type.Optional(Type.String({ description: \"Directory or file to search (default: current directory)\" })),\n\tglob: Type.Optional(Type.String({ description: \"Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'\" })),\n\tignoreCase: Type.Optional(Type.Boolean({ description: \"Case-insensitive search (default: false)\" })),\n\tliteral: Type.Optional(\n\t\tType.Boolean({ description: \"Treat pattern as literal string instead of regex (default: false)\" }),\n\t),\n\tcontext: Type.Optional(\n\t\tType.Number({ description: \"Number of lines to show before and after each match (default: 0)\" }),\n\t),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of matches to return (default: 100)\" })),\n});\n\nexport type GrepToolInput = Static<typeof grepSchema>;\nconst DEFAULT_LIMIT = 100;\n\nexport interface GrepToolDetails {\n\ttruncation?: TruncationResult;\n\tmatchLimitReached?: number;\n\tlinesTruncated?: boolean;\n\t/** Set only when output was packed to an artifact; see tool-output-packer.ts. */\n\tartifactId?: string;\n\t/** Set when this exact query has repeatedly produced broad/truncated results. */\n\tinvalidationCandidate?: boolean;\n}\n\n/**\n * Pluggable operations for the grep tool.\n * Override these to delegate search to remote systems (for example SSH).\n */\nexport interface GrepOperations {\n\t/** Check if path is a directory. Throws if path does not exist. */\n\tisDirectory: (absolutePath: string) => Promise<boolean> | boolean;\n\t/** Read file contents for context lines */\n\treadFile: (absolutePath: string) => Promise<string> | string;\n}\n\nconst defaultGrepOperations: GrepOperations = {\n\tisDirectory: async (p) => (await fsStat(p)).isDirectory(),\n\treadFile: (p) => fsReadFile(p, \"utf-8\"),\n};\n\nexport interface GrepToolOptions {\n\t/** Custom operations for grep. Default: local filesystem plus routed FFF/rg search */\n\toperations?: GrepOperations;\n\t/** FFF backend for resident indexed search. Set false to force ripgrep fallback. */\n\tfff?: FffSearchBackend | false;\n\t/** Pure router that selects FFF or rg from request filters and environment facts. */\n\tsearchRouter?: SearchRouter;\n\t/**\n\t * Opt-in artifact store for first-capture-then-bound output packing (Phase 3). When\n\t * omitted (the default), behavior is byte-for-byte unchanged from before this option\n\t * existed: output is truncated the same way, just never artifact-backed.\n\t */\n\tartifactStore?: ArtifactStore;\n\t/** Opt-in tracker for repeated-broad-query \"do not repeat\" signals. Also default-off. */\n\tbroadQueryTracker?: BroadQueryTracker;\n}\n\nfunction formatGrepCall(\n\targs: { pattern: string; path?: string; glob?: string; limit?: number } | undefined,\n\ttheme: Theme,\n\tcwd: string,\n): string {\n\tconst pattern = str(args?.pattern);\n\tconst rawPath = str(args?.path);\n\tconst path = rawPath !== null ? shortenPath(rawPath || \".\", cwd) : null;\n\tconst glob = str(args?.glob);\n\tconst limit = args?.limit;\n\tconst invalidArg = invalidArgText(theme);\n\tlet text =\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"grep\")) +\n\t\t\" \" +\n\t\t(pattern === null ? invalidArg : theme.fg(\"accent\", `/${pattern || \"\"}/`)) +\n\t\ttheme.fg(\"toolOutput\", ` in ${path === null ? invalidArg : path}`);\n\tif (glob) text += theme.fg(\"toolOutput\", ` (${glob})`);\n\tif (limit !== undefined) text += theme.fg(\"toolOutput\", ` limit ${limit}`);\n\treturn text;\n}\n\nfunction formatGrepResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: GrepToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 15;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\n\tconst matchLimit = result.details?.matchLimitReached;\n\tconst truncation = result.details?.truncation;\n\tconst linesTruncated = result.details?.linesTruncated;\n\tif (matchLimit || truncation?.truncated || linesTruncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (matchLimit) warnings.push(`${matchLimit} matches limit`);\n\t\tif (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\tif (linesTruncated) warnings.push(\"some lines truncated\");\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nfunction globConstraintForFff(glob: string | undefined): string {\n\tif (!glob) return \"\";\n\tif (glob.includes(\"/\") || glob.startsWith(\"**/\")) return glob;\n\treturn `**/${glob}`;\n}\n\nfunction fffGrepQuery(options: {\n\tpattern: string;\n\tglob?: string;\n\tisDirectory: boolean;\n\tsearchPathRelativeToCwd: string;\n}): string | undefined {\n\tconst parts: string[] = [];\n\tif (options.searchPathRelativeToCwd) {\n\t\tparts.push(options.isDirectory ? `${options.searchPathRelativeToCwd}/` : options.searchPathRelativeToCwd);\n\t}\n\tparts.push(globConstraintForFff(options.glob));\n\tparts.push(options.pattern);\n\treturn parts.filter(Boolean).join(\" \");\n}\n\nfunction fffDisplayPath(\n\tmatch: FffGrepMatch,\n\toptions: { isDirectory: boolean; searchPathRelativeToCwd: string },\n): string | undefined {\n\tif (!options.searchPathRelativeToCwd) return match.relativePath;\n\tif (!options.isDirectory) {\n\t\treturn match.relativePath === options.searchPathRelativeToCwd ? path.basename(match.relativePath) : undefined;\n\t}\n\tconst prefix = `${options.searchPathRelativeToCwd}/`;\n\tif (!match.relativePath.startsWith(prefix)) return undefined;\n\treturn match.relativePath.slice(prefix.length);\n}\n\nfunction appendFffMatchLines(options: {\n\tmatch: FffGrepMatch;\n\toutputLines: string[];\n\tlinesTruncated: { value: boolean };\n\tcontextValue: number;\n}): void {\n\tconst before = options.match.contextBefore ?? [];\n\tfor (let i = 0; i < before.length; i++) {\n\t\tconst lineNumber = options.match.lineNumber - before.length + i;\n\t\tconst { text, wasTruncated } = truncateLine(before[i] ?? \"\");\n\t\tif (wasTruncated) options.linesTruncated.value = true;\n\t\toptions.outputLines.push(` ${lineNumber}- ${text}`);\n\t}\n\n\tconst { text, wasTruncated } = truncateLine(options.match.lineContent.replace(/\\r/g, \"\"));\n\tif (wasTruncated) options.linesTruncated.value = true;\n\toptions.outputLines.push(` ${options.match.lineNumber}: ${text}`);\n\n\tif (options.contextValue === 0) return;\n\tconst after = options.match.contextAfter ?? [];\n\tfor (let i = 0; i < after.length; i++) {\n\t\tconst lineNumber = options.match.lineNumber + 1 + i;\n\t\tconst { text: contextText, wasTruncated: contextWasTruncated } = truncateLine(after[i] ?? \"\");\n\t\tif (contextWasTruncated) options.linesTruncated.value = true;\n\t\toptions.outputLines.push(` ${lineNumber}- ${contextText}`);\n\t}\n}\n\n/**\n * Shared \"measure -> pack -> notices\" tail for both the FFF and ripgrep result paths:\n * first-capture the raw output to an artifact if it's oversized and a store was provided\n * (Phase 3 tool-output-artifacts.md boundary rule), then append the same match-limit/\n * byte-limit/line-truncation/broad-query notices either path already produced.\n */\nfunction packGrepOutput(options: {\n\trawOutput: string;\n\ttoolCallId: string;\n\tartifactStore?: ArtifactStore;\n\tbroadQueryTracker?: BroadQueryTracker;\n\tpattern: string;\n\trawPath?: string;\n\tglob?: string;\n\tmatchLimitReached: number | false;\n\tlinesTruncated: boolean;\n}): { text: string; details: GrepToolDetails } {\n\tconst packed = packToolOutput(\n\t\t{\n\t\t\ttoolName: \"grep\",\n\t\t\tpath: options.rawPath,\n\t\t\trawContent: options.rawOutput,\n\t\t\t// No line limit here because the match limit already caps rows; only the byte\n\t\t\t// cap should apply, matching the pre-Slice-B truncateHead call exactly.\n\t\t\ttruncation: { maxLines: Number.MAX_SAFE_INTEGER },\n\t\t},\n\t\toptions.artifactStore,\n\t\toptions.toolCallId,\n\t);\n\tlet output = packed.content;\n\tconst details: GrepToolDetails = {};\n\n\tconst notices: string[] = [];\n\tif (packed.artifactId) {\n\t\tnotices.push(formatArtifactNotice(packed.artifactId));\n\t\tdetails.artifactId = packed.artifactId;\n\t}\n\tif (options.matchLimitReached) {\n\t\tnotices.push(\n\t\t\t`${options.matchLimitReached} matches limit reached. Use limit=${options.matchLimitReached * 2} for more, or refine pattern`,\n\t\t);\n\t\tdetails.matchLimitReached = options.matchLimitReached;\n\t}\n\tif (packed.truncation.truncated) {\n\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t// Drop the duplicated bounded-preview text: it's already in the message's own\n\t\t// content, and re-including it here can push `details` past\n\t\t// MAX_RETAINED_TOOL_RESULT_DETAILS_BYTES (message-retention.ts), which replaces\n\t\t// the *entire* details object with a stub -- silently losing artifactId and every\n\t\t// other field alongside it. This is load-bearing beyond just the retention budget:\n\t\t// agent-session.ts's _releaseGcPackedArtifactReferences() reads artifactId back off\n\t\t// this same canonical message at eviction time (potentially many turns later), so\n\t\t// keeping `details` small here is what keeps that release path working at all. If\n\t\t// this field ever grows a large addition again, add a regression proving artifactId\n\t\t// survives compactToolResultDetailsForRetention (see\n\t\t// test/suite/agent-session-artifact-lifecycle.test.ts), not just a details-size check.\n\t\tdetails.truncation = { ...packed.truncation, content: \"\" };\n\t}\n\tif (options.linesTruncated) {\n\t\tnotices.push(`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`);\n\t\tdetails.linesTruncated = true;\n\t}\n\tif (options.matchLimitReached || packed.truncation.truncated) {\n\t\tconst note = broadQueryInvalidationNote(\n\t\t\toptions.broadQueryTracker,\n\t\t\tnormalizeBroadQueryKey({\n\t\t\t\ttoolName: \"grep\",\n\t\t\t\tpattern: options.pattern,\n\t\t\t\tpath: options.rawPath,\n\t\t\t\tglob: options.glob,\n\t\t\t}),\n\t\t\t`grep \"${options.pattern}\" in ${options.rawPath ?? \".\"}`,\n\t\t);\n\t\tif (note) {\n\t\t\tnotices.push(note);\n\t\t\tdetails.invalidationCandidate = true;\n\t\t}\n\t}\n\tif (notices.length > 0) output += `\\n\\n[${notices.join(\". \")}]`;\n\treturn { text: output, details };\n}\n\nfunction formatFffGrepResult(options: {\n\tresult: FffGrepResult;\n\tisDirectory: boolean;\n\tsearchPathRelativeToCwd: string;\n\teffectiveLimit: number;\n\tcontextValue: number;\n\ttoolCallId: string;\n\tartifactStore?: ArtifactStore;\n\tbroadQueryTracker?: BroadQueryTracker;\n\tpattern: string;\n\trawPath?: string;\n\tglob?: string;\n}): { text: string; details: GrepToolDetails } {\n\tif (options.result.items.length === 0) return { text: \"No matches found\", details: {} };\n\n\tconst outputLines: string[] = [];\n\tconst linesTruncated = { value: false };\n\tconst resultLimitReached =\n\t\toptions.result.items.length > options.effectiveLimit || Boolean(options.result.nextCursor);\n\tconst displayedMatches = resultLimitReached\n\t\t? options.result.items.slice(0, options.effectiveLimit)\n\t\t: options.result.items;\n\tlet currentPath = \"\";\n\tfor (const match of displayedMatches) {\n\t\tconst displayPath = fffDisplayPath(match, options);\n\t\tif (!displayPath) continue;\n\t\tif (displayPath !== currentPath) {\n\t\t\tcurrentPath = displayPath;\n\t\t\toutputLines.push(`${displayPath}:`);\n\t\t}\n\t\tappendFffMatchLines({\n\t\t\tmatch,\n\t\t\toutputLines,\n\t\t\tlinesTruncated,\n\t\t\tcontextValue: options.contextValue,\n\t\t});\n\t}\n\n\tif (outputLines.length === 0) return { text: \"No matches found\", details: {} };\n\tconst rawOutput = outputLines.join(\"\\n\");\n\treturn packGrepOutput({\n\t\trawOutput,\n\t\ttoolCallId: options.toolCallId,\n\t\tartifactStore: options.artifactStore,\n\t\tbroadQueryTracker: options.broadQueryTracker,\n\t\tpattern: options.pattern,\n\t\trawPath: options.rawPath,\n\t\tglob: options.glob,\n\t\tmatchLimitReached: resultLimitReached ? options.effectiveLimit : false,\n\t\tlinesTruncated: linesTruncated.value,\n\t});\n}\n\nexport async function tryFffGrep(options: {\n\tbackend: FffSearchBackend;\n\trouter: SearchRouter;\n\tcwd: string;\n\tsearchPath: string;\n\tpattern: string;\n\tglob?: string;\n\tignoreCase?: boolean;\n\tliteral?: boolean;\n\tcontextValue: number;\n\teffectiveLimit: number;\n\tisDirectory: boolean;\n\ttoolCallId: string;\n\tartifactStore?: ArtifactStore;\n\tbroadQueryTracker?: BroadQueryTracker;\n\trawPath?: string;\n}): Promise<{ text: string; details: GrepToolDetails } | undefined> {\n\t// Kick off the FFF finder -- and, on a machine where fff-node isn't provisioned\n\t// yet, its lazy managed install -- unconditionally and as early as possible.\n\t// See the matching comment in find.ts's tryFffFind: routing below can still\n\t// send THIS call to the rg fallback for reasons unrelated to tool availability\n\t// (chiefly the default match limit exceeding the FFF top-N threshold), and\n\t// that outcome must not gate the install itself. getFinder() is cached per\n\t// basePath, so later calls -- including the `await` below -- reuse this same\n\t// in-flight/resolved promise instead of doing the work twice. safeGetFinder()\n\t// guarantees this can never reject (a non-conforming custom backend that\n\t// throws synchronously still degrades to \"unavailable\"), so it can neither\n\t// produce an unhandled rejection nor fail this tool call outright.\n\tconst finderPromise = safeGetFinder(options.backend, options.cwd);\n\n\tconst searchPathRelativeToCwd = relativePathInside(options.cwd, options.searchPath);\n\tconst baseRoute = options.router.route({\n\t\ttool: \"grep\",\n\t\tglob: Boolean(options.glob),\n\t\tignoreCase: Boolean(options.ignoreCase),\n\t\tlimit: options.effectiveLimit,\n\t\tfinderAvailable: true,\n\t\tpathResolvable: searchPathRelativeToCwd !== undefined,\n\t\tgitignoreInTree: false,\n\t});\n\tif (baseRoute.backend !== \"fff\") return undefined;\n\tif (searchPathRelativeToCwd === undefined) return undefined;\n\n\tconst gitignoreInTree = options.isDirectory ? await hasGitignoreInTree(options.searchPath) : false;\n\tconst semanticRoute = options.router.route({\n\t\ttool: \"grep\",\n\t\tglob: Boolean(options.glob),\n\t\tignoreCase: Boolean(options.ignoreCase),\n\t\tlimit: options.effectiveLimit,\n\t\tfinderAvailable: true,\n\t\tpathResolvable: true,\n\t\tgitignoreInTree,\n\t});\n\tif (semanticRoute.backend !== \"fff\") return undefined;\n\n\tconst finder = await finderPromise;\n\tconst finderRoute = options.router.route({\n\t\ttool: \"grep\",\n\t\tglob: Boolean(options.glob),\n\t\tignoreCase: Boolean(options.ignoreCase),\n\t\tlimit: options.effectiveLimit,\n\t\tfinderAvailable: Boolean(finder),\n\t\tpathResolvable: true,\n\t\tgitignoreInTree: false,\n\t});\n\tif (!finder || finderRoute.backend !== \"fff\") return undefined;\n\n\tconst query = fffGrepQuery({\n\t\tpattern: options.pattern,\n\t\tglob: options.glob,\n\t\tisDirectory: options.isDirectory,\n\t\tsearchPathRelativeToCwd,\n\t});\n\tif (!query) return undefined;\n\n\tconst probeLimit = options.effectiveLimit + 1;\n\tconst result = finder.grep(query, {\n\t\tmode: options.literal ? \"plain\" : \"regex\",\n\t\tsmartCase: false,\n\t\tmaxMatchesPerFile: probeLimit,\n\t\tbeforeContext: options.contextValue,\n\t\tafterContext: options.contextValue,\n\t\tpageSize: probeLimit,\n\t});\n\tif (!result.ok || result.value.regexFallbackError) return undefined;\n\treturn formatFffGrepResult({\n\t\tresult: result.value,\n\t\tisDirectory: options.isDirectory,\n\t\tsearchPathRelativeToCwd,\n\t\teffectiveLimit: options.effectiveLimit,\n\t\tcontextValue: options.contextValue,\n\t\ttoolCallId: options.toolCallId,\n\t\tartifactStore: options.artifactStore,\n\t\tbroadQueryTracker: options.broadQueryTracker,\n\t\tpattern: options.pattern,\n\t\trawPath: options.rawPath,\n\t\tglob: options.glob,\n\t});\n}\n\nexport function createGrepToolDefinition(\n\tcwd: string,\n\toptions?: GrepToolOptions,\n): ToolDefinition<typeof grepSchema, GrepToolDetails | undefined> {\n\tconst customOps = options?.operations;\n\tconst fffBackend = options?.fff === false ? undefined : (options?.fff ?? defaultFffSearchBackend);\n\tconst searchRouter = options?.searchRouter ?? defaultSearchRouter;\n\tconst artifactStore = options?.artifactStore;\n\tconst broadQueryTracker = options?.broadQueryTracker;\n\treturn {\n\t\tname: \"grep\",\n\t\tlabel: \"grep\",\n\t\tdescription: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,\n\t\tpromptSnippet: \"Search file contents for patterns (respects .gitignore)\",\n\t\tparameters: grepSchema,\n\t\ttoolGroup: \"explore\",\n\t\tasync execute(\n\t\t\ttoolCallId,\n\t\t\t{\n\t\t\t\tpattern,\n\t\t\t\tpath: searchDir,\n\t\t\t\tglob,\n\t\t\t\tignoreCase,\n\t\t\t\tliteral,\n\t\t\t\tcontext,\n\t\t\t\tlimit,\n\t\t\t}: {\n\t\t\t\tpattern: string;\n\t\t\t\tpath?: string;\n\t\t\t\tglob?: string;\n\t\t\t\tignoreCase?: boolean;\n\t\t\t\tliteral?: boolean;\n\t\t\t\tcontext?: number;\n\t\t\t\tlimit?: number;\n\t\t\t},\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlet settled = false;\n\t\t\t\tconst settle = (fn: () => void) => {\n\t\t\t\t\tif (!settled) {\n\t\t\t\t\t\tsettled = true;\n\t\t\t\t\t\tfn();\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst searchPath = resolveToCwd(searchDir || \".\", cwd);\n\t\t\t\t\t\tconst ops = customOps ?? defaultGrepOperations;\n\t\t\t\t\t\tlet isDirectory: boolean;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tisDirectory = await ops.isDirectory(searchPath);\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(`Path not found: ${searchPath}`)));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst contextValue = context && context > 0 ? context : 0;\n\t\t\t\t\t\tconst effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);\n\t\t\t\t\t\tconst formatPath = (filePath: string): string => {\n\t\t\t\t\t\t\tif (isDirectory) {\n\t\t\t\t\t\t\t\tconst relative = path.relative(searchPath, filePath);\n\t\t\t\t\t\t\t\tif (relative && !relative.startsWith(\"..\")) {\n\t\t\t\t\t\t\t\t\treturn relative.replace(/\\\\/g, \"/\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn path.basename(filePath);\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tif (!customOps && fffBackend) {\n\t\t\t\t\t\t\tconst fffResult = await tryFffGrep({\n\t\t\t\t\t\t\t\tbackend: fffBackend,\n\t\t\t\t\t\t\t\trouter: searchRouter,\n\t\t\t\t\t\t\t\tcwd,\n\t\t\t\t\t\t\t\tsearchPath,\n\t\t\t\t\t\t\t\tpattern,\n\t\t\t\t\t\t\t\tglob,\n\t\t\t\t\t\t\t\tignoreCase,\n\t\t\t\t\t\t\t\tliteral,\n\t\t\t\t\t\t\t\tcontextValue,\n\t\t\t\t\t\t\t\teffectiveLimit,\n\t\t\t\t\t\t\t\tisDirectory,\n\t\t\t\t\t\t\t\ttoolCallId,\n\t\t\t\t\t\t\t\tartifactStore,\n\t\t\t\t\t\t\t\tbroadQueryTracker,\n\t\t\t\t\t\t\t\trawPath: searchDir,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tif (fffResult) {\n\t\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: fffResult.text }],\n\t\t\t\t\t\t\t\t\t\tdetails: Object.keys(fffResult.details).length > 0 ? fffResult.details : undefined,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst rgPath = await ensureTool(\"rg\", true);\n\t\t\t\t\t\tif (!rgPath) {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(\"ripgrep (rg) is not available and could not be downloaded\")));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst fileCache = new Map<string, string[]>();\n\t\t\t\t\t\tconst getFileLines = async (filePath: string): Promise<string[]> => {\n\t\t\t\t\t\t\tlet lines = fileCache.get(filePath);\n\t\t\t\t\t\t\tif (!lines) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tconst content = await ops.readFile(filePath);\n\t\t\t\t\t\t\t\t\tlines = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\").split(\"\\n\");\n\t\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t\tlines = [];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tfileCache.set(filePath, lines);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn lines;\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tconst args: string[] = [\"--json\", \"--line-number\", \"--color=never\", \"--hidden\"];\n\t\t\t\t\t\tif (ignoreCase) args.push(\"--ignore-case\");\n\t\t\t\t\t\tif (literal) args.push(\"--fixed-strings\");\n\t\t\t\t\t\tif (glob) args.push(\"--glob\", glob);\n\t\t\t\t\t\targs.push(\"--\", pattern, searchPath);\n\n\t\t\t\t\t\tconst child = spawn(rgPath, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\t\t\t\t\tconst rl = createInterface({ input: child.stdout });\n\t\t\t\t\t\tlet stderr = \"\";\n\t\t\t\t\t\tlet matchCount = 0;\n\t\t\t\t\t\tlet matchLimitReached = false;\n\t\t\t\t\t\tlet linesTruncated = false;\n\t\t\t\t\t\tlet aborted = false;\n\t\t\t\t\t\tlet killedDueToLimit = false;\n\t\t\t\t\t\tconst outputLines: string[] = [];\n\n\t\t\t\t\t\tconst cleanup = () => {\n\t\t\t\t\t\t\trl.close();\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst stopChild = (dueToLimit = false) => {\n\t\t\t\t\t\t\tif (!child.killed) {\n\t\t\t\t\t\t\t\tkilledDueToLimit = dueToLimit;\n\t\t\t\t\t\t\t\tchild.kill();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\t\tstopChild();\n\t\t\t\t\t\t};\n\t\t\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t\t\tchild.stderr?.on(\"data\", (chunk) => {\n\t\t\t\t\t\t\tstderr += chunk.toString();\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tconst formatBlock = async (filePath: string, lineNumber: number): Promise<string[]> => {\n\t\t\t\t\t\t\tconst relativePath = formatPath(filePath);\n\t\t\t\t\t\t\tconst lines = await getFileLines(filePath);\n\t\t\t\t\t\t\tif (!lines.length) return [`${relativePath}:${lineNumber}: (unable to read file)`];\n\t\t\t\t\t\t\tconst block: string[] = [];\n\t\t\t\t\t\t\tconst start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;\n\t\t\t\t\t\t\tconst end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;\n\t\t\t\t\t\t\tfor (let current = start; current <= end; current++) {\n\t\t\t\t\t\t\t\tconst lineText = lines[current - 1] ?? \"\";\n\t\t\t\t\t\t\t\tconst sanitized = lineText.replace(/\\r/g, \"\");\n\t\t\t\t\t\t\t\tconst isMatchLine = current === lineNumber;\n\t\t\t\t\t\t\t\t// Truncate long lines so grep output stays compact.\n\t\t\t\t\t\t\t\tconst { text: truncatedText, wasTruncated } = truncateLine(sanitized);\n\t\t\t\t\t\t\t\tif (wasTruncated) linesTruncated = true;\n\t\t\t\t\t\t\t\tif (isMatchLine) block.push(`${relativePath}:${current}: ${truncatedText}`);\n\t\t\t\t\t\t\t\telse block.push(`${relativePath}-${current}- ${truncatedText}`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn block;\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\t// Collect matches during streaming, then format them after rg exits.\n\t\t\t\t\t\tconst matches: Array<{ filePath: string; lineNumber: number; lineText?: string }> = [];\n\t\t\t\t\t\trl.on(\"line\", (line) => {\n\t\t\t\t\t\t\tif (!line.trim() || matchLimitReached) return;\n\t\t\t\t\t\t\tlet event: any;\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tevent = JSON.parse(line);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (event.type === \"match\") {\n\t\t\t\t\t\t\t\tmatchCount++;\n\t\t\t\t\t\t\t\tif (matchCount > effectiveLimit) {\n\t\t\t\t\t\t\t\t\tmatchLimitReached = true;\n\t\t\t\t\t\t\t\t\tstopChild(true);\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tconst filePath = event.data?.path?.text;\n\t\t\t\t\t\t\t\tconst lineNumber = event.data?.line_number;\n\t\t\t\t\t\t\t\tconst lineText = event.data?.lines?.text;\n\t\t\t\t\t\t\t\tif (filePath && typeof lineNumber === \"number\")\n\t\t\t\t\t\t\t\t\tmatches.push({ filePath, lineNumber, lineText });\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tchild.on(\"error\", (error) => {\n\t\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\t\tsettle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`)));\n\t\t\t\t\t\t});\n\t\t\t\t\t\tchild.on(\"close\", async (code) => {\n\t\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!killedDueToLimit && code !== 0 && code !== 1) {\n\t\t\t\t\t\t\t\tconst errorMsg = stderr.trim() || `ripgrep exited with code ${code}`;\n\t\t\t\t\t\t\t\tsettle(() => reject(new Error(errorMsg)));\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (matchCount === 0) {\n\t\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"No matches found\" }], details: undefined }),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Format matches after streaming finishes so custom readFile() backends can be async.\n\t\t\t\t\t\t\tconst fileGroups = new Map<string, string[]>();\n\t\t\t\t\t\t\tfor (const match of matches) {\n\t\t\t\t\t\t\t\tconst relativePath = formatPath(match.filePath);\n\t\t\t\t\t\t\t\tif (!fileGroups.has(relativePath)) {\n\t\t\t\t\t\t\t\t\tfileGroups.set(relativePath, []);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tconst group = fileGroups.get(relativePath)!;\n\n\t\t\t\t\t\t\t\tif (contextValue === 0 && match.lineText !== undefined) {\n\t\t\t\t\t\t\t\t\tconst sanitized = match.lineText\n\t\t\t\t\t\t\t\t\t\t.replace(/\\r\\n/g, \"\\n\")\n\t\t\t\t\t\t\t\t\t\t.replace(/\\r/g, \"\")\n\t\t\t\t\t\t\t\t\t\t.replace(/\\n$/, \"\");\n\t\t\t\t\t\t\t\t\tconst { text: truncatedText, wasTruncated } = truncateLine(sanitized);\n\t\t\t\t\t\t\t\t\tif (wasTruncated) linesTruncated = true;\n\t\t\t\t\t\t\t\t\tgroup.push(` ${match.lineNumber}: ${truncatedText}`);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tconst block = await formatBlock(match.filePath, match.lineNumber);\n\t\t\t\t\t\t\t\t\tfor (const line of block) {\n\t\t\t\t\t\t\t\t\t\tif (line.startsWith(`${relativePath}:`)) {\n\t\t\t\t\t\t\t\t\t\t\tgroup.push(` ${line.slice(relativePath.length + 1)}`);\n\t\t\t\t\t\t\t\t\t\t} else if (line.startsWith(`${relativePath}-`)) {\n\t\t\t\t\t\t\t\t\t\t\tgroup.push(` ${line.slice(relativePath.length + 1)}`);\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tgroup.push(` ${line}`);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tfor (const [relativePath, lines] of fileGroups) {\n\t\t\t\t\t\t\t\toutputLines.push(`${relativePath}:`);\n\t\t\t\t\t\t\t\tlet lastLine = \"\";\n\t\t\t\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\t\t\t\tif (line === lastLine) continue;\n\t\t\t\t\t\t\t\t\toutputLines.push(line);\n\t\t\t\t\t\t\t\t\tlastLine = line;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst rawOutput = outputLines.join(\"\\n\");\n\t\t\t\t\t\t\t// Measure -> pack (artifact-backed if oversized and a store was provided) -> notices.\n\t\t\t\t\t\t\t// There is no line limit here because the match limit already capped rows.\n\t\t\t\t\t\t\tconst { text: output, details } = packGrepOutput({\n\t\t\t\t\t\t\t\trawOutput,\n\t\t\t\t\t\t\t\ttoolCallId,\n\t\t\t\t\t\t\t\tartifactStore,\n\t\t\t\t\t\t\t\tbroadQueryTracker,\n\t\t\t\t\t\t\t\tpattern,\n\t\t\t\t\t\t\t\trawPath: searchDir,\n\t\t\t\t\t\t\t\tglob,\n\t\t\t\t\t\t\t\tmatchLimitReached: matchLimitReached ? effectiveLimit : false,\n\t\t\t\t\t\t\t\tlinesTruncated,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tsettle(() => reject(err as Error));\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatGrepCall(args, theme, context.cwd));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatGrepResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createGrepTool(cwd: string, options?: GrepToolOptions): AgentTool<typeof grepSchema> {\n\treturn wrapToolDefinition(createGrepToolDefinition(cwd, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../../../src/core/tools/grep.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAIN,KAAK,gBAAgB,EAErB,MAAM,gCAAgC,CAAC;AAIxC,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAK5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EACN,KAAK,iBAAiB,EAKtB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AACtF,OAAO,EAIN,KAAK,gBAAgB,EAIrB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAG5E,QAAA,MAAM,UAAU;;;;;;;;EAYd,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAKtD,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,qBAAqB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,mEAAmE;IACnE,WAAW,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAClE,2CAA2C;IAC3C,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;CAC7D;AAOD,MAAM,WAAW,eAAe;IAC/B,sFAAsF;IACtF,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,oFAAoF;IACpF,GAAG,CAAC,EAAE,gBAAgB,GAAG,KAAK,CAAC;IAC/B,qFAAqF;IACrF,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,yFAAyF;IACzF,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACtC;AA+PD,wBAAsB,UAAU,CAAC,OAAO,EAAE;IACzC,OAAO,EAAE,gBAAgB,CAAC;IAC1B,MAAM,EAAE,YAAY,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,GAAG,SAAS,CAAC,CAkFlE;AAED,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC,CAmThE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG","sourcesContent":["import { readFile as fsReadFile, stat as fsStat } from \"node:fs/promises\";\nimport { createInterface } from \"node:readline\";\nimport type { AgentTool } from \"@caupulican/pi-agent-core\";\nimport {\n\tDEFAULT_MAX_BYTES,\n\tformatSize,\n\tGREP_MAX_LINE_LENGTH,\n\ttype TruncationResult,\n\ttruncateLine,\n} from \"@caupulican/pi-agent-core/node\";\nimport { Text } from \"@caupulican/pi-tui\";\nimport { spawn } from \"child_process\";\nimport path from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport { waitForChildProcessWithTermination } from \"../../utils/child-process.ts\";\nimport { ensureTool } from \"../../utils/tools-manager.ts\";\nimport type { ArtifactStore } from \"../context/context-artifacts.ts\";\nimport {\n\ttype BroadQueryTracker,\n\tbroadQueryInvalidationNote,\n\tformatArtifactNotice,\n\tnormalizeBroadQueryKey,\n\tpackToolOutput,\n} from \"../context/tool-output-packer.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport {\n\tdefaultFffSearchBackend,\n\ttype FffGrepMatch,\n\ttype FffGrepResult,\n\ttype FffSearchBackend,\n\thasGitignoreInTree,\n\trelativePathInside,\n\tsafeGetFinder,\n} from \"./fff-search-backend.ts\";\nimport { resolveToCwd } from \"./path-utils.ts\";\nimport { getTextOutput, invalidArgText, shortenPath, str } from \"./render-utils.ts\";\nimport { defaultSearchRouter, type SearchRouter } from \"./search-router.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\nconst grepSchema = Type.Object({\n\tpattern: Type.String({ description: \"Search pattern (regex or literal string)\" }),\n\tpath: Type.Optional(Type.String({ description: \"Directory or file to search (default: current directory)\" })),\n\tglob: Type.Optional(Type.String({ description: \"Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'\" })),\n\tignoreCase: Type.Optional(Type.Boolean({ description: \"Case-insensitive search (default: false)\" })),\n\tliteral: Type.Optional(\n\t\tType.Boolean({ description: \"Treat pattern as literal string instead of regex (default: false)\" }),\n\t),\n\tcontext: Type.Optional(\n\t\tType.Number({ description: \"Number of lines to show before and after each match (default: 0)\" }),\n\t),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of matches to return (default: 100)\" })),\n});\n\nexport type GrepToolInput = Static<typeof grepSchema>;\nconst DEFAULT_LIMIT = 100;\nconst GREP_PROCESS_TIMEOUT_MS = 5 * 60_000;\nconst GREP_PROCESS_KILL_GRACE_MS = 1_000;\n\nexport interface GrepToolDetails {\n\ttruncation?: TruncationResult;\n\tmatchLimitReached?: number;\n\tlinesTruncated?: boolean;\n\t/** Set only when output was packed to an artifact; see tool-output-packer.ts. */\n\tartifactId?: string;\n\t/** Set when this exact query has repeatedly produced broad/truncated results. */\n\tinvalidationCandidate?: boolean;\n}\n\n/**\n * Pluggable operations for the grep tool.\n * Override these to delegate search to remote systems (for example SSH).\n */\nexport interface GrepOperations {\n\t/** Check if path is a directory. Throws if path does not exist. */\n\tisDirectory: (absolutePath: string) => Promise<boolean> | boolean;\n\t/** Read file contents for context lines */\n\treadFile: (absolutePath: string) => Promise<string> | string;\n}\n\nconst defaultGrepOperations: GrepOperations = {\n\tisDirectory: async (p) => (await fsStat(p)).isDirectory(),\n\treadFile: (p) => fsReadFile(p, \"utf-8\"),\n};\n\nexport interface GrepToolOptions {\n\t/** Custom operations for grep. Default: local filesystem plus routed FFF/rg search */\n\toperations?: GrepOperations;\n\t/** FFF backend for resident indexed search. Set false to force ripgrep fallback. */\n\tfff?: FffSearchBackend | false;\n\t/** Pure router that selects FFF or rg from request filters and environment facts. */\n\tsearchRouter?: SearchRouter;\n\t/**\n\t * Opt-in artifact store for first-capture-then-bound output packing (Phase 3). When\n\t * omitted (the default), behavior is byte-for-byte unchanged from before this option\n\t * existed: output is truncated the same way, just never artifact-backed.\n\t */\n\tartifactStore?: ArtifactStore;\n\t/** Opt-in tracker for repeated-broad-query \"do not repeat\" signals. Also default-off. */\n\tbroadQueryTracker?: BroadQueryTracker;\n}\n\nfunction formatGrepCall(\n\targs: { pattern: string; path?: string; glob?: string; limit?: number } | undefined,\n\ttheme: Theme,\n\tcwd: string,\n): string {\n\tconst pattern = str(args?.pattern);\n\tconst rawPath = str(args?.path);\n\tconst path = rawPath !== null ? shortenPath(rawPath || \".\", cwd) : null;\n\tconst glob = str(args?.glob);\n\tconst limit = args?.limit;\n\tconst invalidArg = invalidArgText(theme);\n\tlet text =\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"grep\")) +\n\t\t\" \" +\n\t\t(pattern === null ? invalidArg : theme.fg(\"accent\", `/${pattern || \"\"}/`)) +\n\t\ttheme.fg(\"toolOutput\", ` in ${path === null ? invalidArg : path}`);\n\tif (glob) text += theme.fg(\"toolOutput\", ` (${glob})`);\n\tif (limit !== undefined) text += theme.fg(\"toolOutput\", ` limit ${limit}`);\n\treturn text;\n}\n\nfunction formatGrepResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: GrepToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 15;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\n\tconst matchLimit = result.details?.matchLimitReached;\n\tconst truncation = result.details?.truncation;\n\tconst linesTruncated = result.details?.linesTruncated;\n\tif (matchLimit || truncation?.truncated || linesTruncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (matchLimit) warnings.push(`${matchLimit} matches limit`);\n\t\tif (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\tif (linesTruncated) warnings.push(\"some lines truncated\");\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nfunction globConstraintForFff(glob: string | undefined): string {\n\tif (!glob) return \"\";\n\tif (glob.includes(\"/\") || glob.startsWith(\"**/\")) return glob;\n\treturn `**/${glob}`;\n}\n\nfunction fffGrepQuery(options: {\n\tpattern: string;\n\tglob?: string;\n\tisDirectory: boolean;\n\tsearchPathRelativeToCwd: string;\n}): string | undefined {\n\tconst parts: string[] = [];\n\tif (options.searchPathRelativeToCwd) {\n\t\tparts.push(options.isDirectory ? `${options.searchPathRelativeToCwd}/` : options.searchPathRelativeToCwd);\n\t}\n\tparts.push(globConstraintForFff(options.glob));\n\tparts.push(options.pattern);\n\treturn parts.filter(Boolean).join(\" \");\n}\n\nfunction fffDisplayPath(\n\tmatch: FffGrepMatch,\n\toptions: { isDirectory: boolean; searchPathRelativeToCwd: string },\n): string | undefined {\n\tif (!options.searchPathRelativeToCwd) return match.relativePath;\n\tif (!options.isDirectory) {\n\t\treturn match.relativePath === options.searchPathRelativeToCwd ? path.basename(match.relativePath) : undefined;\n\t}\n\tconst prefix = `${options.searchPathRelativeToCwd}/`;\n\tif (!match.relativePath.startsWith(prefix)) return undefined;\n\treturn match.relativePath.slice(prefix.length);\n}\n\nfunction appendFffMatchLines(options: {\n\tmatch: FffGrepMatch;\n\toutputLines: string[];\n\tlinesTruncated: { value: boolean };\n\tcontextValue: number;\n}): void {\n\tconst before = options.match.contextBefore ?? [];\n\tfor (let i = 0; i < before.length; i++) {\n\t\tconst lineNumber = options.match.lineNumber - before.length + i;\n\t\tconst { text, wasTruncated } = truncateLine(before[i] ?? \"\");\n\t\tif (wasTruncated) options.linesTruncated.value = true;\n\t\toptions.outputLines.push(` ${lineNumber}- ${text}`);\n\t}\n\n\tconst { text, wasTruncated } = truncateLine(options.match.lineContent.replace(/\\r/g, \"\"));\n\tif (wasTruncated) options.linesTruncated.value = true;\n\toptions.outputLines.push(` ${options.match.lineNumber}: ${text}`);\n\n\tif (options.contextValue === 0) return;\n\tconst after = options.match.contextAfter ?? [];\n\tfor (let i = 0; i < after.length; i++) {\n\t\tconst lineNumber = options.match.lineNumber + 1 + i;\n\t\tconst { text: contextText, wasTruncated: contextWasTruncated } = truncateLine(after[i] ?? \"\");\n\t\tif (contextWasTruncated) options.linesTruncated.value = true;\n\t\toptions.outputLines.push(` ${lineNumber}- ${contextText}`);\n\t}\n}\n\n/**\n * Shared \"measure -> pack -> notices\" tail for both the FFF and ripgrep result paths:\n * first-capture the raw output to an artifact if it's oversized and a store was provided\n * (Phase 3 tool-output-artifacts.md boundary rule), then append the same match-limit/\n * byte-limit/line-truncation/broad-query notices either path already produced.\n */\nfunction packGrepOutput(options: {\n\trawOutput: string;\n\ttoolCallId: string;\n\tartifactStore?: ArtifactStore;\n\tbroadQueryTracker?: BroadQueryTracker;\n\tpattern: string;\n\trawPath?: string;\n\tglob?: string;\n\tmatchLimitReached: number | false;\n\tlinesTruncated: boolean;\n}): { text: string; details: GrepToolDetails } {\n\tconst packed = packToolOutput(\n\t\t{\n\t\t\ttoolName: \"grep\",\n\t\t\tpath: options.rawPath,\n\t\t\trawContent: options.rawOutput,\n\t\t\t// No line limit here because the match limit already caps rows; only the byte\n\t\t\t// cap should apply, matching the pre-Slice-B truncateHead call exactly.\n\t\t\ttruncation: { maxLines: Number.MAX_SAFE_INTEGER },\n\t\t},\n\t\toptions.artifactStore,\n\t\toptions.toolCallId,\n\t);\n\tlet output = packed.content;\n\tconst details: GrepToolDetails = {};\n\n\tconst notices: string[] = [];\n\tif (packed.artifactId) {\n\t\tnotices.push(formatArtifactNotice(packed.artifactId));\n\t\tdetails.artifactId = packed.artifactId;\n\t}\n\tif (options.matchLimitReached) {\n\t\tnotices.push(\n\t\t\t`${options.matchLimitReached} matches limit reached. Use limit=${options.matchLimitReached * 2} for more, or refine pattern`,\n\t\t);\n\t\tdetails.matchLimitReached = options.matchLimitReached;\n\t}\n\tif (packed.truncation.truncated) {\n\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t// Drop the duplicated bounded-preview text: it's already in the message's own\n\t\t// content, and re-including it here can push `details` past\n\t\t// MAX_RETAINED_TOOL_RESULT_DETAILS_BYTES (message-retention.ts), which replaces\n\t\t// the *entire* details object with a stub -- silently losing artifactId and every\n\t\t// other field alongside it. This is load-bearing beyond just the retention budget:\n\t\t// agent-session.ts's _releaseGcPackedArtifactReferences() reads artifactId back off\n\t\t// this same canonical message at eviction time (potentially many turns later), so\n\t\t// keeping `details` small here is what keeps that release path working at all. If\n\t\t// this field ever grows a large addition again, add a regression proving artifactId\n\t\t// survives compactToolResultDetailsForRetention (see\n\t\t// test/suite/agent-session-artifact-lifecycle.test.ts), not just a details-size check.\n\t\tdetails.truncation = { ...packed.truncation, content: \"\" };\n\t}\n\tif (options.linesTruncated) {\n\t\tnotices.push(`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`);\n\t\tdetails.linesTruncated = true;\n\t}\n\tif (options.matchLimitReached || packed.truncation.truncated) {\n\t\tconst note = broadQueryInvalidationNote(\n\t\t\toptions.broadQueryTracker,\n\t\t\tnormalizeBroadQueryKey({\n\t\t\t\ttoolName: \"grep\",\n\t\t\t\tpattern: options.pattern,\n\t\t\t\tpath: options.rawPath,\n\t\t\t\tglob: options.glob,\n\t\t\t}),\n\t\t\t`grep \"${options.pattern}\" in ${options.rawPath ?? \".\"}`,\n\t\t);\n\t\tif (note) {\n\t\t\tnotices.push(note);\n\t\t\tdetails.invalidationCandidate = true;\n\t\t}\n\t}\n\tif (notices.length > 0) output += `\\n\\n[${notices.join(\". \")}]`;\n\treturn { text: output, details };\n}\n\nfunction formatFffGrepResult(options: {\n\tresult: FffGrepResult;\n\tisDirectory: boolean;\n\tsearchPathRelativeToCwd: string;\n\teffectiveLimit: number;\n\tcontextValue: number;\n\ttoolCallId: string;\n\tartifactStore?: ArtifactStore;\n\tbroadQueryTracker?: BroadQueryTracker;\n\tpattern: string;\n\trawPath?: string;\n\tglob?: string;\n}): { text: string; details: GrepToolDetails } {\n\tif (options.result.items.length === 0) return { text: \"No matches found\", details: {} };\n\n\tconst outputLines: string[] = [];\n\tconst linesTruncated = { value: false };\n\tconst resultLimitReached =\n\t\toptions.result.items.length > options.effectiveLimit || Boolean(options.result.nextCursor);\n\tconst displayedMatches = resultLimitReached\n\t\t? options.result.items.slice(0, options.effectiveLimit)\n\t\t: options.result.items;\n\tlet currentPath = \"\";\n\tfor (const match of displayedMatches) {\n\t\tconst displayPath = fffDisplayPath(match, options);\n\t\tif (!displayPath) continue;\n\t\tif (displayPath !== currentPath) {\n\t\t\tcurrentPath = displayPath;\n\t\t\toutputLines.push(`${displayPath}:`);\n\t\t}\n\t\tappendFffMatchLines({\n\t\t\tmatch,\n\t\t\toutputLines,\n\t\t\tlinesTruncated,\n\t\t\tcontextValue: options.contextValue,\n\t\t});\n\t}\n\n\tif (outputLines.length === 0) return { text: \"No matches found\", details: {} };\n\tconst rawOutput = outputLines.join(\"\\n\");\n\treturn packGrepOutput({\n\t\trawOutput,\n\t\ttoolCallId: options.toolCallId,\n\t\tartifactStore: options.artifactStore,\n\t\tbroadQueryTracker: options.broadQueryTracker,\n\t\tpattern: options.pattern,\n\t\trawPath: options.rawPath,\n\t\tglob: options.glob,\n\t\tmatchLimitReached: resultLimitReached ? options.effectiveLimit : false,\n\t\tlinesTruncated: linesTruncated.value,\n\t});\n}\n\nexport async function tryFffGrep(options: {\n\tbackend: FffSearchBackend;\n\trouter: SearchRouter;\n\tcwd: string;\n\tsearchPath: string;\n\tpattern: string;\n\tglob?: string;\n\tignoreCase?: boolean;\n\tliteral?: boolean;\n\tcontextValue: number;\n\teffectiveLimit: number;\n\tisDirectory: boolean;\n\ttoolCallId: string;\n\tartifactStore?: ArtifactStore;\n\tbroadQueryTracker?: BroadQueryTracker;\n\trawPath?: string;\n}): Promise<{ text: string; details: GrepToolDetails } | undefined> {\n\t// Kick off the FFF finder -- and, on a machine where fff-node isn't provisioned\n\t// yet, its lazy managed install -- unconditionally and as early as possible.\n\t// See the matching comment in find.ts's tryFffFind: routing below can still\n\t// send THIS call to the rg fallback for reasons unrelated to tool availability\n\t// (chiefly the default match limit exceeding the FFF top-N threshold), and\n\t// that outcome must not gate the install itself. getFinder() is cached per\n\t// basePath, so later calls -- including the `await` below -- reuse this same\n\t// in-flight/resolved promise instead of doing the work twice. safeGetFinder()\n\t// guarantees this can never reject (a non-conforming custom backend that\n\t// throws synchronously still degrades to \"unavailable\"), so it can neither\n\t// produce an unhandled rejection nor fail this tool call outright.\n\tconst finderPromise = safeGetFinder(options.backend, options.cwd);\n\n\tconst searchPathRelativeToCwd = relativePathInside(options.cwd, options.searchPath);\n\tconst baseRoute = options.router.route({\n\t\ttool: \"grep\",\n\t\tglob: Boolean(options.glob),\n\t\tignoreCase: Boolean(options.ignoreCase),\n\t\tlimit: options.effectiveLimit,\n\t\tfinderAvailable: true,\n\t\tpathResolvable: searchPathRelativeToCwd !== undefined,\n\t\tgitignoreInTree: false,\n\t});\n\tif (baseRoute.backend !== \"fff\") return undefined;\n\tif (searchPathRelativeToCwd === undefined) return undefined;\n\n\tconst gitignoreInTree = options.isDirectory ? await hasGitignoreInTree(options.searchPath) : false;\n\tconst semanticRoute = options.router.route({\n\t\ttool: \"grep\",\n\t\tglob: Boolean(options.glob),\n\t\tignoreCase: Boolean(options.ignoreCase),\n\t\tlimit: options.effectiveLimit,\n\t\tfinderAvailable: true,\n\t\tpathResolvable: true,\n\t\tgitignoreInTree,\n\t});\n\tif (semanticRoute.backend !== \"fff\") return undefined;\n\n\tconst finder = await finderPromise;\n\tconst finderRoute = options.router.route({\n\t\ttool: \"grep\",\n\t\tglob: Boolean(options.glob),\n\t\tignoreCase: Boolean(options.ignoreCase),\n\t\tlimit: options.effectiveLimit,\n\t\tfinderAvailable: Boolean(finder),\n\t\tpathResolvable: true,\n\t\tgitignoreInTree: false,\n\t});\n\tif (!finder || finderRoute.backend !== \"fff\") return undefined;\n\n\tconst query = fffGrepQuery({\n\t\tpattern: options.pattern,\n\t\tglob: options.glob,\n\t\tisDirectory: options.isDirectory,\n\t\tsearchPathRelativeToCwd,\n\t});\n\tif (!query) return undefined;\n\n\tconst probeLimit = options.effectiveLimit + 1;\n\tconst result = finder.grep(query, {\n\t\tmode: options.literal ? \"plain\" : \"regex\",\n\t\tsmartCase: false,\n\t\tmaxMatchesPerFile: probeLimit,\n\t\tbeforeContext: options.contextValue,\n\t\tafterContext: options.contextValue,\n\t\tpageSize: probeLimit,\n\t});\n\tif (!result.ok || result.value.regexFallbackError) return undefined;\n\treturn formatFffGrepResult({\n\t\tresult: result.value,\n\t\tisDirectory: options.isDirectory,\n\t\tsearchPathRelativeToCwd,\n\t\teffectiveLimit: options.effectiveLimit,\n\t\tcontextValue: options.contextValue,\n\t\ttoolCallId: options.toolCallId,\n\t\tartifactStore: options.artifactStore,\n\t\tbroadQueryTracker: options.broadQueryTracker,\n\t\tpattern: options.pattern,\n\t\trawPath: options.rawPath,\n\t\tglob: options.glob,\n\t});\n}\n\nexport function createGrepToolDefinition(\n\tcwd: string,\n\toptions?: GrepToolOptions,\n): ToolDefinition<typeof grepSchema, GrepToolDetails | undefined> {\n\tconst customOps = options?.operations;\n\tconst fffBackend = options?.fff === false ? undefined : (options?.fff ?? defaultFffSearchBackend);\n\tconst searchRouter = options?.searchRouter ?? defaultSearchRouter;\n\tconst artifactStore = options?.artifactStore;\n\tconst broadQueryTracker = options?.broadQueryTracker;\n\treturn {\n\t\tname: \"grep\",\n\t\tlabel: \"grep\",\n\t\tdescription: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,\n\t\tpromptSnippet: \"Search file contents for patterns (respects .gitignore)\",\n\t\tparameters: grepSchema,\n\t\ttoolGroup: \"explore\",\n\t\tasync execute(\n\t\t\ttoolCallId,\n\t\t\t{\n\t\t\t\tpattern,\n\t\t\t\tpath: searchDir,\n\t\t\t\tglob,\n\t\t\t\tignoreCase,\n\t\t\t\tliteral,\n\t\t\t\tcontext,\n\t\t\t\tlimit,\n\t\t\t}: {\n\t\t\t\tpattern: string;\n\t\t\t\tpath?: string;\n\t\t\t\tglob?: string;\n\t\t\t\tignoreCase?: boolean;\n\t\t\t\tliteral?: boolean;\n\t\t\t\tcontext?: number;\n\t\t\t\tlimit?: number;\n\t\t\t},\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlet settled = false;\n\t\t\t\tconst settle = (fn: () => void) => {\n\t\t\t\t\tif (!settled) {\n\t\t\t\t\t\tsettled = true;\n\t\t\t\t\t\tfn();\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst searchPath = resolveToCwd(searchDir || \".\", cwd);\n\t\t\t\t\t\tconst ops = customOps ?? defaultGrepOperations;\n\t\t\t\t\t\tlet isDirectory: boolean;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tisDirectory = await ops.isDirectory(searchPath);\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(`Path not found: ${searchPath}`)));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst contextValue = context && context > 0 ? context : 0;\n\t\t\t\t\t\tconst effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);\n\t\t\t\t\t\tconst formatPath = (filePath: string): string => {\n\t\t\t\t\t\t\tif (isDirectory) {\n\t\t\t\t\t\t\t\tconst relative = path.relative(searchPath, filePath);\n\t\t\t\t\t\t\t\tif (relative && !relative.startsWith(\"..\")) {\n\t\t\t\t\t\t\t\t\treturn relative.replace(/\\\\/g, \"/\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn path.basename(filePath);\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tif (!customOps && fffBackend) {\n\t\t\t\t\t\t\tconst fffResult = await tryFffGrep({\n\t\t\t\t\t\t\t\tbackend: fffBackend,\n\t\t\t\t\t\t\t\trouter: searchRouter,\n\t\t\t\t\t\t\t\tcwd,\n\t\t\t\t\t\t\t\tsearchPath,\n\t\t\t\t\t\t\t\tpattern,\n\t\t\t\t\t\t\t\tglob,\n\t\t\t\t\t\t\t\tignoreCase,\n\t\t\t\t\t\t\t\tliteral,\n\t\t\t\t\t\t\t\tcontextValue,\n\t\t\t\t\t\t\t\teffectiveLimit,\n\t\t\t\t\t\t\t\tisDirectory,\n\t\t\t\t\t\t\t\ttoolCallId,\n\t\t\t\t\t\t\t\tartifactStore,\n\t\t\t\t\t\t\t\tbroadQueryTracker,\n\t\t\t\t\t\t\t\trawPath: searchDir,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tif (fffResult) {\n\t\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: fffResult.text }],\n\t\t\t\t\t\t\t\t\t\tdetails: Object.keys(fffResult.details).length > 0 ? fffResult.details : undefined,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst rgPath = await ensureTool(\"rg\", true);\n\t\t\t\t\t\tif (!rgPath) {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(\"ripgrep (rg) is not available and could not be downloaded\")));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst fileCache = new Map<string, string[]>();\n\t\t\t\t\t\tconst getFileLines = async (filePath: string): Promise<string[]> => {\n\t\t\t\t\t\t\tlet lines = fileCache.get(filePath);\n\t\t\t\t\t\t\tif (!lines) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tconst content = await ops.readFile(filePath);\n\t\t\t\t\t\t\t\t\tlines = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\").split(\"\\n\");\n\t\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t\tlines = [];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tfileCache.set(filePath, lines);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn lines;\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tconst args: string[] = [\"--json\", \"--line-number\", \"--color=never\", \"--hidden\"];\n\t\t\t\t\t\tif (ignoreCase) args.push(\"--ignore-case\");\n\t\t\t\t\t\tif (literal) args.push(\"--fixed-strings\");\n\t\t\t\t\t\tif (glob) args.push(\"--glob\", glob);\n\t\t\t\t\t\targs.push(\"--\", pattern, searchPath);\n\n\t\t\t\t\t\tconst child = spawn(rgPath, args, {\n\t\t\t\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\t\t\t});\n\t\t\t\t\t\tconst rl = createInterface({ input: child.stdout });\n\t\t\t\t\t\tlet stderr = \"\";\n\t\t\t\t\t\tlet matchCount = 0;\n\t\t\t\t\t\tlet matchLimitReached = false;\n\t\t\t\t\t\tlet linesTruncated = false;\n\t\t\t\t\t\tlet aborted = false;\n\t\t\t\t\t\tlet killedDueToLimit = false;\n\t\t\t\t\t\tconst outputLines: string[] = [];\n\n\t\t\t\t\t\tconst terminationController = new AbortController();\n\t\t\t\t\t\tconst cleanup = () => {\n\t\t\t\t\t\t\trl.close();\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst stopChild = (dueToLimit = false) => {\n\t\t\t\t\t\t\tif (dueToLimit) killedDueToLimit = true;\n\t\t\t\t\t\t\tterminationController.abort();\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\t\tstopChild();\n\t\t\t\t\t\t};\n\t\t\t\t\t\tif (signal?.aborted) onAbort();\n\t\t\t\t\t\telse signal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t\t\tchild.stderr?.on(\"data\", (chunk) => {\n\t\t\t\t\t\t\tstderr += chunk.toString();\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tconst formatBlock = async (filePath: string, lineNumber: number): Promise<string[]> => {\n\t\t\t\t\t\t\tconst relativePath = formatPath(filePath);\n\t\t\t\t\t\t\tconst lines = await getFileLines(filePath);\n\t\t\t\t\t\t\tif (!lines.length) return [`${relativePath}:${lineNumber}: (unable to read file)`];\n\t\t\t\t\t\t\tconst block: string[] = [];\n\t\t\t\t\t\t\tconst start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;\n\t\t\t\t\t\t\tconst end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;\n\t\t\t\t\t\t\tfor (let current = start; current <= end; current++) {\n\t\t\t\t\t\t\t\tconst lineText = lines[current - 1] ?? \"\";\n\t\t\t\t\t\t\t\tconst sanitized = lineText.replace(/\\r/g, \"\");\n\t\t\t\t\t\t\t\tconst isMatchLine = current === lineNumber;\n\t\t\t\t\t\t\t\t// Truncate long lines so grep output stays compact.\n\t\t\t\t\t\t\t\tconst { text: truncatedText, wasTruncated } = truncateLine(sanitized);\n\t\t\t\t\t\t\t\tif (wasTruncated) linesTruncated = true;\n\t\t\t\t\t\t\t\tif (isMatchLine) block.push(`${relativePath}:${current}: ${truncatedText}`);\n\t\t\t\t\t\t\t\telse block.push(`${relativePath}-${current}- ${truncatedText}`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn block;\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\t// Collect matches during streaming, then format them after rg exits.\n\t\t\t\t\t\tconst matches: Array<{ filePath: string; lineNumber: number; lineText?: string }> = [];\n\t\t\t\t\t\trl.on(\"line\", (line) => {\n\t\t\t\t\t\t\tif (!line.trim() || matchLimitReached) return;\n\t\t\t\t\t\t\tlet event: any;\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tevent = JSON.parse(line);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (event.type === \"match\") {\n\t\t\t\t\t\t\t\tmatchCount++;\n\t\t\t\t\t\t\t\tif (matchCount > effectiveLimit) {\n\t\t\t\t\t\t\t\t\tmatchLimitReached = true;\n\t\t\t\t\t\t\t\t\tstopChild(true);\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tconst filePath = event.data?.path?.text;\n\t\t\t\t\t\t\t\tconst lineNumber = event.data?.line_number;\n\t\t\t\t\t\t\t\tconst lineText = event.data?.lines?.text;\n\t\t\t\t\t\t\t\tif (filePath && typeof lineNumber === \"number\")\n\t\t\t\t\t\t\t\t\tmatches.push({ filePath, lineNumber, lineText });\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tconst terminal = await waitForChildProcessWithTermination(child, {\n\t\t\t\t\t\t\tsignal: terminationController.signal,\n\t\t\t\t\t\t\ttimeoutMs: GREP_PROCESS_TIMEOUT_MS,\n\t\t\t\t\t\t\tkillGraceMs: GREP_PROCESS_KILL_GRACE_MS,\n\t\t\t\t\t\t}).finally(cleanup);\n\t\t\t\t\t\tconst code = terminal.code;\n\t\t\t\t\t\tif (terminal.reason === \"timeout\") {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(`ripgrep timed out after ${GREP_PROCESS_TIMEOUT_MS}ms`)));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!killedDueToLimit && code !== 0 && code !== 1) {\n\t\t\t\t\t\t\tconst errorMsg = stderr.trim() || `ripgrep exited with code ${code}`;\n\t\t\t\t\t\t\tsettle(() => reject(new Error(errorMsg)));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (matchCount === 0) {\n\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"No matches found\" }], details: undefined }),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Format matches after streaming finishes so custom readFile() backends can be async.\n\t\t\t\t\t\tconst fileGroups = new Map<string, string[]>();\n\t\t\t\t\t\tfor (const match of matches) {\n\t\t\t\t\t\t\tconst relativePath = formatPath(match.filePath);\n\t\t\t\t\t\t\tif (!fileGroups.has(relativePath)) {\n\t\t\t\t\t\t\t\tfileGroups.set(relativePath, []);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst group = fileGroups.get(relativePath)!;\n\n\t\t\t\t\t\t\tif (contextValue === 0 && match.lineText !== undefined) {\n\t\t\t\t\t\t\t\tconst sanitized = match.lineText.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\").replace(/\\n$/, \"\");\n\t\t\t\t\t\t\t\tconst { text: truncatedText, wasTruncated } = truncateLine(sanitized);\n\t\t\t\t\t\t\t\tif (wasTruncated) linesTruncated = true;\n\t\t\t\t\t\t\t\tgroup.push(` ${match.lineNumber}: ${truncatedText}`);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tconst block = await formatBlock(match.filePath, match.lineNumber);\n\t\t\t\t\t\t\t\tfor (const line of block) {\n\t\t\t\t\t\t\t\t\tif (line.startsWith(`${relativePath}:`)) {\n\t\t\t\t\t\t\t\t\t\tgroup.push(` ${line.slice(relativePath.length + 1)}`);\n\t\t\t\t\t\t\t\t\t} else if (line.startsWith(`${relativePath}-`)) {\n\t\t\t\t\t\t\t\t\t\tgroup.push(` ${line.slice(relativePath.length + 1)}`);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tgroup.push(` ${line}`);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor (const [relativePath, lines] of fileGroups) {\n\t\t\t\t\t\t\toutputLines.push(`${relativePath}:`);\n\t\t\t\t\t\t\tlet lastLine = \"\";\n\t\t\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\t\t\tif (line === lastLine) continue;\n\t\t\t\t\t\t\t\toutputLines.push(line);\n\t\t\t\t\t\t\t\tlastLine = line;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst rawOutput = outputLines.join(\"\\n\");\n\t\t\t\t\t\t// Measure -> pack (artifact-backed if oversized and a store was provided) -> notices.\n\t\t\t\t\t\t// There is no line limit here because the match limit already capped rows.\n\t\t\t\t\t\tconst { text: output, details } = packGrepOutput({\n\t\t\t\t\t\t\trawOutput,\n\t\t\t\t\t\t\ttoolCallId,\n\t\t\t\t\t\t\tartifactStore,\n\t\t\t\t\t\t\tbroadQueryTracker,\n\t\t\t\t\t\t\tpattern,\n\t\t\t\t\t\t\trawPath: searchDir,\n\t\t\t\t\t\t\tglob,\n\t\t\t\t\t\t\tmatchLimitReached: matchLimitReached ? effectiveLimit : false,\n\t\t\t\t\t\t\tlinesTruncated,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tsettle(() => reject(err as Error));\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatGrepCall(args, theme, context.cwd));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatGrepResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createGrepTool(cwd: string, options?: GrepToolOptions): AgentTool<typeof grepSchema> {\n\treturn wrapToolDefinition(createGrepToolDefinition(cwd, options));\n}\n"]}
|
package/dist/core/tools/grep.js
CHANGED
|
@@ -6,6 +6,7 @@ import { spawn } from "child_process";
|
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { Type } from "typebox";
|
|
8
8
|
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
|
9
|
+
import { waitForChildProcessWithTermination } from "../../utils/child-process.js";
|
|
9
10
|
import { ensureTool } from "../../utils/tools-manager.js";
|
|
10
11
|
import { broadQueryInvalidationNote, formatArtifactNotice, normalizeBroadQueryKey, packToolOutput, } from "../context/tool-output-packer.js";
|
|
11
12
|
import { defaultFffSearchBackend, hasGitignoreInTree, relativePathInside, safeGetFinder, } from "./fff-search-backend.js";
|
|
@@ -23,6 +24,8 @@ const grepSchema = Type.Object({
|
|
|
23
24
|
limit: Type.Optional(Type.Number({ description: "Maximum number of matches to return (default: 100)" })),
|
|
24
25
|
});
|
|
25
26
|
const DEFAULT_LIMIT = 100;
|
|
27
|
+
const GREP_PROCESS_TIMEOUT_MS = 5 * 60_000;
|
|
28
|
+
const GREP_PROCESS_KILL_GRACE_MS = 1_000;
|
|
26
29
|
const defaultGrepOperations = {
|
|
27
30
|
isDirectory: async (p) => (await fsStat(p)).isDirectory(),
|
|
28
31
|
readFile: (p) => fsReadFile(p, "utf-8"),
|
|
@@ -411,7 +414,10 @@ export function createGrepToolDefinition(cwd, options) {
|
|
|
411
414
|
if (glob)
|
|
412
415
|
args.push("--glob", glob);
|
|
413
416
|
args.push("--", pattern, searchPath);
|
|
414
|
-
const child = spawn(rgPath, args, {
|
|
417
|
+
const child = spawn(rgPath, args, {
|
|
418
|
+
detached: process.platform !== "win32",
|
|
419
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
420
|
+
});
|
|
415
421
|
const rl = createInterface({ input: child.stdout });
|
|
416
422
|
let stderr = "";
|
|
417
423
|
let matchCount = 0;
|
|
@@ -420,21 +426,24 @@ export function createGrepToolDefinition(cwd, options) {
|
|
|
420
426
|
let aborted = false;
|
|
421
427
|
let killedDueToLimit = false;
|
|
422
428
|
const outputLines = [];
|
|
429
|
+
const terminationController = new AbortController();
|
|
423
430
|
const cleanup = () => {
|
|
424
431
|
rl.close();
|
|
425
432
|
signal?.removeEventListener("abort", onAbort);
|
|
426
433
|
};
|
|
427
434
|
const stopChild = (dueToLimit = false) => {
|
|
428
|
-
if (
|
|
429
|
-
killedDueToLimit =
|
|
430
|
-
|
|
431
|
-
}
|
|
435
|
+
if (dueToLimit)
|
|
436
|
+
killedDueToLimit = true;
|
|
437
|
+
terminationController.abort();
|
|
432
438
|
};
|
|
433
439
|
const onAbort = () => {
|
|
434
440
|
aborted = true;
|
|
435
441
|
stopChild();
|
|
436
442
|
};
|
|
437
|
-
signal?.
|
|
443
|
+
if (signal?.aborted)
|
|
444
|
+
onAbort();
|
|
445
|
+
else
|
|
446
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
438
447
|
child.stderr?.on("data", (chunk) => {
|
|
439
448
|
stderr += chunk.toString();
|
|
440
449
|
});
|
|
@@ -487,87 +496,87 @@ export function createGrepToolDefinition(cwd, options) {
|
|
|
487
496
|
matches.push({ filePath, lineNumber, lineText });
|
|
488
497
|
}
|
|
489
498
|
});
|
|
490
|
-
child
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
499
|
+
const terminal = await waitForChildProcessWithTermination(child, {
|
|
500
|
+
signal: terminationController.signal,
|
|
501
|
+
timeoutMs: GREP_PROCESS_TIMEOUT_MS,
|
|
502
|
+
killGraceMs: GREP_PROCESS_KILL_GRACE_MS,
|
|
503
|
+
}).finally(cleanup);
|
|
504
|
+
const code = terminal.code;
|
|
505
|
+
if (terminal.reason === "timeout") {
|
|
506
|
+
settle(() => reject(new Error(`ripgrep timed out after ${GREP_PROCESS_TIMEOUT_MS}ms`)));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (aborted) {
|
|
510
|
+
settle(() => reject(new Error("Operation aborted")));
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (!killedDueToLimit && code !== 0 && code !== 1) {
|
|
514
|
+
const errorMsg = stderr.trim() || `ripgrep exited with code ${code}`;
|
|
515
|
+
settle(() => reject(new Error(errorMsg)));
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (matchCount === 0) {
|
|
519
|
+
settle(() => resolve({ content: [{ type: "text", text: "No matches found" }], details: undefined }));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
// Format matches after streaming finishes so custom readFile() backends can be async.
|
|
523
|
+
const fileGroups = new Map();
|
|
524
|
+
for (const match of matches) {
|
|
525
|
+
const relativePath = formatPath(match.filePath);
|
|
526
|
+
if (!fileGroups.has(relativePath)) {
|
|
527
|
+
fileGroups.set(relativePath, []);
|
|
504
528
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
529
|
+
const group = fileGroups.get(relativePath);
|
|
530
|
+
if (contextValue === 0 && match.lineText !== undefined) {
|
|
531
|
+
const sanitized = match.lineText.replace(/\r\n/g, "\n").replace(/\r/g, "").replace(/\n$/, "");
|
|
532
|
+
const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
|
|
533
|
+
if (wasTruncated)
|
|
534
|
+
linesTruncated = true;
|
|
535
|
+
group.push(` ${match.lineNumber}: ${truncatedText}`);
|
|
508
536
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
.
|
|
520
|
-
.replace(/\r/g, "")
|
|
521
|
-
.replace(/\n$/, "");
|
|
522
|
-
const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
|
|
523
|
-
if (wasTruncated)
|
|
524
|
-
linesTruncated = true;
|
|
525
|
-
group.push(` ${match.lineNumber}: ${truncatedText}`);
|
|
526
|
-
}
|
|
527
|
-
else {
|
|
528
|
-
const block = await formatBlock(match.filePath, match.lineNumber);
|
|
529
|
-
for (const line of block) {
|
|
530
|
-
if (line.startsWith(`${relativePath}:`)) {
|
|
531
|
-
group.push(` ${line.slice(relativePath.length + 1)}`);
|
|
532
|
-
}
|
|
533
|
-
else if (line.startsWith(`${relativePath}-`)) {
|
|
534
|
-
group.push(` ${line.slice(relativePath.length + 1)}`);
|
|
535
|
-
}
|
|
536
|
-
else {
|
|
537
|
-
group.push(` ${line}`);
|
|
538
|
-
}
|
|
537
|
+
else {
|
|
538
|
+
const block = await formatBlock(match.filePath, match.lineNumber);
|
|
539
|
+
for (const line of block) {
|
|
540
|
+
if (line.startsWith(`${relativePath}:`)) {
|
|
541
|
+
group.push(` ${line.slice(relativePath.length + 1)}`);
|
|
542
|
+
}
|
|
543
|
+
else if (line.startsWith(`${relativePath}-`)) {
|
|
544
|
+
group.push(` ${line.slice(relativePath.length + 1)}`);
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
group.push(` ${line}`);
|
|
539
548
|
}
|
|
540
549
|
}
|
|
541
550
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
+
}
|
|
552
|
+
for (const [relativePath, lines] of fileGroups) {
|
|
553
|
+
outputLines.push(`${relativePath}:`);
|
|
554
|
+
let lastLine = "";
|
|
555
|
+
for (const line of lines) {
|
|
556
|
+
if (line === lastLine)
|
|
557
|
+
continue;
|
|
558
|
+
outputLines.push(line);
|
|
559
|
+
lastLine = line;
|
|
551
560
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
settle(() => resolve({
|
|
567
|
-
content: [{ type: "text", text: output }],
|
|
568
|
-
details: Object.keys(details).length > 0 ? details : undefined,
|
|
569
|
-
}));
|
|
561
|
+
}
|
|
562
|
+
const rawOutput = outputLines.join("\n");
|
|
563
|
+
// Measure -> pack (artifact-backed if oversized and a store was provided) -> notices.
|
|
564
|
+
// There is no line limit here because the match limit already capped rows.
|
|
565
|
+
const { text: output, details } = packGrepOutput({
|
|
566
|
+
rawOutput,
|
|
567
|
+
toolCallId,
|
|
568
|
+
artifactStore,
|
|
569
|
+
broadQueryTracker,
|
|
570
|
+
pattern,
|
|
571
|
+
rawPath: searchDir,
|
|
572
|
+
glob,
|
|
573
|
+
matchLimitReached: matchLimitReached ? effectiveLimit : false,
|
|
574
|
+
linesTruncated,
|
|
570
575
|
});
|
|
576
|
+
settle(() => resolve({
|
|
577
|
+
content: [{ type: "text", text: output }],
|
|
578
|
+
details: Object.keys(details).length > 0 ? details : undefined,
|
|
579
|
+
}));
|
|
571
580
|
}
|
|
572
581
|
catch (err) {
|
|
573
582
|
settle(() => reject(err));
|