xcoder-dna 0.0.0-stage → 1.3.1
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/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/agents/ai-chat-debate.d.ts +36 -0
- package/dist/agents/ai-chat-debate.js +300 -0
- package/dist/agents/ai-chat-report-writer.d.ts +7 -0
- package/dist/agents/ai-chat-report-writer.js +100 -0
- package/dist/agents/duplicate-detector.d.ts +41 -0
- package/dist/agents/duplicate-detector.js +65 -0
- package/dist/agents/genesis-agent.d.ts +61 -0
- package/dist/agents/genesis-agent.js +340 -0
- package/dist/agents/samurai-agent.d.ts +53 -0
- package/dist/agents/samurai-agent.js +279 -0
- package/dist/agents/sub-agent.d.ts +56 -0
- package/dist/agents/sub-agent.js +353 -0
- package/dist/backlog/backlog-requirement-validator.d.ts +10 -0
- package/dist/backlog/backlog-requirement-validator.js +60 -0
- package/dist/backlog/backlog-store.d.ts +29 -0
- package/dist/backlog/backlog-store.js +148 -0
- package/dist/backlog/po-agent.d.ts +16 -0
- package/dist/backlog/po-agent.js +121 -0
- package/dist/backlog/types.d.ts +25 -0
- package/dist/backlog/types.js +4 -0
- package/dist/bench/run.d.ts +11 -0
- package/dist/bench/run.js +185 -0
- package/dist/bench/types.d.ts +50 -0
- package/dist/bench/types.js +1 -0
- package/dist/cli/swarm.d.ts +10 -0
- package/dist/cli/swarm.js +100 -0
- package/dist/cli/task.d.ts +14 -0
- package/dist/cli/task.js +121 -0
- package/dist/cli/wbc-arg.d.ts +6 -0
- package/dist/cli/wbc-arg.js +40 -0
- package/dist/cli/wbc.d.ts +45 -0
- package/dist/cli/wbc.js +98 -0
- package/dist/commands/license.d.ts +9 -0
- package/dist/commands/license.js +140 -0
- package/dist/commands/release.d.ts +44 -0
- package/dist/commands/release.js +180 -0
- package/dist/commands/test.d.ts +50 -0
- package/dist/commands/test.js +385 -0
- package/dist/config/loadConfig.d.ts +82 -0
- package/dist/config/loadConfig.js +240 -0
- package/dist/config/secure-mode.d.ts +29 -0
- package/dist/config/secure-mode.js +63 -0
- package/dist/config/user-config.d.ts +39 -0
- package/dist/config/user-config.js +202 -0
- package/dist/engine/reasoning-engine.d.ts +21 -0
- package/dist/engine/reasoning-engine.js +59 -0
- package/dist/events/bus.d.ts +22 -0
- package/dist/events/bus.js +18 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +77 -0
- package/dist/indexing/ignoreRules.d.ts +1 -0
- package/dist/indexing/ignoreRules.js +53 -0
- package/dist/instance-swarm/instance-swarm-orchestrator.d.ts +57 -0
- package/dist/instance-swarm/instance-swarm-orchestrator.js +204 -0
- package/dist/instance-swarm/protocol-name-resolver.d.ts +23 -0
- package/dist/instance-swarm/protocol-name-resolver.js +64 -0
- package/dist/license/gate.d.ts +31 -0
- package/dist/license/gate.js +188 -0
- package/dist/license/http-client.d.ts +43 -0
- package/dist/license/http-client.js +195 -0
- package/dist/license/manager.d.ts +39 -0
- package/dist/license/manager.js +175 -0
- package/dist/license/receipt.d.ts +27 -0
- package/dist/license/receipt.js +108 -0
- package/dist/license/state-codec.d.ts +72 -0
- package/dist/license/state-codec.js +218 -0
- package/dist/license/state-paths.d.ts +17 -0
- package/dist/license/state-paths.js +64 -0
- package/dist/license/store.d.ts +44 -0
- package/dist/license/store.js +223 -0
- package/dist/license/trust-anchor.d.ts +24 -0
- package/dist/license/trust-anchor.js +74 -0
- package/dist/license/types.d.ts +34 -0
- package/dist/license/types.js +19 -0
- package/dist/llm/budget.d.ts +23 -0
- package/dist/llm/budget.js +58 -0
- package/dist/llm/client.d.ts +53 -0
- package/dist/llm/client.js +122 -0
- package/dist/llm/factory.d.ts +14 -0
- package/dist/llm/factory.js +32 -0
- package/dist/orchestrator/defect-logger.d.ts +31 -0
- package/dist/orchestrator/defect-logger.js +101 -0
- package/dist/orchestrator/defect-manager.d.ts +118 -0
- package/dist/orchestrator/defect-manager.js +338 -0
- package/dist/orchestrator/defect-migration.d.ts +14 -0
- package/dist/orchestrator/defect-migration.js +174 -0
- package/dist/orchestrator/defect-store.d.ts +48 -0
- package/dist/orchestrator/defect-store.js +76 -0
- package/dist/orchestrator/healing.d.ts +14 -0
- package/dist/orchestrator/healing.js +11 -0
- package/dist/orchestrator/interactive-steering.d.ts +64 -0
- package/dist/orchestrator/interactive-steering.js +107 -0
- package/dist/orchestrator/interrupt-handler.d.ts +30 -0
- package/dist/orchestrator/interrupt-handler.js +70 -0
- package/dist/orchestrator/lesson-manager.d.ts +25 -0
- package/dist/orchestrator/lesson-manager.js +122 -0
- package/dist/orchestrator/progress-summary.d.ts +15 -0
- package/dist/orchestrator/progress-summary.js +106 -0
- package/dist/orchestrator/reasoning-score.d.ts +49 -0
- package/dist/orchestrator/reasoning-score.js +117 -0
- package/dist/orchestrator/swarm.d.ts +36 -0
- package/dist/orchestrator/swarm.js +77 -0
- package/dist/orchestrator/task-factory.d.ts +15 -0
- package/dist/orchestrator/task-factory.js +176 -0
- package/dist/orchestrator/validation-gate.d.ts +23 -0
- package/dist/orchestrator/validation-gate.js +155 -0
- package/dist/protocol/react-contract.d.ts +136 -0
- package/dist/protocol/react-contract.js +698 -0
- package/dist/protocol/wks-protocol-manager.d.ts +22 -0
- package/dist/protocol/wks-protocol-manager.js +72 -0
- package/dist/sdlc-runtime/conversation-runner.d.ts +8 -0
- package/dist/sdlc-runtime/conversation-runner.js +64 -0
- package/dist/sdlc-runtime/dag-utils.d.ts +15 -0
- package/dist/sdlc-runtime/dag-utils.js +67 -0
- package/dist/sdlc-runtime/defect-integration.d.ts +7 -0
- package/dist/sdlc-runtime/defect-integration.js +99 -0
- package/dist/sdlc-runtime/execution-loop.d.ts +38 -0
- package/dist/sdlc-runtime/execution-loop.js +274 -0
- package/dist/sdlc-runtime/lesson-recorder.d.ts +9 -0
- package/dist/sdlc-runtime/lesson-recorder.js +27 -0
- package/dist/sdlc-runtime/plan-initializer.d.ts +47 -0
- package/dist/sdlc-runtime/plan-initializer.js +135 -0
- package/dist/sdlc-runtime/progress-persister.d.ts +10 -0
- package/dist/sdlc-runtime/progress-persister.js +34 -0
- package/dist/sdlc-runtime/protocol-loader.d.ts +10 -0
- package/dist/sdlc-runtime/protocol-loader.js +21 -0
- package/dist/sdlc-runtime/reporting.d.ts +24 -0
- package/dist/sdlc-runtime/reporting.js +68 -0
- package/dist/sdlc-runtime/sdlc-orchestrator.d.ts +59 -0
- package/dist/sdlc-runtime/sdlc-orchestrator.js +170 -0
- package/dist/sdlc-runtime/sdlc-skill.d.ts +44 -0
- package/dist/sdlc-runtime/sdlc-skill.js +148 -0
- package/dist/sdlc-runtime/steering-handler.d.ts +14 -0
- package/dist/sdlc-runtime/steering-handler.js +51 -0
- package/dist/sdlc-runtime/trace-recorder.d.ts +10 -0
- package/dist/sdlc-runtime/trace-recorder.js +7 -0
- package/dist/security/index.d.ts +16 -0
- package/dist/security/index.js +12 -0
- package/dist/security/input-screen.d.ts +30 -0
- package/dist/security/input-screen.js +75 -0
- package/dist/security/tool-call-guard.d.ts +16 -0
- package/dist/security/tool-call-guard.js +163 -0
- package/dist/security/untrusted-content.d.ts +34 -0
- package/dist/security/untrusted-content.js +63 -0
- package/dist/skills/advanced-dfir/SKILL.md +85 -0
- package/dist/skills/advanced-pentest-lab/SKILL.md +88 -0
- package/dist/skills/analyst/SKILL.md +55 -0
- package/dist/skills/architect/SKILL.md +54 -0
- package/dist/skills/aws/SKILL.md +92 -0
- package/dist/skills/azure/SKILL.md +90 -0
- package/dist/skills/bylaw-context.d.ts +45 -0
- package/dist/skills/bylaw-context.js +61 -0
- package/dist/skills/bylaw-review.d.ts +20 -0
- package/dist/skills/bylaw-review.js +101 -0
- package/dist/skills/conversation/SKILL.md +29 -0
- package/dist/skills/design-patterns/SKILL.md +190 -0
- package/dist/skills/devops/SKILL.md +55 -0
- package/dist/skills/directives.d.ts +20 -0
- package/dist/skills/directives.js +313 -0
- package/dist/skills/docker/SKILL.md +76 -0
- package/dist/skills/docker-expert/SKILL.md +56 -0
- package/dist/skills/enterprise-systems-analyst/SKILL.md +83 -0
- package/dist/skills/enterprise-systems-analyst/references/crm.md +41 -0
- package/dist/skills/enterprise-systems-analyst/references/erp.md +40 -0
- package/dist/skills/enterprise-systems-analyst/references/financial-systems.md +35 -0
- package/dist/skills/enterprise-systems-analyst/references/hcm.md +38 -0
- package/dist/skills/enterprise-systems-analyst/references/mrp.md +37 -0
- package/dist/skills/enterprise-systems-analyst/references/scm.md +32 -0
- package/dist/skills/ethical-hacking-methodology/SKILL.md +119 -0
- package/dist/skills/externalSkillLoader.d.ts +25 -0
- package/dist/skills/externalSkillLoader.js +65 -0
- package/dist/skills/externalSkillRegistry.d.ts +33 -0
- package/dist/skills/externalSkillRegistry.js +94 -0
- package/dist/skills/filesystem-management/SKILL.md +61 -0
- package/dist/skills/first-run-seed.d.ts +26 -0
- package/dist/skills/first-run-seed.js +75 -0
- package/dist/skills/git-vcs/SKILL.md +84 -0
- package/dist/skills/kafka/SKILL.md +89 -0
- package/dist/skills/kubernetes/SKILL.md +93 -0
- package/dist/skills/kubernetes-expert/SKILL.md +54 -0
- package/dist/skills/openshift/SKILL.md +93 -0
- package/dist/skills/pentester/SKILL.md +59 -0
- package/dist/skills/performance-tester/SKILL.md +57 -0
- package/dist/skills/playwright-ui-tester/SKILL.md +67 -0
- package/dist/skills/programmer/SKILL.md +58 -0
- package/dist/skills/project-scaffolding/SKILL.md +133 -0
- package/dist/skills/qa-engineer/SKILL.md +91 -0
- package/dist/skills/rca/SKILL.md +58 -0
- package/dist/skills/redhat/SKILL.md +92 -0
- package/dist/skills/rosa/SKILL.md +80 -0
- package/dist/skills/scrum-framework/SKILL.md +74 -0
- package/dist/skills/scrum-framework/references/artifacts_and_quality.md +79 -0
- package/dist/skills/scrum-framework/references/roles_and_events.md +105 -0
- package/dist/skills/scrum-master-agent/SKILL.md +74 -0
- package/dist/skills/scrum-master-agent/references/behaviors.md +56 -0
- package/dist/skills/sdlc-skill.d.ts +1 -0
- package/dist/skills/sdlc-skill.js +4 -0
- package/dist/skills/secops/SKILL.md +52 -0
- package/dist/skills/secure-architect.bylaws.d.ts +68 -0
- package/dist/skills/secure-architect.bylaws.js +525 -0
- package/dist/skills/seed-skills.d.ts +48 -0
- package/dist/skills/seed-skills.js +213 -0
- package/dist/skills/skill-authoring/SKILL.md +123 -0
- package/dist/skills/skill-header.d.ts +51 -0
- package/dist/skills/skill-header.js +163 -0
- package/dist/skills/skill-path-guard.d.ts +41 -0
- package/dist/skills/skill-path-guard.js +95 -0
- package/dist/skills/skillRouter.d.ts +51 -0
- package/dist/skills/skillRouter.js +90 -0
- package/dist/skills/skills-root.d.ts +64 -0
- package/dist/skills/skills-root.js +168 -0
- package/dist/skills/software-architect/SKILL.md +77 -0
- package/dist/skills/software-engineer/SKILL.md +74 -0
- package/dist/skills/solid-principles/SKILL.md +94 -0
- package/dist/skills/task-planning/SKILL.md +115 -0
- package/dist/skills/tester/SKILL.md +54 -0
- package/dist/skills/ubuntu/SKILL.md +87 -0
- package/dist/skills/ui-ux-design/SKILL.md +114 -0
- package/dist/skills/workspace-context/SKILL.md +103 -0
- package/dist/sprint/controller.d.ts +37 -0
- package/dist/sprint/controller.js +220 -0
- package/dist/sprint/dag-builder.d.ts +14 -0
- package/dist/sprint/dag-builder.js +105 -0
- package/dist/sprint/reporter.d.ts +11 -0
- package/dist/sprint/reporter.js +97 -0
- package/dist/sprint/state-store.d.ts +10 -0
- package/dist/sprint/state-store.js +71 -0
- package/dist/sprint/types.d.ts +24 -0
- package/dist/sprint/types.js +4 -0
- package/dist/store/file-version-store.d.ts +24 -0
- package/dist/store/file-version-store.js +73 -0
- package/dist/store/task-store.d.ts +90 -0
- package/dist/store/task-store.js +166 -0
- package/dist/task-ingest/architect-detailer.d.ts +43 -0
- package/dist/task-ingest/architect-detailer.js +141 -0
- package/dist/task-ingest/detail-schema.d.ts +9 -0
- package/dist/task-ingest/detail-schema.js +195 -0
- package/dist/task-ingest/detailed-run.d.ts +36 -0
- package/dist/task-ingest/detailed-run.js +108 -0
- package/dist/task-ingest/extract-id.d.ts +9 -0
- package/dist/task-ingest/extract-id.js +24 -0
- package/dist/task-ingest/index.d.ts +23 -0
- package/dist/task-ingest/index.js +14 -0
- package/dist/task-ingest/ingest.d.ts +45 -0
- package/dist/task-ingest/ingest.js +163 -0
- package/dist/task-ingest/list-ingested.d.ts +13 -0
- package/dist/task-ingest/list-ingested.js +30 -0
- package/dist/task-ingest/pending-filter.d.ts +30 -0
- package/dist/task-ingest/pending-filter.js +59 -0
- package/dist/task-ingest/scanner.d.ts +56 -0
- package/dist/task-ingest/scanner.js +216 -0
- package/dist/task-ingest/source-reader.d.ts +25 -0
- package/dist/task-ingest/source-reader.js +93 -0
- package/dist/task-ingest/spec-builder.d.ts +34 -0
- package/dist/task-ingest/spec-builder.js +89 -0
- package/dist/task-ingest/wbs-extract.d.ts +50 -0
- package/dist/task-ingest/wbs-extract.js +226 -0
- package/dist/task-ingest/wbs-parser.d.ts +36 -0
- package/dist/task-ingest/wbs-parser.js +170 -0
- package/dist/tools/apiTestTool.d.ts +37 -0
- package/dist/tools/apiTestTool.js +128 -0
- package/dist/tools/ast-tools.d.ts +183 -0
- package/dist/tools/ast-tools.js +853 -0
- package/dist/tools/browse-tool.d.ts +105 -0
- package/dist/tools/browse-tool.js +394 -0
- package/dist/tools/credential.d.ts +17 -0
- package/dist/tools/credential.js +32 -0
- package/dist/tools/dependency-graph.d.ts +23 -0
- package/dist/tools/dependency-graph.js +164 -0
- package/dist/tools/edit-tool.d.ts +34 -0
- package/dist/tools/edit-tool.js +95 -0
- package/dist/tools/file-header-manager.d.ts +27 -0
- package/dist/tools/file-header-manager.js +153 -0
- package/dist/tools/fs-tools.d.ts +154 -0
- package/dist/tools/fs-tools.js +337 -0
- package/dist/tools/git-tools.d.ts +79 -0
- package/dist/tools/git-tools.js +348 -0
- package/dist/tools/github-tool.d.ts +48 -0
- package/dist/tools/github-tool.js +147 -0
- package/dist/tools/globTool.d.ts +20 -0
- package/dist/tools/globTool.js +34 -0
- package/dist/tools/grepTool.d.ts +19 -0
- package/dist/tools/grepTool.js +44 -0
- package/dist/tools/index-freshness.d.ts +58 -0
- package/dist/tools/index-freshness.js +190 -0
- package/dist/tools/index-glob.d.ts +48 -0
- package/dist/tools/index-glob.js +175 -0
- package/dist/tools/load-skill-tool.d.ts +15 -0
- package/dist/tools/load-skill-tool.js +114 -0
- package/dist/tools/patch-tools.d.ts +58 -0
- package/dist/tools/patch-tools.js +153 -0
- package/dist/tools/playwright-tool.d.ts +66 -0
- package/dist/tools/playwright-tool.js +121 -0
- package/dist/tools/project-detect.d.ts +20 -0
- package/dist/tools/project-detect.js +87 -0
- package/dist/tools/prompt-guidance.d.ts +22 -0
- package/dist/tools/prompt-guidance.js +23 -0
- package/dist/tools/scheduleTool.d.ts +19 -0
- package/dist/tools/scheduleTool.js +181 -0
- package/dist/tools/search-tools.d.ts +27 -0
- package/dist/tools/search-tools.js +78 -0
- package/dist/tools/sed-tool.d.ts +66 -0
- package/dist/tools/sed-tool.js +402 -0
- package/dist/tools/shell-result.d.ts +11 -0
- package/dist/tools/shell-result.js +1 -0
- package/dist/tools/shell-tool.d.ts +45 -0
- package/dist/tools/shell-tool.js +141 -0
- package/dist/tools/siteCrawlerTool.d.ts +32 -0
- package/dist/tools/siteCrawlerTool.js +205 -0
- package/dist/tools/tool-registry.d.ts +67 -0
- package/dist/tools/tool-registry.js +596 -0
- package/dist/tools/validate-tool.d.ts +23 -0
- package/dist/tools/validate-tool.js +201 -0
- package/dist/tools/workspace-index.d.ts +46 -0
- package/dist/tools/workspace-index.js +173 -0
- package/dist/types.d.ts +222 -0
- package/dist/types.js +7 -0
- package/dist/updater/index.d.ts +14 -0
- package/dist/updater/index.js +120 -0
- package/dist/util/banner.d.ts +25 -0
- package/dist/util/banner.js +140 -0
- package/dist/util/command-suggester.d.ts +6 -0
- package/dist/util/command-suggester.js +25 -0
- package/dist/util/confirm-interrupt.d.ts +10 -0
- package/dist/util/confirm-interrupt.js +31 -0
- package/dist/util/continue-trigger.d.ts +11 -0
- package/dist/util/continue-trigger.js +20 -0
- package/dist/util/env-info.d.ts +18 -0
- package/dist/util/env-info.js +37 -0
- package/dist/util/file-access-log.d.ts +35 -0
- package/dist/util/file-access-log.js +140 -0
- package/dist/util/footer-manager.d.ts +124 -0
- package/dist/util/footer-manager.js +195 -0
- package/dist/util/footer-renderer.d.ts +67 -0
- package/dist/util/footer-renderer.js +319 -0
- package/dist/util/footer-state.d.ts +34 -0
- package/dist/util/footer-state.js +18 -0
- package/dist/util/footer-text.d.ts +51 -0
- package/dist/util/footer-text.js +74 -0
- package/dist/util/ids.d.ts +1 -0
- package/dist/util/ids.js +4 -0
- package/dist/util/levenshtein.d.ts +6 -0
- package/dist/util/levenshtein.js +34 -0
- package/dist/util/logger.d.ts +10 -0
- package/dist/util/logger.js +41 -0
- package/dist/util/logs.d.ts +26 -0
- package/dist/util/logs.js +88 -0
- package/dist/util/output-handler.d.ts +89 -0
- package/dist/util/output-handler.js +193 -0
- package/dist/util/output-interceptor.d.ts +18 -0
- package/dist/util/output-interceptor.js +49 -0
- package/dist/util/output-paths.d.ts +43 -0
- package/dist/util/output-paths.js +121 -0
- package/dist/util/package-json.d.ts +6 -0
- package/dist/util/package-json.js +36 -0
- package/dist/util/process-kill.d.ts +27 -0
- package/dist/util/process-kill.js +148 -0
- package/dist/util/protocol-error-logger.d.ts +33 -0
- package/dist/util/protocol-error-logger.js +73 -0
- package/dist/util/redact.d.ts +25 -0
- package/dist/util/redact.js +103 -0
- package/dist/util/scroll-region-guard.d.ts +33 -0
- package/dist/util/scroll-region-guard.js +63 -0
- package/dist/util/spinner.d.ts +15 -0
- package/dist/util/spinner.js +49 -0
- package/dist/util/terminal-output.d.ts +34 -0
- package/dist/util/terminal-output.js +1 -0
- package/dist/util/terminal-silent.d.ts +6 -0
- package/dist/util/terminal-silent.js +20 -0
- package/dist/util/terminal.d.ts +56 -0
- package/dist/util/terminal.js +124 -0
- package/dist/util/text-sanitize.d.ts +8 -0
- package/dist/util/text-sanitize.js +45 -0
- package/dist/util/thinking-spinner.d.ts +27 -0
- package/dist/util/thinking-spinner.js +189 -0
- package/dist/util/tool-error-logger.d.ts +33 -0
- package/dist/util/tool-error-logger.js +77 -0
- package/dist/util/tty.d.ts +13 -0
- package/dist/util/tty.js +16 -0
- package/dist/util/xcoder-paths.d.ts +56 -0
- package/dist/util/xcoder-paths.js +63 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/dist/worker-agent/to-subtask-node.d.ts +35 -0
- package/dist/worker-agent/to-subtask-node.js +41 -0
- package/dist/worker-agent/worker-orchestrator.d.ts +104 -0
- package/dist/worker-agent/worker-orchestrator.js +201 -0
- package/package.json +38 -3
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// xcoder:version 2 | xcoder:task task-456d3f | xcoder:updated 2026-08-03T15:41:37.782Z | xcoder:subtask code-st-97a4d4
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { getOrBuildIndex, upsertEntry, toPosixPath } from "./workspace-index.js";
|
|
5
|
+
import { indexGlobTool } from "./search-tools.js";
|
|
6
|
+
import { getCommentSyntax, injectVersionHeader, } from "./file-header-manager.js";
|
|
7
|
+
import { incrementFileVersion } from "../store/file-version-store.js";
|
|
8
|
+
// ── Constants ────────────────────────────────────────────────────────────
|
|
9
|
+
const ALLOWED_FLAGS = new Set(["g", "i", "m", "s"]);
|
|
10
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
11
|
+
class WorkspaceEscapeError extends Error {
|
|
12
|
+
}
|
|
13
|
+
function resolveInWorkspace(root, relPath) {
|
|
14
|
+
const full = path.resolve(root, relPath);
|
|
15
|
+
const normalizedRoot = path.resolve(root) + path.sep;
|
|
16
|
+
if (full + path.sep !== normalizedRoot && !full.startsWith(normalizedRoot)) {
|
|
17
|
+
throw new WorkspaceEscapeError(`path "${relPath}" resolves outside the workspace root — refusing.`);
|
|
18
|
+
}
|
|
19
|
+
return full;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validate the SedToolInput before processing.
|
|
23
|
+
* Returns an error string if invalid, null if valid.
|
|
24
|
+
*/
|
|
25
|
+
function validateInput(input) {
|
|
26
|
+
// Operation must be valid
|
|
27
|
+
const validOps = ["substitute", "delete", "deleteRange", "insert", "appendAfter"];
|
|
28
|
+
if (!validOps.includes(input.operation)) {
|
|
29
|
+
return `invalid_operation: "${input.operation}" is not a valid operation. Must be one of: ${validOps.join(", ")}`;
|
|
30
|
+
}
|
|
31
|
+
// Target: glob takes precedence, then path, then neither → no_target
|
|
32
|
+
const hasPath = input.path !== undefined && input.path !== "";
|
|
33
|
+
const hasGlob = input.glob !== undefined && input.glob !== "";
|
|
34
|
+
if (!hasPath && !hasGlob) {
|
|
35
|
+
return "no_target: neither 'path' nor 'glob' was provided. Provide at least one target.";
|
|
36
|
+
}
|
|
37
|
+
// Operation-specific validation
|
|
38
|
+
switch (input.operation) {
|
|
39
|
+
case "substitute":
|
|
40
|
+
if (input.pattern === undefined || input.pattern === "") {
|
|
41
|
+
return "invalid_operation: 'substitute' requires a 'pattern' field.";
|
|
42
|
+
}
|
|
43
|
+
if (input.replacement === undefined) {
|
|
44
|
+
return "invalid_operation: 'substitute' requires a 'replacement' field.";
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
case "delete":
|
|
48
|
+
if (input.pattern === undefined || input.pattern === "") {
|
|
49
|
+
return "invalid_operation: 'delete' requires a 'pattern' field.";
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
case "deleteRange":
|
|
53
|
+
if (input.startLine === undefined || input.endLine === undefined) {
|
|
54
|
+
return "invalid_operation: 'deleteRange' requires both 'startLine' and 'endLine'.";
|
|
55
|
+
}
|
|
56
|
+
if (!Number.isInteger(input.startLine) || input.startLine < 1) {
|
|
57
|
+
return "invalid_operation: 'startLine' must be an integer >= 1.";
|
|
58
|
+
}
|
|
59
|
+
if (!Number.isInteger(input.endLine) || input.endLine < 1) {
|
|
60
|
+
return "invalid_operation: 'endLine' must be an integer >= 1.";
|
|
61
|
+
}
|
|
62
|
+
if (input.endLine < input.startLine) {
|
|
63
|
+
return "invalid_operation: 'endLine' must be >= 'startLine'.";
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
case "insert":
|
|
67
|
+
if (input.text === undefined) {
|
|
68
|
+
return "invalid_operation: 'insert' requires a 'text' field.";
|
|
69
|
+
}
|
|
70
|
+
if (input.afterLine !== undefined && (!Number.isInteger(input.afterLine) || input.afterLine < 0)) {
|
|
71
|
+
return "invalid_operation: 'afterLine' must be an integer >= 0.";
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
case "appendAfter":
|
|
75
|
+
if (input.pattern === undefined || input.pattern === "") {
|
|
76
|
+
return "invalid_operation: 'appendAfter' requires a 'pattern' field.";
|
|
77
|
+
}
|
|
78
|
+
if (input.text === undefined) {
|
|
79
|
+
return "invalid_operation: 'appendAfter' requires a 'text' field.";
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
// Flags validation (if provided)
|
|
84
|
+
if (input.flags !== undefined) {
|
|
85
|
+
for (const ch of input.flags) {
|
|
86
|
+
if (!ALLOWED_FLAGS.has(ch)) {
|
|
87
|
+
return `invalid_regex: flag "${ch}" is not allowed. Allowed flags: ${[...ALLOWED_FLAGS].join("")}`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Regex validation (if pattern provided)
|
|
92
|
+
if (input.pattern) {
|
|
93
|
+
try {
|
|
94
|
+
new RegExp(input.pattern, input.flags ?? "");
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
return `invalid_regex: ${e.message}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null; // valid
|
|
101
|
+
}
|
|
102
|
+
// ── Target Resolution ────────────────────────────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* Resolve target files: glob takes precedence over path.
|
|
105
|
+
*/
|
|
106
|
+
async function resolveTargetFiles(root, input) {
|
|
107
|
+
if (input.glob !== undefined && input.glob !== "") {
|
|
108
|
+
const index = await getOrBuildIndex(root);
|
|
109
|
+
const result = indexGlobTool(index, input.glob);
|
|
110
|
+
return result.matches;
|
|
111
|
+
}
|
|
112
|
+
if (input.path !== undefined && input.path !== "") {
|
|
113
|
+
return [input.path];
|
|
114
|
+
}
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
// ── Line Ending Detection & Normalization ────────────────────────────────
|
|
118
|
+
function detectLineEndings(content) {
|
|
119
|
+
const crlfCount = (content.match(/\r\n/g) || []).length;
|
|
120
|
+
// Match LF not preceded by CR (pure LF lines)
|
|
121
|
+
const lfOnlyCount = (content.match(/(?<!\r)\n/g) || []).length;
|
|
122
|
+
if (crlfCount > 0 && lfOnlyCount === 0)
|
|
123
|
+
return "CRLF";
|
|
124
|
+
if (lfOnlyCount > 0 && crlfCount === 0)
|
|
125
|
+
return "LF";
|
|
126
|
+
if (crlfCount === 0 && lfOnlyCount === 0)
|
|
127
|
+
return "LF"; // empty or single line without newline
|
|
128
|
+
return "mixed";
|
|
129
|
+
}
|
|
130
|
+
function normalizeToLF(content, _style) {
|
|
131
|
+
return content.replace(/\r\n/g, "\n");
|
|
132
|
+
}
|
|
133
|
+
function restoreLineEndings(normalized, style) {
|
|
134
|
+
if (style === "CRLF") {
|
|
135
|
+
return normalized.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n");
|
|
136
|
+
}
|
|
137
|
+
return normalized;
|
|
138
|
+
}
|
|
139
|
+
// ── Operations ───────────────────────────────────────────────────────────
|
|
140
|
+
function buildRegex(pattern, flags) {
|
|
141
|
+
return new RegExp(pattern, flags);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Replace back-reference placeholders in the replacement string.
|
|
145
|
+
* Supports $& (full match), $1-$9 (capture groups), and $$ (literal $).
|
|
146
|
+
*/
|
|
147
|
+
function expandReplacement(replacement, match, ...groups) {
|
|
148
|
+
return replacement.replace(/\$([&1-9]|\$)/g, (_, ref) => {
|
|
149
|
+
if (ref === "&")
|
|
150
|
+
return match;
|
|
151
|
+
if (ref === "$")
|
|
152
|
+
return "$";
|
|
153
|
+
const idx = parseInt(ref, 10);
|
|
154
|
+
return groups[idx - 1] ?? "$" + ref;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function applySubstitute(lines, pattern, replacement, flags) {
|
|
158
|
+
const re = buildRegex(pattern, flags);
|
|
159
|
+
let changes = 0;
|
|
160
|
+
const result = lines.map((line) => {
|
|
161
|
+
let changed = false;
|
|
162
|
+
const newLine = line.replace(re, (...args) => {
|
|
163
|
+
changed = true;
|
|
164
|
+
const match = args[0];
|
|
165
|
+
const groups = args.slice(1, -2);
|
|
166
|
+
return expandReplacement(replacement, match, ...groups);
|
|
167
|
+
});
|
|
168
|
+
if (changed)
|
|
169
|
+
changes++;
|
|
170
|
+
return newLine;
|
|
171
|
+
});
|
|
172
|
+
return { lines: result, changes };
|
|
173
|
+
}
|
|
174
|
+
function applyDelete(lines, pattern, flags) {
|
|
175
|
+
const re = buildRegex(pattern, flags);
|
|
176
|
+
let changes = 0;
|
|
177
|
+
const result = lines.filter((line) => {
|
|
178
|
+
const match = re.test(line);
|
|
179
|
+
if (match)
|
|
180
|
+
changes++;
|
|
181
|
+
return !match;
|
|
182
|
+
});
|
|
183
|
+
return { lines: result, changes };
|
|
184
|
+
}
|
|
185
|
+
function applyDeleteRange(lines, startLine, endLine) {
|
|
186
|
+
const max = lines.length;
|
|
187
|
+
const start = Math.max(1, startLine);
|
|
188
|
+
const end = Math.min(max, endLine);
|
|
189
|
+
if (start > max || end < start) {
|
|
190
|
+
return { lines, changes: 0 };
|
|
191
|
+
}
|
|
192
|
+
const count = end - start + 1;
|
|
193
|
+
const result = [...lines.slice(0, start - 1), ...lines.slice(end)];
|
|
194
|
+
return { lines: result, changes: count };
|
|
195
|
+
}
|
|
196
|
+
function applyInsert(lines, afterLine, text) {
|
|
197
|
+
// afterLine 0 = before first line
|
|
198
|
+
// Clamp: if afterLine > lines.length, append at end
|
|
199
|
+
const pos = Math.min(afterLine, lines.length);
|
|
200
|
+
const result = [...lines.slice(0, pos), text, ...lines.slice(pos)];
|
|
201
|
+
return { lines: result, changes: 1 };
|
|
202
|
+
}
|
|
203
|
+
function applyAppendAfter(lines, pattern, text, flags) {
|
|
204
|
+
const re = buildRegex(pattern, flags);
|
|
205
|
+
let changes = 0;
|
|
206
|
+
const result = [];
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
result.push(line);
|
|
209
|
+
if (re.test(line)) {
|
|
210
|
+
result.push(text);
|
|
211
|
+
changes++;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { lines: result, changes };
|
|
215
|
+
}
|
|
216
|
+
// ── Per-File Processing ──────────────────────────────────────────────────
|
|
217
|
+
async function processFile(root, relPath, input, ctx) {
|
|
218
|
+
let full;
|
|
219
|
+
try {
|
|
220
|
+
full = resolveInWorkspace(root, relPath);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return {
|
|
224
|
+
relPath,
|
|
225
|
+
matched: false,
|
|
226
|
+
changes: 0,
|
|
227
|
+
output: undefined,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// Read file
|
|
231
|
+
let content;
|
|
232
|
+
try {
|
|
233
|
+
content = await fs.readFile(full, "utf8");
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return {
|
|
237
|
+
relPath,
|
|
238
|
+
matched: false,
|
|
239
|
+
changes: 0,
|
|
240
|
+
output: undefined,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
// Detect and normalize line endings
|
|
244
|
+
const lineEndingStyle = detectLineEndings(content);
|
|
245
|
+
const normalized = normalizeToLF(content, lineEndingStyle);
|
|
246
|
+
// Split into lines. A trailing empty string from split("\n") represents
|
|
247
|
+
// a trailing newline in the file — we strip it before operating on
|
|
248
|
+
// "real" lines (so line counts match user expectations), then re-add it
|
|
249
|
+
// after the join so the file's trailing-newline convention is preserved.
|
|
250
|
+
const rawLines = normalized.split("\n");
|
|
251
|
+
const hasTrailingNewline = rawLines.length > 0 && rawLines[rawLines.length - 1] === "";
|
|
252
|
+
const lines = hasTrailingNewline ? rawLines.slice(0, -1) : rawLines;
|
|
253
|
+
// Apply operation
|
|
254
|
+
let resultLines;
|
|
255
|
+
let changes;
|
|
256
|
+
switch (input.operation) {
|
|
257
|
+
case "substitute": {
|
|
258
|
+
const r = applySubstitute(lines, input.pattern, input.replacement, input.flags ?? "g");
|
|
259
|
+
resultLines = r.lines;
|
|
260
|
+
changes = r.changes;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
case "delete": {
|
|
264
|
+
const r = applyDelete(lines, input.pattern, input.flags ?? "");
|
|
265
|
+
resultLines = r.lines;
|
|
266
|
+
changes = r.changes;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case "deleteRange": {
|
|
270
|
+
const r = applyDeleteRange(lines, input.startLine, input.endLine);
|
|
271
|
+
resultLines = r.lines;
|
|
272
|
+
changes = r.changes;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
case "insert": {
|
|
276
|
+
const r = applyInsert(lines, input.afterLine ?? lines.length, input.text);
|
|
277
|
+
resultLines = r.lines;
|
|
278
|
+
changes = r.changes;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case "appendAfter": {
|
|
282
|
+
const r = applyAppendAfter(lines, input.pattern, input.text, input.flags ?? "");
|
|
283
|
+
resultLines = r.lines;
|
|
284
|
+
changes = r.changes;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
default:
|
|
288
|
+
return { relPath, matched: false, changes: 0 };
|
|
289
|
+
}
|
|
290
|
+
// Rejoin and restore line endings. Preserve trailing newline convention.
|
|
291
|
+
const joined = resultLines.join("\n") + (hasTrailingNewline ? "\n" : "");
|
|
292
|
+
const outputContent = restoreLineEndings(joined, lineEndingStyle);
|
|
293
|
+
const matched = changes > 0;
|
|
294
|
+
// Dry run: return output but don't write
|
|
295
|
+
if (input.dryRun) {
|
|
296
|
+
return { relPath, matched, changes, output: outputContent };
|
|
297
|
+
}
|
|
298
|
+
// Write back (with optional header injection)
|
|
299
|
+
try {
|
|
300
|
+
let finalContent = outputContent;
|
|
301
|
+
if (matched && ctx) {
|
|
302
|
+
const syntax = getCommentSyntax(relPath);
|
|
303
|
+
if (syntax) {
|
|
304
|
+
const meta = await incrementFileVersion(root, relPath, ctx.taskId, ctx.subtaskId);
|
|
305
|
+
const injected = injectVersionHeader(finalContent, syntax, meta);
|
|
306
|
+
finalContent = injected.content;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
await fs.writeFile(full, finalContent, "utf8");
|
|
310
|
+
await upsertEntry(root, path.relative(root, full));
|
|
311
|
+
return { relPath, matched, changes };
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return { relPath, matched: false, changes: 0 };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// ── Main Export ──────────────────────────────────────────────────────────
|
|
318
|
+
/**
|
|
319
|
+
* Perform stream-editing operations on workspace files.
|
|
320
|
+
* Supports substitute, delete, deleteRange, insert, and appendAfter operations.
|
|
321
|
+
*/
|
|
322
|
+
export async function sedTool(root, input, ctx) {
|
|
323
|
+
// Validate input
|
|
324
|
+
const validationError = validateInput(input);
|
|
325
|
+
if (validationError) {
|
|
326
|
+
const code = validationError.startsWith("no_target")
|
|
327
|
+
? "no_target"
|
|
328
|
+
: validationError.startsWith("invalid_regex")
|
|
329
|
+
? "invalid_regex"
|
|
330
|
+
: "invalid_operation";
|
|
331
|
+
return { ok: false, code, error: validationError };
|
|
332
|
+
}
|
|
333
|
+
// Resolve target files
|
|
334
|
+
let targetFiles;
|
|
335
|
+
try {
|
|
336
|
+
targetFiles = await resolveTargetFiles(root, input);
|
|
337
|
+
}
|
|
338
|
+
catch (e) {
|
|
339
|
+
return { ok: false, code: "io_error", error: `Failed to resolve target files: ${e.message}` };
|
|
340
|
+
}
|
|
341
|
+
if (targetFiles.length === 0) {
|
|
342
|
+
const target = input.glob ? `glob "${input.glob}"` : `path "${input.path}"`;
|
|
343
|
+
return {
|
|
344
|
+
ok: false,
|
|
345
|
+
code: "file_not_found",
|
|
346
|
+
error: `No files matched ${target}. Check the pattern or run reindex_workspace to refresh the index.`,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
// When using a single path (not glob), verify the file actually exists
|
|
350
|
+
// on disk before proceeding.
|
|
351
|
+
if (!input.glob && input.path) {
|
|
352
|
+
try {
|
|
353
|
+
const full = resolveInWorkspace(root, input.path);
|
|
354
|
+
try {
|
|
355
|
+
await fs.access(full);
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
code: "file_not_found",
|
|
361
|
+
error: `File "${input.path}" does not exist in the workspace.`,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return {
|
|
367
|
+
ok: false,
|
|
368
|
+
code: "workspace_escape",
|
|
369
|
+
error: `Path "${input.path}" resolves outside the workspace root.`,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Process each file
|
|
374
|
+
const results = [];
|
|
375
|
+
let workspaceEscape = false;
|
|
376
|
+
for (const relPath of targetFiles) {
|
|
377
|
+
// Check for workspace escape
|
|
378
|
+
try {
|
|
379
|
+
resolveInWorkspace(root, relPath);
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
workspaceEscape = true;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const posixPath = toPosixPath(relPath).replace(/^\.\//, "");
|
|
386
|
+
try {
|
|
387
|
+
const result = await processFile(root, posixPath, input, ctx);
|
|
388
|
+
results.push(result);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
results.push({ relPath: posixPath, matched: false, changes: 0 });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (workspaceEscape && results.length === 0) {
|
|
395
|
+
return {
|
|
396
|
+
ok: false,
|
|
397
|
+
code: "workspace_escape",
|
|
398
|
+
error: `One or more target paths resolve outside the workspace root.`,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return { ok: true, results };
|
|
402
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared result interface for shell command execution.
|
|
3
|
+
* Used by both shell-tool.ts (exec-based) and runCommandTool.ts (spawn-based).
|
|
4
|
+
*/
|
|
5
|
+
export interface ShellResult {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
exitCode: number | null;
|
|
8
|
+
stdout: string;
|
|
9
|
+
stderr: string;
|
|
10
|
+
timedOut: boolean;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ShellResult } from "./shell-result.js";
|
|
2
|
+
export type { ShellResult } from "./shell-result.js";
|
|
3
|
+
/**
|
|
4
|
+
* Minimal shell-quote-aware split: respects single-quoted and double-quoted
|
|
5
|
+
* strings. Does NOT handle escaped quotes, nested quoting, or advanced
|
|
6
|
+
* features — it just needs to be good enough to split "npm run build" into
|
|
7
|
+
* ["npm", "run", "build"] and preserve "echo 'hello world'" as
|
|
8
|
+
* ["echo", "hello world"].
|
|
9
|
+
*
|
|
10
|
+
* Migrated from runCommandTool.ts. Exported for testing but intended as
|
|
11
|
+
* an internal utility of shell-tool.ts.
|
|
12
|
+
*/
|
|
13
|
+
export declare function splitCommand(command: string): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Windows-specific PATHEXT resolution: wraps .cmd/.bat/.ps1 commands
|
|
16
|
+
* with the appropriate interpreter so they work on Windows even when
|
|
17
|
+
* passed to exec(). This is a transparent safety net — exec() already
|
|
18
|
+
* handles .cmd/.bat natively in many shells, but wrapping ensures
|
|
19
|
+
* consistent behavior across all Windows configurations.
|
|
20
|
+
*
|
|
21
|
+
* On non-Windows platforms this is a no-op.
|
|
22
|
+
*
|
|
23
|
+
* Migrated from runCommandTool.ts. Exported for testing but intended as
|
|
24
|
+
* an internal utility of shell-tool.ts.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveWindowsExecutable(command: string, args: string[]): {
|
|
27
|
+
command: string;
|
|
28
|
+
args: string[];
|
|
29
|
+
};
|
|
30
|
+
export declare function isDenied(command: string): string | null;
|
|
31
|
+
/**
|
|
32
|
+
* The single entry point for executing shell commands via the run_command
|
|
33
|
+
* tool. Internally:
|
|
34
|
+
* 1. Checks the denylist — denied commands return immediately.
|
|
35
|
+
* 2. Splits the command to identify the executable.
|
|
36
|
+
* 3. On Windows, wraps .cmd/.bat/.ps1 executables with the appropriate
|
|
37
|
+
* interpreter via resolveWindowsExecutable().
|
|
38
|
+
* 4. Executes via child_process.exec().
|
|
39
|
+
*
|
|
40
|
+
* @param command - The full command string, e.g. "npm run build".
|
|
41
|
+
* @param cwd - Working directory (the workspace root).
|
|
42
|
+
* @param timeoutMs - Optional timeout in milliseconds (default 120_000).
|
|
43
|
+
* @returns A ShellResult promise.
|
|
44
|
+
*/
|
|
45
|
+
export declare function runCommandTool(command: string, cwd: string, timeoutMs?: number): Promise<ShellResult>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// xcoder:version 2 | xcoder:task task-a73fdc | xcoder:updated 2026-08-03T20:52:06.503Z | xcoder:subtask code-st-dd8b38
|
|
2
|
+
import { exec } from "node:child_process";
|
|
3
|
+
/**
|
|
4
|
+
* A denylist, not a sandbox. This catches the obviously destructive
|
|
5
|
+
* one-liners a model might produce while "cleaning up" or "starting
|
|
6
|
+
* fresh" — it is NOT a security boundary against an adversarial actor.
|
|
7
|
+
* Xcoder SDLC is designed to run each task inside its own disposable container
|
|
8
|
+
* or VM (the same isolation principle Agent Factory's Tool Execution
|
|
9
|
+
* Sandbox uses) — this denylist is a courtesy guardrail on top of that,
|
|
10
|
+
* not a substitute for it.
|
|
11
|
+
*/
|
|
12
|
+
const DENYLIST_PATTERNS = [
|
|
13
|
+
/\brm\s+-rf\s+\/(?:\s|$)/, // rm -rf /
|
|
14
|
+
/\brm\s+-rf\s+~(?:\s|$)/, // rm -rf ~
|
|
15
|
+
/\bmkfs\b/,
|
|
16
|
+
/:\(\)\{.*:\|:.*\};:/, // fork bomb
|
|
17
|
+
/\bdd\s+if=.*of=\/dev\/(sd|nvme|hd)/,
|
|
18
|
+
/\bchmod\s+-R\s+777\s+\//,
|
|
19
|
+
/\bcurl\b.*\|\s*sh\b/, // pipe-to-shell from a URL — force an explicit review step instead
|
|
20
|
+
/>\s*\/dev\/sd/,
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Minimal shell-quote-aware split: respects single-quoted and double-quoted
|
|
24
|
+
* strings. Does NOT handle escaped quotes, nested quoting, or advanced
|
|
25
|
+
* features — it just needs to be good enough to split "npm run build" into
|
|
26
|
+
* ["npm", "run", "build"] and preserve "echo 'hello world'" as
|
|
27
|
+
* ["echo", "hello world"].
|
|
28
|
+
*
|
|
29
|
+
* Migrated from runCommandTool.ts. Exported for testing but intended as
|
|
30
|
+
* an internal utility of shell-tool.ts.
|
|
31
|
+
*/
|
|
32
|
+
export function splitCommand(command) {
|
|
33
|
+
const parts = [];
|
|
34
|
+
let current = "";
|
|
35
|
+
let quote = null;
|
|
36
|
+
for (let i = 0; i < command.length; i++) {
|
|
37
|
+
const ch = command[i];
|
|
38
|
+
if (quote) {
|
|
39
|
+
if (ch === quote) {
|
|
40
|
+
quote = null;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
current += ch;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (ch === "'" || ch === '"') {
|
|
47
|
+
quote = ch;
|
|
48
|
+
}
|
|
49
|
+
else if (ch === " " || ch === "\t") {
|
|
50
|
+
if (current.length > 0) {
|
|
51
|
+
parts.push(current);
|
|
52
|
+
current = "";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
current += ch;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (current.length > 0) {
|
|
60
|
+
parts.push(current);
|
|
61
|
+
}
|
|
62
|
+
return parts;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Windows-specific PATHEXT resolution: wraps .cmd/.bat/.ps1 commands
|
|
66
|
+
* with the appropriate interpreter so they work on Windows even when
|
|
67
|
+
* passed to exec(). This is a transparent safety net — exec() already
|
|
68
|
+
* handles .cmd/.bat natively in many shells, but wrapping ensures
|
|
69
|
+
* consistent behavior across all Windows configurations.
|
|
70
|
+
*
|
|
71
|
+
* On non-Windows platforms this is a no-op.
|
|
72
|
+
*
|
|
73
|
+
* Migrated from runCommandTool.ts. Exported for testing but intended as
|
|
74
|
+
* an internal utility of shell-tool.ts.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveWindowsExecutable(command, args) {
|
|
77
|
+
if (process.platform !== "win32")
|
|
78
|
+
return { command, args };
|
|
79
|
+
const lower = command.toLowerCase();
|
|
80
|
+
if (lower.endsWith(".cmd") || lower.endsWith(".bat")) {
|
|
81
|
+
return { command: "cmd.exe", args: ["/c", command, ...args] };
|
|
82
|
+
}
|
|
83
|
+
if (lower.endsWith(".ps1")) {
|
|
84
|
+
return { command: "powershell.exe", args: ["-File", command, ...args] };
|
|
85
|
+
}
|
|
86
|
+
return { command, args };
|
|
87
|
+
}
|
|
88
|
+
export function isDenied(command) {
|
|
89
|
+
for (const pattern of DENYLIST_PATTERNS) {
|
|
90
|
+
if (pattern.test(command)) {
|
|
91
|
+
return `command matches a denylisted destructive pattern (${pattern}) and was not run.`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The single entry point for executing shell commands via the run_command
|
|
98
|
+
* tool. Internally:
|
|
99
|
+
* 1. Checks the denylist — denied commands return immediately.
|
|
100
|
+
* 2. Splits the command to identify the executable.
|
|
101
|
+
* 3. On Windows, wraps .cmd/.bat/.ps1 executables with the appropriate
|
|
102
|
+
* interpreter via resolveWindowsExecutable().
|
|
103
|
+
* 4. Executes via child_process.exec().
|
|
104
|
+
*
|
|
105
|
+
* @param command - The full command string, e.g. "npm run build".
|
|
106
|
+
* @param cwd - Working directory (the workspace root).
|
|
107
|
+
* @param timeoutMs - Optional timeout in milliseconds (default 120_000).
|
|
108
|
+
* @returns A ShellResult promise.
|
|
109
|
+
*/
|
|
110
|
+
export function runCommandTool(command, cwd, timeoutMs = 120_000) {
|
|
111
|
+
const denyReason = isDenied(command);
|
|
112
|
+
if (denyReason) {
|
|
113
|
+
return Promise.resolve({ ok: false, exitCode: null, stdout: "", stderr: denyReason, timedOut: false });
|
|
114
|
+
}
|
|
115
|
+
// Apply OS-aware executable wrapping for Windows .cmd/.bat/.ps1 commands
|
|
116
|
+
const trimmed = command.trim();
|
|
117
|
+
let finalCommand = trimmed;
|
|
118
|
+
if (trimmed.length > 0 && process.platform === "win32") {
|
|
119
|
+
const parts = splitCommand(trimmed);
|
|
120
|
+
const executable = parts[0];
|
|
121
|
+
const rawArgs = parts.slice(1);
|
|
122
|
+
const { command: resolvedCmd, args } = resolveWindowsExecutable(executable, rawArgs);
|
|
123
|
+
if (resolvedCmd !== executable) {
|
|
124
|
+
// The executable was wrapped — reconstruct the command string
|
|
125
|
+
finalCommand = [resolvedCmd, ...args].map((a) => a.includes(" ") ? `"${a}"` : a).join(" ");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
const child = exec(finalCommand, { cwd, timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
130
|
+
const timedOut = !!error && error.killed === true && error.signal === "SIGTERM";
|
|
131
|
+
resolve({
|
|
132
|
+
ok: !error,
|
|
133
|
+
exitCode: error ? (typeof error.code === "number" ? error.code : 1) : 0,
|
|
134
|
+
stdout,
|
|
135
|
+
stderr,
|
|
136
|
+
timedOut,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
void child;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface SitePage {
|
|
2
|
+
url: string;
|
|
3
|
+
title: string;
|
|
4
|
+
depth: number;
|
|
5
|
+
links: string[];
|
|
6
|
+
status: number;
|
|
7
|
+
}
|
|
8
|
+
export interface SiteMap {
|
|
9
|
+
rootUrl: string;
|
|
10
|
+
totalPages: number;
|
|
11
|
+
maxDepth: number;
|
|
12
|
+
pages: SitePage[];
|
|
13
|
+
/** Adjacency list: page URL -> list of linked page URLs (internal only) */
|
|
14
|
+
graph: Record<string, string[]>;
|
|
15
|
+
/** Breadcrumb / hierarchy: page URL -> parent URL (first discovered parent) */
|
|
16
|
+
parent: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Crawl a URL and build a site map of all discoverable internal pages.
|
|
20
|
+
*
|
|
21
|
+
* Uses BFS to traverse same-domain links up to `maxPages` pages.
|
|
22
|
+
* Respects `maxDepth` to limit how deep the crawl goes.
|
|
23
|
+
*/
|
|
24
|
+
export declare function crawlSiteMap(url: string, options?: {
|
|
25
|
+
maxPages?: number;
|
|
26
|
+
maxDepth?: number;
|
|
27
|
+
sameDomain?: boolean;
|
|
28
|
+
}): Promise<SiteMap>;
|
|
29
|
+
/**
|
|
30
|
+
* Format the site map as a human-readable tree string.
|
|
31
|
+
*/
|
|
32
|
+
export declare function formatSiteMap(siteMap: SiteMap): string;
|