plugsuits 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +3 -0
- package/dist/agent-reasoning-default.test.d.ts +2 -0
- package/dist/agent-reasoning-default.test.d.ts.map +1 -0
- package/dist/agent-reasoning-default.test.js +38 -0
- package/dist/agent-reasoning-default.test.js.map +1 -0
- package/dist/agent.d.ts +61 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +308 -0
- package/dist/agent.js.map +1 -0
- package/dist/agent.test.d.ts +2 -0
- package/dist/agent.test.d.ts.map +1 -0
- package/dist/agent.test.js +38 -0
- package/dist/agent.test.js.map +1 -0
- package/dist/cli-args.d.ts +15 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +63 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli-args.test.d.ts +2 -0
- package/dist/cli-args.test.d.ts.map +1 -0
- package/dist/cli-args.test.js +105 -0
- package/dist/cli-args.test.js.map +1 -0
- package/dist/commands/aliases-and-tool-fallback.test.d.ts +2 -0
- package/dist/commands/aliases-and-tool-fallback.test.d.ts.map +1 -0
- package/dist/commands/aliases-and-tool-fallback.test.js +132 -0
- package/dist/commands/aliases-and-tool-fallback.test.js.map +1 -0
- package/dist/commands/clear.d.ts +3 -0
- package/dist/commands/clear.d.ts.map +1 -0
- package/dist/commands/clear.js +12 -0
- package/dist/commands/clear.js.map +1 -0
- package/dist/commands/factories/create-toggle-command.d.ts +13 -0
- package/dist/commands/factories/create-toggle-command.d.ts.map +1 -0
- package/dist/commands/factories/create-toggle-command.js +38 -0
- package/dist/commands/factories/create-toggle-command.js.map +1 -0
- package/dist/commands/help.d.ts +3 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/help.js +23 -0
- package/dist/commands/help.js.map +1 -0
- package/dist/commands/index.d.ts +18 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +82 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/model.d.ts +15 -0
- package/dist/commands/model.d.ts.map +1 -0
- package/dist/commands/model.js +100 -0
- package/dist/commands/model.js.map +1 -0
- package/dist/commands/reasoning-mode.d.ts +3 -0
- package/dist/commands/reasoning-mode.d.ts.map +1 -0
- package/dist/commands/reasoning-mode.js +47 -0
- package/dist/commands/reasoning-mode.js.map +1 -0
- package/dist/commands/render.d.ts +19 -0
- package/dist/commands/render.d.ts.map +1 -0
- package/dist/commands/render.js +140 -0
- package/dist/commands/render.js.map +1 -0
- package/dist/commands/render.test.d.ts +2 -0
- package/dist/commands/render.test.d.ts.map +1 -0
- package/dist/commands/render.test.js +36 -0
- package/dist/commands/render.test.js.map +1 -0
- package/dist/commands/tool-fallback.d.ts +3 -0
- package/dist/commands/tool-fallback.d.ts.map +1 -0
- package/dist/commands/tool-fallback.js +38 -0
- package/dist/commands/tool-fallback.js.map +1 -0
- package/dist/commands/translate.d.ts +3 -0
- package/dist/commands/translate.d.ts.map +1 -0
- package/dist/commands/translate.js +12 -0
- package/dist/commands/translate.js.map +1 -0
- package/dist/commands/translate.test.d.ts +2 -0
- package/dist/commands/translate.test.d.ts.map +1 -0
- package/dist/commands/translate.test.js +49 -0
- package/dist/commands/translate.test.js.map +1 -0
- package/dist/commands/types.d.ts +17 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/context/environment-context.d.ts +2 -0
- package/dist/context/environment-context.d.ts.map +1 -0
- package/dist/context/environment-context.js +11 -0
- package/dist/context/environment-context.js.map +1 -0
- package/dist/context/paths.d.ts +3 -0
- package/dist/context/paths.d.ts.map +1 -0
- package/dist/context/paths.js +3 -0
- package/dist/context/paths.js.map +1 -0
- package/dist/context/session.d.ts +4 -0
- package/dist/context/session.d.ts.map +1 -0
- package/dist/context/session.js +16 -0
- package/dist/context/session.js.map +1 -0
- package/dist/context/skill-command-prefix.d.ts +4 -0
- package/dist/context/skill-command-prefix.d.ts.map +1 -0
- package/dist/context/skill-command-prefix.js +15 -0
- package/dist/context/skill-command-prefix.js.map +1 -0
- package/dist/context/skills-integration.test.d.ts +2 -0
- package/dist/context/skills-integration.test.d.ts.map +1 -0
- package/dist/context/skills-integration.test.js +87 -0
- package/dist/context/skills-integration.test.js.map +1 -0
- package/dist/context/skills.d.ts +18 -0
- package/dist/context/skills.d.ts.map +1 -0
- package/dist/context/skills.js +431 -0
- package/dist/context/skills.js.map +1 -0
- package/dist/context/skills.test.d.ts +2 -0
- package/dist/context/skills.test.d.ts.map +1 -0
- package/dist/context/skills.test.js +20 -0
- package/dist/context/skills.test.js.map +1 -0
- package/dist/context/system-prompt.d.ts +2 -0
- package/dist/context/system-prompt.d.ts.map +1 -0
- package/dist/context/system-prompt.js +100 -0
- package/dist/context/system-prompt.js.map +1 -0
- package/dist/context/translation-integration.test.d.ts +2 -0
- package/dist/context/translation-integration.test.d.ts.map +1 -0
- package/dist/context/translation-integration.test.js +138 -0
- package/dist/context/translation-integration.test.js.map +1 -0
- package/dist/context/translation.d.ts +21 -0
- package/dist/context/translation.d.ts.map +1 -0
- package/dist/context/translation.js +82 -0
- package/dist/context/translation.js.map +1 -0
- package/dist/context/translation.test.d.ts +2 -0
- package/dist/context/translation.test.d.ts.map +1 -0
- package/dist/context/translation.test.js +129 -0
- package/dist/context/translation.test.js.map +1 -0
- package/dist/entrypoints/cli-input-rendering.test.d.ts +2 -0
- package/dist/entrypoints/cli-input-rendering.test.d.ts.map +1 -0
- package/dist/entrypoints/cli-input-rendering.test.js +192 -0
- package/dist/entrypoints/cli-input-rendering.test.js.map +1 -0
- package/dist/entrypoints/cli.d.ts +3 -0
- package/dist/entrypoints/cli.d.ts.map +1 -0
- package/dist/entrypoints/cli.js +1268 -0
- package/dist/entrypoints/cli.js.map +1 -0
- package/dist/entrypoints/headless-agent-config.d.ts +21 -0
- package/dist/entrypoints/headless-agent-config.d.ts.map +1 -0
- package/dist/entrypoints/headless-agent-config.js +15 -0
- package/dist/entrypoints/headless-agent-config.js.map +1 -0
- package/dist/entrypoints/headless-agent-config.test.d.ts +2 -0
- package/dist/entrypoints/headless-agent-config.test.d.ts.map +1 -0
- package/dist/entrypoints/headless-agent-config.test.js +63 -0
- package/dist/entrypoints/headless-agent-config.test.js.map +1 -0
- package/dist/entrypoints/headless.d.ts +3 -0
- package/dist/entrypoints/headless.d.ts.map +1 -0
- package/dist/entrypoints/headless.js +396 -0
- package/dist/entrypoints/headless.js.map +1 -0
- package/dist/env.d.ts +8 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +14 -0
- package/dist/env.js.map +1 -0
- package/dist/friendli-models.d.ts +21 -0
- package/dist/friendli-models.d.ts.map +1 -0
- package/dist/friendli-models.js +57 -0
- package/dist/friendli-models.js.map +1 -0
- package/dist/friendli-reasoning.d.ts +10 -0
- package/dist/friendli-reasoning.d.ts.map +1 -0
- package/dist/friendli-reasoning.js +181 -0
- package/dist/friendli-reasoning.js.map +1 -0
- package/dist/friendli-reasoning.test.d.ts +2 -0
- package/dist/friendli-reasoning.test.d.ts.map +1 -0
- package/dist/friendli-reasoning.test.js +77 -0
- package/dist/friendli-reasoning.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/interaction/colors.d.ts +22 -0
- package/dist/interaction/colors.d.ts.map +1 -0
- package/dist/interaction/colors.js +24 -0
- package/dist/interaction/colors.js.map +1 -0
- package/dist/interaction/pi-tui-stream-renderer.d.ts +19 -0
- package/dist/interaction/pi-tui-stream-renderer.d.ts.map +1 -0
- package/dist/interaction/pi-tui-stream-renderer.js +1509 -0
- package/dist/interaction/pi-tui-stream-renderer.js.map +1 -0
- package/dist/interaction/pi-tui-stream-renderer.test.d.ts +2 -0
- package/dist/interaction/pi-tui-stream-renderer.test.d.ts.map +1 -0
- package/dist/interaction/pi-tui-stream-renderer.test.js +1314 -0
- package/dist/interaction/pi-tui-stream-renderer.test.js.map +1 -0
- package/dist/interaction/spinner.d.ts +13 -0
- package/dist/interaction/spinner.d.ts.map +1 -0
- package/dist/interaction/spinner.js +51 -0
- package/dist/interaction/spinner.js.map +1 -0
- package/dist/middleware/index.d.ts +7 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +15 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/todo-continuation.d.ts +11 -0
- package/dist/middleware/todo-continuation.d.ts.map +1 -0
- package/dist/middleware/todo-continuation.js +103 -0
- package/dist/middleware/todo-continuation.js.map +1 -0
- package/dist/middleware/trim-leading-newlines.d.ts +3 -0
- package/dist/middleware/trim-leading-newlines.d.ts.map +1 -0
- package/dist/middleware/trim-leading-newlines.js +49 -0
- package/dist/middleware/trim-leading-newlines.js.map +1 -0
- package/dist/reasoning-mode.d.ts +5 -0
- package/dist/reasoning-mode.d.ts.map +1 -0
- package/dist/reasoning-mode.js +30 -0
- package/dist/reasoning-mode.js.map +1 -0
- package/dist/reasoning-mode.test.d.ts +2 -0
- package/dist/reasoning-mode.test.d.ts.map +1 -0
- package/dist/reasoning-mode.test.js +22 -0
- package/dist/reasoning-mode.test.js.map +1 -0
- package/dist/tool-fallback-mode.d.ts +6 -0
- package/dist/tool-fallback-mode.d.ts.map +1 -0
- package/dist/tool-fallback-mode.js +25 -0
- package/dist/tool-fallback-mode.js.map +1 -0
- package/dist/tools/execute/shell-execute.d.ts +17 -0
- package/dist/tools/execute/shell-execute.d.ts.map +1 -0
- package/dist/tools/execute/shell-execute.js +55 -0
- package/dist/tools/execute/shell-execute.js.map +1 -0
- package/dist/tools/execute/shell-execute.test.d.ts +2 -0
- package/dist/tools/execute/shell-execute.test.d.ts.map +1 -0
- package/dist/tools/execute/shell-execute.test.js +86 -0
- package/dist/tools/execute/shell-execute.test.js.map +1 -0
- package/dist/tools/execute/shell-interact.d.ts +10 -0
- package/dist/tools/execute/shell-interact.d.ts.map +1 -0
- package/dist/tools/execute/shell-interact.js +122 -0
- package/dist/tools/execute/shell-interact.js.map +1 -0
- package/dist/tools/execute/shell-interact.test.d.ts +2 -0
- package/dist/tools/execute/shell-interact.test.d.ts.map +1 -0
- package/dist/tools/execute/shell-interact.test.js +175 -0
- package/dist/tools/execute/shell-interact.test.js.map +1 -0
- package/dist/tools/explore/glob.d.ts +15 -0
- package/dist/tools/explore/glob.d.ts.map +1 -0
- package/dist/tools/explore/glob.js +107 -0
- package/dist/tools/explore/glob.js.map +1 -0
- package/dist/tools/explore/glob.test.d.ts +2 -0
- package/dist/tools/explore/glob.test.d.ts.map +1 -0
- package/dist/tools/explore/glob.test.js +183 -0
- package/dist/tools/explore/glob.test.js.map +1 -0
- package/dist/tools/explore/grep.d.ts +27 -0
- package/dist/tools/explore/grep.d.ts.map +1 -0
- package/dist/tools/explore/grep.js +203 -0
- package/dist/tools/explore/grep.js.map +1 -0
- package/dist/tools/explore/grep.test.d.ts +2 -0
- package/dist/tools/explore/grep.test.d.ts.map +1 -0
- package/dist/tools/explore/grep.test.js +132 -0
- package/dist/tools/explore/grep.test.js.map +1 -0
- package/dist/tools/explore/read-file.d.ts +23 -0
- package/dist/tools/explore/read-file.d.ts.map +1 -0
- package/dist/tools/explore/read-file.js +84 -0
- package/dist/tools/explore/read-file.js.map +1 -0
- package/dist/tools/explore/read-file.test.d.ts +2 -0
- package/dist/tools/explore/read-file.test.d.ts.map +1 -0
- package/dist/tools/explore/read-file.test.js +278 -0
- package/dist/tools/explore/read-file.test.js.map +1 -0
- package/dist/tools/index.d.ts +71 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +26 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/modify/delete-file.d.ts +19 -0
- package/dist/tools/modify/delete-file.d.ts.map +1 -0
- package/dist/tools/modify/delete-file.js +71 -0
- package/dist/tools/modify/delete-file.js.map +1 -0
- package/dist/tools/modify/delete-file.test.d.ts +2 -0
- package/dist/tools/modify/delete-file.test.d.ts.map +1 -0
- package/dist/tools/modify/delete-file.test.js +136 -0
- package/dist/tools/modify/delete-file.test.js.map +1 -0
- package/dist/tools/modify/edit-file-diagnostics.d.ts +17 -0
- package/dist/tools/modify/edit-file-diagnostics.d.ts.map +1 -0
- package/dist/tools/modify/edit-file-diagnostics.js +157 -0
- package/dist/tools/modify/edit-file-diagnostics.js.map +1 -0
- package/dist/tools/modify/edit-file-repair.d.ts +13 -0
- package/dist/tools/modify/edit-file-repair.d.ts.map +1 -0
- package/dist/tools/modify/edit-file-repair.js +135 -0
- package/dist/tools/modify/edit-file-repair.js.map +1 -0
- package/dist/tools/modify/edit-file-stress.test.d.ts +2 -0
- package/dist/tools/modify/edit-file-stress.test.d.ts.map +1 -0
- package/dist/tools/modify/edit-file-stress.test.js +163 -0
- package/dist/tools/modify/edit-file-stress.test.js.map +1 -0
- package/dist/tools/modify/edit-file-validation.d.ts +9 -0
- package/dist/tools/modify/edit-file-validation.d.ts.map +1 -0
- package/dist/tools/modify/edit-file-validation.js +86 -0
- package/dist/tools/modify/edit-file-validation.js.map +1 -0
- package/dist/tools/modify/edit-file-whitespace.test.d.ts +2 -0
- package/dist/tools/modify/edit-file-whitespace.test.d.ts.map +1 -0
- package/dist/tools/modify/edit-file-whitespace.test.js +90 -0
- package/dist/tools/modify/edit-file-whitespace.test.js.map +1 -0
- package/dist/tools/modify/edit-file.d.ts +33 -0
- package/dist/tools/modify/edit-file.d.ts.map +1 -0
- package/dist/tools/modify/edit-file.js +177 -0
- package/dist/tools/modify/edit-file.js.map +1 -0
- package/dist/tools/modify/edit-file.test.d.ts +2 -0
- package/dist/tools/modify/edit-file.test.d.ts.map +1 -0
- package/dist/tools/modify/edit-file.test.js +948 -0
- package/dist/tools/modify/edit-file.test.js.map +1 -0
- package/dist/tools/modify/write-file.d.ts +17 -0
- package/dist/tools/modify/write-file.d.ts.map +1 -0
- package/dist/tools/modify/write-file.js +39 -0
- package/dist/tools/modify/write-file.js.map +1 -0
- package/dist/tools/modify/write-file.test.d.ts +2 -0
- package/dist/tools/modify/write-file.test.d.ts.map +1 -0
- package/dist/tools/modify/write-file.test.js +168 -0
- package/dist/tools/modify/write-file.test.js.map +1 -0
- package/dist/tools/planning/load-skill.d.ts +13 -0
- package/dist/tools/planning/load-skill.d.ts.map +1 -0
- package/dist/tools/planning/load-skill.js +101 -0
- package/dist/tools/planning/load-skill.js.map +1 -0
- package/dist/tools/planning/load-skill.test.d.ts +2 -0
- package/dist/tools/planning/load-skill.test.d.ts.map +1 -0
- package/dist/tools/planning/load-skill.test.js +37 -0
- package/dist/tools/planning/load-skill.test.js.map +1 -0
- package/dist/tools/planning/todo-write.d.ts +49 -0
- package/dist/tools/planning/todo-write.d.ts.map +1 -0
- package/dist/tools/planning/todo-write.js +118 -0
- package/dist/tools/planning/todo-write.js.map +1 -0
- package/dist/tools/planning/todo-write.test.d.ts +2 -0
- package/dist/tools/planning/todo-write.test.d.ts.map +1 -0
- package/dist/tools/planning/todo-write.test.js +82 -0
- package/dist/tools/planning/todo-write.test.js.map +1 -0
- package/dist/tools/utils/execute/format-utils.d.ts +4 -0
- package/dist/tools/utils/execute/format-utils.d.ts.map +1 -0
- package/dist/tools/utils/execute/format-utils.js +31 -0
- package/dist/tools/utils/execute/format-utils.js.map +1 -0
- package/dist/tools/utils/execute/format-utils.test.d.ts +2 -0
- package/dist/tools/utils/execute/format-utils.test.d.ts.map +1 -0
- package/dist/tools/utils/execute/format-utils.test.js +40 -0
- package/dist/tools/utils/execute/format-utils.test.js.map +1 -0
- package/dist/tools/utils/execute/noninteractive-wrapper.d.ts +12 -0
- package/dist/tools/utils/execute/noninteractive-wrapper.d.ts.map +1 -0
- package/dist/tools/utils/execute/noninteractive-wrapper.js +269 -0
- package/dist/tools/utils/execute/noninteractive-wrapper.js.map +1 -0
- package/dist/tools/utils/execute/noninteractive-wrapper.test.d.ts +2 -0
- package/dist/tools/utils/execute/noninteractive-wrapper.test.d.ts.map +1 -0
- package/dist/tools/utils/execute/noninteractive-wrapper.test.js +233 -0
- package/dist/tools/utils/execute/noninteractive-wrapper.test.js.map +1 -0
- package/dist/tools/utils/execute/output-handler.d.ts +14 -0
- package/dist/tools/utils/execute/output-handler.d.ts.map +1 -0
- package/dist/tools/utils/execute/output-handler.js +71 -0
- package/dist/tools/utils/execute/output-handler.js.map +1 -0
- package/dist/tools/utils/execute/output-handler.test.d.ts +2 -0
- package/dist/tools/utils/execute/output-handler.test.d.ts.map +1 -0
- package/dist/tools/utils/execute/output-handler.test.js +58 -0
- package/dist/tools/utils/execute/output-handler.test.js.map +1 -0
- package/dist/tools/utils/execute/process-manager.d.ts +17 -0
- package/dist/tools/utils/execute/process-manager.d.ts.map +1 -0
- package/dist/tools/utils/execute/process-manager.js +229 -0
- package/dist/tools/utils/execute/process-manager.js.map +1 -0
- package/dist/tools/utils/execute/process-manager.test.d.ts +2 -0
- package/dist/tools/utils/execute/process-manager.test.d.ts.map +1 -0
- package/dist/tools/utils/execute/process-manager.test.js +139 -0
- package/dist/tools/utils/execute/process-manager.test.js.map +1 -0
- package/dist/tools/utils/execute/shell-detection.d.ts +3 -0
- package/dist/tools/utils/execute/shell-detection.d.ts.map +1 -0
- package/dist/tools/utils/execute/shell-detection.js +56 -0
- package/dist/tools/utils/execute/shell-detection.js.map +1 -0
- package/dist/tools/utils/execute/shell-detection.test.d.ts +2 -0
- package/dist/tools/utils/execute/shell-detection.test.d.ts.map +1 -0
- package/dist/tools/utils/execute/shell-detection.test.js +86 -0
- package/dist/tools/utils/execute/shell-detection.test.js.map +1 -0
- package/dist/tools/utils/hashline/autocorrect-replacement-lines.d.ts +5 -0
- package/dist/tools/utils/hashline/autocorrect-replacement-lines.d.ts.map +1 -0
- package/dist/tools/utils/hashline/autocorrect-replacement-lines.js +113 -0
- package/dist/tools/utils/hashline/autocorrect-replacement-lines.js.map +1 -0
- package/dist/tools/utils/hashline/constants.d.ts +5 -0
- package/dist/tools/utils/hashline/constants.d.ts.map +1 -0
- package/dist/tools/utils/hashline/constants.js +11 -0
- package/dist/tools/utils/hashline/constants.js.map +1 -0
- package/dist/tools/utils/hashline/diff-utils.d.ts +7 -0
- package/dist/tools/utils/hashline/diff-utils.d.ts.map +1 -0
- package/dist/tools/utils/hashline/diff-utils.js +50 -0
- package/dist/tools/utils/hashline/diff-utils.js.map +1 -0
- package/dist/tools/utils/hashline/diff-utils.test.d.ts +2 -0
- package/dist/tools/utils/hashline/diff-utils.test.d.ts.map +1 -0
- package/dist/tools/utils/hashline/diff-utils.test.js +46 -0
- package/dist/tools/utils/hashline/diff-utils.test.js.map +1 -0
- package/dist/tools/utils/hashline/edit-deduplication.d.ts +6 -0
- package/dist/tools/utils/hashline/edit-deduplication.d.ts.map +1 -0
- package/dist/tools/utils/hashline/edit-deduplication.js +32 -0
- package/dist/tools/utils/hashline/edit-deduplication.js.map +1 -0
- package/dist/tools/utils/hashline/edit-operation-primitives.d.ts +11 -0
- package/dist/tools/utils/hashline/edit-operation-primitives.d.ts.map +1 -0
- package/dist/tools/utils/hashline/edit-operation-primitives.js +93 -0
- package/dist/tools/utils/hashline/edit-operation-primitives.js.map +1 -0
- package/dist/tools/utils/hashline/edit-operations.d.ts +9 -0
- package/dist/tools/utils/hashline/edit-operations.d.ts.map +1 -0
- package/dist/tools/utils/hashline/edit-operations.js +101 -0
- package/dist/tools/utils/hashline/edit-operations.js.map +1 -0
- package/dist/tools/utils/hashline/edit-operations.test.d.ts +2 -0
- package/dist/tools/utils/hashline/edit-operations.test.d.ts.map +1 -0
- package/dist/tools/utils/hashline/edit-operations.test.js +135 -0
- package/dist/tools/utils/hashline/edit-operations.test.js.map +1 -0
- package/dist/tools/utils/hashline/edit-ordering.d.ts +5 -0
- package/dist/tools/utils/hashline/edit-ordering.d.ts.map +1 -0
- package/dist/tools/utils/hashline/edit-ordering.js +54 -0
- package/dist/tools/utils/hashline/edit-ordering.js.map +1 -0
- package/dist/tools/utils/hashline/edit-text-normalization.d.ts +9 -0
- package/dist/tools/utils/hashline/edit-text-normalization.d.ts.map +1 -0
- package/dist/tools/utils/hashline/edit-text-normalization.js +135 -0
- package/dist/tools/utils/hashline/edit-text-normalization.js.map +1 -0
- package/dist/tools/utils/hashline/file-text-canonicalization.d.ts +8 -0
- package/dist/tools/utils/hashline/file-text-canonicalization.d.ts.map +1 -0
- package/dist/tools/utils/hashline/file-text-canonicalization.js +42 -0
- package/dist/tools/utils/hashline/file-text-canonicalization.js.map +1 -0
- package/dist/tools/utils/hashline/hash-computation.d.ts +14 -0
- package/dist/tools/utils/hashline/hash-computation.d.ts.map +1 -0
- package/dist/tools/utils/hashline/hash-computation.js +158 -0
- package/dist/tools/utils/hashline/hash-computation.js.map +1 -0
- package/dist/tools/utils/hashline/hash-computation.test.d.ts +2 -0
- package/dist/tools/utils/hashline/hash-computation.test.d.ts.map +1 -0
- package/dist/tools/utils/hashline/hash-computation.test.js +63 -0
- package/dist/tools/utils/hashline/hash-computation.test.js.map +1 -0
- package/dist/tools/utils/hashline/hashline-chunk-formatter.d.ts +11 -0
- package/dist/tools/utils/hashline/hashline-chunk-formatter.d.ts.map +1 -0
- package/dist/tools/utils/hashline/hashline-chunk-formatter.js +41 -0
- package/dist/tools/utils/hashline/hashline-chunk-formatter.js.map +1 -0
- package/dist/tools/utils/hashline/hashline-edit-diff.d.ts +2 -0
- package/dist/tools/utils/hashline/hashline-edit-diff.d.ts.map +1 -0
- package/dist/tools/utils/hashline/hashline-edit-diff.js +27 -0
- package/dist/tools/utils/hashline/hashline-edit-diff.js.map +1 -0
- package/dist/tools/utils/hashline/index.d.ts +14 -0
- package/dist/tools/utils/hashline/index.d.ts.map +1 -0
- package/dist/tools/utils/hashline/index.js +11 -0
- package/dist/tools/utils/hashline/index.js.map +1 -0
- package/dist/tools/utils/hashline/merge-expansion.d.ts +4 -0
- package/dist/tools/utils/hashline/merge-expansion.d.ts.map +1 -0
- package/dist/tools/utils/hashline/merge-expansion.js +105 -0
- package/dist/tools/utils/hashline/merge-expansion.js.map +1 -0
- package/dist/tools/utils/hashline/normalize-edits.d.ts +11 -0
- package/dist/tools/utils/hashline/normalize-edits.d.ts.map +1 -0
- package/dist/tools/utils/hashline/normalize-edits.js +81 -0
- package/dist/tools/utils/hashline/normalize-edits.js.map +1 -0
- package/dist/tools/utils/hashline/types.d.ts +18 -0
- package/dist/tools/utils/hashline/types.d.ts.map +1 -0
- package/dist/tools/utils/hashline/types.js +2 -0
- package/dist/tools/utils/hashline/types.js.map +1 -0
- package/dist/tools/utils/hashline/validation.d.ts +19 -0
- package/dist/tools/utils/hashline/validation.d.ts.map +1 -0
- package/dist/tools/utils/hashline/validation.js +161 -0
- package/dist/tools/utils/hashline/validation.js.map +1 -0
- package/dist/tools/utils/hashline/validation.test.d.ts +2 -0
- package/dist/tools/utils/hashline/validation.test.d.ts.map +1 -0
- package/dist/tools/utils/hashline/validation.test.js +86 -0
- package/dist/tools/utils/hashline/validation.test.js.map +1 -0
- package/dist/tools/utils/safety-utils.d.ts +66 -0
- package/dist/tools/utils/safety-utils.d.ts.map +1 -0
- package/dist/tools/utils/safety-utils.js +681 -0
- package/dist/tools/utils/safety-utils.js.map +1 -0
- package/dist/utils/tools-manager.d.ts +16 -0
- package/dist/utils/tools-manager.d.ts.map +1 -0
- package/dist/utils/tools-manager.js +257 -0
- package/dist/utils/tools-manager.js.map +1 -0
- package/package.json +49 -0
- package/src/AGENTS.md +52 -0
- package/src/agent-reasoning-default.test.ts +48 -0
- package/src/agent.test.ts +49 -0
- package/src/agent.ts +455 -0
- package/src/cli-args.test.ts +152 -0
- package/src/cli-args.ts +90 -0
- package/src/commands/aliases-and-tool-fallback.test.ts +172 -0
- package/src/commands/clear.ts +14 -0
- package/src/commands/factories/create-toggle-command.ts +68 -0
- package/src/commands/help.ts +30 -0
- package/src/commands/index.ts +125 -0
- package/src/commands/model.ts +146 -0
- package/src/commands/reasoning-mode.ts +55 -0
- package/src/commands/render.test.ts +47 -0
- package/src/commands/render.ts +205 -0
- package/src/commands/tool-fallback.ts +47 -0
- package/src/commands/translate.test.ts +62 -0
- package/src/commands/translate.ts +14 -0
- package/src/commands/types.ts +18 -0
- package/src/context/environment-context.ts +11 -0
- package/src/context/paths.ts +2 -0
- package/src/context/session.ts +18 -0
- package/src/context/skill-command-prefix.ts +18 -0
- package/src/context/skills-integration.test.ts +113 -0
- package/src/context/skills.test.ts +25 -0
- package/src/context/skills.ts +566 -0
- package/src/context/system-prompt.ts +100 -0
- package/src/context/translation-integration.test.ts +194 -0
- package/src/context/translation.test.ts +186 -0
- package/src/context/translation.ts +122 -0
- package/src/entrypoints/AGENTS.md +33 -0
- package/src/entrypoints/cli-input-rendering.test.ts +236 -0
- package/src/entrypoints/cli.ts +1845 -0
- package/src/entrypoints/headless-agent-config.test.ts +82 -0
- package/src/entrypoints/headless-agent-config.ts +42 -0
- package/src/entrypoints/headless.ts +622 -0
- package/src/env.ts +14 -0
- package/src/friendli-models.ts +81 -0
- package/src/friendli-reasoning.test.ts +147 -0
- package/src/friendli-reasoning.ts +280 -0
- package/src/index.ts +3 -0
- package/src/interaction/colors.ts +24 -0
- package/src/interaction/pi-tui-stream-renderer.test.ts +1471 -0
- package/src/interaction/pi-tui-stream-renderer.ts +2150 -0
- package/src/interaction/spinner.ts +61 -0
- package/src/middleware/index.ts +32 -0
- package/src/middleware/todo-continuation.ts +128 -0
- package/src/middleware/trim-leading-newlines.ts +66 -0
- package/src/reasoning-mode.test.ts +24 -0
- package/src/reasoning-mode.ts +40 -0
- package/src/skills/example/SKILL.md +44 -0
- package/src/skills/example/references/api.md +37 -0
- package/src/skills/example/scripts/setup.sh +13 -0
- package/src/skills/git-workflow.md +405 -0
- package/src/tool-fallback-mode.ts +34 -0
- package/src/tools/AGENTS.md +44 -0
- package/src/tools/execute/shell-execute.test.ts +114 -0
- package/src/tools/execute/shell-execute.ts +74 -0
- package/src/tools/execute/shell-execute.txt +27 -0
- package/src/tools/execute/shell-interact.test.ts +236 -0
- package/src/tools/execute/shell-interact.ts +151 -0
- package/src/tools/execute/shell-interact.txt +15 -0
- package/src/tools/explore/glob-files.txt +8 -0
- package/src/tools/explore/glob.test.ts +217 -0
- package/src/tools/explore/glob.ts +137 -0
- package/src/tools/explore/grep-files.txt +12 -0
- package/src/tools/explore/grep.test.ts +183 -0
- package/src/tools/explore/grep.ts +266 -0
- package/src/tools/explore/read-file.test.ts +355 -0
- package/src/tools/explore/read-file.ts +102 -0
- package/src/tools/explore/read-file.txt +24 -0
- package/src/tools/index.ts +29 -0
- package/src/tools/modify/AGENTS.md +38 -0
- package/src/tools/modify/delete-file.test.ts +200 -0
- package/src/tools/modify/delete-file.ts +95 -0
- package/src/tools/modify/delete-file.txt +9 -0
- package/src/tools/modify/edit-file-diagnostics.ts +210 -0
- package/src/tools/modify/edit-file-repair.ts +183 -0
- package/src/tools/modify/edit-file-stress.test.ts +200 -0
- package/src/tools/modify/edit-file-validation.ts +134 -0
- package/src/tools/modify/edit-file-whitespace.test.ts +117 -0
- package/src/tools/modify/edit-file.test.ts +1231 -0
- package/src/tools/modify/edit-file.ts +252 -0
- package/src/tools/modify/edit-file.txt +73 -0
- package/src/tools/modify/write-file.test.ts +240 -0
- package/src/tools/modify/write-file.ts +56 -0
- package/src/tools/modify/write-file.txt +9 -0
- package/src/tools/planning/load-skill.test.ts +48 -0
- package/src/tools/planning/load-skill.ts +136 -0
- package/src/tools/planning/load-skill.txt +6 -0
- package/src/tools/planning/todo-write.test.ts +91 -0
- package/src/tools/planning/todo-write.ts +141 -0
- package/src/tools/planning/todo-write.txt +7 -0
- package/src/tools/utils/execute/format-utils.test.ts +53 -0
- package/src/tools/utils/execute/format-utils.ts +37 -0
- package/src/tools/utils/execute/noninteractive-wrapper.test.ts +306 -0
- package/src/tools/utils/execute/noninteractive-wrapper.ts +314 -0
- package/src/tools/utils/execute/output-handler.test.ts +72 -0
- package/src/tools/utils/execute/output-handler.ts +101 -0
- package/src/tools/utils/execute/process-manager.test.ts +175 -0
- package/src/tools/utils/execute/process-manager.ts +310 -0
- package/src/tools/utils/execute/shell-detection.test.ts +112 -0
- package/src/tools/utils/execute/shell-detection.ts +72 -0
- package/src/tools/utils/hashline/autocorrect-replacement-lines.ts +159 -0
- package/src/tools/utils/hashline/constants.ts +13 -0
- package/src/tools/utils/hashline/diff-utils.test.ts +61 -0
- package/src/tools/utils/hashline/diff-utils.ts +64 -0
- package/src/tools/utils/hashline/edit-deduplication.ts +40 -0
- package/src/tools/utils/hashline/edit-operation-primitives.ts +149 -0
- package/src/tools/utils/hashline/edit-operations.test.ts +154 -0
- package/src/tools/utils/hashline/edit-operations.ts +132 -0
- package/src/tools/utils/hashline/edit-ordering.ts +60 -0
- package/src/tools/utils/hashline/edit-text-normalization.ts +180 -0
- package/src/tools/utils/hashline/file-text-canonicalization.ts +58 -0
- package/src/tools/utils/hashline/hash-computation.test.ts +82 -0
- package/src/tools/utils/hashline/hash-computation.ts +199 -0
- package/src/tools/utils/hashline/hashline-chunk-formatter.ts +61 -0
- package/src/tools/utils/hashline/hashline-edit-diff.ts +35 -0
- package/src/tools/utils/hashline/index.ts +55 -0
- package/src/tools/utils/hashline/merge-expansion.ts +120 -0
- package/src/tools/utils/hashline/normalize-edits.ts +127 -0
- package/src/tools/utils/hashline/types.ts +20 -0
- package/src/tools/utils/hashline/validation.test.ts +109 -0
- package/src/tools/utils/hashline/validation.ts +212 -0
- package/src/tools/utils/safety-utils.ts +938 -0
- package/src/utils/tools-manager.ts +353 -0
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import type { Dirent } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
lstat,
|
|
5
|
+
open,
|
|
6
|
+
readdir,
|
|
7
|
+
readFile,
|
|
8
|
+
realpath,
|
|
9
|
+
rename,
|
|
10
|
+
stat,
|
|
11
|
+
unlink,
|
|
12
|
+
} from "node:fs/promises";
|
|
13
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
14
|
+
import ignore from "ignore";
|
|
15
|
+
import { computeFileHash, formatHashlineNumberedLines } from "./hashline";
|
|
16
|
+
|
|
17
|
+
const FILE_READ_POLICY = {
|
|
18
|
+
maxFileSizeBytes: 1024 * 1024, // 1MB
|
|
19
|
+
maxLinesPerRead: 2000,
|
|
20
|
+
binarySampleBytes: 4096,
|
|
21
|
+
nonPrintableThreshold: 0.3,
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
const LEADING_DOT_SLASH_PATTERN = /^\.\//;
|
|
25
|
+
const MULTIPLE_SLASH_PATTERN = /\/+/g;
|
|
26
|
+
const LINE_SPLIT_PATTERN = /\r?\n/;
|
|
27
|
+
const PATH_SEGMENT_SPLIT_PATTERN = /[\\/]/;
|
|
28
|
+
|
|
29
|
+
const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"] as const;
|
|
30
|
+
|
|
31
|
+
const DEFAULT_IGNORED_DIRECTORIES = [
|
|
32
|
+
"node_modules",
|
|
33
|
+
".git",
|
|
34
|
+
".svn",
|
|
35
|
+
".hg",
|
|
36
|
+
".idea",
|
|
37
|
+
".vscode",
|
|
38
|
+
".pnpm-store",
|
|
39
|
+
".npm",
|
|
40
|
+
".next",
|
|
41
|
+
".nuxt",
|
|
42
|
+
"dist",
|
|
43
|
+
"build",
|
|
44
|
+
"out",
|
|
45
|
+
"target",
|
|
46
|
+
"vendor",
|
|
47
|
+
"bin",
|
|
48
|
+
"obj",
|
|
49
|
+
"coverage",
|
|
50
|
+
"logs",
|
|
51
|
+
"tmp",
|
|
52
|
+
"temp",
|
|
53
|
+
".cache",
|
|
54
|
+
".turbo",
|
|
55
|
+
".vercel",
|
|
56
|
+
".output",
|
|
57
|
+
".gradle",
|
|
58
|
+
".history",
|
|
59
|
+
"__pycache__",
|
|
60
|
+
".venv",
|
|
61
|
+
"venv",
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const DEFAULT_IGNORED_FILE_PATTERNS = [
|
|
65
|
+
"*.pyc",
|
|
66
|
+
"*.swp",
|
|
67
|
+
"*.swo",
|
|
68
|
+
"*.log",
|
|
69
|
+
".env.local",
|
|
70
|
+
".env.*.local",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const DEFAULT_IGNORED_DIRECTORY_SET = new Set(DEFAULT_IGNORED_DIRECTORIES);
|
|
74
|
+
const IGNORE_FILE_NAME_SET = new Set<string>(IGNORE_FILE_NAMES);
|
|
75
|
+
|
|
76
|
+
export interface IgnoreFilter {
|
|
77
|
+
ignores: (pathFromBaseDir: string) => boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildDefaultIgnorePatterns(): string[] {
|
|
81
|
+
return [...DEFAULT_IGNORED_DIRECTORIES, ...DEFAULT_IGNORED_FILE_PATTERNS];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
|
|
85
|
+
return typeof error === "object" && error !== null && "code" in error;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toPosixPath(pathValue: string): string {
|
|
89
|
+
return pathValue.replaceAll("\\", "/");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizePathForIgnore(pathValue: string): string {
|
|
93
|
+
return toPosixPath(pathValue)
|
|
94
|
+
.replace(LEADING_DOT_SLASH_PATTERN, "")
|
|
95
|
+
.replace(MULTIPLE_SLASH_PATTERN, "/");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function shouldIgnoreReadError(error: unknown): boolean {
|
|
99
|
+
return (
|
|
100
|
+
isErrnoException(error) &&
|
|
101
|
+
(error.code === "ENOENT" || error.code === "EACCES")
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function prefixIgnorePattern(line: string, prefix: string): string[] {
|
|
106
|
+
const trimmed = line.trim();
|
|
107
|
+
if (trimmed.length === 0) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (trimmed.startsWith("#") && !trimmed.startsWith("\\#")) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let pattern = trimmed;
|
|
116
|
+
let negated = false;
|
|
117
|
+
|
|
118
|
+
if (pattern.startsWith("!")) {
|
|
119
|
+
negated = true;
|
|
120
|
+
pattern = pattern.slice(1);
|
|
121
|
+
} else if (pattern.startsWith("\\!")) {
|
|
122
|
+
pattern = pattern.slice(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (pattern.startsWith("/")) {
|
|
126
|
+
pattern = pattern.slice(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const prefixedPattern = prefix ? `${prefix}${pattern}` : pattern;
|
|
130
|
+
const patterns = [prefixedPattern];
|
|
131
|
+
|
|
132
|
+
if (prefix.length > 0 && !pattern.includes("/")) {
|
|
133
|
+
patterns.push(`${prefix}**/${pattern}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!negated) {
|
|
137
|
+
return patterns;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return patterns.map((candidatePattern) => `!${candidatePattern}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function readIgnoreFilePatterns(params: {
|
|
144
|
+
ignoreFilePath: string;
|
|
145
|
+
prefix: string;
|
|
146
|
+
}): Promise<string[]> {
|
|
147
|
+
const { ignoreFilePath, prefix } = params;
|
|
148
|
+
try {
|
|
149
|
+
const content = await readFile(ignoreFilePath, "utf-8");
|
|
150
|
+
return content
|
|
151
|
+
.split(LINE_SPLIT_PATTERN)
|
|
152
|
+
.flatMap((line) => prefixIgnorePattern(line, prefix));
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (shouldIgnoreReadError(error)) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function findGitRoot(startDir: string): Promise<string | null> {
|
|
162
|
+
let currentDir = resolve(startDir);
|
|
163
|
+
while (true) {
|
|
164
|
+
try {
|
|
165
|
+
await stat(join(currentDir, ".git"));
|
|
166
|
+
return currentDir;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (!(isErrnoException(error) && error.code === "ENOENT")) {
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const parentDir = dirname(currentDir);
|
|
174
|
+
if (parentDir === currentDir) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
currentDir = parentDir;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function buildDirectoryChain(rootDir: string, leafDir: string): string[] {
|
|
182
|
+
const resolvedRoot = resolve(rootDir);
|
|
183
|
+
const resolvedLeaf = resolve(leafDir);
|
|
184
|
+
const relativePath = relative(resolvedRoot, resolvedLeaf);
|
|
185
|
+
|
|
186
|
+
if (
|
|
187
|
+
relativePath.startsWith("..") ||
|
|
188
|
+
isAbsolute(relativePath) ||
|
|
189
|
+
relativePath.length === 0
|
|
190
|
+
) {
|
|
191
|
+
return [resolvedLeaf];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const segments = relativePath
|
|
195
|
+
.split(PATH_SEGMENT_SPLIT_PATTERN)
|
|
196
|
+
.filter((segment) => segment.length > 0);
|
|
197
|
+
const chain = [resolvedRoot];
|
|
198
|
+
let cursor = resolvedRoot;
|
|
199
|
+
|
|
200
|
+
for (const segment of segments) {
|
|
201
|
+
cursor = join(cursor, segment);
|
|
202
|
+
chain.push(cursor);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return chain;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getPathDepth(pathValue: string): number {
|
|
209
|
+
return resolve(pathValue)
|
|
210
|
+
.split(PATH_SEGMENT_SPLIT_PATTERN)
|
|
211
|
+
.filter((segment) => segment.length > 0).length;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function shouldSkipDirectoryTraversal(directoryName: string): boolean {
|
|
215
|
+
return DEFAULT_IGNORED_DIRECTORY_SET.has(directoryName);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function readDirectoryEntries(
|
|
219
|
+
pathValue: string
|
|
220
|
+
): Promise<Dirent[] | null> {
|
|
221
|
+
try {
|
|
222
|
+
return await readdir(pathValue, { withFileTypes: true });
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (shouldIgnoreReadError(error)) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function collectNestedIgnoreDirectories(
|
|
232
|
+
baseDir: string
|
|
233
|
+
): Promise<string[]> {
|
|
234
|
+
const resolvedBaseDir = resolve(baseDir);
|
|
235
|
+
const discovered = new Set<string>();
|
|
236
|
+
const stack = [resolvedBaseDir];
|
|
237
|
+
|
|
238
|
+
while (stack.length > 0) {
|
|
239
|
+
const currentDir = stack.pop();
|
|
240
|
+
if (!currentDir) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const entries = await readDirectoryEntries(currentDir);
|
|
245
|
+
if (!entries) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const hasIgnoreFile = entries.some((entry) => {
|
|
250
|
+
return entry.isFile() && IGNORE_FILE_NAME_SET.has(entry.name);
|
|
251
|
+
});
|
|
252
|
+
if (hasIgnoreFile) {
|
|
253
|
+
discovered.add(currentDir);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
if (!entry.isDirectory()) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (shouldSkipDirectoryTraversal(entry.name)) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
stack.push(join(currentDir, entry.name));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return Array.from(discovered);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function buildIgnoreMatcher(baseDir: string): Promise<{
|
|
273
|
+
basePrefixFromRoot: string;
|
|
274
|
+
matcher: ReturnType<typeof ignore>;
|
|
275
|
+
}> {
|
|
276
|
+
const resolvedBaseDir = resolve(baseDir);
|
|
277
|
+
const ignoreRootDir = (await findGitRoot(resolvedBaseDir)) ?? resolvedBaseDir;
|
|
278
|
+
|
|
279
|
+
const directories = new Set<string>(
|
|
280
|
+
buildDirectoryChain(ignoreRootDir, resolvedBaseDir)
|
|
281
|
+
);
|
|
282
|
+
const nestedDirectories =
|
|
283
|
+
await collectNestedIgnoreDirectories(resolvedBaseDir);
|
|
284
|
+
for (const dir of nestedDirectories) {
|
|
285
|
+
directories.add(dir);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const orderedDirectories = Array.from(directories).sort((left, right) => {
|
|
289
|
+
const depthDiff = getPathDepth(left) - getPathDepth(right);
|
|
290
|
+
if (depthDiff !== 0) {
|
|
291
|
+
return depthDiff;
|
|
292
|
+
}
|
|
293
|
+
return left.localeCompare(right);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const patterns: string[] = [];
|
|
297
|
+
for (const dir of orderedDirectories) {
|
|
298
|
+
const relativeDir = relative(ignoreRootDir, dir);
|
|
299
|
+
const normalizedPrefix =
|
|
300
|
+
relativeDir.length > 0 && relativeDir !== "."
|
|
301
|
+
? `${toPosixPath(relativeDir)}/`
|
|
302
|
+
: "";
|
|
303
|
+
|
|
304
|
+
for (const fileName of IGNORE_FILE_NAMES) {
|
|
305
|
+
const ignoreFilePath = join(dir, fileName);
|
|
306
|
+
const filePatterns = await readIgnoreFilePatterns({
|
|
307
|
+
ignoreFilePath,
|
|
308
|
+
prefix: normalizedPrefix,
|
|
309
|
+
});
|
|
310
|
+
patterns.push(...filePatterns);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const gitInfoExcludePatterns = await readIgnoreFilePatterns({
|
|
315
|
+
ignoreFilePath: join(ignoreRootDir, ".git", "info", "exclude"),
|
|
316
|
+
prefix: "",
|
|
317
|
+
});
|
|
318
|
+
patterns.push(...gitInfoExcludePatterns);
|
|
319
|
+
|
|
320
|
+
const matcher = ignore().add(buildDefaultIgnorePatterns()).add(patterns);
|
|
321
|
+
const baseRelativeFromRoot = relative(ignoreRootDir, resolvedBaseDir);
|
|
322
|
+
const basePrefixFromRoot =
|
|
323
|
+
baseRelativeFromRoot.length > 0 && baseRelativeFromRoot !== "."
|
|
324
|
+
? `${toPosixPath(baseRelativeFromRoot)}/`
|
|
325
|
+
: "";
|
|
326
|
+
|
|
327
|
+
return { basePrefixFromRoot, matcher };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export async function getIgnoreFilter(
|
|
331
|
+
baseDir = process.cwd()
|
|
332
|
+
): Promise<IgnoreFilter> {
|
|
333
|
+
const { basePrefixFromRoot, matcher } = await buildIgnoreMatcher(baseDir);
|
|
334
|
+
return {
|
|
335
|
+
ignores(pathFromBaseDir: string): boolean {
|
|
336
|
+
const normalizedPath = normalizePathForIgnore(pathFromBaseDir);
|
|
337
|
+
if (normalizedPath.length === 0) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const pathForMatcher = basePrefixFromRoot
|
|
342
|
+
? `${basePrefixFromRoot}${normalizedPath}`
|
|
343
|
+
: normalizedPath;
|
|
344
|
+
return matcher.ignores(pathForMatcher);
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function isLikelyBinaryFile(
|
|
350
|
+
filePath: string,
|
|
351
|
+
fileSize: number
|
|
352
|
+
): Promise<boolean> {
|
|
353
|
+
if (fileSize === 0) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const sampleSize = Math.min(FILE_READ_POLICY.binarySampleBytes, fileSize);
|
|
358
|
+
const handle = await open(filePath, "r");
|
|
359
|
+
try {
|
|
360
|
+
const bytes = Buffer.alloc(sampleSize);
|
|
361
|
+
const result = await handle.read(bytes, 0, sampleSize, 0);
|
|
362
|
+
if (result.bytesRead === 0) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let nonPrintableCount = 0;
|
|
367
|
+
for (let i = 0; i < result.bytesRead; i++) {
|
|
368
|
+
const value = bytes[i];
|
|
369
|
+
if (value === 0) {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (value < 9 || (value > 13 && value < 32)) {
|
|
374
|
+
nonPrintableCount++;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
nonPrintableCount / result.bytesRead >
|
|
380
|
+
FILE_READ_POLICY.nonPrintableThreshold
|
|
381
|
+
);
|
|
382
|
+
} finally {
|
|
383
|
+
await handle.close();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
interface FileCheckResult {
|
|
388
|
+
allowed: boolean;
|
|
389
|
+
lastModified?: string;
|
|
390
|
+
reason?: string;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
interface FileReadGuardContext {
|
|
394
|
+
filePath: string;
|
|
395
|
+
ig: IgnoreFilter;
|
|
396
|
+
lastModified: string;
|
|
397
|
+
pathForIgnoreCheck: string | null;
|
|
398
|
+
resolvedFilePath: string;
|
|
399
|
+
respectGitIgnore: boolean;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
type FileReadGuard = (
|
|
403
|
+
context: FileReadGuardContext
|
|
404
|
+
) => FileCheckResult | null | Promise<FileCheckResult | null>;
|
|
405
|
+
|
|
406
|
+
function getPathForIgnoreCheck(
|
|
407
|
+
filePath: string,
|
|
408
|
+
baseDir: string
|
|
409
|
+
): string | null {
|
|
410
|
+
const absolutePath = isAbsolute(filePath)
|
|
411
|
+
? filePath
|
|
412
|
+
: join(baseDir, filePath);
|
|
413
|
+
const relativePath = relative(baseDir, absolutePath);
|
|
414
|
+
const isInsideBaseDir = !(
|
|
415
|
+
relativePath.startsWith("..") || isAbsolute(relativePath)
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
if (!isInsideBaseDir) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return normalizePathForIgnore(relativePath);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const checkIgnoreGuard: FileReadGuard = ({
|
|
426
|
+
filePath,
|
|
427
|
+
ig,
|
|
428
|
+
pathForIgnoreCheck,
|
|
429
|
+
respectGitIgnore,
|
|
430
|
+
}) => {
|
|
431
|
+
if (!respectGitIgnore) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!(pathForIgnoreCheck && ig.ignores(pathForIgnoreCheck))) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
allowed: false,
|
|
441
|
+
reason: `File '${filePath}' is excluded by ignore rules (.gitignore/.ignore/.fdignore or fallback safety patterns). Set respect_git_ignore: false to bypass for this read.`,
|
|
442
|
+
};
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const checkFileStatGuards: FileReadGuard = async (context) => {
|
|
446
|
+
const { filePath, resolvedFilePath } = context;
|
|
447
|
+
try {
|
|
448
|
+
const stats = await stat(resolvedFilePath);
|
|
449
|
+
context.lastModified = stats.mtime.toISOString();
|
|
450
|
+
|
|
451
|
+
if (stats.size > FILE_READ_POLICY.maxFileSizeBytes) {
|
|
452
|
+
return {
|
|
453
|
+
allowed: false,
|
|
454
|
+
reason: `File too large (${Math.round(stats.size / 1024)}KB > ${FILE_READ_POLICY.maxFileSizeBytes / 1024}KB): ${filePath}`,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (await isLikelyBinaryFile(resolvedFilePath, stats.size)) {
|
|
459
|
+
return {
|
|
460
|
+
allowed: false,
|
|
461
|
+
reason: `File '${filePath}' is binary. read_file only supports text files. Use appropriate tools for binary content.`,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (
|
|
466
|
+
typeof error === "object" &&
|
|
467
|
+
error !== null &&
|
|
468
|
+
"code" in error &&
|
|
469
|
+
(error as NodeJS.ErrnoException).code !== "ENOENT"
|
|
470
|
+
) {
|
|
471
|
+
return {
|
|
472
|
+
allowed: false,
|
|
473
|
+
reason: `Unable to inspect file metadata for '${filePath}'.`,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return null;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const FILE_READ_GUARDS: FileReadGuard[] = [
|
|
482
|
+
checkIgnoreGuard,
|
|
483
|
+
checkFileStatGuards,
|
|
484
|
+
];
|
|
485
|
+
|
|
486
|
+
async function checkFileReadable(
|
|
487
|
+
filePath: string,
|
|
488
|
+
options?: {
|
|
489
|
+
respectGitIgnore?: boolean;
|
|
490
|
+
}
|
|
491
|
+
): Promise<FileCheckResult> {
|
|
492
|
+
const cwd = process.cwd();
|
|
493
|
+
const resolvedFilePath = isAbsolute(filePath)
|
|
494
|
+
? filePath
|
|
495
|
+
: resolve(cwd, filePath);
|
|
496
|
+
const insideCwd = getPathForIgnoreCheck(resolvedFilePath, cwd) !== null;
|
|
497
|
+
const baseDir = insideCwd ? cwd : dirname(resolvedFilePath);
|
|
498
|
+
|
|
499
|
+
const ig = await getIgnoreFilter(baseDir);
|
|
500
|
+
const context: FileReadGuardContext = {
|
|
501
|
+
filePath,
|
|
502
|
+
resolvedFilePath,
|
|
503
|
+
ig,
|
|
504
|
+
lastModified: "unknown",
|
|
505
|
+
pathForIgnoreCheck: getPathForIgnoreCheck(resolvedFilePath, baseDir),
|
|
506
|
+
respectGitIgnore: options?.respectGitIgnore ?? true,
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
for (const guard of FILE_READ_GUARDS) {
|
|
510
|
+
const result = await guard(context);
|
|
511
|
+
if (result) {
|
|
512
|
+
return result;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return { allowed: true, lastModified: context.lastModified };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export interface ReadFileOptions {
|
|
520
|
+
limit?: number;
|
|
521
|
+
offset?: number;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function formatNumberedLines(
|
|
525
|
+
lines: string[],
|
|
526
|
+
startLine1: number
|
|
527
|
+
): string {
|
|
528
|
+
return formatHashlineNumberedLines(lines, startLine1);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function formatBlock(title: string, body: string): string {
|
|
532
|
+
return `======== ${title} ========\n${body}\n======== end ========`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export interface LineWindow {
|
|
536
|
+
endLine1: number;
|
|
537
|
+
startLine1: number;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function computeLineWindow(params: {
|
|
541
|
+
aroundLine1: number;
|
|
542
|
+
before: number;
|
|
543
|
+
after: number;
|
|
544
|
+
totalLines: number;
|
|
545
|
+
}): LineWindow {
|
|
546
|
+
const { aroundLine1, before, after, totalLines } = params;
|
|
547
|
+
const startLine1 = Math.max(1, aroundLine1 - before);
|
|
548
|
+
const endLine1 = Math.min(totalLines, aroundLine1 + after);
|
|
549
|
+
return { startLine1, endLine1 };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export interface ReadFileResultEnhanced {
|
|
553
|
+
bytes: number;
|
|
554
|
+
content: string;
|
|
555
|
+
endLine1: number;
|
|
556
|
+
fileHash: string;
|
|
557
|
+
lastModified: string;
|
|
558
|
+
numberedContent: string;
|
|
559
|
+
startLine1: number;
|
|
560
|
+
totalLines: number;
|
|
561
|
+
truncated: boolean;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export async function safeReadFileEnhanced(
|
|
565
|
+
path: string,
|
|
566
|
+
options?: ReadFileOptions & {
|
|
567
|
+
around_line?: number;
|
|
568
|
+
before?: number;
|
|
569
|
+
after?: number;
|
|
570
|
+
respect_git_ignore?: boolean;
|
|
571
|
+
}
|
|
572
|
+
): Promise<ReadFileResultEnhanced> {
|
|
573
|
+
const check = await checkFileReadable(path, {
|
|
574
|
+
respectGitIgnore: options?.respect_git_ignore,
|
|
575
|
+
});
|
|
576
|
+
if (!check.allowed) {
|
|
577
|
+
throw new Error(check.reason);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const rawContent = await readFile(path, "utf-8");
|
|
581
|
+
const allLines = rawContent.split("\n");
|
|
582
|
+
const totalLines = allLines.length;
|
|
583
|
+
const bytes = Buffer.byteLength(rawContent, "utf-8");
|
|
584
|
+
|
|
585
|
+
let startLine1: number;
|
|
586
|
+
let endLine1: number;
|
|
587
|
+
|
|
588
|
+
if (options?.around_line !== undefined) {
|
|
589
|
+
const before = Math.max(options.before ?? 5, 0);
|
|
590
|
+
const after = Math.max(options.after ?? 10, 0);
|
|
591
|
+
const clampedAroundLine = Math.min(
|
|
592
|
+
Math.max(options.around_line, 1),
|
|
593
|
+
totalLines
|
|
594
|
+
);
|
|
595
|
+
const window = computeLineWindow({
|
|
596
|
+
aroundLine1: clampedAroundLine,
|
|
597
|
+
before,
|
|
598
|
+
after,
|
|
599
|
+
totalLines,
|
|
600
|
+
});
|
|
601
|
+
startLine1 = window.startLine1;
|
|
602
|
+
endLine1 = window.endLine1;
|
|
603
|
+
} else {
|
|
604
|
+
const offset = Math.max(options?.offset ?? 0, 0);
|
|
605
|
+
const limit = Math.max(
|
|
606
|
+
options?.limit ?? FILE_READ_POLICY.maxLinesPerRead,
|
|
607
|
+
1
|
|
608
|
+
);
|
|
609
|
+
startLine1 = Math.min(offset + 1, totalLines);
|
|
610
|
+
endLine1 = Math.min(offset + limit, totalLines);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const selectedLines = allLines.slice(startLine1 - 1, endLine1);
|
|
614
|
+
const truncated = endLine1 < totalLines || startLine1 > 1;
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
content: selectedLines.join("\n"),
|
|
618
|
+
numberedContent: formatNumberedLines(selectedLines, startLine1),
|
|
619
|
+
totalLines,
|
|
620
|
+
startLine1,
|
|
621
|
+
endLine1,
|
|
622
|
+
truncated,
|
|
623
|
+
bytes,
|
|
624
|
+
fileHash: computeFileHash(rawContent),
|
|
625
|
+
lastModified: check.lastModified ?? "unknown",
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ─── Write Safety Guards (C-1, C-2, H-1) ───────────────────────────────
|
|
630
|
+
|
|
631
|
+
interface FileWriteGuardContext {
|
|
632
|
+
/** realpath()-based root, used for physical symlink checks (C-2). */
|
|
633
|
+
canonicalRoot: string;
|
|
634
|
+
filePath: string;
|
|
635
|
+
resolvedFilePath: string;
|
|
636
|
+
/** resolve()-based root, used for logical path containment (C-1). */
|
|
637
|
+
rootDir: string;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
type FileWriteGuard = (
|
|
641
|
+
context: FileWriteGuardContext
|
|
642
|
+
) => FileCheckResult | null | Promise<FileCheckResult | null>;
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* C-1: Path traversal guard.
|
|
646
|
+
* Resolves the input path and rejects it if it escapes the project root.
|
|
647
|
+
*/
|
|
648
|
+
const checkPathTraversalGuard: FileWriteGuard = ({
|
|
649
|
+
filePath,
|
|
650
|
+
resolvedFilePath,
|
|
651
|
+
rootDir,
|
|
652
|
+
}) => {
|
|
653
|
+
// After resolve(), all '..' segments are normalized
|
|
654
|
+
if (
|
|
655
|
+
resolvedFilePath !== rootDir &&
|
|
656
|
+
!resolvedFilePath.startsWith(rootDir + sep)
|
|
657
|
+
) {
|
|
658
|
+
return {
|
|
659
|
+
allowed: false,
|
|
660
|
+
reason:
|
|
661
|
+
`Path traversal blocked: '${filePath}' resolves to '${resolvedFilePath}' ` +
|
|
662
|
+
`which is outside the project root '${rootDir}'.`,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
return null;
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* C-2: Symlink guard.
|
|
670
|
+
* Uses lstat() (doesn't follow symlinks) to detect symlinks, then
|
|
671
|
+
* fs.realpath() to verify the target stays within the project root.
|
|
672
|
+
* For existing non-symlink files, realpath() catches intermediate
|
|
673
|
+
* directory symlinks that escape the project root.
|
|
674
|
+
* For non-existent files, walks ancestors to detect symlink escapes.
|
|
675
|
+
*/
|
|
676
|
+
const checkSymlinkGuard: FileWriteGuard = async ({
|
|
677
|
+
filePath,
|
|
678
|
+
resolvedFilePath,
|
|
679
|
+
canonicalRoot,
|
|
680
|
+
}) => {
|
|
681
|
+
try {
|
|
682
|
+
const stats = await lstat(resolvedFilePath);
|
|
683
|
+
if (stats.isSymbolicLink()) {
|
|
684
|
+
// Resolve where the symlink actually points
|
|
685
|
+
let realTarget: string;
|
|
686
|
+
try {
|
|
687
|
+
realTarget = await realpath(resolvedFilePath);
|
|
688
|
+
} catch {
|
|
689
|
+
return {
|
|
690
|
+
allowed: false,
|
|
691
|
+
reason: `Refusing to modify dangling symlink: '${filePath}'.`,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (
|
|
696
|
+
realTarget !== canonicalRoot &&
|
|
697
|
+
!realTarget.startsWith(canonicalRoot + sep)
|
|
698
|
+
) {
|
|
699
|
+
return {
|
|
700
|
+
allowed: false,
|
|
701
|
+
reason:
|
|
702
|
+
`Symlink escape blocked: '${filePath}' is a symlink pointing to ` +
|
|
703
|
+
`'${realTarget}' outside project root.`,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return {
|
|
708
|
+
allowed: false,
|
|
709
|
+
reason:
|
|
710
|
+
`Refusing to modify through symlink: '${filePath}'. ` +
|
|
711
|
+
`Use the real path '${realTarget}' instead.`,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Existing non-symlink file: verify realpath stays within root.
|
|
716
|
+
// This catches intermediate directory symlinks (e.g., root/link→/etc,
|
|
717
|
+
// where lstat(root/link/file) sees a regular file but the real location
|
|
718
|
+
// is /etc/file which is outside the project root).
|
|
719
|
+
const realPath = await realpath(resolvedFilePath);
|
|
720
|
+
if (
|
|
721
|
+
realPath !== canonicalRoot &&
|
|
722
|
+
!realPath.startsWith(canonicalRoot + sep)
|
|
723
|
+
) {
|
|
724
|
+
return {
|
|
725
|
+
allowed: false,
|
|
726
|
+
reason:
|
|
727
|
+
`Symlink escape blocked: '${filePath}' resolves via intermediate ` +
|
|
728
|
+
`symlink to '${realPath}' outside project root.`,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
} catch (error) {
|
|
732
|
+
// Re-throw symlink-specific errors
|
|
733
|
+
if (
|
|
734
|
+
error instanceof Error &&
|
|
735
|
+
(error.message.includes("symlink") || error.message.includes("Symlink"))
|
|
736
|
+
) {
|
|
737
|
+
return {
|
|
738
|
+
allowed: false,
|
|
739
|
+
reason: error.message,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ENOENT: file doesn't exist yet — check closest existing ancestor
|
|
744
|
+
if (isErrnoException(error) && error.code === "ENOENT") {
|
|
745
|
+
return await checkAncestorSymlinkSafety(
|
|
746
|
+
filePath,
|
|
747
|
+
resolvedFilePath,
|
|
748
|
+
canonicalRoot
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Propagate unexpected errors as safety failures
|
|
753
|
+
return {
|
|
754
|
+
allowed: false,
|
|
755
|
+
reason:
|
|
756
|
+
`Unable to verify safety of '${filePath}': ` +
|
|
757
|
+
`${error instanceof Error ? error.message : "unknown error"}`,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return null;
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Walk up from a non-existent path to the closest existing ancestor,
|
|
766
|
+
* resolve it with realpath(), and verify it stays within the project root.
|
|
767
|
+
*
|
|
768
|
+
* @param canonicalRoot - The realpath()-resolved project root
|
|
769
|
+
*/
|
|
770
|
+
async function checkAncestorSymlinkSafety(
|
|
771
|
+
filePath: string,
|
|
772
|
+
resolvedFilePath: string,
|
|
773
|
+
canonicalRoot: string
|
|
774
|
+
): Promise<FileCheckResult | null> {
|
|
775
|
+
let current = dirname(resolvedFilePath);
|
|
776
|
+
|
|
777
|
+
// Walk upward until we find an existing directory or hit the root dir
|
|
778
|
+
while (current !== canonicalRoot && current !== dirname(current)) {
|
|
779
|
+
try {
|
|
780
|
+
const realParent = await realpath(current);
|
|
781
|
+
if (
|
|
782
|
+
realParent !== canonicalRoot &&
|
|
783
|
+
!realParent.startsWith(canonicalRoot + sep)
|
|
784
|
+
) {
|
|
785
|
+
return {
|
|
786
|
+
allowed: false,
|
|
787
|
+
reason:
|
|
788
|
+
`Symlink escape blocked: ancestor directory of '${filePath}' ` +
|
|
789
|
+
`resolves to '${realParent}' outside project root.`,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
// Ancestor is valid — safe to proceed
|
|
793
|
+
return null;
|
|
794
|
+
} catch (ancestorError) {
|
|
795
|
+
if (isErrnoException(ancestorError) && ancestorError.code === "ENOENT") {
|
|
796
|
+
current = dirname(current);
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
allowed: false,
|
|
801
|
+
reason: `Unable to verify ancestor safety for '${filePath}'.`,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const FILE_WRITE_GUARDS: FileWriteGuard[] = [
|
|
810
|
+
checkPathTraversalGuard,
|
|
811
|
+
checkSymlinkGuard,
|
|
812
|
+
];
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Assert that a file path is safe for write/edit/delete operations.
|
|
816
|
+
*
|
|
817
|
+
* Runs the path through all write safety guards:
|
|
818
|
+
* C-1: Path traversal protection (rejects paths that escape project root)
|
|
819
|
+
* C-2: Symlink resolution via fs.realpath() (rejects symlink targets
|
|
820
|
+
* and intermediate directory symlinks escaping project root)
|
|
821
|
+
*
|
|
822
|
+
* @param filePath - The user-supplied file path
|
|
823
|
+
* @param rootDir - Project root directory (defaults to process.cwd())
|
|
824
|
+
* @returns The resolved absolute path, guaranteed to be within rootDir
|
|
825
|
+
* @throws Error if any safety guard rejects the path
|
|
826
|
+
*/
|
|
827
|
+
export async function assertWriteSafety(
|
|
828
|
+
filePath: string,
|
|
829
|
+
rootDir: string = process.cwd()
|
|
830
|
+
): Promise<string> {
|
|
831
|
+
const resolvedRoot = resolve(rootDir);
|
|
832
|
+
|
|
833
|
+
// Canonicalize rootDir by resolving all symlinks.
|
|
834
|
+
// This ensures consistent comparison with realpath() results in C-2 guards.
|
|
835
|
+
let canonicalRoot: string;
|
|
836
|
+
try {
|
|
837
|
+
canonicalRoot = await realpath(resolvedRoot);
|
|
838
|
+
} catch {
|
|
839
|
+
// If rootDir doesn't exist or can't be resolved, fall back to resolve()
|
|
840
|
+
canonicalRoot = resolvedRoot;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const resolvedFilePath = isAbsolute(filePath)
|
|
844
|
+
? resolve(filePath)
|
|
845
|
+
: resolve(resolvedRoot, filePath);
|
|
846
|
+
|
|
847
|
+
const context: FileWriteGuardContext = {
|
|
848
|
+
filePath,
|
|
849
|
+
resolvedFilePath,
|
|
850
|
+
rootDir: resolvedRoot,
|
|
851
|
+
canonicalRoot,
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
for (const guard of FILE_WRITE_GUARDS) {
|
|
855
|
+
const result = await guard(context);
|
|
856
|
+
if (result && !result.allowed) {
|
|
857
|
+
throw new Error(result.reason);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return resolvedFilePath;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* H-1: Atomic file write using temp-file + rename.
|
|
866
|
+
*
|
|
867
|
+
* Uses O_EXCL to prevent symlink pre-creation attacks on the temp file,
|
|
868
|
+
* crypto.randomBytes for unpredictable temp names, and POSIX rename()
|
|
869
|
+
* which atomically replaces the directory entry (does not follow symlinks).
|
|
870
|
+
*
|
|
871
|
+
* @param targetPath - The final destination path (must be safe-validated)
|
|
872
|
+
* @param content - The content to write
|
|
873
|
+
* @returns Whether the file previously existed (for reporting purposes)
|
|
874
|
+
*/
|
|
875
|
+
export async function safeAtomicWriteFile(
|
|
876
|
+
targetPath: string,
|
|
877
|
+
content: string
|
|
878
|
+
): Promise<{ existed: boolean }> {
|
|
879
|
+
// Check existence with lstat (does not follow symlinks)
|
|
880
|
+
let existed = false;
|
|
881
|
+
let originalMode = 0o644;
|
|
882
|
+
try {
|
|
883
|
+
const stats = await lstat(targetPath);
|
|
884
|
+
existed = true;
|
|
885
|
+
originalMode = stats.mode % 0o1000;
|
|
886
|
+
// Belt-and-suspenders: reject symlinks even after assertWriteSafety
|
|
887
|
+
if (stats.isSymbolicLink()) {
|
|
888
|
+
throw new Error(
|
|
889
|
+
`Refusing to write through symlink: '${targetPath}'. ` +
|
|
890
|
+
"Use the real path instead."
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
} catch (error) {
|
|
894
|
+
if (error instanceof Error && error.message.includes("symlink")) {
|
|
895
|
+
throw error;
|
|
896
|
+
}
|
|
897
|
+
if (isErrnoException(error) && error.code === "ENOENT") {
|
|
898
|
+
// ENOENT is expected for new files
|
|
899
|
+
existed = false;
|
|
900
|
+
} else {
|
|
901
|
+
throw error;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Atomic write: temp file + rename.
|
|
906
|
+
// - crypto.randomBytes prevents temp name prediction
|
|
907
|
+
// - 'wx' flag (O_WRONLY|O_CREAT|O_EXCL) fails if file already exists,
|
|
908
|
+
// preventing symlink pre-creation attacks on the temp path
|
|
909
|
+
// - rename() replaces the directory entry atomically on POSIX,
|
|
910
|
+
// does NOT follow symlinks at the target
|
|
911
|
+
const tmpSuffix = `.tmp-${randomBytes(8).toString("hex")}`;
|
|
912
|
+
const tmpPath = `${targetPath}${tmpSuffix}`;
|
|
913
|
+
let fd: Awaited<ReturnType<typeof open>> | undefined;
|
|
914
|
+
try {
|
|
915
|
+
fd = await open(tmpPath, "wx", originalMode);
|
|
916
|
+
await fd.writeFile(content, "utf-8");
|
|
917
|
+
await fd.close();
|
|
918
|
+
fd = undefined;
|
|
919
|
+
await rename(tmpPath, targetPath);
|
|
920
|
+
} catch (error) {
|
|
921
|
+
if (fd) {
|
|
922
|
+
try {
|
|
923
|
+
await fd.close();
|
|
924
|
+
} catch {
|
|
925
|
+
/* ignore close errors */
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
// Only clean up temp file if we potentially created it
|
|
929
|
+
try {
|
|
930
|
+
await unlink(tmpPath);
|
|
931
|
+
} catch {
|
|
932
|
+
/* ignore cleanup errors */
|
|
933
|
+
}
|
|
934
|
+
throw error;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return { existed };
|
|
938
|
+
}
|