salmon-loop 0.4.1 → 0.5.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/dist/cli/authorization/provider.js +2 -10
- package/dist/cli/commands/config.js +2 -2
- package/dist/cli/commands/mode.js +2 -2
- package/dist/cli/commands/run/handler.js +3 -1
- package/dist/cli/commands/run/loop-params.js +1 -0
- package/dist/cli/commands/run/runtime-options.js +3 -1
- package/dist/cli/config.js +0 -8
- package/dist/cli/locales/en.js +2 -2
- package/dist/cli/reporters/standard.js +10 -0
- package/dist/core/adapters/fs/file-adapter.js +3 -1
- package/dist/core/adapters/git/git-adapter.js +6 -3
- package/dist/core/adapters/git/git-runner.js +5 -2
- package/dist/core/adapters/git/lock-manager.js +7 -4
- package/dist/core/checkpoint-domain/manifest-store.js +21 -13
- package/dist/core/checkpoint-domain/service.js +3 -1
- package/dist/core/config/limits.js +1 -1
- package/dist/core/config/model-pricing.js +61 -0
- package/dist/core/context/ast/skeleton-extractor.js +225 -0
- package/dist/core/context/ast/source-outline.js +24 -1
- package/dist/core/context/budget/dynamic-adjuster.js +20 -5
- package/dist/core/context/builder.js +7 -3
- package/dist/core/context/cache/store-factory.js +3 -1
- package/dist/core/context/dependencies.js +2 -1
- package/dist/core/context/effectiveness/persistence.js +50 -0
- package/dist/core/context/effectiveness/tracker.js +24 -0
- package/dist/core/context/gatherers/architecture-gatherer.js +2 -1
- package/dist/core/context/gatherers/artifact-gatherer.js +7 -4
- package/dist/core/context/gatherers/ast-gatherer.js +30 -28
- package/dist/core/context/gatherers/git-history-gatherer.js +3 -1
- package/dist/core/context/gatherers/knowledge-gatherer.js +18 -2
- package/dist/core/context/gatherers/metadata-gatherer.js +12 -7
- package/dist/core/context/gatherers/ripgrep-gatherer.js +6 -3
- package/dist/core/context/service.js +4 -2
- package/dist/core/context/steps/context-gather.js +14 -3
- package/dist/core/context/steps/context-targets.js +1 -0
- package/dist/core/context/targeting/target-resolver.js +29 -11
- package/dist/core/context/token/cache.js +5 -2
- package/dist/core/context/truncation/strategies/json.js +5 -2
- package/dist/core/context/truncation/type-detector.js +3 -1
- package/dist/core/extensions/paths.js +2 -2
- package/dist/core/facades/cli-authorization-provider.js +1 -0
- package/dist/core/feedback/parsers.js +290 -1
- package/dist/core/grizzco/dsl/llm-strategy.js +1 -1
- package/dist/core/grizzco/engine/observability/loop-telemetry.js +5 -2
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +15 -3
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +44 -20
- package/dist/core/grizzco/engine/transaction/transaction-runner.js +40 -34
- package/dist/core/grizzco/execution/RejectionManager.js +7 -5
- package/dist/core/grizzco/runtime/apply-back-runtime.js +3 -1
- package/dist/core/grizzco/services/implementations/default/GitConfigService.js +2 -1
- package/dist/core/grizzco/steps/autopilot.js +21 -32
- package/dist/core/grizzco/steps/explore.js +5 -2
- package/dist/core/grizzco/steps/generateReview.js +3 -1
- package/dist/core/grizzco/steps/research.js +3 -1
- package/dist/core/grizzco/steps/verify.js +7 -1
- package/dist/core/grizzco/validation/AstValidationService.js +3 -1
- package/dist/core/history/input-history.js +3 -1
- package/dist/core/intent/chat-intent.js +3 -1
- package/dist/core/llm/ai-sdk/message-mapper.js +13 -8
- package/dist/core/llm/ai-sdk/request-params.js +1 -3
- package/dist/core/llm/ai-sdk/retry-classifier.js +12 -4
- package/dist/core/llm/ai-sdk/retry-executor.js +1 -1
- package/dist/core/llm/errors.js +5 -4
- package/dist/core/llm/retry-utils.js +8 -2
- package/dist/core/llm/stream-utils.js +5 -3
- package/dist/core/llm/sub-agent-factory.js +3 -0
- package/dist/core/mcp/bridge/resource-context-provider.js +3 -1
- package/dist/core/mcp/catalog/discovery.js +3 -1
- package/dist/core/mcp/client/connection-manager.js +4 -2
- package/dist/core/mcp/client/transport-factory.js +7 -3
- package/dist/core/observability/audit-file.js +2 -1
- package/dist/core/observability/audit-trail.js +3 -1
- package/dist/core/observability/logger.js +2 -1
- package/dist/core/observability/monitor.js +24 -0
- package/dist/core/observability/run-outcome-reporter.js +1 -0
- package/dist/core/permission-gate/default-gate.js +5 -8
- package/dist/core/plan/storage.js +7 -4
- package/dist/core/plugin/loader.js +3 -1
- package/dist/core/prompts/registry.js +1 -1
- package/dist/core/prompts/runtime.js +3 -1
- package/dist/core/prompts/templates/system/autopilot_system.hbs +28 -4
- package/dist/core/protocols/a2a/sdk/executor.js +3 -1
- package/dist/core/protocols/a2a/sdk/server.js +3 -1
- package/dist/core/protocols/acp/acp-command-runner.js +7 -6
- package/dist/core/protocols/acp/acp-session-persistence.js +13 -10
- package/dist/core/protocols/acp/formal-agent.js +3 -2
- package/dist/core/protocols/acp/permission-provider.js +3 -2
- package/dist/core/reflection/engine.js +114 -14
- package/dist/core/runtime/batch-runner.js +81 -0
- package/dist/core/runtime/initialize.js +2 -1
- package/dist/core/runtime/loop-finalize.js +3 -0
- package/dist/core/runtime/loop-session-runner.js +5 -0
- package/dist/core/runtime/loop.js +4 -0
- package/dist/core/runtime/paths.js +9 -6
- package/dist/core/runtime/spawn-interactive.js +5 -4
- package/dist/core/security/redaction.js +3 -2
- package/dist/core/session/compression.js +3 -1
- package/dist/core/session/manager.js +2 -1
- package/dist/core/session/pruning-strategy.js +2 -1
- package/dist/core/session/token-tracker.js +11 -4
- package/dist/core/skills/permissions.js +2 -2
- package/dist/core/strata/checkpoint/manager.js +16 -10
- package/dist/core/strata/checkpoint/snapshot-create.js +5 -4
- package/dist/core/strata/checkpoint/snapshot-write-tree.js +7 -3
- package/dist/core/strata/engine/shadow-merge-engine.js +4 -2
- package/dist/core/strata/interaction/file-system-provider.js +2 -1
- package/dist/core/strata/layers/file-state-resolver.js +9 -7
- package/dist/core/strata/layers/immutable-git-layer.js +3 -1
- package/dist/core/strata/layers/shadow-driver/readonly-lock.js +8 -6
- package/dist/core/strata/layers/shadow-driver/shadow-driver.js +2 -1
- package/dist/core/strata/layers/worktree.js +2 -1
- package/dist/core/strata/runtime/environment.js +2 -1
- package/dist/core/strata/runtime/synchronizer.js +18 -17
- package/dist/core/structured-output/json-extract.js +3 -1
- package/dist/core/sub-agent/artifacts/store.js +2 -1
- package/dist/core/sub-agent/core/manager.js +24 -1
- package/dist/core/sub-agent/registry-defaults.js +2 -2
- package/dist/core/sub-agent/summary.js +96 -0
- package/dist/core/sub-agent/tools/task-spawn.js +7 -4
- package/dist/core/target-runtime/profile.js +3 -1
- package/dist/core/tools/audit.js +3 -2
- package/dist/core/tools/budget.js +3 -1
- package/dist/core/tools/builtin/ast.js +144 -0
- package/dist/core/tools/builtin/code-search/backends/powershell.js +3 -1
- package/dist/core/tools/builtin/code-search/backends/rg.js +3 -1
- package/dist/core/tools/builtin/code-search/parse/plain-grep.js +3 -1
- package/dist/core/tools/builtin/code-search/parse/rg-json.js +3 -1
- package/dist/core/tools/builtin/fs.js +76 -1
- package/dist/core/tools/builtin/git.js +242 -0
- package/dist/core/tools/builtin/glob.js +79 -0
- package/dist/core/tools/builtin/index.js +12 -4
- package/dist/core/tools/builtin/knowledge.js +146 -4
- package/dist/core/tools/builtin/proposal.js +3 -1
- package/dist/core/tools/builtin/verify.js +35 -3
- package/dist/core/tools/permissions/permission-rules.js +3 -1
- package/dist/core/tools/router.js +88 -5
- package/dist/core/tools/session.js +10 -5
- package/dist/core/types/batch.js +2 -0
- package/dist/core/utils/sanitizer.js +5 -2
- package/dist/core/utils/serialize.js +5 -2
- package/dist/core/verification/detect-runner.js +86 -0
- package/dist/core/verification/runner.js +76 -0
- package/dist/core/version.js +3 -1
- package/dist/languages/python/index.js +154 -0
- package/dist/locales/en.js +6 -0
- package/package.json +2 -1
|
@@ -3,8 +3,13 @@ import { z } from 'zod';
|
|
|
3
3
|
import { text } from '../../../locales/index.js';
|
|
4
4
|
import { readFile } from '../../adapters/fs/node-fs.js';
|
|
5
5
|
import { AstParser } from '../../ast/parser.js';
|
|
6
|
+
import { extractImportSpecifiers } from '../../context/ast/import-extractor.js';
|
|
7
|
+
import { resolveImportCandidates } from '../../context/ast/module-resolver.js';
|
|
8
|
+
import { getLogger } from '../../observability/logger.js';
|
|
6
9
|
import { tryGetPluginRegistry } from '../../plugin/registry.js';
|
|
10
|
+
import { spawnCommand } from '../../runtime/process-runner.js';
|
|
7
11
|
import { Phase } from '../../types/runtime.js';
|
|
12
|
+
import { normalizePath } from '../../utils/path.js';
|
|
8
13
|
import { pathPrefixResource } from '../parallel/resource-helpers.js';
|
|
9
14
|
export const astDefsRefsSpec = {
|
|
10
15
|
name: 'code.ast',
|
|
@@ -59,4 +64,143 @@ export async function executeAstDefsRefs(input, ctx) {
|
|
|
59
64
|
// Tree deletion is handled by AstParser's cache cleanup logic or explicit delete if needed
|
|
60
65
|
}
|
|
61
66
|
}
|
|
67
|
+
// ── code.find_references ──────────────────────────────────────────────
|
|
68
|
+
const MAX_SCAN_FILES = 30;
|
|
69
|
+
const RG_TIMEOUT_MS = 10_000;
|
|
70
|
+
export const codeFindReferencesSpec = {
|
|
71
|
+
name: 'code.find_references',
|
|
72
|
+
source: 'builtin',
|
|
73
|
+
intent: 'SEARCH',
|
|
74
|
+
description: text.tools.codeFindReferencesDescription,
|
|
75
|
+
riskLevel: 'low',
|
|
76
|
+
sideEffects: ['fs_read'],
|
|
77
|
+
concurrency: 'parallel_ok',
|
|
78
|
+
computeResources: (input, ctx) => [pathPrefixResource(ctx, input.file)],
|
|
79
|
+
inputSchema: z.object({
|
|
80
|
+
file: z.string().describe('Relative path to the file where the symbol is defined'),
|
|
81
|
+
symbol: z.string().describe('The symbol name to find references for'),
|
|
82
|
+
}),
|
|
83
|
+
outputSchema: z.object({
|
|
84
|
+
definition: z
|
|
85
|
+
.object({
|
|
86
|
+
file: z.string(),
|
|
87
|
+
name: z.string(),
|
|
88
|
+
location: z.any(),
|
|
89
|
+
})
|
|
90
|
+
.nullable(),
|
|
91
|
+
references: z.array(z.object({
|
|
92
|
+
file: z.string(),
|
|
93
|
+
name: z.string(),
|
|
94
|
+
location: z.any(),
|
|
95
|
+
})),
|
|
96
|
+
filesScanned: z.number(),
|
|
97
|
+
}),
|
|
98
|
+
allowedPhases: [Phase.CONTEXT, Phase.EXPLORE, Phase.PLAN, Phase.AUTOPILOT],
|
|
99
|
+
};
|
|
100
|
+
export async function executeCodeFindReferences(input, ctx) {
|
|
101
|
+
const repoRoot = ctx.worktreeRoot || ctx.repoRoot;
|
|
102
|
+
const registry = ctx.languagePlugins ?? tryGetPluginRegistry();
|
|
103
|
+
// 1. Parse the target file to find the definition
|
|
104
|
+
const targetPath = join(repoRoot, input.file);
|
|
105
|
+
const targetCode = await readFile(targetPath, 'utf-8');
|
|
106
|
+
const targetLang = registry?.getByExtension(input.file)?.meta.id;
|
|
107
|
+
let definition = null;
|
|
108
|
+
if (targetLang) {
|
|
109
|
+
const tree = await AstParser.parse(targetCode, targetLang);
|
|
110
|
+
const defs = await AstParser.identifyDefinitions(tree, targetLang);
|
|
111
|
+
const match = defs.find((d) => d.name === input.symbol);
|
|
112
|
+
if (match) {
|
|
113
|
+
definition = { file: input.file, name: match.name, location: match.location };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// 2. Collect candidate files via ripgrep (fast pre-filter)
|
|
117
|
+
const candidates = await rgFindCandidates(repoRoot, input.symbol, input.file);
|
|
118
|
+
// 3. Also collect import neighbors of the target file
|
|
119
|
+
const importNeighbors = await resolveImportNeighbors(input.file, targetCode, repoRoot);
|
|
120
|
+
for (const neighbor of importNeighbors) {
|
|
121
|
+
if (!candidates.includes(neighbor) && neighbor !== input.file) {
|
|
122
|
+
candidates.push(neighbor);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// 4. Cap the number of files to scan
|
|
126
|
+
const toScan = candidates.slice(0, MAX_SCAN_FILES);
|
|
127
|
+
// 5. Parse each candidate and find references
|
|
128
|
+
const references = [];
|
|
129
|
+
for (const file of toScan) {
|
|
130
|
+
try {
|
|
131
|
+
const lang = registry?.getByExtension(file)?.meta.id;
|
|
132
|
+
if (!lang)
|
|
133
|
+
continue;
|
|
134
|
+
const fullPath = join(repoRoot, file);
|
|
135
|
+
const code = await readFile(fullPath, 'utf-8');
|
|
136
|
+
const tree = await AstParser.parse(code, lang);
|
|
137
|
+
const refs = await AstParser.identifyReferences(tree, lang);
|
|
138
|
+
for (const ref of refs) {
|
|
139
|
+
if (ref.name === input.symbol) {
|
|
140
|
+
references.push({ file, name: ref.name, location: ref.location });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
// Skip files that can't be parsed
|
|
146
|
+
getLogger().debug(`[CodeAst] Failed to parse file ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
definition,
|
|
151
|
+
references,
|
|
152
|
+
filesScanned: toScan.length,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Use ripgrep to find files that mention the symbol (fast pre-filter).
|
|
157
|
+
* Returns repo-relative paths, excluding the target file itself.
|
|
158
|
+
*/
|
|
159
|
+
async function rgFindCandidates(repoRoot, symbol, excludeFile) {
|
|
160
|
+
let stdout = '';
|
|
161
|
+
try {
|
|
162
|
+
const result = await spawnCommand({
|
|
163
|
+
command: 'rg',
|
|
164
|
+
args: ['--files-with-matches', '--fixed-strings', '--max-count', '1', symbol, '.'],
|
|
165
|
+
cwd: repoRoot,
|
|
166
|
+
env: process.env,
|
|
167
|
+
timeoutMs: RG_TIMEOUT_MS,
|
|
168
|
+
onStdoutChunk: (chunk) => {
|
|
169
|
+
stdout += Buffer.from(chunk).toString();
|
|
170
|
+
},
|
|
171
|
+
onStderrChunk: () => { },
|
|
172
|
+
});
|
|
173
|
+
if (result.error || result.timedOut || (result.code !== 0 && result.code !== 1)) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
const normalizedExclude = normalizePath(excludeFile).replace(/^(\.\/|\/)+/, '');
|
|
177
|
+
return stdout
|
|
178
|
+
.split('\n')
|
|
179
|
+
.map((line) => normalizePath(line.trim()).replace(/^(\.\/|\/)+/, ''))
|
|
180
|
+
.filter((f) => f && f !== normalizedExclude);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
getLogger().debug(`[CodeAst] rg candidate search failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Resolve import neighbors of a file — files that the target imports.
|
|
189
|
+
*/
|
|
190
|
+
async function resolveImportNeighbors(targetFile, targetCode, _repoRoot) {
|
|
191
|
+
const specifiers = extractImportSpecifiers(targetCode);
|
|
192
|
+
const neighbors = [];
|
|
193
|
+
for (const spec of specifiers) {
|
|
194
|
+
if (!spec.startsWith('.'))
|
|
195
|
+
continue;
|
|
196
|
+
const candidates = resolveImportCandidates({ currentFile: targetFile, specifier: spec });
|
|
197
|
+
for (const candidate of candidates) {
|
|
198
|
+
const normalized = normalizePath(candidate).replace(/^(\.\/|\/)+/, '');
|
|
199
|
+
if (normalized && !neighbors.includes(normalized)) {
|
|
200
|
+
neighbors.push(normalized);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return neighbors;
|
|
205
|
+
}
|
|
62
206
|
//# sourceMappingURL=ast.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LIMITS } from '../../../../config/limits.js';
|
|
2
|
+
import { getLogger } from '../../../../observability/logger.js';
|
|
2
3
|
import { parsePlainMatches } from '../parse/plain-grep.js';
|
|
3
4
|
export const psBackend = {
|
|
4
5
|
id: 'powershell',
|
|
@@ -12,7 +13,8 @@ export const psBackend = {
|
|
|
12
13
|
});
|
|
13
14
|
return res.exitCode === 0;
|
|
14
15
|
}
|
|
15
|
-
catch {
|
|
16
|
+
catch (error) {
|
|
17
|
+
getLogger().debug(`[CodeSearch] PowerShell compatibility check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
16
18
|
return false;
|
|
17
19
|
}
|
|
18
20
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolve } from 'path';
|
|
2
2
|
import { LIMITS } from '../../../../config/limits.js';
|
|
3
|
+
import { getLogger } from '../../../../observability/logger.js';
|
|
3
4
|
import { parseRgJson } from '../parse/rg-json.js';
|
|
4
5
|
export const rgBackend = {
|
|
5
6
|
id: 'rg',
|
|
@@ -9,7 +10,8 @@ export const rgBackend = {
|
|
|
9
10
|
const res = await ctx.runner.execFile('rg', ['--version'], { timeoutMs: 1500 });
|
|
10
11
|
return res.exitCode === 0;
|
|
11
12
|
}
|
|
12
|
-
catch {
|
|
13
|
+
catch (error) {
|
|
14
|
+
getLogger().debug(`[CodeSearch] rg compatibility check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
13
15
|
return false;
|
|
14
16
|
}
|
|
15
17
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getLogger } from '../../../../observability/logger.js';
|
|
1
2
|
/**
|
|
2
3
|
* A versatile parser for non-JSON-native search tools.
|
|
3
4
|
* Supports PowerShell JSON objects and traditional line-based formats.
|
|
@@ -29,8 +30,9 @@ function parsePsJson(stdout, maxMatches) {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
|
-
catch {
|
|
33
|
+
catch (error) {
|
|
33
34
|
// If JSON parsing fails, fallback to empty
|
|
35
|
+
getLogger().debug(`[CodeSearch] Failed to parse PowerShell JSON output: ${error instanceof Error ? error.message : String(error)}`);
|
|
34
36
|
}
|
|
35
37
|
return { matches, truncated };
|
|
36
38
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getLogger } from '../../../../observability/logger.js';
|
|
1
2
|
/**
|
|
2
3
|
* Parses the newline-delimited JSON output from ripgrep (--json).
|
|
3
4
|
*/
|
|
@@ -22,8 +23,9 @@ export function parseRgJson(stdout, opts) {
|
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
|
-
catch {
|
|
26
|
+
catch (error) {
|
|
26
27
|
// Ignore malformed JSON lines
|
|
28
|
+
getLogger().debug(`[CodeSearch] Failed to parse rg JSON line: ${error instanceof Error ? error.message : String(error)}`);
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
return { matches, truncated };
|
|
@@ -5,6 +5,7 @@ import { z } from 'zod';
|
|
|
5
5
|
import { text } from '../../../locales/index.js';
|
|
6
6
|
import { AtomicFileWriter } from '../../adapters/fs/atomic-file-writer.js';
|
|
7
7
|
import { mkdir, readFile, readdir, stat } from '../../adapters/fs/node-fs.js';
|
|
8
|
+
import { getLogger } from '../../observability/logger.js';
|
|
8
9
|
import { Phase } from '../../types/runtime.js';
|
|
9
10
|
import { normalizeRepoRelativePath } from '../../utils/path.js';
|
|
10
11
|
import { isRecord } from '../../utils/serialize.js';
|
|
@@ -219,7 +220,8 @@ function shouldIncludeListedEntry(dir, entryName, includeHidden) {
|
|
|
219
220
|
assertNotReservedRepoPrefix(childPath);
|
|
220
221
|
return true;
|
|
221
222
|
}
|
|
222
|
-
catch {
|
|
223
|
+
catch (error) {
|
|
224
|
+
getLogger().debug(`[Fs] Reserved path check failed for "${childPath}": ${error instanceof Error ? error.message : String(error)}`);
|
|
223
225
|
return false;
|
|
224
226
|
}
|
|
225
227
|
}
|
|
@@ -366,6 +368,79 @@ export async function executeFsWriteFile(input, ctx) {
|
|
|
366
368
|
bytesWritten: contentBytes.length,
|
|
367
369
|
};
|
|
368
370
|
}
|
|
371
|
+
// ── fs.edit_file ──────────────────────────────────────────────────────
|
|
372
|
+
const fsEditFileInputSchema = z.preprocess((raw) => {
|
|
373
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
374
|
+
return raw;
|
|
375
|
+
const input = raw;
|
|
376
|
+
if (typeof input.file === 'string')
|
|
377
|
+
return input;
|
|
378
|
+
const alias = input.path ?? input.file_path ?? input.filePath;
|
|
379
|
+
if (typeof alias !== 'string')
|
|
380
|
+
return input;
|
|
381
|
+
return {
|
|
382
|
+
...input,
|
|
383
|
+
file: alias,
|
|
384
|
+
};
|
|
385
|
+
}, z.object({
|
|
386
|
+
file: z.string().describe('Relative path to the file from the repository root'),
|
|
387
|
+
old_string: z.string().min(1).describe('The exact text to find in the file'),
|
|
388
|
+
new_string: z.string().describe('The replacement text'),
|
|
389
|
+
replace_all: z
|
|
390
|
+
.boolean()
|
|
391
|
+
.optional()
|
|
392
|
+
.describe('Replace all occurrences instead of just the first one'),
|
|
393
|
+
}));
|
|
394
|
+
export const fsEditFileSpec = {
|
|
395
|
+
name: 'fs.edit_file',
|
|
396
|
+
source: 'builtin',
|
|
397
|
+
intent: 'WRITE',
|
|
398
|
+
description: text.tools.fsEditFileDescription,
|
|
399
|
+
riskLevel: 'high',
|
|
400
|
+
sideEffects: ['fs_write'],
|
|
401
|
+
concurrency: 'serial_only',
|
|
402
|
+
computeResources: (input, ctx) => [pathPrefixResource(ctx, input.file)],
|
|
403
|
+
allowedPhases: [Phase.SLASH, Phase.AUTOPILOT],
|
|
404
|
+
inputSchema: fsEditFileInputSchema,
|
|
405
|
+
outputSchema: z.object({
|
|
406
|
+
ok: z.boolean(),
|
|
407
|
+
path: z.string(),
|
|
408
|
+
replacements: z.number().int().nonnegative(),
|
|
409
|
+
}),
|
|
410
|
+
summarizeArgsForAuthorization: async (args) => {
|
|
411
|
+
const a = isRecord(args) ? args : {};
|
|
412
|
+
return JSON.stringify({
|
|
413
|
+
file: typeof a.file === 'string' ? a.file : undefined,
|
|
414
|
+
oldString: String(a.old_string ?? '').slice(0, 80),
|
|
415
|
+
newString: String(a.new_string ?? '').slice(0, 80),
|
|
416
|
+
replaceAll: Boolean(a.replace_all),
|
|
417
|
+
});
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
export async function executeFsEditFile(input, ctx) {
|
|
421
|
+
if (ctx.dryRun) {
|
|
422
|
+
return { ok: true, path: input.file, replacements: 0 };
|
|
423
|
+
}
|
|
424
|
+
const { absolutePath } = resolveRepoRelativePath(ctx.repoRoot, input.file);
|
|
425
|
+
const content = await readFile(absolutePath, 'utf-8');
|
|
426
|
+
const count = input.old_string ? content.split(input.old_string).length - 1 : 0;
|
|
427
|
+
if (count === 0) {
|
|
428
|
+
throw new Error(`old_string not found in file "${input.file}". Ensure the string matches exactly, including whitespace and indentation.`);
|
|
429
|
+
}
|
|
430
|
+
if (count > 1 && !input.replace_all) {
|
|
431
|
+
throw new Error(`old_string found ${count} times in "${input.file}", expected exactly 1. Use replace_all: true to replace all occurrences, or provide more surrounding context to uniquely identify the location.`);
|
|
432
|
+
}
|
|
433
|
+
const updated = input.replace_all
|
|
434
|
+
? content.replaceAll(input.old_string, input.new_string)
|
|
435
|
+
: content.replace(input.old_string, input.new_string);
|
|
436
|
+
const writer = new AtomicFileWriter();
|
|
437
|
+
await writer.writeAtomic(absolutePath, Buffer.from(updated, 'utf8'));
|
|
438
|
+
return {
|
|
439
|
+
ok: true,
|
|
440
|
+
path: input.file,
|
|
441
|
+
replacements: count,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
369
444
|
const fsCreateDirectoryInputSchema = z.preprocess((raw) => {
|
|
370
445
|
if (typeof raw === 'string')
|
|
371
446
|
return { path: raw };
|
|
@@ -115,4 +115,246 @@ export async function executeGitStatus(input, ctx) {
|
|
|
115
115
|
status: res.stdout.toString('utf8'),
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
|
+
// ── git.blame ─────────────────────────────────────────────────────────
|
|
119
|
+
export const gitBlameSpec = {
|
|
120
|
+
name: 'git.blame',
|
|
121
|
+
source: 'builtin',
|
|
122
|
+
intent: 'READ',
|
|
123
|
+
description: text.tools.gitBlameDescription,
|
|
124
|
+
riskLevel: 'low',
|
|
125
|
+
sideEffects: ['git_read'],
|
|
126
|
+
concurrency: 'parallel_ok',
|
|
127
|
+
computeResources: (_input, ctx) => [repoResource(ctx)],
|
|
128
|
+
inputSchema: z.object({
|
|
129
|
+
file: z.string().describe('Relative path to the file'),
|
|
130
|
+
ref: z.string().optional().describe('Git reference (default: HEAD)'),
|
|
131
|
+
range: z
|
|
132
|
+
.object({
|
|
133
|
+
start: z.number().int().min(1).describe('Start line number'),
|
|
134
|
+
end: z.number().int().min(1).describe('End line number'),
|
|
135
|
+
})
|
|
136
|
+
.optional()
|
|
137
|
+
.describe('Line range to blame'),
|
|
138
|
+
}),
|
|
139
|
+
outputSchema: z.object({
|
|
140
|
+
file: z.string(),
|
|
141
|
+
lines: z.array(z.object({
|
|
142
|
+
line: z.number(),
|
|
143
|
+
content: z.string(),
|
|
144
|
+
commit: z.string(),
|
|
145
|
+
author: z.string(),
|
|
146
|
+
authorTime: z.number(),
|
|
147
|
+
})),
|
|
148
|
+
}),
|
|
149
|
+
allowedPhases: [Phase.SLASH, Phase.CONTEXT, Phase.EXPLORE, Phase.PLAN, Phase.AUTOPILOT],
|
|
150
|
+
};
|
|
151
|
+
export async function executeGitBlame(input, ctx) {
|
|
152
|
+
const { file, ref, range } = input;
|
|
153
|
+
if (file.includes('..') || file.startsWith('/') || /^[a-zA-Z]:/.test(file)) {
|
|
154
|
+
throw new Error(text.tools.invalidRelativePath(file));
|
|
155
|
+
}
|
|
156
|
+
const args = ['blame', '--porcelain'];
|
|
157
|
+
if (range)
|
|
158
|
+
args.push('-L', `${range.start},${range.end}`);
|
|
159
|
+
if (ref)
|
|
160
|
+
args.push(ref);
|
|
161
|
+
args.push('--', file);
|
|
162
|
+
const repoRoot = ctx.worktreeRoot || ctx.repoRoot;
|
|
163
|
+
const git = new GitAdapter(repoRoot);
|
|
164
|
+
const res = await git.execMeta(args, {
|
|
165
|
+
cwd: repoRoot,
|
|
166
|
+
env: ctx.env,
|
|
167
|
+
limits: { maxStdoutBytes: LIMITS.maxToolOutputBytes, maxStderrChars: 16_384 },
|
|
168
|
+
timeoutMs: LIMITS.gitTimeoutMs,
|
|
169
|
+
});
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
if (res.error?.message)
|
|
172
|
+
throw new Error(text.git.processError(res.error.message));
|
|
173
|
+
throw new Error(text.git.commandFailedDetailed(res.code, res.stderr.trim()));
|
|
174
|
+
}
|
|
175
|
+
if (res.stdoutTruncated)
|
|
176
|
+
throw new Error(text.git.outputTruncated(LIMITS.maxToolOutputBytes));
|
|
177
|
+
const lines = parseBlamePorcelain(res.stdout.toString('utf8'));
|
|
178
|
+
return { file, lines };
|
|
179
|
+
}
|
|
180
|
+
function parseBlamePorcelain(raw) {
|
|
181
|
+
const result = [];
|
|
182
|
+
const commitMeta = new Map();
|
|
183
|
+
let currentCommit = '';
|
|
184
|
+
let currentAuthor = '';
|
|
185
|
+
let currentAuthorTime = 0;
|
|
186
|
+
for (const line of raw.split('\n')) {
|
|
187
|
+
if (!line)
|
|
188
|
+
continue;
|
|
189
|
+
// Header line: "hash origLine finalLine [numLines]"
|
|
190
|
+
const headerMatch = /^([0-9a-f]{40})\s+\d+\s+(\d+)/.exec(line);
|
|
191
|
+
if (headerMatch) {
|
|
192
|
+
currentCommit = headerMatch[1];
|
|
193
|
+
if (!commitMeta.has(currentCommit)) {
|
|
194
|
+
const meta = commitMeta.get(currentCommit);
|
|
195
|
+
if (meta) {
|
|
196
|
+
currentAuthor = meta.author;
|
|
197
|
+
currentAuthorTime = meta.authorTime;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (line.startsWith('author ')) {
|
|
203
|
+
currentAuthor = line.slice(7);
|
|
204
|
+
commitMeta.set(currentCommit, { author: currentAuthor, authorTime: currentAuthorTime });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (line.startsWith('author-time ')) {
|
|
208
|
+
currentAuthorTime = Number(line.slice(12));
|
|
209
|
+
const existing = commitMeta.get(currentCommit);
|
|
210
|
+
if (existing)
|
|
211
|
+
existing.authorTime = currentAuthorTime;
|
|
212
|
+
else
|
|
213
|
+
commitMeta.set(currentCommit, { author: currentAuthor, authorTime: currentAuthorTime });
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
// Content line starts with tab
|
|
217
|
+
if (line.startsWith('\t')) {
|
|
218
|
+
const meta = commitMeta.get(currentCommit) || {
|
|
219
|
+
author: currentAuthor,
|
|
220
|
+
authorTime: currentAuthorTime,
|
|
221
|
+
};
|
|
222
|
+
result.push({
|
|
223
|
+
line: result.length + 1,
|
|
224
|
+
content: line.slice(1),
|
|
225
|
+
commit: currentCommit,
|
|
226
|
+
author: meta.author,
|
|
227
|
+
authorTime: meta.authorTime,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
// ── git.log ───────────────────────────────────────────────────────────
|
|
234
|
+
export const gitLogSpec = {
|
|
235
|
+
name: 'git.log',
|
|
236
|
+
source: 'builtin',
|
|
237
|
+
intent: 'READ',
|
|
238
|
+
description: text.tools.gitLogDescription,
|
|
239
|
+
riskLevel: 'low',
|
|
240
|
+
sideEffects: ['git_read'],
|
|
241
|
+
concurrency: 'parallel_ok',
|
|
242
|
+
computeResources: (_input, ctx) => [repoResource(ctx)],
|
|
243
|
+
inputSchema: z.object({
|
|
244
|
+
ref: z.string().optional().describe('Starting ref (default: HEAD)'),
|
|
245
|
+
maxCount: z.number().int().min(1).max(200).default(20).describe('Max commits to return'),
|
|
246
|
+
file: z.string().optional().describe('Limit to commits touching this file'),
|
|
247
|
+
diffStat: z.boolean().default(false).describe('Include diffstat for each commit'),
|
|
248
|
+
}),
|
|
249
|
+
outputSchema: z.object({
|
|
250
|
+
commits: z.array(z.object({
|
|
251
|
+
hash: z.string(),
|
|
252
|
+
subject: z.string(),
|
|
253
|
+
author: z.string(),
|
|
254
|
+
date: z.string(),
|
|
255
|
+
parentHashes: z.string(),
|
|
256
|
+
stat: z.string().optional(),
|
|
257
|
+
})),
|
|
258
|
+
}),
|
|
259
|
+
allowedPhases: [Phase.SLASH, Phase.CONTEXT, Phase.EXPLORE, Phase.PLAN, Phase.AUTOPILOT],
|
|
260
|
+
};
|
|
261
|
+
export async function executeGitLog(input, ctx) {
|
|
262
|
+
const { ref, maxCount, file, diffStat } = input;
|
|
263
|
+
const args = ['log', `--format=%H%x00%s%x00%an%x00%aI%x00%P`, '-z', `-n${maxCount}`];
|
|
264
|
+
if (diffStat)
|
|
265
|
+
args.push('--stat');
|
|
266
|
+
if (ref)
|
|
267
|
+
args.push(ref);
|
|
268
|
+
if (file)
|
|
269
|
+
args.push('--', file);
|
|
270
|
+
const repoRoot = ctx.worktreeRoot || ctx.repoRoot;
|
|
271
|
+
const git = new GitAdapter(repoRoot);
|
|
272
|
+
const res = await git.execMeta(args, {
|
|
273
|
+
cwd: repoRoot,
|
|
274
|
+
env: ctx.env,
|
|
275
|
+
limits: { maxStdoutBytes: LIMITS.maxToolOutputBytes, maxStderrChars: 16_384 },
|
|
276
|
+
timeoutMs: LIMITS.gitTimeoutMs,
|
|
277
|
+
});
|
|
278
|
+
if (!res.ok) {
|
|
279
|
+
if (res.error?.message)
|
|
280
|
+
throw new Error(text.git.processError(res.error.message));
|
|
281
|
+
throw new Error(text.git.commandFailedDetailed(res.code, res.stderr.trim()));
|
|
282
|
+
}
|
|
283
|
+
if (res.stdoutTruncated)
|
|
284
|
+
throw new Error(text.git.outputTruncated(LIMITS.maxToolOutputBytes));
|
|
285
|
+
const commits = parseLogOutput(res.stdout.toString('utf8'), diffStat);
|
|
286
|
+
return { commits };
|
|
287
|
+
}
|
|
288
|
+
function parseLogOutput(raw, includeStat) {
|
|
289
|
+
// With -z, records are NUL-separated. Each record is: hash\0subject\0author\0date\0parents[\0stat...]
|
|
290
|
+
const records = raw.split('\0');
|
|
291
|
+
const commits = [];
|
|
292
|
+
// Each commit has 5 core fields; stat is optional extra
|
|
293
|
+
const fieldsPerCommit = 5;
|
|
294
|
+
let i = 0;
|
|
295
|
+
while (i + fieldsPerCommit - 1 < records.length) {
|
|
296
|
+
const hash = records[i].replace(/^\n/, '');
|
|
297
|
+
const subject = records[i + 1];
|
|
298
|
+
const author = records[i + 2];
|
|
299
|
+
const date = records[i + 3];
|
|
300
|
+
const parentHashes = records[i + 4];
|
|
301
|
+
i += fieldsPerCommit;
|
|
302
|
+
let stat;
|
|
303
|
+
if (includeStat && i < records.length) {
|
|
304
|
+
// The stat portion may contain newlines; collect until next commit header or end
|
|
305
|
+
const nextCommitIdx = records.findIndex((r, idx) => idx >= i && /^[0-9a-f]{40}/.test(r.replace(/^\n/, '')));
|
|
306
|
+
const endIdx = nextCommitIdx > i ? nextCommitIdx : records.length;
|
|
307
|
+
stat = records.slice(i, endIdx).join('\0').trim() || undefined;
|
|
308
|
+
i = endIdx;
|
|
309
|
+
}
|
|
310
|
+
commits.push({ hash, subject, author, date, parentHashes, stat });
|
|
311
|
+
}
|
|
312
|
+
return commits;
|
|
313
|
+
}
|
|
314
|
+
// ── git.show ──────────────────────────────────────────────────────────
|
|
315
|
+
export const gitShowSpec = {
|
|
316
|
+
name: 'git.show',
|
|
317
|
+
source: 'builtin',
|
|
318
|
+
intent: 'READ',
|
|
319
|
+
description: text.tools.gitShowDescription,
|
|
320
|
+
riskLevel: 'low',
|
|
321
|
+
sideEffects: ['git_read'],
|
|
322
|
+
concurrency: 'parallel_ok',
|
|
323
|
+
computeResources: (_input, ctx) => [repoResource(ctx)],
|
|
324
|
+
inputSchema: z.object({
|
|
325
|
+
ref: z.string().describe('Git reference (commit hash, branch, tag)'),
|
|
326
|
+
stat: z.boolean().default(true).describe('Include diffstat'),
|
|
327
|
+
}),
|
|
328
|
+
outputSchema: z.object({
|
|
329
|
+
ref: z.string(),
|
|
330
|
+
content: z.string(),
|
|
331
|
+
}),
|
|
332
|
+
allowedPhases: [Phase.SLASH, Phase.CONTEXT, Phase.EXPLORE, Phase.PLAN, Phase.AUTOPILOT],
|
|
333
|
+
};
|
|
334
|
+
export async function executeGitShow(input, ctx) {
|
|
335
|
+
const { ref, stat } = input;
|
|
336
|
+
const args = ['show'];
|
|
337
|
+
if (stat)
|
|
338
|
+
args.push('--stat');
|
|
339
|
+
args.push(ref);
|
|
340
|
+
const repoRoot = ctx.worktreeRoot || ctx.repoRoot;
|
|
341
|
+
const git = new GitAdapter(repoRoot);
|
|
342
|
+
const res = await git.execMeta(args, {
|
|
343
|
+
cwd: repoRoot,
|
|
344
|
+
env: ctx.env,
|
|
345
|
+
limits: { maxStdoutBytes: LIMITS.maxToolOutputBytes, maxStderrChars: 16_384 },
|
|
346
|
+
timeoutMs: LIMITS.gitTimeoutMs,
|
|
347
|
+
});
|
|
348
|
+
if (!res.ok) {
|
|
349
|
+
if (res.error?.message)
|
|
350
|
+
throw new Error(text.git.processError(res.error.message));
|
|
351
|
+
throw new Error(text.git.commandFailedDetailed(res.code, res.stderr.trim()));
|
|
352
|
+
}
|
|
353
|
+
if (res.stdoutTruncated)
|
|
354
|
+
throw new Error(text.git.outputTruncated(LIMITS.maxToolOutputBytes));
|
|
355
|
+
return {
|
|
356
|
+
ref,
|
|
357
|
+
content: res.stdout.toString('utf8'),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
118
360
|
//# sourceMappingURL=git.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { text } from '../../../locales/index.js';
|
|
4
|
+
import { spawnCommand } from '../../runtime/process-runner.js';
|
|
5
|
+
import { Phase } from '../../types/runtime.js';
|
|
6
|
+
import { normalizePath } from '../../utils/path.js';
|
|
7
|
+
import { pathPrefixResource } from '../parallel/resource-helpers.js';
|
|
8
|
+
const RG_TIMEOUT_MS = 10_000;
|
|
9
|
+
export const globFindSpec = {
|
|
10
|
+
name: 'glob.find',
|
|
11
|
+
source: 'builtin',
|
|
12
|
+
intent: 'SEARCH',
|
|
13
|
+
description: text.tools.globFindDescription,
|
|
14
|
+
riskLevel: 'low',
|
|
15
|
+
sideEffects: ['fs_read'],
|
|
16
|
+
concurrency: 'parallel_ok',
|
|
17
|
+
computeResources: (_input, ctx) => [pathPrefixResource(ctx, '.')],
|
|
18
|
+
inputSchema: z.object({
|
|
19
|
+
pattern: z.string().describe('Glob pattern (e.g. "**/*.ts", "src/**/*.test.*")'),
|
|
20
|
+
directory: z.string().optional().describe('Directory to search in (relative, default: ".")'),
|
|
21
|
+
maxMatches: z
|
|
22
|
+
.number()
|
|
23
|
+
.int()
|
|
24
|
+
.min(1)
|
|
25
|
+
.max(5000)
|
|
26
|
+
.default(200)
|
|
27
|
+
.describe('Maximum number of files to return'),
|
|
28
|
+
respectGitignore: z.boolean().default(true).describe('Respect .gitignore (default: true)'),
|
|
29
|
+
includeHidden: z.boolean().default(false).describe('Include hidden files (starting with ".")'),
|
|
30
|
+
}),
|
|
31
|
+
outputSchema: z.object({
|
|
32
|
+
files: z.array(z.string()),
|
|
33
|
+
truncated: z.boolean(),
|
|
34
|
+
totalFound: z.number(),
|
|
35
|
+
}),
|
|
36
|
+
allowedPhases: [
|
|
37
|
+
Phase.SLASH,
|
|
38
|
+
Phase.CONTEXT,
|
|
39
|
+
Phase.EXPLORE,
|
|
40
|
+
Phase.PLAN,
|
|
41
|
+
Phase.AUTOPILOT,
|
|
42
|
+
Phase.VERIFY,
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
export async function executeGlobFind(input, ctx) {
|
|
46
|
+
const repoRoot = ctx.worktreeRoot || ctx.repoRoot;
|
|
47
|
+
const cwd = input.directory ? join(repoRoot, input.directory) : repoRoot;
|
|
48
|
+
const args = ['--files'];
|
|
49
|
+
if (!input.respectGitignore)
|
|
50
|
+
args.push('--no-ignore');
|
|
51
|
+
if (input.includeHidden)
|
|
52
|
+
args.push('--hidden');
|
|
53
|
+
args.push('--glob', input.pattern);
|
|
54
|
+
args.push(cwd);
|
|
55
|
+
let stdout = '';
|
|
56
|
+
const result = await spawnCommand({
|
|
57
|
+
command: 'rg',
|
|
58
|
+
args,
|
|
59
|
+
cwd: repoRoot,
|
|
60
|
+
env: ctx.env ?? process.env,
|
|
61
|
+
timeoutMs: RG_TIMEOUT_MS,
|
|
62
|
+
onStdoutChunk: (chunk) => {
|
|
63
|
+
stdout += Buffer.from(chunk).toString();
|
|
64
|
+
},
|
|
65
|
+
onStderrChunk: () => { },
|
|
66
|
+
});
|
|
67
|
+
if (result.error || result.timedOut || (result.code !== 0 && result.code !== 1)) {
|
|
68
|
+
return { files: [], truncated: false, totalFound: 0 };
|
|
69
|
+
}
|
|
70
|
+
const allFiles = stdout
|
|
71
|
+
.split('\n')
|
|
72
|
+
.map((line) => normalizePath(line.trim()).replace(/^(\.\/|\/)+/, ''))
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
const totalFound = allFiles.length;
|
|
75
|
+
const truncated = totalFound > input.maxMatches;
|
|
76
|
+
const files = allFiles.slice(0, input.maxMatches);
|
|
77
|
+
return { files, truncated, totalFound };
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=glob.js.map
|