orch-code 0.1.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.
Files changed (116) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +624 -0
  4. package/cmd/apply.go +111 -0
  5. package/cmd/auth.go +393 -0
  6. package/cmd/auth_test.go +100 -0
  7. package/cmd/diff.go +57 -0
  8. package/cmd/doctor.go +149 -0
  9. package/cmd/explain.go +192 -0
  10. package/cmd/explain_test.go +62 -0
  11. package/cmd/init.go +100 -0
  12. package/cmd/interactive.go +1372 -0
  13. package/cmd/interactive_input.go +45 -0
  14. package/cmd/interactive_input_test.go +55 -0
  15. package/cmd/logs.go +72 -0
  16. package/cmd/model.go +84 -0
  17. package/cmd/plan.go +149 -0
  18. package/cmd/provider.go +189 -0
  19. package/cmd/provider_model_doctor_test.go +91 -0
  20. package/cmd/root.go +67 -0
  21. package/cmd/run.go +123 -0
  22. package/cmd/run_engine.go +208 -0
  23. package/cmd/run_engine_test.go +30 -0
  24. package/cmd/session.go +589 -0
  25. package/cmd/session_helpers.go +54 -0
  26. package/cmd/session_integration_test.go +30 -0
  27. package/cmd/session_list_current_test.go +87 -0
  28. package/cmd/session_messages_test.go +163 -0
  29. package/cmd/session_runs_test.go +68 -0
  30. package/cmd/sprint1_integration_test.go +119 -0
  31. package/cmd/stats.go +173 -0
  32. package/cmd/stats_test.go +71 -0
  33. package/cmd/version.go +4 -0
  34. package/go.mod +45 -0
  35. package/go.sum +108 -0
  36. package/internal/agents/agent.go +31 -0
  37. package/internal/agents/coder.go +167 -0
  38. package/internal/agents/planner.go +155 -0
  39. package/internal/agents/reviewer.go +118 -0
  40. package/internal/agents/runtime.go +25 -0
  41. package/internal/agents/runtime_test.go +77 -0
  42. package/internal/auth/account.go +78 -0
  43. package/internal/auth/oauth.go +523 -0
  44. package/internal/auth/store.go +287 -0
  45. package/internal/confidence/policy.go +174 -0
  46. package/internal/confidence/policy_test.go +71 -0
  47. package/internal/confidence/scorer.go +253 -0
  48. package/internal/confidence/scorer_test.go +83 -0
  49. package/internal/config/config.go +331 -0
  50. package/internal/config/config_defaults_test.go +138 -0
  51. package/internal/execution/contract_builder.go +160 -0
  52. package/internal/execution/contract_builder_test.go +68 -0
  53. package/internal/execution/plan_compliance.go +161 -0
  54. package/internal/execution/plan_compliance_test.go +71 -0
  55. package/internal/execution/retry_directive.go +132 -0
  56. package/internal/execution/scope_guard.go +69 -0
  57. package/internal/logger/logger.go +120 -0
  58. package/internal/models/contracts_test.go +100 -0
  59. package/internal/models/models.go +269 -0
  60. package/internal/orchestrator/orchestrator.go +701 -0
  61. package/internal/orchestrator/orchestrator_retry_test.go +135 -0
  62. package/internal/orchestrator/review_engine_test.go +50 -0
  63. package/internal/orchestrator/state.go +42 -0
  64. package/internal/orchestrator/test_classifier_test.go +68 -0
  65. package/internal/patch/applier.go +131 -0
  66. package/internal/patch/applier_test.go +25 -0
  67. package/internal/patch/parser.go +89 -0
  68. package/internal/patch/patch.go +60 -0
  69. package/internal/patch/summary.go +30 -0
  70. package/internal/patch/validator.go +104 -0
  71. package/internal/planning/normalizer.go +416 -0
  72. package/internal/planning/normalizer_test.go +64 -0
  73. package/internal/providers/errors.go +35 -0
  74. package/internal/providers/openai/client.go +498 -0
  75. package/internal/providers/openai/client_test.go +187 -0
  76. package/internal/providers/provider.go +47 -0
  77. package/internal/providers/registry.go +32 -0
  78. package/internal/providers/registry_test.go +57 -0
  79. package/internal/providers/router.go +52 -0
  80. package/internal/providers/state.go +114 -0
  81. package/internal/providers/state_test.go +64 -0
  82. package/internal/repo/analyzer.go +188 -0
  83. package/internal/repo/context.go +83 -0
  84. package/internal/review/engine.go +267 -0
  85. package/internal/review/engine_test.go +103 -0
  86. package/internal/runstore/store.go +137 -0
  87. package/internal/runstore/store_test.go +59 -0
  88. package/internal/runtime/lock.go +150 -0
  89. package/internal/runtime/lock_test.go +57 -0
  90. package/internal/session/compaction.go +260 -0
  91. package/internal/session/compaction_test.go +36 -0
  92. package/internal/session/service.go +117 -0
  93. package/internal/session/service_test.go +113 -0
  94. package/internal/storage/storage.go +1498 -0
  95. package/internal/storage/storage_test.go +413 -0
  96. package/internal/testing/classifier.go +80 -0
  97. package/internal/testing/classifier_test.go +36 -0
  98. package/internal/tools/command.go +160 -0
  99. package/internal/tools/command_test.go +56 -0
  100. package/internal/tools/file.go +111 -0
  101. package/internal/tools/git.go +77 -0
  102. package/internal/tools/invalid_params_test.go +36 -0
  103. package/internal/tools/policy.go +98 -0
  104. package/internal/tools/policy_test.go +36 -0
  105. package/internal/tools/registry_test.go +52 -0
  106. package/internal/tools/result.go +30 -0
  107. package/internal/tools/search.go +86 -0
  108. package/internal/tools/tool.go +94 -0
  109. package/main.go +9 -0
  110. package/npm/orch.js +25 -0
  111. package/package.json +41 -0
  112. package/scripts/changelog.js +20 -0
  113. package/scripts/check-release-version.js +21 -0
  114. package/scripts/lib/release-utils.js +223 -0
  115. package/scripts/postinstall.js +157 -0
  116. package/scripts/release.js +52 -0
@@ -0,0 +1,56 @@
1
+ package tools
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ func TestRunCommandBlocksRiskyCommandWithoutApproval(t *testing.T) {
9
+ tool := NewRunCommandTool(t.TempDir())
10
+
11
+ result, err := tool.Execute(map[string]string{"command": "rm -rf /tmp/orch-test"})
12
+ if err != nil {
13
+ t.Fatalf("unexpected execute error: %v", err)
14
+ }
15
+ if result.Success {
16
+ t.Fatalf("expected risky command to be blocked")
17
+ }
18
+ if result.ErrorCode != ErrCodePolicyBlocked {
19
+ t.Fatalf("unexpected error code: %s", result.ErrorCode)
20
+ }
21
+ }
22
+
23
+ func TestRunCommandTimesOut(t *testing.T) {
24
+ tool := NewRunCommandTool(t.TempDir())
25
+
26
+ result, err := tool.Execute(map[string]string{
27
+ "command": "sleep 2",
28
+ "timeout_seconds": "1",
29
+ })
30
+ if err != nil {
31
+ t.Fatalf("unexpected execute error: %v", err)
32
+ }
33
+ if result.Success {
34
+ t.Fatalf("expected timeout failure")
35
+ }
36
+ if result.ErrorCode != ErrCodeTimeout {
37
+ t.Fatalf("unexpected timeout code: %s", result.ErrorCode)
38
+ }
39
+ }
40
+
41
+ func TestRunCommandTruncatesLargeOutputToFile(t *testing.T) {
42
+ tool := NewRunCommandTool(t.TempDir())
43
+
44
+ result, err := tool.Execute(map[string]string{
45
+ "command": "seq 1 25000",
46
+ })
47
+ if err != nil {
48
+ t.Fatalf("unexpected execute error: %v", err)
49
+ }
50
+ if !result.Success {
51
+ t.Fatalf("expected command success, got error: %s", result.Error)
52
+ }
53
+ if result.Metadata == nil || strings.TrimSpace(result.Metadata["output_file"]) == "" {
54
+ t.Fatalf("expected truncated output metadata with output_file")
55
+ }
56
+ }
@@ -0,0 +1,111 @@
1
+ package tools
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "path/filepath"
7
+ "strings"
8
+
9
+ "github.com/furkanbeydemir/orch/internal/models"
10
+ )
11
+
12
+ type ReadFileTool struct {
13
+ repoRoot string
14
+ }
15
+
16
+ func NewReadFileTool(repoRoot string) *ReadFileTool {
17
+ return &ReadFileTool{repoRoot: repoRoot}
18
+ }
19
+
20
+ func (t *ReadFileTool) Name() string { return "read_file" }
21
+
22
+ func (t *ReadFileTool) Description() string { return "Reads contents of the specified file" }
23
+
24
+ func (t *ReadFileTool) Execute(params map[string]string) (*models.ToolResult, error) {
25
+ path, ok := params["path"]
26
+ if !ok {
27
+ return Failure("read_file", ErrCodeInvalidParams, "read_file: 'path' parameter is required", ""), nil
28
+ }
29
+
30
+ fullPath := filepath.Join(t.repoRoot, path)
31
+ data, err := os.ReadFile(fullPath)
32
+ if err != nil {
33
+ return Failure("read_file", ErrCodeExecution, err.Error(), ""), nil
34
+ }
35
+
36
+ return Success("read_file", string(data)), nil
37
+ }
38
+
39
+ type WriteFileTool struct {
40
+ repoRoot string
41
+ }
42
+
43
+ func NewWriteFileTool(repoRoot string) *WriteFileTool {
44
+ return &WriteFileTool{repoRoot: repoRoot}
45
+ }
46
+
47
+ func (t *WriteFileTool) Name() string { return "write_file" }
48
+
49
+ func (t *WriteFileTool) Description() string { return "Writes content to the specified file" }
50
+
51
+ func (t *WriteFileTool) Execute(params map[string]string) (*models.ToolResult, error) {
52
+ path, ok := params["path"]
53
+ if !ok {
54
+ return Failure("write_file", ErrCodeInvalidParams, "write_file: 'path' parameter is required", ""), nil
55
+ }
56
+
57
+ content, ok := params["content"]
58
+ if !ok {
59
+ return Failure("write_file", ErrCodeInvalidParams, "write_file: 'content' parameter is required", ""), nil
60
+ }
61
+
62
+ fullPath := filepath.Join(t.repoRoot, path)
63
+
64
+ dir := filepath.Dir(fullPath)
65
+ if err := os.MkdirAll(dir, 0o755); err != nil {
66
+ return Failure("write_file", ErrCodeExecution, err.Error(), ""), nil
67
+ }
68
+
69
+ if err := os.WriteFile(fullPath, []byte(content), 0o644); err != nil {
70
+ return Failure("write_file", ErrCodeExecution, err.Error(), ""), nil
71
+ }
72
+
73
+ return Success("write_file", fmt.Sprintf("File written: %s", path)), nil
74
+ }
75
+
76
+ type ListFilesTool struct {
77
+ repoRoot string
78
+ }
79
+
80
+ func NewListFilesTool(repoRoot string) *ListFilesTool {
81
+ return &ListFilesTool{repoRoot: repoRoot}
82
+ }
83
+
84
+ func (t *ListFilesTool) Name() string { return "list_files" }
85
+
86
+ func (t *ListFilesTool) Description() string { return "Lists files in the specified directory" }
87
+
88
+ // Params: "path" optional directory path (default: ".").
89
+ func (t *ListFilesTool) Execute(params map[string]string) (*models.ToolResult, error) {
90
+ path := params["path"]
91
+ if path == "" {
92
+ path = "."
93
+ }
94
+
95
+ fullPath := filepath.Join(t.repoRoot, path)
96
+ entries, err := os.ReadDir(fullPath)
97
+ if err != nil {
98
+ return Failure("list_files", ErrCodeExecution, err.Error(), ""), nil
99
+ }
100
+
101
+ var files []string
102
+ for _, entry := range entries {
103
+ prefix := "F"
104
+ if entry.IsDir() {
105
+ prefix = "D"
106
+ }
107
+ files = append(files, fmt.Sprintf("[%s] %s", prefix, entry.Name()))
108
+ }
109
+
110
+ return Success("list_files", strings.Join(files, "\n")), nil
111
+ }
@@ -0,0 +1,77 @@
1
+ // - ApplyPatch: patch application
2
+ package tools
3
+
4
+ import (
5
+ "os/exec"
6
+ "strings"
7
+
8
+ "github.com/furkanbeydemir/orch/internal/models"
9
+ )
10
+
11
+ type GitDiffTool struct {
12
+ repoRoot string
13
+ }
14
+
15
+ func NewGitDiffTool(repoRoot string) *GitDiffTool {
16
+ return &GitDiffTool{repoRoot: repoRoot}
17
+ }
18
+
19
+ func (t *GitDiffTool) Name() string { return "git_diff" }
20
+
21
+ func (t *GitDiffTool) Description() string { return "Produces git diff output" }
22
+
23
+ func (t *GitDiffTool) Execute(params map[string]string) (*models.ToolResult, error) {
24
+ args := []string{"diff"}
25
+ if path, ok := params["path"]; ok && path != "" {
26
+ args = append(args, "--", path)
27
+ }
28
+
29
+ cmd := exec.Command("git", args...)
30
+ cmd.Dir = t.repoRoot
31
+
32
+ output, err := cmd.CombinedOutput()
33
+ if err != nil {
34
+ return Failure("git_diff", ErrCodeExecution, err.Error(), string(output)), nil
35
+ }
36
+
37
+ return Success("git_diff", string(output)), nil
38
+ }
39
+
40
+ type ApplyPatchTool struct {
41
+ repoRoot string
42
+ }
43
+
44
+ func NewApplyPatchTool(repoRoot string) *ApplyPatchTool {
45
+ return &ApplyPatchTool{repoRoot: repoRoot}
46
+ }
47
+
48
+ func (t *ApplyPatchTool) Name() string { return "apply_patch" }
49
+
50
+ func (t *ApplyPatchTool) Description() string { return "Applies a patch with git" }
51
+
52
+ // Execute applies a patch using git apply.
53
+ func (t *ApplyPatchTool) Execute(params map[string]string) (*models.ToolResult, error) {
54
+ patchContent, ok := params["patch"]
55
+ if !ok {
56
+ return Failure("apply_patch", ErrCodeInvalidParams, "apply_patch: 'patch' parameter is required", ""), nil
57
+ }
58
+
59
+ args := []string{"apply"}
60
+
61
+ if dryRun, exists := params["dry_run"]; exists && dryRun == "true" {
62
+ args = append(args, "--check")
63
+ }
64
+
65
+ args = append(args, "-")
66
+
67
+ cmd := exec.Command("git", args...)
68
+ cmd.Dir = t.repoRoot
69
+ cmd.Stdin = strings.NewReader(patchContent)
70
+
71
+ output, err := cmd.CombinedOutput()
72
+ if err != nil {
73
+ return Failure("apply_patch", ErrCodeExecution, err.Error(), string(output)), nil
74
+ }
75
+
76
+ return Success("apply_patch", "Patch applied successfully"), nil
77
+ }
@@ -0,0 +1,36 @@
1
+ package tools
2
+
3
+ import "testing"
4
+
5
+ func TestToolsReturnInvalidParamsContract(t *testing.T) {
6
+ repoRoot := t.TempDir()
7
+
8
+ tests := []struct {
9
+ name string
10
+ tool Tool
11
+ }{
12
+ {name: "read_file", tool: NewReadFileTool(repoRoot)},
13
+ {name: "write_file", tool: NewWriteFileTool(repoRoot)},
14
+ {name: "search_code", tool: NewSearchCodeTool(repoRoot)},
15
+ {name: "run_command", tool: NewRunCommandTool(repoRoot)},
16
+ {name: "apply_patch", tool: NewApplyPatchTool(repoRoot)},
17
+ }
18
+
19
+ for _, tc := range tests {
20
+ t.Run(tc.name, func(t *testing.T) {
21
+ result, err := tc.tool.Execute(map[string]string{})
22
+ if err != nil {
23
+ t.Fatalf("unexpected execute error: %v", err)
24
+ }
25
+ if result == nil {
26
+ t.Fatalf("expected tool result")
27
+ }
28
+ if result.Success {
29
+ t.Fatalf("expected failure result for invalid params")
30
+ }
31
+ if result.ErrorCode != ErrCodeInvalidParams {
32
+ t.Fatalf("unexpected error code: %s", result.ErrorCode)
33
+ }
34
+ })
35
+ }
36
+ }
@@ -0,0 +1,98 @@
1
+ package tools
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+
7
+ "github.com/furkanbeydemir/orch/internal/models"
8
+ )
9
+
10
+ const (
11
+ ModeRun = "run"
12
+ ModePlan = "plan"
13
+ )
14
+
15
+ type Policy struct {
16
+ Mode string
17
+ RequireDestructiveApproval bool
18
+ }
19
+
20
+ type policyDecision struct {
21
+ allowed bool
22
+ reason string
23
+ }
24
+
25
+ func (p Policy) Decide(toolName string, params map[string]string) policyDecision {
26
+ if p.Mode == "" {
27
+ p.Mode = ModeRun
28
+ }
29
+
30
+ if p.Mode == ModePlan {
31
+ switch toolName {
32
+ case "write_file", "apply_patch", "run_command":
33
+ return policyDecision{
34
+ allowed: false,
35
+ reason: fmt.Sprintf("%s blocked in plan mode (read-only)", toolName),
36
+ }
37
+ }
38
+ }
39
+
40
+ if p.RequireDestructiveApproval && isDestructiveTool(toolName) {
41
+ if strings.TrimSpace(params["approved"]) != "true" {
42
+ return policyDecision{
43
+ allowed: false,
44
+ reason: fmt.Sprintf("%s requires explicit approval (set approved=true)", toolName),
45
+ }
46
+ }
47
+ }
48
+
49
+ return policyDecision{allowed: true}
50
+ }
51
+
52
+ func isDestructiveTool(toolName string) bool {
53
+ switch toolName {
54
+ case "write_file", "apply_patch", "run_command":
55
+ return true
56
+ default:
57
+ return false
58
+ }
59
+ }
60
+
61
+ type guardedTool struct {
62
+ inner Tool
63
+ policy Policy
64
+ logf func(string)
65
+ }
66
+
67
+ func (t *guardedTool) Name() string {
68
+ return t.inner.Name()
69
+ }
70
+
71
+ func (t *guardedTool) Description() string {
72
+ return t.inner.Description()
73
+ }
74
+
75
+ func (t *guardedTool) Execute(params map[string]string) (*models.ToolResult, error) {
76
+ decision := t.policy.Decide(t.inner.Name(), params)
77
+ t.logDecision(t.inner.Name(), decision)
78
+ if !decision.allowed {
79
+ return Failure(t.inner.Name(), ErrCodePolicyBlocked, decision.reason, ""), nil
80
+ }
81
+
82
+ return t.inner.Execute(params)
83
+ }
84
+
85
+ func (t *guardedTool) logDecision(toolName string, decision policyDecision) {
86
+ if t.logf == nil {
87
+ return
88
+ }
89
+ result := "allow"
90
+ if !decision.allowed {
91
+ result = "deny"
92
+ }
93
+ message := fmt.Sprintf("policy decision tool=%s mode=%s result=%s", toolName, t.policy.Mode, result)
94
+ if decision.reason != "" {
95
+ message = fmt.Sprintf("%s reason=%s", message, decision.reason)
96
+ }
97
+ t.logf(message)
98
+ }
@@ -0,0 +1,36 @@
1
+ package tools
2
+
3
+ import "testing"
4
+
5
+ func TestPolicyPlanModeBlocksDestructiveTools(t *testing.T) {
6
+ policy := Policy{Mode: ModePlan}
7
+
8
+ decision := policy.Decide("write_file", map[string]string{"path": "a.txt"})
9
+ if decision.allowed {
10
+ t.Fatalf("expected write_file to be blocked in plan mode")
11
+ }
12
+
13
+ decision = policy.Decide("apply_patch", map[string]string{"patch": "diff --git a/a b/a"})
14
+ if decision.allowed {
15
+ t.Fatalf("expected apply_patch to be blocked in plan mode")
16
+ }
17
+
18
+ decision = policy.Decide("run_command", map[string]string{"command": "rm -rf /tmp/x"})
19
+ if decision.allowed {
20
+ t.Fatalf("expected run_command to be blocked in plan mode")
21
+ }
22
+ }
23
+
24
+ func TestPolicyRequiresApprovalForDestructiveTools(t *testing.T) {
25
+ policy := Policy{Mode: ModeRun, RequireDestructiveApproval: true}
26
+
27
+ decision := policy.Decide("write_file", map[string]string{"path": "a.txt"})
28
+ if decision.allowed {
29
+ t.Fatalf("expected write_file to require approval")
30
+ }
31
+
32
+ decision = policy.Decide("write_file", map[string]string{"path": "a.txt", "approved": "true"})
33
+ if !decision.allowed {
34
+ t.Fatalf("expected write_file to be allowed with approval")
35
+ }
36
+ }
@@ -0,0 +1,52 @@
1
+ package tools
2
+
3
+ import (
4
+ "errors"
5
+ "testing"
6
+
7
+ "github.com/furkanbeydemir/orch/internal/models"
8
+ )
9
+
10
+ func TestRegistryExecuteReturnsStructuredNotFound(t *testing.T) {
11
+ reg := NewRegistry()
12
+
13
+ result, err := reg.Execute("missing_tool", map[string]string{})
14
+ if err != nil {
15
+ t.Fatalf("unexpected error: %v", err)
16
+ }
17
+ if result == nil {
18
+ t.Fatalf("expected result")
19
+ }
20
+ if result.Success {
21
+ t.Fatalf("expected failure result")
22
+ }
23
+ if result.ErrorCode != ErrCodeToolNotFound {
24
+ t.Fatalf("unexpected error code: %s", result.ErrorCode)
25
+ }
26
+ }
27
+
28
+ func TestRegistryExecuteWrapsToolErrors(t *testing.T) {
29
+ reg := NewRegistry()
30
+ reg.Register(&failingTool{})
31
+
32
+ result, err := reg.Execute("failing_tool", map[string]string{})
33
+ if err != nil {
34
+ t.Fatalf("unexpected error: %v", err)
35
+ }
36
+ if result.Success {
37
+ t.Fatalf("expected failure result")
38
+ }
39
+ if result.ErrorCode != ErrCodeExecution {
40
+ t.Fatalf("unexpected error code: %s", result.ErrorCode)
41
+ }
42
+ }
43
+
44
+ type failingTool struct{}
45
+
46
+ func (f *failingTool) Name() string { return "failing_tool" }
47
+
48
+ func (f *failingTool) Description() string { return "fails intentionally" }
49
+
50
+ func (f *failingTool) Execute(params map[string]string) (*models.ToolResult, error) {
51
+ return nil, errors.New("boom")
52
+ }
@@ -0,0 +1,30 @@
1
+ package tools
2
+
3
+ import "github.com/furkanbeydemir/orch/internal/models"
4
+
5
+ const (
6
+ ErrCodeInvalidParams = "invalid_params"
7
+ ErrCodeExecution = "execution_error"
8
+ ErrCodePolicyBlocked = "policy_blocked"
9
+ ErrCodeTimeout = "timeout"
10
+ ErrCodeOutputTrunc = "output_truncated"
11
+ ErrCodeToolNotFound = "tool_not_found"
12
+ )
13
+
14
+ func Success(toolName, output string) *models.ToolResult {
15
+ return &models.ToolResult{
16
+ ToolName: toolName,
17
+ Success: true,
18
+ Output: output,
19
+ }
20
+ }
21
+
22
+ func Failure(toolName, code, message, output string) *models.ToolResult {
23
+ return &models.ToolResult{
24
+ ToolName: toolName,
25
+ Success: false,
26
+ Output: output,
27
+ Error: message,
28
+ ErrorCode: code,
29
+ }
30
+ }
@@ -0,0 +1,86 @@
1
+ package tools
2
+
3
+ import (
4
+ "bufio"
5
+ "fmt"
6
+ "os"
7
+ "path/filepath"
8
+ "strings"
9
+
10
+ "github.com/furkanbeydemir/orch/internal/models"
11
+ )
12
+
13
+ type SearchCodeTool struct {
14
+ repoRoot string
15
+ }
16
+
17
+ func NewSearchCodeTool(repoRoot string) *SearchCodeTool {
18
+ return &SearchCodeTool{repoRoot: repoRoot}
19
+ }
20
+
21
+ func (t *SearchCodeTool) Name() string { return "search_code" }
22
+
23
+ func (t *SearchCodeTool) Description() string {
24
+ return "Searches text/patterns in repository"
25
+ }
26
+
27
+ // Execute searches text in repository files.
28
+ // Params: "query" text to search, "path" optional search root.
29
+ func (t *SearchCodeTool) Execute(params map[string]string) (*models.ToolResult, error) {
30
+ query, ok := params["query"]
31
+ if !ok {
32
+ return Failure("search_code", ErrCodeInvalidParams, "search_code: 'query' parameter is required", ""), nil
33
+ }
34
+
35
+ searchPath := params["path"]
36
+ if searchPath == "" {
37
+ searchPath = "."
38
+ }
39
+
40
+ fullPath := filepath.Join(t.repoRoot, searchPath)
41
+ var results []string
42
+
43
+ err := filepath.Walk(fullPath, func(path string, info os.FileInfo, err error) error {
44
+ if err != nil {
45
+ return nil
46
+ }
47
+
48
+ if info.IsDir() {
49
+ name := info.Name()
50
+ if strings.HasPrefix(name, ".") || name == "node_modules" || name == "vendor" {
51
+ return filepath.SkipDir
52
+ }
53
+ return nil
54
+ }
55
+
56
+ file, err := os.Open(path)
57
+ if err != nil {
58
+ return nil
59
+ }
60
+ defer file.Close()
61
+
62
+ relPath, _ := filepath.Rel(t.repoRoot, path)
63
+ ext := strings.ToLower(filepath.Ext(path))
64
+ if ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".gif" || ext == ".pdf" || ext == ".zip" {
65
+ return nil
66
+ }
67
+ scanner := bufio.NewScanner(file)
68
+ lineNum := 0
69
+
70
+ for scanner.Scan() {
71
+ lineNum++
72
+ line := scanner.Text()
73
+ if strings.Contains(strings.ToLower(line), strings.ToLower(query)) {
74
+ results = append(results, fmt.Sprintf("%s:%d: %s", relPath, lineNum, strings.TrimSpace(line)))
75
+ }
76
+ }
77
+
78
+ return nil
79
+ })
80
+
81
+ if err != nil {
82
+ return Failure("search_code", ErrCodeExecution, err.Error(), ""), nil
83
+ }
84
+
85
+ return Success("search_code", strings.Join(results, "\n")), nil
86
+ }
@@ -0,0 +1,94 @@
1
+ // - ReadFile: file reading
2
+ // - WriteFile: file writing
3
+ // - ListFiles: file listing
4
+ // - SearchCode: code search
5
+ // - ApplyPatch: patch application
6
+ package tools
7
+
8
+ import (
9
+ "fmt"
10
+
11
+ "github.com/furkanbeydemir/orch/internal/models"
12
+ )
13
+
14
+ type Tool interface {
15
+ Name() string
16
+
17
+ Description() string
18
+
19
+ Execute(params map[string]string) (*models.ToolResult, error)
20
+ }
21
+
22
+ type Registry struct {
23
+ tools map[string]Tool
24
+ }
25
+
26
+ func NewRegistry() *Registry {
27
+ return &Registry{
28
+ tools: make(map[string]Tool),
29
+ }
30
+ }
31
+
32
+ func (r *Registry) Register(tool Tool) {
33
+ r.tools[tool.Name()] = tool
34
+ }
35
+
36
+ func (r *Registry) Get(name string) (Tool, error) {
37
+ tool, ok := r.tools[name]
38
+ if !ok {
39
+ return nil, fmt.Errorf("tool not found: %s", name)
40
+ }
41
+ return tool, nil
42
+ }
43
+
44
+ func (r *Registry) Execute(name string, params map[string]string) (*models.ToolResult, error) {
45
+ tool, ok := r.tools[name]
46
+ if !ok {
47
+ return Failure(name, ErrCodeToolNotFound, fmt.Sprintf("tool not found: %s", name), ""), nil
48
+ }
49
+
50
+ result, err := tool.Execute(params)
51
+ if err != nil {
52
+ return Failure(name, ErrCodeExecution, err.Error(), ""), nil
53
+ }
54
+
55
+ if result == nil {
56
+ return Failure(name, ErrCodeExecution, "tool returned nil result", ""), nil
57
+ }
58
+
59
+ return result, nil
60
+ }
61
+
62
+ func (r *Registry) List() []string {
63
+ names := make([]string, 0, len(r.tools))
64
+ for name := range r.tools {
65
+ names = append(names, name)
66
+ }
67
+ return names
68
+ }
69
+
70
+ func DefaultRegistry(repoRoot string) *Registry {
71
+ return DefaultRegistryWithPolicy(repoRoot, Policy{Mode: ModeRun}, nil)
72
+ }
73
+
74
+ func DefaultRegistryWithPolicy(repoRoot string, policy Policy, logf func(string)) *Registry {
75
+ reg := NewRegistry()
76
+
77
+ reg.Register(wrapWithPolicy(NewReadFileTool(repoRoot), policy, logf))
78
+ reg.Register(wrapWithPolicy(NewWriteFileTool(repoRoot), policy, logf))
79
+ reg.Register(wrapWithPolicy(NewListFilesTool(repoRoot), policy, logf))
80
+
81
+ reg.Register(wrapWithPolicy(NewSearchCodeTool(repoRoot), policy, logf))
82
+
83
+ reg.Register(wrapWithPolicy(NewRunCommandTool(repoRoot), policy, logf))
84
+ reg.Register(wrapWithPolicy(NewRunTestsTool(repoRoot), policy, logf))
85
+
86
+ reg.Register(wrapWithPolicy(NewGitDiffTool(repoRoot), policy, logf))
87
+ reg.Register(wrapWithPolicy(NewApplyPatchTool(repoRoot), policy, logf))
88
+
89
+ return reg
90
+ }
91
+
92
+ func wrapWithPolicy(tool Tool, policy Policy, logf func(string)) Tool {
93
+ return &guardedTool{inner: tool, policy: policy, logf: logf}
94
+ }
package/main.go ADDED
@@ -0,0 +1,9 @@
1
+ package main
2
+
3
+ import (
4
+ "github.com/furkanbeydemir/orch/cmd"
5
+ )
6
+
7
+ func main() {
8
+ cmd.Execute()
9
+ }