indusagi 0.12.34 → 0.13.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/agent.js +1247 -184
- package/dist/ai.js +72 -4
- package/dist/capabilities.js +69 -2
- package/dist/cli.js +83 -13
- package/dist/connectors-saas.js +66 -0
- package/dist/index.js +83 -13
- package/dist/interop.js +66 -0
- package/dist/mcp.js +270 -363
- package/dist/react-ink.js +15 -11
- package/dist/shell-app.js +83 -13
- package/dist/smithy.js +69 -2
- package/dist/swarm.js +69 -2
- package/dist/types/capabilities/backends/node-backends.d.ts +3 -1
- package/dist/types/capabilities/files/read-state-gate.d.ts +69 -0
- package/dist/types/capabilities/files/read-state-gate.test.d.ts +14 -0
- package/dist/types/capabilities/kernel/context.d.ts +4 -0
- package/dist/types/capabilities/kernel/index.d.ts +2 -2
- package/dist/types/capabilities/kernel/spec.d.ts +55 -0
- package/dist/types/facade/bot/actions/bash.d.ts +15 -0
- package/dist/types/facade/bot/actions/bash.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/checkpoint.d.ts +49 -0
- package/dist/types/facade/bot/actions/checkpoint.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/edit-utils.d.ts +86 -0
- package/dist/types/facade/bot/actions/edit.d.ts +18 -0
- package/dist/types/facade/bot/actions/edit.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/find.d.ts +2 -0
- package/dist/types/facade/bot/actions/find.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/grep.d.ts +10 -0
- package/dist/types/facade/bot/actions/grep.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/index.d.ts +16 -0
- package/dist/types/facade/bot/actions/read-state.d.ts +83 -0
- package/dist/types/facade/bot/actions/read-state.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/read.d.ts +7 -0
- package/dist/types/facade/bot/actions/read.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/sandbox-backend.d.ts +99 -0
- package/dist/types/facade/bot/actions/sandbox-backend.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/websearch.d.ts +5 -2
- package/dist/types/facade/bot/actions/websearch.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/write.d.ts +15 -0
- package/dist/types/facade/bot/agent-loop.d.ts +10 -0
- package/dist/types/facade/bot/agent-loop.test.d.ts +1 -0
- package/dist/types/facade/bot/agent.d.ts +9 -1
- package/dist/types/facade/bot/permission-gate.test.d.ts +1 -0
- package/dist/types/facade/bot/types.d.ts +60 -0
- package/dist/types/facade/mcp-core/client.d.ts +71 -15
- package/dist/types/facade/mcp-core/client.test.d.ts +18 -0
- package/dist/types/facade/mcp-core/types.d.ts +10 -0
- package/dist/types/facade/ml/adapters/anthropic-retry.test.d.ts +1 -0
- package/dist/types/facade/ml/adapters/anthropic.d.ts +17 -0
- package/dist/types/facade/ml/adapters/simple-options.d.ts +13 -0
- package/dist/types/facade/ml/adapters/simple-options.test.d.ts +1 -0
- package/dist/types/react-ink/components/StatusLine.d.ts +10 -1
- package/dist/types/react-ink/components/ToolEventBlock.d.ts +2 -1
- package/dist/types/react-ink/components/ToolEventBlock.test.d.ts +1 -0
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -13006,24 +13006,58 @@ function normalizeProviderError(error) {
|
|
|
13006
13006
|
}
|
|
13007
13007
|
return new SimpleOptionsProviderError("Unknown provider error", "unknown", error);
|
|
13008
13008
|
}
|
|
13009
|
+
function abortReason(signal) {
|
|
13010
|
+
const reason = signal.reason;
|
|
13011
|
+
if (reason instanceof SimpleOptionsProviderError) return reason;
|
|
13012
|
+
if (reason instanceof Error) return new SimpleOptionsProviderError(reason.message, "unknown", reason);
|
|
13013
|
+
return new SimpleOptionsProviderError("Request was aborted", "unknown", reason);
|
|
13014
|
+
}
|
|
13015
|
+
function abortableSleep(ms, signal) {
|
|
13016
|
+
return new Promise((resolve5, reject) => {
|
|
13017
|
+
if (signal?.aborted) {
|
|
13018
|
+
reject(abortReason(signal));
|
|
13019
|
+
return;
|
|
13020
|
+
}
|
|
13021
|
+
let onAbort;
|
|
13022
|
+
const timer = setTimeout(() => {
|
|
13023
|
+
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
13024
|
+
resolve5();
|
|
13025
|
+
}, ms);
|
|
13026
|
+
if (signal) {
|
|
13027
|
+
onAbort = () => {
|
|
13028
|
+
clearTimeout(timer);
|
|
13029
|
+
signal.removeEventListener("abort", onAbort);
|
|
13030
|
+
reject(abortReason(signal));
|
|
13031
|
+
};
|
|
13032
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
13033
|
+
}
|
|
13034
|
+
});
|
|
13035
|
+
}
|
|
13009
13036
|
async function executeWithRetry(operation, policy) {
|
|
13010
13037
|
let attempt = 0;
|
|
13011
13038
|
let lastError;
|
|
13012
13039
|
while (attempt < policy.maxAttempts) {
|
|
13040
|
+
if (policy.signal?.aborted) {
|
|
13041
|
+
throw abortReason(policy.signal);
|
|
13042
|
+
}
|
|
13013
13043
|
attempt++;
|
|
13014
13044
|
try {
|
|
13015
13045
|
return await operation();
|
|
13016
13046
|
} catch (error) {
|
|
13017
13047
|
lastError = error;
|
|
13018
13048
|
const normalized = normalizeProviderError(error);
|
|
13049
|
+
if (policy.signal?.aborted) {
|
|
13050
|
+
throw normalized;
|
|
13051
|
+
}
|
|
13019
13052
|
const defaultRetryable = normalized.code === "rate_limit" || normalized.code === "timeout" || normalized.code === "network";
|
|
13020
13053
|
const retryable = policy.shouldRetry ? policy.shouldRetry(error, attempt) : defaultRetryable;
|
|
13021
13054
|
if (!retryable || attempt >= policy.maxAttempts) {
|
|
13022
13055
|
throw normalized;
|
|
13023
13056
|
}
|
|
13024
13057
|
const maxDelay = policy.maxDelayMs ?? Number.MAX_SAFE_INTEGER;
|
|
13025
|
-
const
|
|
13026
|
-
|
|
13058
|
+
const retryAfterMs = policy.getRetryAfterMs?.(error) ?? null;
|
|
13059
|
+
const backoff = retryAfterMs != null ? Math.min(retryAfterMs, maxDelay) : Math.min(policy.baseDelayMs * 2 ** (attempt - 1), maxDelay);
|
|
13060
|
+
await abortableSleep(backoff, policy.signal);
|
|
13027
13061
|
}
|
|
13028
13062
|
}
|
|
13029
13063
|
throw normalizeProviderError(lastError);
|
|
@@ -13499,6 +13533,30 @@ var AnthropicEventReducer = class {
|
|
|
13499
13533
|
calculateCost(this.model, this.output.usage);
|
|
13500
13534
|
}
|
|
13501
13535
|
};
|
|
13536
|
+
function parseAnthropicRetryAfterMs(error) {
|
|
13537
|
+
if (error instanceof Anthropic.APIError) {
|
|
13538
|
+
const header = error.headers?.get?.("retry-after");
|
|
13539
|
+
const seconds = header ? parseInt(header, 10) : Number.NaN;
|
|
13540
|
+
if (!Number.isNaN(seconds) && seconds >= 0) return seconds * 1e3;
|
|
13541
|
+
}
|
|
13542
|
+
return null;
|
|
13543
|
+
}
|
|
13544
|
+
function shouldRetryAnthropic(error, _attempt) {
|
|
13545
|
+
if (error instanceof Anthropic.APIError) {
|
|
13546
|
+
const status = error.status;
|
|
13547
|
+
if (status === 408 || status === 409 || status === 429 || status === 529) return true;
|
|
13548
|
+
if (typeof status === "number" && status >= 500) return true;
|
|
13549
|
+
if (typeof status === "number" && status >= 400 && status < 500) return false;
|
|
13550
|
+
const body = `${error.message} ${JSON.stringify(error.error ?? "")}`.toLowerCase();
|
|
13551
|
+
if (body.includes('"type":"overloaded_error"') || body.includes("overloaded")) return true;
|
|
13552
|
+
return false;
|
|
13553
|
+
}
|
|
13554
|
+
if (error instanceof Error) {
|
|
13555
|
+
const msg = error.message.toLowerCase();
|
|
13556
|
+
return msg.includes("rate limit") || msg.includes("rate_limit") || msg.includes("timeout") || msg.includes("timed out") || msg.includes("network") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("overloaded");
|
|
13557
|
+
}
|
|
13558
|
+
return false;
|
|
13559
|
+
}
|
|
13502
13560
|
var streamAnthropic = (model, context, options) => {
|
|
13503
13561
|
const stream = new AssistantMessageEventStream();
|
|
13504
13562
|
const output = createAssistantMessageOutput(model);
|
|
@@ -13526,8 +13584,16 @@ var streamAnthropic = (model, context, options) => {
|
|
|
13526
13584
|
});
|
|
13527
13585
|
},
|
|
13528
13586
|
{
|
|
13529
|
-
|
|
13530
|
-
|
|
13587
|
+
// Transient retries are NO LONGER gated on signal presence:
|
|
13588
|
+
// the live path always carries a signal, so the old ternary
|
|
13589
|
+
// collapsed to a single attempt = zero retries on 429/529/5xx.
|
|
13590
|
+
// Abort still short-circuits immediately (see executeWithRetry).
|
|
13591
|
+
maxAttempts: 3,
|
|
13592
|
+
baseDelayMs: 500,
|
|
13593
|
+
maxDelayMs: 32e3,
|
|
13594
|
+
signal: options?.signal,
|
|
13595
|
+
shouldRetry: shouldRetryAnthropic,
|
|
13596
|
+
getRetryAfterMs: parseAnthropicRetryAfterMs
|
|
13531
13597
|
}
|
|
13532
13598
|
);
|
|
13533
13599
|
if (options?.signal?.aborted) {
|
|
@@ -18407,6 +18473,63 @@ var AgentTelemetry = class {
|
|
|
18407
18473
|
// src/facade/bot/agent-loop.ts
|
|
18408
18474
|
var errorHandler = new AgentErrorHandler();
|
|
18409
18475
|
var telemetry = new AgentTelemetry();
|
|
18476
|
+
var READ_ONLY_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
18477
|
+
"read",
|
|
18478
|
+
"ls",
|
|
18479
|
+
"grep",
|
|
18480
|
+
"find",
|
|
18481
|
+
"websearch",
|
|
18482
|
+
"webfetch",
|
|
18483
|
+
"todoread"
|
|
18484
|
+
]);
|
|
18485
|
+
function maxToolConcurrency(override) {
|
|
18486
|
+
if (typeof override === "number" && Number.isFinite(override) && override >= 1) {
|
|
18487
|
+
return Math.floor(override);
|
|
18488
|
+
}
|
|
18489
|
+
const fromEnv = parseInt(
|
|
18490
|
+
process.env.CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY || process.env.INDUS_MAX_TOOL_CONCURRENCY || "",
|
|
18491
|
+
10
|
|
18492
|
+
);
|
|
18493
|
+
return Number.isFinite(fromEnv) && fromEnv >= 1 ? fromEnv : 10;
|
|
18494
|
+
}
|
|
18495
|
+
function isConcurrencySafe(toolCall, tools, readOnly) {
|
|
18496
|
+
if (!readOnly.has(toolCall.name)) return false;
|
|
18497
|
+
return Boolean(tools?.some((candidate) => candidate.name === toolCall.name));
|
|
18498
|
+
}
|
|
18499
|
+
function partitionToolCalls(calls, tools, readOnly) {
|
|
18500
|
+
const batches = [];
|
|
18501
|
+
for (const call of calls) {
|
|
18502
|
+
const concurrent = isConcurrencySafe(call, tools, readOnly);
|
|
18503
|
+
const trailing = batches[batches.length - 1];
|
|
18504
|
+
if (concurrent && trailing && trailing.concurrent) {
|
|
18505
|
+
trailing.calls.push(call);
|
|
18506
|
+
} else {
|
|
18507
|
+
batches.push({ concurrent, calls: [call] });
|
|
18508
|
+
}
|
|
18509
|
+
}
|
|
18510
|
+
return batches;
|
|
18511
|
+
}
|
|
18512
|
+
async function runToolBatchConcurrently(calls, tools, signal, stream, limit, canUseTool) {
|
|
18513
|
+
const results = new Array(calls.length);
|
|
18514
|
+
let next = 0;
|
|
18515
|
+
const poolSize = Math.max(1, Math.min(limit, calls.length));
|
|
18516
|
+
const worker = async () => {
|
|
18517
|
+
for (; ; ) {
|
|
18518
|
+
const index = next++;
|
|
18519
|
+
if (index >= calls.length) return;
|
|
18520
|
+
const toolCall = calls[index];
|
|
18521
|
+
const key = `tool:${toolCall.name}:${toolCall.id}`;
|
|
18522
|
+
telemetry.start(key);
|
|
18523
|
+
results[index] = await executeToolCall(toolCall, tools, signal, stream, {
|
|
18524
|
+
emitMessageEvents: true,
|
|
18525
|
+
canUseTool
|
|
18526
|
+
});
|
|
18527
|
+
telemetry.log(`tool:${toolCall.name}`, telemetry.end(key));
|
|
18528
|
+
}
|
|
18529
|
+
};
|
|
18530
|
+
await Promise.all(Array.from({ length: poolSize }, () => worker()));
|
|
18531
|
+
return results;
|
|
18532
|
+
}
|
|
18410
18533
|
function agentLoop(prompts, context, config, signal, streamFn) {
|
|
18411
18534
|
const stream = createAgentStream();
|
|
18412
18535
|
void (async () => {
|
|
@@ -18496,7 +18619,12 @@ async function runLoop(liveContext, produced, config, signal, stream, streamFn)
|
|
|
18496
18619
|
assistantMessage,
|
|
18497
18620
|
signal,
|
|
18498
18621
|
stream,
|
|
18499
|
-
config.getSteeringMessages
|
|
18622
|
+
config.getSteeringMessages,
|
|
18623
|
+
{
|
|
18624
|
+
readOnlyToolNames: config.readOnlyToolNames ?? READ_ONLY_TOOL_NAMES,
|
|
18625
|
+
maxConcurrency: maxToolConcurrency(config.maxToolConcurrency),
|
|
18626
|
+
canUseTool: config.canUseTool
|
|
18627
|
+
}
|
|
18500
18628
|
);
|
|
18501
18629
|
for (const result of dispatch.toolResults) {
|
|
18502
18630
|
toolResults.push(result);
|
|
@@ -18581,7 +18709,40 @@ async function executeToolCall(toolCall, tools, signal, stream, options) {
|
|
|
18581
18709
|
let isError = false;
|
|
18582
18710
|
try {
|
|
18583
18711
|
if (!matchedTool) throw new Error(`No registered tool named "${toolCall.name}".`);
|
|
18584
|
-
|
|
18712
|
+
let validatedArgs = validateToolArguments(matchedTool, toolCall);
|
|
18713
|
+
if (options.canUseTool) {
|
|
18714
|
+
const decision = await options.canUseTool(toolCall.name, validatedArgs, { signal });
|
|
18715
|
+
if (decision.behavior === "deny") {
|
|
18716
|
+
const denied = {
|
|
18717
|
+
content: [{ type: "text", text: decision.message }],
|
|
18718
|
+
details: {},
|
|
18719
|
+
isError: true
|
|
18720
|
+
};
|
|
18721
|
+
stream.push({
|
|
18722
|
+
type: "tool_execution_end",
|
|
18723
|
+
toolCallId: toolCall.id,
|
|
18724
|
+
toolName: toolCall.name,
|
|
18725
|
+
result: denied,
|
|
18726
|
+
isError: true
|
|
18727
|
+
});
|
|
18728
|
+
const deniedMessage = {
|
|
18729
|
+
role: "toolResult",
|
|
18730
|
+
toolCallId: toolCall.id,
|
|
18731
|
+
toolName: toolCall.name,
|
|
18732
|
+
content: denied.content,
|
|
18733
|
+
details: denied.details,
|
|
18734
|
+
isError: true,
|
|
18735
|
+
timestamp: Date.now()
|
|
18736
|
+
};
|
|
18737
|
+
if (options.emitMessageEvents) {
|
|
18738
|
+
emitMessageStartEnd(stream, deniedMessage);
|
|
18739
|
+
}
|
|
18740
|
+
return deniedMessage;
|
|
18741
|
+
}
|
|
18742
|
+
if (decision.updatedInput !== void 0) {
|
|
18743
|
+
validatedArgs = decision.updatedInput;
|
|
18744
|
+
}
|
|
18745
|
+
}
|
|
18585
18746
|
result = await matchedTool.execute(toolCall.id, validatedArgs, signal, (partialResult) => {
|
|
18586
18747
|
stream.push({
|
|
18587
18748
|
type: "tool_execution_update",
|
|
@@ -18621,16 +18782,30 @@ async function executeToolCall(toolCall, tools, signal, stream, options) {
|
|
|
18621
18782
|
}
|
|
18622
18783
|
return toolResultMessage;
|
|
18623
18784
|
}
|
|
18624
|
-
async function executeToolCalls(tools, assistantMessage, signal, stream, getSteeringMessages) {
|
|
18785
|
+
async function executeToolCalls(tools, assistantMessage, signal, stream, getSteeringMessages, options) {
|
|
18625
18786
|
const requestedCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
|
|
18787
|
+
const readOnly = options?.readOnlyToolNames ?? READ_ONLY_TOOL_NAMES;
|
|
18788
|
+
const limit = options?.maxConcurrency ?? maxToolConcurrency();
|
|
18789
|
+
const canUseTool = options?.canUseTool;
|
|
18626
18790
|
const results = [];
|
|
18627
18791
|
let steeringMessages;
|
|
18628
|
-
|
|
18629
|
-
|
|
18630
|
-
|
|
18631
|
-
|
|
18632
|
-
|
|
18633
|
-
|
|
18792
|
+
const batches = partitionToolCalls(requestedCalls, tools, readOnly);
|
|
18793
|
+
let dispatched = 0;
|
|
18794
|
+
for (const batch of batches) {
|
|
18795
|
+
if (batch.concurrent && batch.calls.length > 1) {
|
|
18796
|
+
const batchResults = await runToolBatchConcurrently(batch.calls, tools, signal, stream, limit, canUseTool);
|
|
18797
|
+
results.push(...batchResults);
|
|
18798
|
+
} else {
|
|
18799
|
+
const toolCall = batch.calls[0];
|
|
18800
|
+
telemetry.start(`tool:${toolCall.name}`);
|
|
18801
|
+
const toolResult = await executeToolCall(toolCall, tools, signal, stream, {
|
|
18802
|
+
emitMessageEvents: true,
|
|
18803
|
+
canUseTool
|
|
18804
|
+
});
|
|
18805
|
+
telemetry.log(`tool:${toolCall.name}`, telemetry.end(`tool:${toolCall.name}`));
|
|
18806
|
+
results.push(toolResult);
|
|
18807
|
+
}
|
|
18808
|
+
dispatched += batch.calls.length;
|
|
18634
18809
|
if (!getSteeringMessages) {
|
|
18635
18810
|
continue;
|
|
18636
18811
|
}
|
|
@@ -18639,7 +18814,7 @@ async function executeToolCalls(tools, assistantMessage, signal, stream, getStee
|
|
|
18639
18814
|
continue;
|
|
18640
18815
|
}
|
|
18641
18816
|
steeringMessages = steering;
|
|
18642
|
-
for (const skipped of requestedCalls.slice(
|
|
18817
|
+
for (const skipped of requestedCalls.slice(dispatched)) {
|
|
18643
18818
|
results.push(skipToolCall(skipped, stream));
|
|
18644
18819
|
}
|
|
18645
18820
|
break;
|
|
@@ -18868,6 +19043,7 @@ var LoopConfigFactory = class {
|
|
|
18868
19043
|
convertToLlm: args.convertToLlm,
|
|
18869
19044
|
transformContext: args.transformContext,
|
|
18870
19045
|
getApiKey: args.getApiKey,
|
|
19046
|
+
canUseTool: args.canUseTool,
|
|
18871
19047
|
getSteeringMessages: args.getSteeringMessages,
|
|
18872
19048
|
getFollowUpMessages: args.getFollowUpMessages
|
|
18873
19049
|
};
|
|
@@ -18940,6 +19116,7 @@ var Agent = class {
|
|
|
18940
19116
|
streamFn;
|
|
18941
19117
|
_sessionId;
|
|
18942
19118
|
getApiKey;
|
|
19119
|
+
canUseTool;
|
|
18943
19120
|
runningPrompt;
|
|
18944
19121
|
resolveRunningPrompt;
|
|
18945
19122
|
_thinkingBudgets;
|
|
@@ -18950,6 +19127,7 @@ var Agent = class {
|
|
|
18950
19127
|
this.streamFn = opts.streamFn || streamSimple;
|
|
18951
19128
|
this._sessionId = opts.sessionId;
|
|
18952
19129
|
this.getApiKey = opts.getApiKey;
|
|
19130
|
+
this.canUseTool = opts.canUseTool;
|
|
18953
19131
|
this._thinkingBudgets = opts.thinkingBudgets;
|
|
18954
19132
|
this.queuePolicy = new MessageQueuePolicy(opts.steeringMode || "one-at-a-time", opts.followUpMode || "one-at-a-time");
|
|
18955
19133
|
}
|
|
@@ -19109,6 +19287,7 @@ var Agent = class {
|
|
|
19109
19287
|
convertToLlm: this.convertToLlm,
|
|
19110
19288
|
transformContext: this.transformContext,
|
|
19111
19289
|
getApiKey: this.getApiKey,
|
|
19290
|
+
canUseTool: this.canUseTool,
|
|
19112
19291
|
getSteeringMessages: async () => this.queuePolicy.dequeueSteeringMessages(),
|
|
19113
19292
|
getFollowUpMessages: async () => this.queuePolicy.dequeueFollowUpMessages()
|
|
19114
19293
|
});
|
|
@@ -19421,7 +19600,7 @@ function isToolCall(value) {
|
|
|
19421
19600
|
// src/facade/bot/actions/bash.ts
|
|
19422
19601
|
import { randomBytes } from "node:crypto";
|
|
19423
19602
|
import { createWriteStream, existsSync as existsSync2 } from "node:fs";
|
|
19424
|
-
import { tmpdir } from "node:os";
|
|
19603
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
19425
19604
|
import { join } from "node:path";
|
|
19426
19605
|
import { Type as Type2 } from "@sinclair/typebox";
|
|
19427
19606
|
import { spawn as spawn2 } from "child_process";
|
|
@@ -19500,6 +19679,128 @@ function killProcessTree(pid) {
|
|
|
19500
19679
|
}
|
|
19501
19680
|
}
|
|
19502
19681
|
|
|
19682
|
+
// src/facade/bot/actions/sandbox-backend.ts
|
|
19683
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
19684
|
+
import { tmpdir } from "node:os";
|
|
19685
|
+
function resolveWritableRoots(config, cwd) {
|
|
19686
|
+
const tmp = config.tmpDir ?? tmpdir();
|
|
19687
|
+
const extra = config.writableRoots ?? [];
|
|
19688
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19689
|
+
const roots = [];
|
|
19690
|
+
for (const p of [cwd, tmp, ...extra]) {
|
|
19691
|
+
if (p && !seen.has(p)) {
|
|
19692
|
+
seen.add(p);
|
|
19693
|
+
roots.push(p);
|
|
19694
|
+
}
|
|
19695
|
+
}
|
|
19696
|
+
return roots;
|
|
19697
|
+
}
|
|
19698
|
+
function escapeSeatbeltPath(p) {
|
|
19699
|
+
return p.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
19700
|
+
}
|
|
19701
|
+
function buildSeatbeltProfile(config, cwd) {
|
|
19702
|
+
const roots = resolveWritableRoots(config, cwd);
|
|
19703
|
+
const writeRules = roots.map((p) => `(allow file-write* (subpath "${escapeSeatbeltPath(p)}"))`).join("\n");
|
|
19704
|
+
const networkRule = config.allowNetwork ? "(allow network*)" : "(deny network* (with no-log))";
|
|
19705
|
+
return [
|
|
19706
|
+
"(version 1)",
|
|
19707
|
+
"(deny default)",
|
|
19708
|
+
// Allow the command and its children to run.
|
|
19709
|
+
"(allow process-exec)",
|
|
19710
|
+
"(allow process-fork)",
|
|
19711
|
+
"(allow signal (target same-sandbox))",
|
|
19712
|
+
"(allow sysctl-read)",
|
|
19713
|
+
// Read is permitted everywhere; the boundary we enforce is on writes.
|
|
19714
|
+
"(allow file-read*)",
|
|
19715
|
+
writeRules,
|
|
19716
|
+
networkRule
|
|
19717
|
+
].filter((line) => line.length > 0).join("\n");
|
|
19718
|
+
}
|
|
19719
|
+
function buildSeatbeltArgv(config, cwd, command) {
|
|
19720
|
+
const profile = buildSeatbeltProfile(config, cwd);
|
|
19721
|
+
return ["sandbox-exec", "-p", profile, "/bin/sh", "-c", command];
|
|
19722
|
+
}
|
|
19723
|
+
function buildBwrapArgv(config, cwd, command) {
|
|
19724
|
+
const roots = resolveWritableRoots(config, cwd);
|
|
19725
|
+
const argv = ["bwrap", "--ro-bind", "/", "/"];
|
|
19726
|
+
for (const root of roots) {
|
|
19727
|
+
argv.push("--bind", root, root);
|
|
19728
|
+
}
|
|
19729
|
+
argv.push("--dev", "/dev", "--proc", "/proc");
|
|
19730
|
+
if (!config.allowNetwork) {
|
|
19731
|
+
argv.push("--unshare-net");
|
|
19732
|
+
}
|
|
19733
|
+
argv.push("--chdir", cwd, "/bin/sh", "-c", command);
|
|
19734
|
+
return argv;
|
|
19735
|
+
}
|
|
19736
|
+
function binaryOnPath(binary) {
|
|
19737
|
+
try {
|
|
19738
|
+
const result = spawnSync2("/bin/sh", ["-c", `command -v ${binary}`], {
|
|
19739
|
+
encoding: "utf8",
|
|
19740
|
+
timeout: 5e3
|
|
19741
|
+
});
|
|
19742
|
+
return result.status === 0 && typeof result.stdout === "string" && result.stdout.trim().length > 0;
|
|
19743
|
+
} catch {
|
|
19744
|
+
return false;
|
|
19745
|
+
}
|
|
19746
|
+
}
|
|
19747
|
+
function sandboxAvailability(platform = process.platform, probe = binaryOnPath) {
|
|
19748
|
+
if (platform === "darwin") {
|
|
19749
|
+
const binaryPresent = probe("sandbox-exec");
|
|
19750
|
+
return {
|
|
19751
|
+
platform: "macos",
|
|
19752
|
+
binary: "sandbox-exec",
|
|
19753
|
+
binaryPresent,
|
|
19754
|
+
reason: binaryPresent ? void 0 : "sandbox-exec not found on PATH"
|
|
19755
|
+
};
|
|
19756
|
+
}
|
|
19757
|
+
if (platform === "linux") {
|
|
19758
|
+
const binaryPresent = probe("bwrap");
|
|
19759
|
+
return {
|
|
19760
|
+
platform: "linux",
|
|
19761
|
+
binary: "bwrap",
|
|
19762
|
+
binaryPresent,
|
|
19763
|
+
reason: binaryPresent ? void 0 : "bwrap (bubblewrap) not found on PATH"
|
|
19764
|
+
};
|
|
19765
|
+
}
|
|
19766
|
+
return {
|
|
19767
|
+
platform: "unsupported",
|
|
19768
|
+
binary: null,
|
|
19769
|
+
binaryPresent: false,
|
|
19770
|
+
reason: `OS sandbox is not supported on platform "${platform}"`
|
|
19771
|
+
};
|
|
19772
|
+
}
|
|
19773
|
+
function buildSandboxArgv(config, cwd, command, availability = sandboxAvailability()) {
|
|
19774
|
+
if (!availability.binaryPresent) return null;
|
|
19775
|
+
if (availability.platform === "macos") return buildSeatbeltArgv(config, cwd, command);
|
|
19776
|
+
if (availability.platform === "linux") return buildBwrapArgv(config, cwd, command);
|
|
19777
|
+
return null;
|
|
19778
|
+
}
|
|
19779
|
+
function shellQuote(token) {
|
|
19780
|
+
return `'${token.replace(/'/g, "'\\''")}'`;
|
|
19781
|
+
}
|
|
19782
|
+
function argvToCommandString(argv) {
|
|
19783
|
+
return argv.map(shellQuote).join(" ");
|
|
19784
|
+
}
|
|
19785
|
+
function createSandboxedBashOperations(config, options) {
|
|
19786
|
+
if (!config.enabled) return options.base;
|
|
19787
|
+
const availability = options.availability ?? sandboxAvailability();
|
|
19788
|
+
return {
|
|
19789
|
+
exec: (command, cwd, execOptions) => {
|
|
19790
|
+
const argv = buildSandboxArgv(config, cwd, command, availability);
|
|
19791
|
+
if (!argv) {
|
|
19792
|
+
options.onNote?.(
|
|
19793
|
+
`sandbox NOT applied (${availability.reason ?? "unavailable"}); ran un-sandboxed`
|
|
19794
|
+
);
|
|
19795
|
+
return options.base.exec(command, cwd, execOptions);
|
|
19796
|
+
}
|
|
19797
|
+
options.onNote?.(`sandbox applied via ${availability.binary}`);
|
|
19798
|
+
const wrapped = argvToCommandString(argv);
|
|
19799
|
+
return options.base.exec(wrapped, cwd, execOptions);
|
|
19800
|
+
}
|
|
19801
|
+
};
|
|
19802
|
+
}
|
|
19803
|
+
|
|
19503
19804
|
// src/facade/bot/actions/truncate.ts
|
|
19504
19805
|
var DEFAULT_MAX_LINES = 2500;
|
|
19505
19806
|
var DEFAULT_MAX_BYTES = 64 * 1024;
|
|
@@ -19687,7 +19988,7 @@ function truncateLine(line, maxChars = GREP_MAX_LINE_LENGTH) {
|
|
|
19687
19988
|
// src/facade/bot/actions/bash.ts
|
|
19688
19989
|
function allocateSpillFilePath() {
|
|
19689
19990
|
const suffix = randomBytes(8).toString("hex");
|
|
19690
|
-
return join(
|
|
19991
|
+
return join(tmpdir2(), `indusvx-bash-${suffix}.log`);
|
|
19691
19992
|
}
|
|
19692
19993
|
var BashCommandBuilder = class {
|
|
19693
19994
|
constructor(prefix) {
|
|
@@ -19864,9 +20165,15 @@ ${note}` : note);
|
|
|
19864
20165
|
}
|
|
19865
20166
|
};
|
|
19866
20167
|
var DEFAULT_BLOCKED_PATTERNS = [/\brm\s+-rf\s+\/$/, /:\(\)\s*\{\s*:\|:\s*&\s*\};:/];
|
|
20168
|
+
var DEFAULT_BASH_TIMEOUT_SECONDS = 120;
|
|
20169
|
+
var MAX_BASH_TIMEOUT_SECONDS = 600;
|
|
20170
|
+
function computeEffectiveTimeout(requested, def, max) {
|
|
20171
|
+
const base = requested !== void 0 && requested > 0 ? requested : def;
|
|
20172
|
+
return Math.min(base, max);
|
|
20173
|
+
}
|
|
19867
20174
|
var bashSchema = Type2.Object({
|
|
19868
20175
|
command: Type2.String({ description: "The bash command line to execute" }),
|
|
19869
|
-
timeout: Type2.Optional(Type2.Number({ description:
|
|
20176
|
+
timeout: Type2.Optional(Type2.Number({ description: `Optional limit, in seconds, after which the command is terminated (default ${DEFAULT_BASH_TIMEOUT_SECONDS}s, max ${MAX_BASH_TIMEOUT_SECONDS}s).` }))
|
|
19870
20177
|
});
|
|
19871
20178
|
var defaultBashOperations = {
|
|
19872
20179
|
exec: (command, cwd, { onData, signal, timeout, env }) => {
|
|
@@ -19932,21 +20239,28 @@ var defaultBashOperations = {
|
|
|
19932
20239
|
}
|
|
19933
20240
|
};
|
|
19934
20241
|
function createBashTool(cwd, options) {
|
|
19935
|
-
const
|
|
20242
|
+
const baseOps = options?.operations ?? defaultBashOperations;
|
|
20243
|
+
const ops = options?.sandbox?.enabled ? createSandboxedBashOperations(options.sandbox, {
|
|
20244
|
+
base: baseOps,
|
|
20245
|
+
onNote: options.onSandboxNote
|
|
20246
|
+
}) : baseOps;
|
|
19936
20247
|
const commandBuilder = new BashCommandBuilder(options?.commandPrefix);
|
|
19937
20248
|
const securityPolicy = new BashSecurityPolicy(
|
|
19938
20249
|
options?.security?.blockedPatterns ?? DEFAULT_BLOCKED_PATTERNS,
|
|
19939
20250
|
options?.commandHistory
|
|
19940
20251
|
);
|
|
19941
20252
|
const hookRunner = options?.hookRunner;
|
|
20253
|
+
const defaultTimeoutSeconds = options?.defaultTimeoutSeconds ?? DEFAULT_BASH_TIMEOUT_SECONDS;
|
|
20254
|
+
const maxTimeoutSeconds = Math.max(options?.maxTimeoutSeconds ?? MAX_BASH_TIMEOUT_SECONDS, defaultTimeoutSeconds);
|
|
19942
20255
|
return {
|
|
19943
20256
|
name: "bash",
|
|
19944
20257
|
label: "bash",
|
|
19945
|
-
description: `Execute a bash command in the current working directory and return its merged stdout and stderr. At most the final ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB are returned, whichever limit is hit first; once that occurs the complete output is saved to a temp file. An optional timeout in seconds may be supplied.`,
|
|
20258
|
+
description: `Execute a bash command in the current working directory and return its merged stdout and stderr. At most the final ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB are returned, whichever limit is hit first; once that occurs the complete output is saved to a temp file. An optional timeout in seconds may be supplied. Commands time out after ${defaultTimeoutSeconds}s by default (max ${maxTimeoutSeconds}s).`,
|
|
19946
20259
|
parameters: bashSchema,
|
|
19947
20260
|
execute: async (_toolCallId, { command, timeout }, signal, onUpdate) => {
|
|
19948
20261
|
securityPolicy.assertAllowed(command);
|
|
19949
20262
|
securityPolicy.record(command);
|
|
20263
|
+
const effectiveTimeout = computeEffectiveTimeout(timeout, defaultTimeoutSeconds, maxTimeoutSeconds);
|
|
19950
20264
|
const resolvedCommand = commandBuilder.build(command);
|
|
19951
20265
|
const hookEnv = hookRunner?.hasHandlers("shell.env") ? (await hookRunner.trigger("shell.env", { cwd }, { env: {} })).env : void 0;
|
|
19952
20266
|
const safeEnv = hookEnv ? Object.fromEntries(
|
|
@@ -19958,7 +20272,7 @@ function createBashTool(cwd, options) {
|
|
|
19958
20272
|
ops.exec(resolvedCommand, cwd, {
|
|
19959
20273
|
onData: (data) => outputCapture.appendChunk(data, emitPartial),
|
|
19960
20274
|
signal,
|
|
19961
|
-
timeout,
|
|
20275
|
+
timeout: effectiveTimeout,
|
|
19962
20276
|
env: safeEnv
|
|
19963
20277
|
}).then(({ exitCode }) => {
|
|
19964
20278
|
outputCapture.close();
|
|
@@ -20291,6 +20605,216 @@ async function computeEditDiff(path9, oldText, newText, cwd) {
|
|
|
20291
20605
|
return computation.compute(path9, oldText, newText);
|
|
20292
20606
|
}
|
|
20293
20607
|
|
|
20608
|
+
// src/facade/bot/actions/edit-utils.ts
|
|
20609
|
+
import { readdirSync } from "node:fs";
|
|
20610
|
+
import { realpath, stat } from "node:fs/promises";
|
|
20611
|
+
import { basename, dirname, extname, join as join2, relative, sep } from "node:path";
|
|
20612
|
+
var LEFT_SINGLE_CURLY_QUOTE = "\u2018";
|
|
20613
|
+
var RIGHT_SINGLE_CURLY_QUOTE = "\u2019";
|
|
20614
|
+
var LEFT_DOUBLE_CURLY_QUOTE = "\u201C";
|
|
20615
|
+
var RIGHT_DOUBLE_CURLY_QUOTE = "\u201D";
|
|
20616
|
+
function isOpeningContext(chars, index) {
|
|
20617
|
+
if (index === 0) {
|
|
20618
|
+
return true;
|
|
20619
|
+
}
|
|
20620
|
+
const prev = chars[index - 1];
|
|
20621
|
+
return prev === " " || prev === " " || prev === "\n" || prev === "\r" || prev === "(" || prev === "[" || prev === "{" || prev === "\u2014" || // em dash
|
|
20622
|
+
prev === "\u2013";
|
|
20623
|
+
}
|
|
20624
|
+
function applyCurlyDoubleQuotes(str) {
|
|
20625
|
+
const chars = [...str];
|
|
20626
|
+
const result = [];
|
|
20627
|
+
for (let i = 0; i < chars.length; i++) {
|
|
20628
|
+
if (chars[i] === '"') {
|
|
20629
|
+
result.push(isOpeningContext(chars, i) ? LEFT_DOUBLE_CURLY_QUOTE : RIGHT_DOUBLE_CURLY_QUOTE);
|
|
20630
|
+
} else {
|
|
20631
|
+
result.push(chars[i]);
|
|
20632
|
+
}
|
|
20633
|
+
}
|
|
20634
|
+
return result.join("");
|
|
20635
|
+
}
|
|
20636
|
+
function applyCurlySingleQuotes(str) {
|
|
20637
|
+
const chars = [...str];
|
|
20638
|
+
const result = [];
|
|
20639
|
+
for (let i = 0; i < chars.length; i++) {
|
|
20640
|
+
if (chars[i] === "'") {
|
|
20641
|
+
const prev = i > 0 ? chars[i - 1] : void 0;
|
|
20642
|
+
const next = i < chars.length - 1 ? chars[i + 1] : void 0;
|
|
20643
|
+
const prevIsLetter = prev !== void 0 && new RegExp("\\p{L}", "u").test(prev);
|
|
20644
|
+
const nextIsLetter = next !== void 0 && new RegExp("\\p{L}", "u").test(next);
|
|
20645
|
+
if (prevIsLetter && nextIsLetter) {
|
|
20646
|
+
result.push(RIGHT_SINGLE_CURLY_QUOTE);
|
|
20647
|
+
} else {
|
|
20648
|
+
result.push(isOpeningContext(chars, i) ? LEFT_SINGLE_CURLY_QUOTE : RIGHT_SINGLE_CURLY_QUOTE);
|
|
20649
|
+
}
|
|
20650
|
+
} else {
|
|
20651
|
+
result.push(chars[i]);
|
|
20652
|
+
}
|
|
20653
|
+
}
|
|
20654
|
+
return result.join("");
|
|
20655
|
+
}
|
|
20656
|
+
function preserveQuoteStyle(oldString, actualOldString, newString) {
|
|
20657
|
+
if (oldString === actualOldString) {
|
|
20658
|
+
return newString;
|
|
20659
|
+
}
|
|
20660
|
+
const hasDoubleQuotes = actualOldString.includes(LEFT_DOUBLE_CURLY_QUOTE) || actualOldString.includes(RIGHT_DOUBLE_CURLY_QUOTE);
|
|
20661
|
+
const hasSingleQuotes = actualOldString.includes(LEFT_SINGLE_CURLY_QUOTE) || actualOldString.includes(RIGHT_SINGLE_CURLY_QUOTE);
|
|
20662
|
+
if (!hasDoubleQuotes && !hasSingleQuotes) {
|
|
20663
|
+
return newString;
|
|
20664
|
+
}
|
|
20665
|
+
let result = newString;
|
|
20666
|
+
if (hasDoubleQuotes) {
|
|
20667
|
+
result = applyCurlyDoubleQuotes(result);
|
|
20668
|
+
}
|
|
20669
|
+
if (hasSingleQuotes) {
|
|
20670
|
+
result = applyCurlySingleQuotes(result);
|
|
20671
|
+
}
|
|
20672
|
+
return result;
|
|
20673
|
+
}
|
|
20674
|
+
var DESANITIZATIONS = {
|
|
20675
|
+
"<fnr>": "<function_results>",
|
|
20676
|
+
"<n>": "<name>",
|
|
20677
|
+
"</n>": "</name>",
|
|
20678
|
+
"<o>": "<output>",
|
|
20679
|
+
"</o>": "</output>",
|
|
20680
|
+
"<e>": "<error>",
|
|
20681
|
+
"</e>": "</error>",
|
|
20682
|
+
"<s>": "<system>",
|
|
20683
|
+
"</s>": "</system>",
|
|
20684
|
+
"<r>": "<result>",
|
|
20685
|
+
"</r>": "</result>",
|
|
20686
|
+
"< META_START >": "<META_START>",
|
|
20687
|
+
"< META_END >": "<META_END>",
|
|
20688
|
+
"< EOT >": "<EOT>",
|
|
20689
|
+
"< META >": "<META>",
|
|
20690
|
+
"< SOS >": "<SOS>",
|
|
20691
|
+
"\n\nH:": "\n\nHuman:",
|
|
20692
|
+
"\n\nA:": "\n\nAssistant:"
|
|
20693
|
+
};
|
|
20694
|
+
function desanitizeMatchString(matchString) {
|
|
20695
|
+
let result = matchString;
|
|
20696
|
+
const appliedReplacements = [];
|
|
20697
|
+
for (const [from, to] of Object.entries(DESANITIZATIONS)) {
|
|
20698
|
+
const beforeReplace = result;
|
|
20699
|
+
result = result.split(from).join(to);
|
|
20700
|
+
if (beforeReplace !== result) {
|
|
20701
|
+
appliedReplacements.push({ from, to });
|
|
20702
|
+
}
|
|
20703
|
+
}
|
|
20704
|
+
return { result, appliedReplacements };
|
|
20705
|
+
}
|
|
20706
|
+
function findSimilarFile(filePath) {
|
|
20707
|
+
try {
|
|
20708
|
+
const dir = dirname(filePath);
|
|
20709
|
+
const fileBaseName = basename(filePath, extname(filePath));
|
|
20710
|
+
const files = readdirSync(dir, { withFileTypes: true });
|
|
20711
|
+
const similarFiles = files.filter(
|
|
20712
|
+
(file) => basename(file.name, extname(file.name)) === fileBaseName && join2(dir, file.name) !== filePath
|
|
20713
|
+
);
|
|
20714
|
+
const firstMatch = similarFiles[0];
|
|
20715
|
+
return firstMatch ? firstMatch.name : void 0;
|
|
20716
|
+
} catch {
|
|
20717
|
+
return void 0;
|
|
20718
|
+
}
|
|
20719
|
+
}
|
|
20720
|
+
var FILE_NOT_FOUND_CWD_NOTE = "Note: your current working directory is";
|
|
20721
|
+
async function suggestPathUnderCwd(requestedPath, cwd) {
|
|
20722
|
+
const cwdParent = dirname(cwd);
|
|
20723
|
+
let resolvedPath = requestedPath;
|
|
20724
|
+
try {
|
|
20725
|
+
const resolvedDir = await realpath(dirname(requestedPath));
|
|
20726
|
+
resolvedPath = join2(resolvedDir, basename(requestedPath));
|
|
20727
|
+
} catch {
|
|
20728
|
+
}
|
|
20729
|
+
const cwdParentPrefix = cwdParent === sep ? sep : cwdParent + sep;
|
|
20730
|
+
if (!resolvedPath.startsWith(cwdParentPrefix) || resolvedPath.startsWith(cwd + sep) || resolvedPath === cwd) {
|
|
20731
|
+
return void 0;
|
|
20732
|
+
}
|
|
20733
|
+
const relFromParent = relative(cwdParent, resolvedPath);
|
|
20734
|
+
const correctedPath = join2(cwd, relFromParent);
|
|
20735
|
+
try {
|
|
20736
|
+
await stat(correctedPath);
|
|
20737
|
+
return correctedPath;
|
|
20738
|
+
} catch {
|
|
20739
|
+
return void 0;
|
|
20740
|
+
}
|
|
20741
|
+
}
|
|
20742
|
+
function replaceAllLiteral(haystack, needle, value) {
|
|
20743
|
+
if (needle.length === 0) return haystack;
|
|
20744
|
+
const pieces = [];
|
|
20745
|
+
let from = 0;
|
|
20746
|
+
for (; ; ) {
|
|
20747
|
+
const at = haystack.indexOf(needle, from);
|
|
20748
|
+
if (at === -1) {
|
|
20749
|
+
pieces.push(haystack.slice(from));
|
|
20750
|
+
break;
|
|
20751
|
+
}
|
|
20752
|
+
pieces.push(haystack.slice(from, at), value);
|
|
20753
|
+
from = at + needle.length;
|
|
20754
|
+
}
|
|
20755
|
+
return pieces.join("");
|
|
20756
|
+
}
|
|
20757
|
+
|
|
20758
|
+
// src/facade/bot/actions/checkpoint.ts
|
|
20759
|
+
import { readFileSync, statSync } from "node:fs";
|
|
20760
|
+
function recordCheckpoint(handle, absPath) {
|
|
20761
|
+
if (!handle) return;
|
|
20762
|
+
let previous;
|
|
20763
|
+
try {
|
|
20764
|
+
statSync(absPath);
|
|
20765
|
+
previous = readFileSync(absPath, "utf-8");
|
|
20766
|
+
} catch (error) {
|
|
20767
|
+
if (error?.code === "ENOENT") {
|
|
20768
|
+
previous = null;
|
|
20769
|
+
} else {
|
|
20770
|
+
return;
|
|
20771
|
+
}
|
|
20772
|
+
}
|
|
20773
|
+
try {
|
|
20774
|
+
handle.record(absPath, previous);
|
|
20775
|
+
} catch {
|
|
20776
|
+
}
|
|
20777
|
+
}
|
|
20778
|
+
|
|
20779
|
+
// src/facade/bot/actions/read-state.ts
|
|
20780
|
+
import { statSync as statSync2 } from "node:fs";
|
|
20781
|
+
var READ_BEFORE_EDIT_MESSAGE = "File has not been read yet. Read it first before writing to it.";
|
|
20782
|
+
var MODIFIED_SINCE_READ_MESSAGE = "File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.";
|
|
20783
|
+
function recordReadState(handle, absPath) {
|
|
20784
|
+
if (!handle) return;
|
|
20785
|
+
try {
|
|
20786
|
+
const info = statSync2(absPath);
|
|
20787
|
+
const mtimeMs = Math.floor(info.mtimeMs);
|
|
20788
|
+
handle.set(absPath, {
|
|
20789
|
+
mtimeMs,
|
|
20790
|
+
size: info.size,
|
|
20791
|
+
readAt: mtimeMs
|
|
20792
|
+
});
|
|
20793
|
+
} catch {
|
|
20794
|
+
}
|
|
20795
|
+
}
|
|
20796
|
+
function enforceReadGate(handle, absPath) {
|
|
20797
|
+
if (!handle) return { ok: true };
|
|
20798
|
+
if (!handle.has(absPath)) {
|
|
20799
|
+
return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
|
|
20800
|
+
}
|
|
20801
|
+
const recorded = handle.get(absPath);
|
|
20802
|
+
if (!recorded) {
|
|
20803
|
+
return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
|
|
20804
|
+
}
|
|
20805
|
+
let info;
|
|
20806
|
+
try {
|
|
20807
|
+
info = statSync2(absPath);
|
|
20808
|
+
} catch {
|
|
20809
|
+
return { ok: true };
|
|
20810
|
+
}
|
|
20811
|
+
const currentMtime = Math.floor(info.mtimeMs);
|
|
20812
|
+
if (currentMtime > recorded.mtimeMs || info.size !== recorded.size) {
|
|
20813
|
+
return { ok: false, message: MODIFIED_SINCE_READ_MESSAGE };
|
|
20814
|
+
}
|
|
20815
|
+
return { ok: true };
|
|
20816
|
+
}
|
|
20817
|
+
|
|
20294
20818
|
// src/facade/bot/actions/edit.ts
|
|
20295
20819
|
var ExactThenFuzzyStrategy = class {
|
|
20296
20820
|
find(content, oldText) {
|
|
@@ -20311,7 +20835,12 @@ function rejectWith(message) {
|
|
|
20311
20835
|
var editSchema = Type3.Object({
|
|
20312
20836
|
path: Type3.String({ description: "File to modify, given as a relative or absolute path" }),
|
|
20313
20837
|
oldText: Type3.String({ description: "The current text to locate; it must be reproduced verbatim" }),
|
|
20314
|
-
newText: Type3.String({ description: "The text that should take the place of oldText" })
|
|
20838
|
+
newText: Type3.String({ description: "The text that should take the place of oldText" }),
|
|
20839
|
+
replaceAll: Type3.Optional(
|
|
20840
|
+
Type3.Boolean({
|
|
20841
|
+
description: "Replace every occurrence of oldText instead of requiring a single unique match. Defaults to false, which refuses the edit when oldText appears more than once."
|
|
20842
|
+
})
|
|
20843
|
+
)
|
|
20315
20844
|
});
|
|
20316
20845
|
var defaultEditOperations = {
|
|
20317
20846
|
readFile: (path9) => fsReadFile(path9),
|
|
@@ -20353,38 +20882,76 @@ function withAbort(signal, task) {
|
|
|
20353
20882
|
});
|
|
20354
20883
|
}
|
|
20355
20884
|
var EditSession = class {
|
|
20356
|
-
constructor(input, absolutePath, ops, matchingStrategy) {
|
|
20885
|
+
constructor(input, absolutePath, ops, matchingStrategy, cwd, readState, checkpoint) {
|
|
20357
20886
|
this.input = input;
|
|
20358
20887
|
this.absolutePath = absolutePath;
|
|
20359
20888
|
this.ops = ops;
|
|
20360
20889
|
this.matchingStrategy = matchingStrategy;
|
|
20890
|
+
this.cwd = cwd;
|
|
20891
|
+
this.readState = readState;
|
|
20892
|
+
this.checkpoint = checkpoint;
|
|
20893
|
+
this.replaceAll = input.replaceAll === true;
|
|
20361
20894
|
}
|
|
20362
20895
|
input;
|
|
20363
20896
|
absolutePath;
|
|
20364
20897
|
ops;
|
|
20365
20898
|
matchingStrategy;
|
|
20899
|
+
cwd;
|
|
20900
|
+
readState;
|
|
20901
|
+
checkpoint;
|
|
20902
|
+
/** Whether every occurrence should be swapped (vs. requiring a unique hit). */
|
|
20903
|
+
replaceAll;
|
|
20366
20904
|
async run(throwIfAborted2) {
|
|
20367
20905
|
throwIfAborted2();
|
|
20906
|
+
const gate = enforceReadGate(this.readState, this.absolutePath);
|
|
20907
|
+
if (!gate.ok) {
|
|
20908
|
+
rejectWith(gate.message);
|
|
20909
|
+
}
|
|
20910
|
+
recordCheckpoint(this.checkpoint, this.absolutePath);
|
|
20368
20911
|
const { sourceText } = await this.loadSource();
|
|
20369
20912
|
throwIfAborted2();
|
|
20370
20913
|
const normalized = this.normalizeInputs(sourceText);
|
|
20371
|
-
const
|
|
20372
|
-
const
|
|
20914
|
+
const search = this.resolveSearch(normalized);
|
|
20915
|
+
const match = this.locateUniqueMatch(normalized, search);
|
|
20916
|
+
const replacement = this.applyReplacement(normalized, search, match);
|
|
20373
20917
|
throwIfAborted2();
|
|
20374
20918
|
await this.persist(replacement.finalContent);
|
|
20375
20919
|
throwIfAborted2();
|
|
20376
|
-
return this.buildResponse(replacement.editableBase, replacement.replacedContent);
|
|
20920
|
+
return this.buildResponse(replacement.editableBase, replacement.replacedContent, replacement.replacements);
|
|
20377
20921
|
}
|
|
20378
20922
|
async loadSource() {
|
|
20379
20923
|
try {
|
|
20380
20924
|
await this.ops.access(this.absolutePath);
|
|
20381
20925
|
} catch {
|
|
20382
|
-
rejectWith(
|
|
20926
|
+
rejectWith(await this.buildNotFoundMessage());
|
|
20383
20927
|
}
|
|
20384
20928
|
const buffer = await this.ops.readFile(this.absolutePath);
|
|
20385
20929
|
const sourceText = buffer.toString("utf-8");
|
|
20386
20930
|
return { sourceText };
|
|
20387
20931
|
}
|
|
20932
|
+
/**
|
|
20933
|
+
* Compose the "No readable file …" rejection, enriched with a "Did you mean
|
|
20934
|
+
* …?" hint when we can spot either a sibling file sharing the base name or a
|
|
20935
|
+
* corrected path re-rooted under the working directory. Falls back to the
|
|
20936
|
+
* bare message when no suggestion applies, so the default behaviour is
|
|
20937
|
+
* preserved for the common case.
|
|
20938
|
+
*/
|
|
20939
|
+
async buildNotFoundMessage() {
|
|
20940
|
+
let message = `No readable file at ${this.input.path}. ${FILE_NOT_FOUND_CWD_NOTE} ${this.cwd}.`;
|
|
20941
|
+
try {
|
|
20942
|
+
const cwdSuggestion = await suggestPathUnderCwd(this.absolutePath, this.cwd);
|
|
20943
|
+
if (cwdSuggestion) {
|
|
20944
|
+
message += ` Did you mean ${cwdSuggestion}?`;
|
|
20945
|
+
return message;
|
|
20946
|
+
}
|
|
20947
|
+
const similarFilename = findSimilarFile(this.absolutePath);
|
|
20948
|
+
if (similarFilename) {
|
|
20949
|
+
message += ` Did you mean ${similarFilename}?`;
|
|
20950
|
+
}
|
|
20951
|
+
} catch {
|
|
20952
|
+
}
|
|
20953
|
+
return message;
|
|
20954
|
+
}
|
|
20388
20955
|
normalizeInputs(sourceText) {
|
|
20389
20956
|
const { bom, text } = stripBom(sourceText);
|
|
20390
20957
|
const originalEnding = detectLineEnding(text);
|
|
@@ -20399,41 +20966,106 @@ var EditSession = class {
|
|
|
20399
20966
|
normalizedNewText
|
|
20400
20967
|
};
|
|
20401
20968
|
}
|
|
20402
|
-
|
|
20403
|
-
|
|
20969
|
+
/**
|
|
20970
|
+
* Decide which oldText/newText pair to actually edit with. The literal
|
|
20971
|
+
* normalized inputs win whenever they appear in the file; only when they are
|
|
20972
|
+
* absent do we try a de-sanitized variant (repairing mangled tag tokens like
|
|
20973
|
+
* `<o>` → `<output>`), mirroring the same token expansions onto newText so
|
|
20974
|
+
* the replacement stays consistent. The de-sanitize table overlaps ordinary
|
|
20975
|
+
* code, so it is deliberately a *fallback*, never the first attempt.
|
|
20976
|
+
*/
|
|
20977
|
+
resolveSearch(normalized) {
|
|
20978
|
+
const literal = {
|
|
20979
|
+
searchOldText: normalized.normalizedOldText,
|
|
20980
|
+
searchNewText: normalized.normalizedNewText
|
|
20981
|
+
};
|
|
20982
|
+
if (this.matchingStrategy.find(normalized.normalizedContent, normalized.normalizedOldText).found) {
|
|
20983
|
+
return literal;
|
|
20984
|
+
}
|
|
20985
|
+
const { result: desanitizedOldText, appliedReplacements } = desanitizeMatchString(
|
|
20986
|
+
normalized.normalizedOldText
|
|
20987
|
+
);
|
|
20988
|
+
if (appliedReplacements.length === 0) {
|
|
20989
|
+
return literal;
|
|
20990
|
+
}
|
|
20991
|
+
if (!this.matchingStrategy.find(normalized.normalizedContent, desanitizedOldText).found) {
|
|
20992
|
+
return literal;
|
|
20993
|
+
}
|
|
20994
|
+
let desanitizedNewText = normalized.normalizedNewText;
|
|
20995
|
+
for (const { from, to } of appliedReplacements) {
|
|
20996
|
+
desanitizedNewText = desanitizedNewText.split(from).join(to);
|
|
20997
|
+
}
|
|
20998
|
+
return { searchOldText: desanitizedOldText, searchNewText: desanitizedNewText };
|
|
20999
|
+
}
|
|
21000
|
+
locateUniqueMatch(normalized, search) {
|
|
21001
|
+
const matchResult = this.matchingStrategy.find(normalized.normalizedContent, search.searchOldText);
|
|
20404
21002
|
if (!matchResult.found) {
|
|
20405
21003
|
rejectWith(
|
|
20406
21004
|
`The supplied text was not located in ${this.input.path}. It has to line up character for character, including every space and line break.`
|
|
20407
21005
|
);
|
|
20408
21006
|
}
|
|
20409
21007
|
const fuzzyContent = normalizeForFuzzyMatch(normalized.normalizedContent);
|
|
20410
|
-
const fuzzyOldText = normalizeForFuzzyMatch(
|
|
21008
|
+
const fuzzyOldText = normalizeForFuzzyMatch(search.searchOldText);
|
|
20411
21009
|
const matchCount = fuzzyContent.split(fuzzyOldText).length - 1;
|
|
20412
|
-
if (matchCount > 1) {
|
|
21010
|
+
if (matchCount > 1 && !this.replaceAll) {
|
|
20413
21011
|
rejectWith(
|
|
20414
|
-
`The text appears ${matchCount} times in ${this.input.path}, so the target is ambiguous. Include surrounding lines so it matches in exactly one place.`
|
|
21012
|
+
`The text appears ${matchCount} times in ${this.input.path}, so the target is ambiguous. Include surrounding lines so it matches in exactly one place, or set replaceAll to true to change every occurrence.`
|
|
20415
21013
|
);
|
|
20416
21014
|
}
|
|
20417
21015
|
return { matchResult, matchCount };
|
|
20418
21016
|
}
|
|
20419
|
-
applyReplacement(normalized, match) {
|
|
20420
|
-
const
|
|
20421
|
-
const
|
|
21017
|
+
applyReplacement(normalized, search, match) {
|
|
21018
|
+
const usedFuzzy = match.matchResult.usedFuzzyMatch;
|
|
21019
|
+
const matchedNeedle = usedFuzzy ? this.recoverOriginalSpan(normalized.normalizedContent, search.searchOldText) : search.searchOldText;
|
|
21020
|
+
const editableBase = normalized.normalizedContent;
|
|
21021
|
+
const replacementText = usedFuzzy ? preserveQuoteStyle(search.searchOldText, matchedNeedle, search.searchNewText) : search.searchNewText;
|
|
21022
|
+
const firstIndex = editableBase.indexOf(matchedNeedle);
|
|
21023
|
+
if (firstIndex === -1) {
|
|
21024
|
+
rejectWith(
|
|
21025
|
+
`The supplied text was not located in ${this.input.path}. It has to line up character for character, including every space and line break.`
|
|
21026
|
+
);
|
|
21027
|
+
}
|
|
21028
|
+
const replacedContent = this.replaceAll ? replaceAllLiteral(editableBase, matchedNeedle, replacementText) : editableBase.substring(0, firstIndex) + replacementText + editableBase.substring(firstIndex + matchedNeedle.length);
|
|
20422
21029
|
if (editableBase === replacedContent) {
|
|
20423
21030
|
rejectWith(
|
|
20424
21031
|
`The edit left ${this.input.path} unchanged because the replacement matches the original. Often this points to unexpected special characters or text that is not actually present.`
|
|
20425
21032
|
);
|
|
20426
21033
|
}
|
|
21034
|
+
const replacements = this.replaceAll ? Math.max(match.matchCount, 1) : 1;
|
|
20427
21035
|
const finalContent = normalized.bom + restoreLineEndings(replacedContent, normalized.originalEnding);
|
|
20428
|
-
return { editableBase, replacedContent, finalContent };
|
|
21036
|
+
return { editableBase, replacedContent, finalContent, replacements };
|
|
21037
|
+
}
|
|
21038
|
+
/**
|
|
21039
|
+
* Recover the original (un-folded) span in `content` whose glyph-folded form
|
|
21040
|
+
* equals the folded `searchOldText`. Mirrors induscode's `findActualString`:
|
|
21041
|
+
* locate the match in fully-folded space, then slice the same window out of
|
|
21042
|
+
* the original content so curly quotes and other glyphs are preserved. Falls
|
|
21043
|
+
* back to the supplied text if the window can't be recovered.
|
|
21044
|
+
*/
|
|
21045
|
+
recoverOriginalSpan(content, searchOldText) {
|
|
21046
|
+
const foldedContent = normalizeForFuzzyMatch(content);
|
|
21047
|
+
const foldedNeedle = normalizeForFuzzyMatch(searchOldText);
|
|
21048
|
+
const at = foldedContent.indexOf(foldedNeedle);
|
|
21049
|
+
if (at === -1) {
|
|
21050
|
+
return searchOldText;
|
|
21051
|
+
}
|
|
21052
|
+
for (let len = foldedNeedle.length; at + len <= content.length; len++) {
|
|
21053
|
+
const candidate = content.slice(at, at + len);
|
|
21054
|
+
if (normalizeForFuzzyMatch(candidate) === foldedNeedle) {
|
|
21055
|
+
return candidate;
|
|
21056
|
+
}
|
|
21057
|
+
}
|
|
21058
|
+
return content.slice(at, at + foldedNeedle.length);
|
|
20429
21059
|
}
|
|
20430
21060
|
async persist(finalContent) {
|
|
20431
21061
|
await this.ops.writeFile(this.absolutePath, finalContent);
|
|
21062
|
+
recordReadState(this.readState, this.absolutePath);
|
|
20432
21063
|
}
|
|
20433
|
-
buildResponse(editableBase, replacedContent) {
|
|
21064
|
+
buildResponse(editableBase, replacedContent, replacements) {
|
|
20434
21065
|
const diffResult = generateDiffString(editableBase, replacedContent);
|
|
21066
|
+
const summary = replacements > 1 ? `Replaced the target text in ${this.input.path} (${replacements} occurrences).` : `Replaced the target text in ${this.input.path}.`;
|
|
20435
21067
|
return {
|
|
20436
|
-
content: [{ type: "text", text:
|
|
21068
|
+
content: [{ type: "text", text: summary }],
|
|
20437
21069
|
details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine }
|
|
20438
21070
|
};
|
|
20439
21071
|
}
|
|
@@ -20451,12 +21083,20 @@ function createEditTool(cwd, options) {
|
|
|
20451
21083
|
return {
|
|
20452
21084
|
name: "edit",
|
|
20453
21085
|
label: "edit",
|
|
20454
|
-
description: "Make a targeted change to a file by swapping one exact span of text for another. Provide oldText that reproduces the existing content character for character, whitespace included. Best for narrow, precise edits.",
|
|
21086
|
+
description: "Make a targeted change to a file by swapping one exact span of text for another. Provide oldText that reproduces the existing content character for character, whitespace included. Best for narrow, precise edits. By default oldText must match exactly once; set replaceAll to true to change every occurrence instead.",
|
|
20455
21087
|
parameters: editSchema,
|
|
20456
21088
|
execute: async (_toolCallId, input, signal) => {
|
|
20457
21089
|
validator.validate(input.path, input.oldText);
|
|
20458
21090
|
const absolutePath = resolveToCwd(input.path, cwd);
|
|
20459
|
-
const session = new EditSession(
|
|
21091
|
+
const session = new EditSession(
|
|
21092
|
+
input,
|
|
21093
|
+
absolutePath,
|
|
21094
|
+
ops,
|
|
21095
|
+
matchingStrategy,
|
|
21096
|
+
cwd,
|
|
21097
|
+
options?.readState,
|
|
21098
|
+
options?.checkpoint
|
|
21099
|
+
);
|
|
20460
21100
|
return withAbort(signal, async ({ throwIfAborted: throwIfAborted2 }) => session.run(throwIfAborted2));
|
|
20461
21101
|
}
|
|
20462
21102
|
};
|
|
@@ -20465,28 +21105,96 @@ var editTool = createEditTool(process.cwd());
|
|
|
20465
21105
|
|
|
20466
21106
|
// src/facade/bot/actions/find.ts
|
|
20467
21107
|
import { Type as Type4 } from "@sinclair/typebox";
|
|
20468
|
-
import { existsSync as existsSync3 } from "fs";
|
|
21108
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
20469
21109
|
import path from "path";
|
|
20470
21110
|
var findSchema = Type4.Object({
|
|
20471
21111
|
pattern: Type4.String({
|
|
20472
21112
|
description: 'Glob that the filenames should match, such as "*.ts", "**/*.json", or "lib/**/*.test.ts"'
|
|
20473
21113
|
}),
|
|
20474
21114
|
path: Type4.Optional(Type4.String({ description: "Directory tree to traverse. Falls back to the working directory." })),
|
|
21115
|
+
type: Type4.Optional(
|
|
21116
|
+
Type4.String({
|
|
21117
|
+
description: `Restrict results to a language/category (for example "ts", "js", "py"). Filters by the entry's file extension. Defaults to no type filter.`
|
|
21118
|
+
})
|
|
21119
|
+
),
|
|
20475
21120
|
limit: Type4.Optional(Type4.Number({ description: "Maximum count of paths handed back. Defaults to 1024." }))
|
|
20476
21121
|
});
|
|
20477
21122
|
var DEFAULT_LIMIT = 1024;
|
|
20478
21123
|
var DEFAULT_EXCLUDES = ["node_modules", ".git"];
|
|
20479
21124
|
var MAX_DIRECTORY_DEPTH = 20;
|
|
21125
|
+
function escapeRegexLiteral(literal) {
|
|
21126
|
+
return literal.replace(/[\\.*+?^${}()|[\]]/g, "\\$&");
|
|
21127
|
+
}
|
|
21128
|
+
function compileGlob(pattern) {
|
|
21129
|
+
let body = "";
|
|
21130
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
21131
|
+
const ch = pattern[i];
|
|
21132
|
+
if (ch === "*") {
|
|
21133
|
+
if (pattern[i + 1] === "*") {
|
|
21134
|
+
i++;
|
|
21135
|
+
if (pattern[i + 1] === "/") {
|
|
21136
|
+
i++;
|
|
21137
|
+
body += "(?:[^/]*/)*";
|
|
21138
|
+
} else {
|
|
21139
|
+
body += ".*";
|
|
21140
|
+
}
|
|
21141
|
+
} else {
|
|
21142
|
+
body += "[^/]*";
|
|
21143
|
+
}
|
|
21144
|
+
} else if (ch === "?") {
|
|
21145
|
+
body += "[^/]";
|
|
21146
|
+
} else {
|
|
21147
|
+
body += escapeRegexLiteral(ch);
|
|
21148
|
+
}
|
|
21149
|
+
}
|
|
21150
|
+
try {
|
|
21151
|
+
return new RegExp(`^${body}$`);
|
|
21152
|
+
} catch {
|
|
21153
|
+
return null;
|
|
21154
|
+
}
|
|
21155
|
+
}
|
|
20480
21156
|
var GlobMatcher = class {
|
|
20481
|
-
|
|
21157
|
+
relRegex;
|
|
21158
|
+
baseRegex;
|
|
21159
|
+
hasSeparator;
|
|
20482
21160
|
constructor(pattern) {
|
|
20483
|
-
|
|
20484
|
-
this.
|
|
20485
|
-
|
|
20486
|
-
|
|
20487
|
-
|
|
21161
|
+
this.hasSeparator = pattern.includes("/");
|
|
21162
|
+
this.relRegex = compileGlob(pattern);
|
|
21163
|
+
this.baseRegex = this.hasSeparator ? null : this.relRegex;
|
|
21164
|
+
}
|
|
21165
|
+
/** Test against the root-relative path (preferred), falling back to basename. */
|
|
21166
|
+
matchesPath(relativePath) {
|
|
21167
|
+
const normalized = relativePath.split(path.sep).join("/");
|
|
21168
|
+
if (this.relRegex && this.relRegex.test(normalized)) {
|
|
21169
|
+
return true;
|
|
21170
|
+
}
|
|
21171
|
+
if (this.baseRegex) {
|
|
21172
|
+
const base = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
21173
|
+
return this.baseRegex.test(base);
|
|
21174
|
+
}
|
|
21175
|
+
return false;
|
|
20488
21176
|
}
|
|
20489
21177
|
};
|
|
21178
|
+
function typeToExtensions(type) {
|
|
21179
|
+
const key = type.trim().toLowerCase();
|
|
21180
|
+
if (key.length === 0) return null;
|
|
21181
|
+
const TABLE = {
|
|
21182
|
+
ts: ["ts", "tsx", "mts", "cts"],
|
|
21183
|
+
js: ["js", "jsx", "mjs", "cjs"],
|
|
21184
|
+
py: ["py", "pyi"],
|
|
21185
|
+
rs: ["rs"],
|
|
21186
|
+
go: ["go"],
|
|
21187
|
+
java: ["java"],
|
|
21188
|
+
c: ["c", "h"],
|
|
21189
|
+
cpp: ["cpp", "cc", "cxx", "hpp", "hh"],
|
|
21190
|
+
md: ["md", "markdown"],
|
|
21191
|
+
json: ["json"],
|
|
21192
|
+
yaml: ["yaml", "yml"],
|
|
21193
|
+
sh: ["sh", "bash", "zsh"]
|
|
21194
|
+
};
|
|
21195
|
+
const exts = TABLE[key] ?? [key];
|
|
21196
|
+
return new Set(exts.map((e) => e.toLowerCase()));
|
|
21197
|
+
}
|
|
20490
21198
|
var DirectoryWalkPolicy = class {
|
|
20491
21199
|
constructor(limit) {
|
|
20492
21200
|
this.limit = limit;
|
|
@@ -20500,39 +21208,50 @@ var DirectoryWalkPolicy = class {
|
|
|
20500
21208
|
}
|
|
20501
21209
|
};
|
|
20502
21210
|
var DirectoryFinder = class {
|
|
20503
|
-
constructor(matcher, walkPolicy, limit) {
|
|
21211
|
+
constructor(matcher, walkPolicy, limit, root, typeExtensions) {
|
|
20504
21212
|
this.matcher = matcher;
|
|
20505
21213
|
this.walkPolicy = walkPolicy;
|
|
20506
21214
|
this.limit = limit;
|
|
21215
|
+
this.root = root;
|
|
21216
|
+
this.typeExtensions = typeExtensions;
|
|
20507
21217
|
}
|
|
20508
21218
|
matcher;
|
|
20509
21219
|
walkPolicy;
|
|
20510
21220
|
limit;
|
|
20511
|
-
|
|
21221
|
+
root;
|
|
21222
|
+
typeExtensions;
|
|
20512
21223
|
async find(dir) {
|
|
20513
21224
|
const results = [];
|
|
20514
21225
|
await this.searchRecursive(dir, 0, results);
|
|
20515
21226
|
return results;
|
|
20516
21227
|
}
|
|
21228
|
+
extensionOf(name) {
|
|
21229
|
+
const dot = name.lastIndexOf(".");
|
|
21230
|
+
return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
|
|
21231
|
+
}
|
|
20517
21232
|
async searchRecursive(currentDir, depth, results) {
|
|
20518
21233
|
if (!this.walkPolicy.canContinue(results.length, depth)) {
|
|
20519
21234
|
return;
|
|
20520
21235
|
}
|
|
20521
21236
|
try {
|
|
20522
|
-
const entries =
|
|
21237
|
+
const entries = readdirSync2(currentDir);
|
|
20523
21238
|
for (const entry of entries) {
|
|
20524
21239
|
if (results.length >= this.limit) {
|
|
20525
21240
|
break;
|
|
20526
21241
|
}
|
|
20527
21242
|
const fullPath = path.join(currentDir, entry);
|
|
20528
|
-
const
|
|
20529
|
-
if (
|
|
21243
|
+
const stat3 = statSync3(fullPath);
|
|
21244
|
+
if (stat3.isDirectory()) {
|
|
20530
21245
|
if (this.walkPolicy.shouldDescend(entry)) {
|
|
20531
21246
|
await this.searchRecursive(fullPath, depth + 1, results);
|
|
20532
21247
|
}
|
|
20533
21248
|
continue;
|
|
20534
21249
|
}
|
|
20535
|
-
if (this.
|
|
21250
|
+
if (this.typeExtensions && !this.typeExtensions.has(this.extensionOf(entry))) {
|
|
21251
|
+
continue;
|
|
21252
|
+
}
|
|
21253
|
+
const relativePath = path.relative(this.root, fullPath);
|
|
21254
|
+
if (this.matcher.matchesPath(relativePath)) {
|
|
20536
21255
|
results.push(fullPath);
|
|
20537
21256
|
}
|
|
20538
21257
|
}
|
|
@@ -20587,9 +21306,10 @@ function createFindTool(cwd, options) {
|
|
|
20587
21306
|
return {
|
|
20588
21307
|
name: "find",
|
|
20589
21308
|
label: "find",
|
|
21309
|
+
readOnly: true,
|
|
20590
21310
|
description: `Find files whose names match a glob, returning each result as a path relative to the directory that was searched. The listing halts at ${DEFAULT_LIMIT} paths or ${DEFAULT_MAX_BYTES / 1024}KB of output, whichever it reaches first.`,
|
|
20591
21311
|
parameters: findSchema,
|
|
20592
|
-
execute: async (_toolCallId, { pattern, path: searchDir, limit }, signal) => {
|
|
21312
|
+
execute: async (_toolCallId, { pattern, path: searchDir, type, limit }, signal) => {
|
|
20593
21313
|
return new Promise((resolve5, reject) => {
|
|
20594
21314
|
if (signal?.aborted) {
|
|
20595
21315
|
reject(new Error("Operation aborted"));
|
|
@@ -20608,7 +21328,8 @@ function createFindTool(cwd, options) {
|
|
|
20608
21328
|
}
|
|
20609
21329
|
const matcher = new GlobMatcher(pattern);
|
|
20610
21330
|
const walkPolicy = new DirectoryWalkPolicy(effectiveLimit);
|
|
20611
|
-
const
|
|
21331
|
+
const typeExtensions = type ? typeToExtensions(type) : null;
|
|
21332
|
+
const finder = new DirectoryFinder(matcher, walkPolicy, effectiveLimit, searchPath, typeExtensions);
|
|
20612
21333
|
let results = await finder.find(searchPath);
|
|
20613
21334
|
results = results.filter(
|
|
20614
21335
|
(entryPath) => ![...excludes].some((excluded) => entryPath.includes(`/${excluded}/`) || entryPath.endsWith(`/${excluded}`))
|
|
@@ -20634,7 +21355,7 @@ var findTool = createFindTool(process.cwd());
|
|
|
20634
21355
|
|
|
20635
21356
|
// src/facade/bot/actions/grep.ts
|
|
20636
21357
|
import { Type as Type5 } from "@sinclair/typebox";
|
|
20637
|
-
import { readFileSync, statSync } from "fs";
|
|
21358
|
+
import { readFileSync as readFileSync2, statSync as statSync4, readdirSync as readdirSync3 } from "fs";
|
|
20638
21359
|
import path2 from "path";
|
|
20639
21360
|
var grepSchema = Type5.Object({
|
|
20640
21361
|
pattern: Type5.String({ description: "What to search for, given either as a regular expression or as ordinary text" }),
|
|
@@ -20644,20 +21365,158 @@ var grepSchema = Type5.Object({
|
|
|
20644
21365
|
Type5.Boolean({ description: "Set to true to treat the pattern as exact text instead of a regex. Defaults to regex mode." })
|
|
20645
21366
|
),
|
|
20646
21367
|
context: Type5.Optional(
|
|
20647
|
-
Type5.Number({ description: "Number of neighboring lines to print above and below each hit. Defaults to zero." })
|
|
21368
|
+
Type5.Number({ description: "Number of neighboring lines to print above and below each hit (content mode). Defaults to zero." })
|
|
21369
|
+
),
|
|
21370
|
+
before: Type5.Optional(
|
|
21371
|
+
Type5.Number({ description: "Number of lines to print BEFORE each hit (-B). Overrides `context` for leading lines." })
|
|
21372
|
+
),
|
|
21373
|
+
after: Type5.Optional(
|
|
21374
|
+
Type5.Number({ description: "Number of lines to print AFTER each hit (-A). Overrides `context` for trailing lines." })
|
|
21375
|
+
),
|
|
21376
|
+
output_mode: Type5.Optional(
|
|
21377
|
+
Type5.Union(
|
|
21378
|
+
[Type5.Literal("content"), Type5.Literal("files_with_matches"), Type5.Literal("count")],
|
|
21379
|
+
{
|
|
21380
|
+
description: 'How results are reported: "content" (default) shows matching lines, "files_with_matches" lists matching file paths, "count" reports per-file occurrence totals.'
|
|
21381
|
+
}
|
|
21382
|
+
)
|
|
21383
|
+
),
|
|
21384
|
+
glob: Type5.Optional(
|
|
21385
|
+
Type5.String({
|
|
21386
|
+
description: 'Only search files whose root-relative path matches this glob (e.g. "*.ts" or "src/**/*.test.ts"). Multiple globs may be comma-separated. Defaults to no glob filter.'
|
|
21387
|
+
})
|
|
21388
|
+
),
|
|
21389
|
+
type: Type5.Optional(
|
|
21390
|
+
Type5.String({
|
|
21391
|
+
description: 'Only search files of this language/category (for example "ts", "js", "py"), matched by extension. Defaults to no type filter.'
|
|
21392
|
+
})
|
|
20648
21393
|
),
|
|
20649
21394
|
limit: Type5.Optional(Type5.Number({ description: "Upper bound on the count of hits returned. Defaults to 128." }))
|
|
20650
21395
|
});
|
|
20651
21396
|
var DEFAULT_LIMIT2 = 128;
|
|
20652
21397
|
var regexCache = /* @__PURE__ */ new Map();
|
|
20653
21398
|
var REGEX_CACHE_MAX_SIZE = 256;
|
|
20654
|
-
var
|
|
20655
|
-
|
|
20656
|
-
|
|
20657
|
-
|
|
20658
|
-
|
|
20659
|
-
|
|
21399
|
+
var PRUNED_DIRS = /* @__PURE__ */ new Set([".git", ".svn", ".hg", ".bzr", ".jj", ".sl", "node_modules"]);
|
|
21400
|
+
function escapeGlobLiteral(literal) {
|
|
21401
|
+
return literal.replace(/[\\.*+?^${}()|[\]]/g, "\\$&");
|
|
21402
|
+
}
|
|
21403
|
+
function compileGlob2(glob) {
|
|
21404
|
+
let body = "";
|
|
21405
|
+
for (let i = 0; i < glob.length; i++) {
|
|
21406
|
+
const ch = glob[i];
|
|
21407
|
+
if (ch === "*") {
|
|
21408
|
+
if (glob[i + 1] === "*") {
|
|
21409
|
+
i++;
|
|
21410
|
+
if (glob[i + 1] === "/") {
|
|
21411
|
+
i++;
|
|
21412
|
+
body += "(?:[^/]*/)*";
|
|
21413
|
+
} else {
|
|
21414
|
+
body += ".*";
|
|
21415
|
+
}
|
|
21416
|
+
} else {
|
|
21417
|
+
body += "[^/]*";
|
|
21418
|
+
}
|
|
21419
|
+
} else if (ch === "?") {
|
|
21420
|
+
body += "[^/]";
|
|
21421
|
+
} else {
|
|
21422
|
+
body += escapeGlobLiteral(ch);
|
|
21423
|
+
}
|
|
21424
|
+
}
|
|
21425
|
+
try {
|
|
21426
|
+
return new RegExp(`^${body}$`);
|
|
21427
|
+
} catch {
|
|
21428
|
+
return null;
|
|
21429
|
+
}
|
|
21430
|
+
}
|
|
21431
|
+
function buildIncludePredicate(glob, type) {
|
|
21432
|
+
const globRegexes = [];
|
|
21433
|
+
if (glob && glob.trim().length > 0) {
|
|
21434
|
+
for (const part of glob.split(",")) {
|
|
21435
|
+
const trimmed = part.trim();
|
|
21436
|
+
if (trimmed.length === 0) continue;
|
|
21437
|
+
const re = compileGlob2(trimmed);
|
|
21438
|
+
if (re) globRegexes.push(re);
|
|
21439
|
+
}
|
|
21440
|
+
}
|
|
21441
|
+
const extensions = type ? typeToExtensions2(type) : null;
|
|
21442
|
+
if (globRegexes.length === 0 && !extensions) return null;
|
|
21443
|
+
return (relPath) => {
|
|
21444
|
+
const normalized = relPath.split(path2.sep).join("/");
|
|
21445
|
+
if (globRegexes.length > 0) {
|
|
21446
|
+
const matchesGlob = globRegexes.some((re) => {
|
|
21447
|
+
if (re.test(normalized)) return true;
|
|
21448
|
+
const base = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
21449
|
+
return re.test(base);
|
|
21450
|
+
});
|
|
21451
|
+
if (!matchesGlob) return false;
|
|
21452
|
+
}
|
|
21453
|
+
if (extensions) {
|
|
21454
|
+
const base = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
21455
|
+
const dot = base.lastIndexOf(".");
|
|
21456
|
+
const ext = dot > 0 ? base.slice(dot + 1).toLowerCase() : "";
|
|
21457
|
+
if (!extensions.has(ext)) return false;
|
|
21458
|
+
}
|
|
21459
|
+
return true;
|
|
21460
|
+
};
|
|
21461
|
+
}
|
|
21462
|
+
function typeToExtensions2(type) {
|
|
21463
|
+
const key = type.trim().toLowerCase();
|
|
21464
|
+
if (key.length === 0) return null;
|
|
21465
|
+
const TABLE = {
|
|
21466
|
+
ts: ["ts", "tsx", "mts", "cts"],
|
|
21467
|
+
js: ["js", "jsx", "mjs", "cjs"],
|
|
21468
|
+
py: ["py", "pyi"],
|
|
21469
|
+
rs: ["rs"],
|
|
21470
|
+
go: ["go"],
|
|
21471
|
+
java: ["java"],
|
|
21472
|
+
c: ["c", "h"],
|
|
21473
|
+
cpp: ["cpp", "cc", "cxx", "hpp", "hh"],
|
|
21474
|
+
md: ["md", "markdown"],
|
|
21475
|
+
json: ["json"],
|
|
21476
|
+
yaml: ["yaml", "yml"],
|
|
21477
|
+
sh: ["sh", "bash", "zsh"]
|
|
21478
|
+
};
|
|
21479
|
+
const exts = TABLE[key] ?? [key];
|
|
21480
|
+
return new Set(exts.map((e) => e.toLowerCase()));
|
|
21481
|
+
}
|
|
21482
|
+
async function loadGitignore(rootDir, readFile3) {
|
|
21483
|
+
let body;
|
|
21484
|
+
try {
|
|
21485
|
+
body = await readFile3(path2.join(rootDir, ".gitignore"));
|
|
21486
|
+
} catch {
|
|
21487
|
+
return () => false;
|
|
21488
|
+
}
|
|
21489
|
+
const regexes = [];
|
|
21490
|
+
for (const raw of body.split("\n")) {
|
|
21491
|
+
const line = raw.trim();
|
|
21492
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
21493
|
+
const anchored = line.startsWith("/");
|
|
21494
|
+
const pat = anchored ? line.slice(1) : line;
|
|
21495
|
+
const re = compileGlob2(pat.replace(/\/$/, ""));
|
|
21496
|
+
if (!re) continue;
|
|
21497
|
+
if (anchored) {
|
|
21498
|
+
regexes.push(new RegExp(`${re.source.slice(0, -1)}(?:/.*)?$`));
|
|
21499
|
+
} else {
|
|
21500
|
+
regexes.push(new RegExp(`(?:^|/)${re.source.slice(1, -1)}(?:/.*)?$`));
|
|
21501
|
+
}
|
|
20660
21502
|
}
|
|
21503
|
+
if (regexes.length === 0) return () => false;
|
|
21504
|
+
return (relPath) => {
|
|
21505
|
+
const normalized = relPath.split(path2.sep).join("/");
|
|
21506
|
+
return regexes.some((re) => re.test(normalized));
|
|
21507
|
+
};
|
|
21508
|
+
}
|
|
21509
|
+
function looksBinary(text) {
|
|
21510
|
+
const limit = Math.min(text.length, 4096);
|
|
21511
|
+
for (let i = 0; i < limit; i++) {
|
|
21512
|
+
if (text.charCodeAt(i) === 0) return true;
|
|
21513
|
+
}
|
|
21514
|
+
return false;
|
|
21515
|
+
}
|
|
21516
|
+
var defaultGrepOperations = {
|
|
21517
|
+
isDirectory: (p) => statSync4(p).isDirectory(),
|
|
21518
|
+
readFile: (p) => readFileSync2(p, "utf-8"),
|
|
21519
|
+
readdir: (p) => readdirSync3(p)
|
|
20661
21520
|
};
|
|
20662
21521
|
var RegexFactory = class {
|
|
20663
21522
|
static create(pattern, ignoreCase, literal) {
|
|
@@ -20678,12 +21537,22 @@ var RegexFactory = class {
|
|
|
20678
21537
|
}
|
|
20679
21538
|
};
|
|
20680
21539
|
var SearchPlan = class {
|
|
20681
|
-
constructor(searchPath, ops) {
|
|
21540
|
+
constructor(searchPath, ops, options) {
|
|
20682
21541
|
this.searchPath = searchPath;
|
|
20683
21542
|
this.ops = ops;
|
|
21543
|
+
this.include = options?.include ?? null;
|
|
21544
|
+
this.ignored = options?.ignored ?? null;
|
|
20684
21545
|
}
|
|
20685
21546
|
searchPath;
|
|
20686
21547
|
ops;
|
|
21548
|
+
include;
|
|
21549
|
+
ignored;
|
|
21550
|
+
/**
|
|
21551
|
+
* Fully recursive (depth-first) enumeration of every readable file beneath
|
|
21552
|
+
* the search root. Pruned VCS/dependency folders and dot-directories are
|
|
21553
|
+
* skipped, directories are NEVER pushed as if they were files, and the
|
|
21554
|
+
* optional include/gitignore predicates scope or exclude the result set.
|
|
21555
|
+
*/
|
|
20687
21556
|
async collectFiles() {
|
|
20688
21557
|
const filesToSearch = [];
|
|
20689
21558
|
const isDir = await this.ops.isDirectory(this.searchPath);
|
|
@@ -20691,38 +21560,57 @@ var SearchPlan = class {
|
|
|
20691
21560
|
filesToSearch.push(this.searchPath);
|
|
20692
21561
|
return filesToSearch;
|
|
20693
21562
|
}
|
|
20694
|
-
|
|
21563
|
+
await this.walk(this.searchPath, filesToSearch);
|
|
21564
|
+
return filesToSearch;
|
|
21565
|
+
}
|
|
21566
|
+
relativeOf(fullPath) {
|
|
21567
|
+
return path2.relative(this.searchPath, fullPath);
|
|
21568
|
+
}
|
|
21569
|
+
async walk(dir, sink) {
|
|
21570
|
+
let entries;
|
|
21571
|
+
try {
|
|
21572
|
+
entries = await this.ops.readdir(dir);
|
|
21573
|
+
} catch {
|
|
21574
|
+
return;
|
|
21575
|
+
}
|
|
21576
|
+
entries.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
20695
21577
|
for (const entry of entries) {
|
|
20696
|
-
const fullPath = path2.join(
|
|
21578
|
+
const fullPath = path2.join(dir, entry);
|
|
21579
|
+
let entryIsDir;
|
|
20697
21580
|
try {
|
|
20698
|
-
|
|
20699
|
-
const subEntries = await this.ops.readdir(fullPath);
|
|
20700
|
-
for (const subEntry of subEntries) {
|
|
20701
|
-
filesToSearch.push(path2.join(fullPath, subEntry));
|
|
20702
|
-
}
|
|
20703
|
-
} else if (!await this.ops.isDirectory(fullPath)) {
|
|
20704
|
-
filesToSearch.push(fullPath);
|
|
20705
|
-
}
|
|
21581
|
+
entryIsDir = await this.ops.isDirectory(fullPath);
|
|
20706
21582
|
} catch {
|
|
21583
|
+
continue;
|
|
21584
|
+
}
|
|
21585
|
+
const rel = this.relativeOf(fullPath);
|
|
21586
|
+
if (entryIsDir) {
|
|
21587
|
+
if (PRUNED_DIRS.has(entry) || entry.startsWith(".")) continue;
|
|
21588
|
+
if (this.ignored && this.ignored(rel)) continue;
|
|
21589
|
+
await this.walk(fullPath, sink);
|
|
21590
|
+
continue;
|
|
20707
21591
|
}
|
|
21592
|
+
if (this.ignored && this.ignored(rel)) continue;
|
|
21593
|
+
if (this.include && !this.include(rel)) continue;
|
|
21594
|
+
sink.push(fullPath);
|
|
20708
21595
|
}
|
|
20709
|
-
return filesToSearch;
|
|
20710
21596
|
}
|
|
20711
21597
|
};
|
|
20712
21598
|
var MatchRenderer = class {
|
|
20713
|
-
constructor(searchPath, contextValue) {
|
|
21599
|
+
constructor(searchPath, contextValue, before, after) {
|
|
20714
21600
|
this.searchPath = searchPath;
|
|
20715
|
-
this.
|
|
21601
|
+
this.before = before !== void 0 && before > 0 ? before : contextValue;
|
|
21602
|
+
this.after = after !== void 0 && after > 0 ? after : contextValue;
|
|
20716
21603
|
}
|
|
20717
21604
|
searchPath;
|
|
20718
|
-
|
|
21605
|
+
before;
|
|
21606
|
+
after;
|
|
20719
21607
|
renderMatch(filePath, lines, lineIndex) {
|
|
20720
21608
|
const relativePath = path2.relative(this.searchPath, filePath);
|
|
20721
21609
|
const line = lines[lineIndex] ?? "";
|
|
20722
21610
|
const { wasTruncated } = truncateLine(line, GREP_MAX_LINE_LENGTH);
|
|
20723
21611
|
const output = [];
|
|
20724
|
-
const start = Math.max(0, lineIndex - this.
|
|
20725
|
-
const end = Math.min(lines.length - 1, lineIndex + this.
|
|
21612
|
+
const start = Math.max(0, lineIndex - this.before);
|
|
21613
|
+
const end = Math.min(lines.length - 1, lineIndex + this.after);
|
|
20726
21614
|
for (let j = start; j <= end; j++) {
|
|
20727
21615
|
const isMatch = j === lineIndex;
|
|
20728
21616
|
const prefix = isMatch ? ":" : "-";
|
|
@@ -20768,12 +21656,32 @@ var MatchRenderer = class {
|
|
|
20768
21656
|
details: Object.keys(details).length > 0 ? details : void 0
|
|
20769
21657
|
};
|
|
20770
21658
|
}
|
|
21659
|
+
/** `files_with_matches` mode: one root-relative path per matching file. */
|
|
21660
|
+
formatFilesWithMatches(files) {
|
|
21661
|
+
if (files.length === 0) {
|
|
21662
|
+
return { output: "Pattern not found in any file" };
|
|
21663
|
+
}
|
|
21664
|
+
return { output: files.join("\n") };
|
|
21665
|
+
}
|
|
21666
|
+
/** `count` mode: `path:N` lines plus a roll-up of occurrences across files. */
|
|
21667
|
+
formatCount(counts) {
|
|
21668
|
+
if (counts.length === 0) {
|
|
21669
|
+
return { output: "Pattern not found in any file" };
|
|
21670
|
+
}
|
|
21671
|
+
const lines = counts.map((c) => `${c.file}:${c.count}`);
|
|
21672
|
+
const total = counts.reduce((sum, c) => sum + c.count, 0);
|
|
21673
|
+
const fileWord = counts.length === 1 ? "file" : "files";
|
|
21674
|
+
const occWord = total === 1 ? "occurrence" : "occurrences";
|
|
21675
|
+
lines.push("", `Found ${total} ${occWord} across ${counts.length} ${fileWord}`);
|
|
21676
|
+
return { output: lines.join("\n") };
|
|
21677
|
+
}
|
|
20771
21678
|
};
|
|
20772
21679
|
function createGrepTool(cwd, options) {
|
|
20773
21680
|
const customOps = options?.operations;
|
|
20774
21681
|
return {
|
|
20775
21682
|
name: "grep",
|
|
20776
21683
|
label: "grep",
|
|
21684
|
+
readOnly: true,
|
|
20777
21685
|
description: `Search file contents for a pattern, returning every hit alongside its path and line number. The search halts at ${DEFAULT_LIMIT2} hits or ${DEFAULT_MAX_BYTES / 1024}KB of output, whichever it reaches first. Any line over ${GREP_MAX_LINE_LENGTH} characters is shortened.`,
|
|
20778
21686
|
parameters: grepSchema,
|
|
20779
21687
|
execute: async (_toolCallId, {
|
|
@@ -20782,6 +21690,11 @@ function createGrepTool(cwd, options) {
|
|
|
20782
21690
|
ignoreCase,
|
|
20783
21691
|
literal,
|
|
20784
21692
|
context,
|
|
21693
|
+
before,
|
|
21694
|
+
after,
|
|
21695
|
+
output_mode,
|
|
21696
|
+
glob,
|
|
21697
|
+
type,
|
|
20785
21698
|
limit
|
|
20786
21699
|
}, signal) => {
|
|
20787
21700
|
return new Promise((resolve5, reject) => {
|
|
@@ -20797,7 +21710,10 @@ function createGrepTool(cwd, options) {
|
|
|
20797
21710
|
const ops = customOps ?? defaultGrepOperations;
|
|
20798
21711
|
const effectiveLimit = limit ?? DEFAULT_LIMIT2;
|
|
20799
21712
|
const contextValue = context && context > 0 ? context : 0;
|
|
20800
|
-
const
|
|
21713
|
+
const mode = output_mode ?? "content";
|
|
21714
|
+
const include = buildIncludePredicate(glob, type);
|
|
21715
|
+
const ignored = await loadGitignore(searchPath, (p) => ops.readFile(p));
|
|
21716
|
+
const searchPlan = new SearchPlan(searchPath, ops, { include, ignored });
|
|
20801
21717
|
const filesToSearch = await searchPlan.collectFiles();
|
|
20802
21718
|
if (filesToSearch.length === 0) {
|
|
20803
21719
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -20812,34 +21728,63 @@ function createGrepTool(cwd, options) {
|
|
|
20812
21728
|
reject(new Error(`Invalid regex pattern: ${e.message}`));
|
|
20813
21729
|
return;
|
|
20814
21730
|
}
|
|
20815
|
-
const matchRenderer = new MatchRenderer(searchPath, contextValue);
|
|
21731
|
+
const matchRenderer = new MatchRenderer(searchPath, contextValue, before, after);
|
|
20816
21732
|
const matches = [];
|
|
21733
|
+
const filesWithMatches = [];
|
|
21734
|
+
const counts = [];
|
|
20817
21735
|
let linesTruncated = false;
|
|
21736
|
+
let stopScanning = false;
|
|
20818
21737
|
for (const filePath of filesToSearch) {
|
|
20819
|
-
if (
|
|
21738
|
+
if (stopScanning) break;
|
|
21739
|
+
if (mode === "content" && matches.length >= effectiveLimit) {
|
|
20820
21740
|
break;
|
|
20821
21741
|
}
|
|
20822
21742
|
try {
|
|
20823
21743
|
const content = await ops.readFile(filePath);
|
|
21744
|
+
if (looksBinary(content)) {
|
|
21745
|
+
continue;
|
|
21746
|
+
}
|
|
20824
21747
|
const lines = content.split("\n");
|
|
21748
|
+
const relativePath = path2.relative(searchPath, filePath);
|
|
21749
|
+
let fileCount = 0;
|
|
20825
21750
|
for (let i = 0; i < lines.length; i++) {
|
|
20826
|
-
if (matches.length >= effectiveLimit) {
|
|
21751
|
+
if (mode === "content" && matches.length >= effectiveLimit) {
|
|
21752
|
+
stopScanning = true;
|
|
20827
21753
|
break;
|
|
20828
21754
|
}
|
|
20829
21755
|
const line = lines[i] ?? "";
|
|
20830
21756
|
if (regex.test(line)) {
|
|
20831
|
-
|
|
20832
|
-
if (
|
|
20833
|
-
|
|
21757
|
+
fileCount++;
|
|
21758
|
+
if (mode === "content") {
|
|
21759
|
+
const rendered = matchRenderer.renderMatch(filePath, lines, i);
|
|
21760
|
+
if (rendered.lineTruncated) {
|
|
21761
|
+
linesTruncated = true;
|
|
21762
|
+
}
|
|
21763
|
+
matches.push({ file: relativePath, line: i + 1, text: rendered.text });
|
|
21764
|
+
} else if (mode === "files_with_matches") {
|
|
21765
|
+
break;
|
|
20834
21766
|
}
|
|
20835
|
-
|
|
21767
|
+
}
|
|
21768
|
+
}
|
|
21769
|
+
if (fileCount > 0) {
|
|
21770
|
+
if (mode === "files_with_matches") {
|
|
21771
|
+
filesWithMatches.push(relativePath);
|
|
21772
|
+
} else if (mode === "count") {
|
|
21773
|
+
counts.push({ file: relativePath, count: fileCount });
|
|
20836
21774
|
}
|
|
20837
21775
|
}
|
|
20838
21776
|
} catch {
|
|
20839
21777
|
}
|
|
20840
21778
|
}
|
|
20841
21779
|
signal?.removeEventListener("abort", onAbort);
|
|
20842
|
-
|
|
21780
|
+
let formatted;
|
|
21781
|
+
if (mode === "files_with_matches") {
|
|
21782
|
+
formatted = matchRenderer.formatFilesWithMatches(filesWithMatches);
|
|
21783
|
+
} else if (mode === "count") {
|
|
21784
|
+
formatted = matchRenderer.formatCount(counts);
|
|
21785
|
+
} else {
|
|
21786
|
+
formatted = matchRenderer.formatOutput(matches, effectiveLimit, linesTruncated);
|
|
21787
|
+
}
|
|
20843
21788
|
resolve5({
|
|
20844
21789
|
content: [{ type: "text", text: formatted.output }],
|
|
20845
21790
|
details: formatted.details
|
|
@@ -20857,7 +21802,7 @@ var grepTool = createGrepTool(process.cwd());
|
|
|
20857
21802
|
|
|
20858
21803
|
// src/facade/bot/actions/ls.ts
|
|
20859
21804
|
import { Type as Type6 } from "@sinclair/typebox";
|
|
20860
|
-
import { existsSync as existsSync4, readdirSync, statSync as
|
|
21805
|
+
import { existsSync as existsSync4, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
20861
21806
|
import nodePath from "path";
|
|
20862
21807
|
var lsSchema = Type6.Object({
|
|
20863
21808
|
path: Type6.Optional(Type6.String({ description: "Directory whose entries should be listed. Falls back to the working directory." })),
|
|
@@ -20866,8 +21811,8 @@ var lsSchema = Type6.Object({
|
|
|
20866
21811
|
var DEFAULT_LIMIT3 = 512;
|
|
20867
21812
|
var defaultLsOperations = {
|
|
20868
21813
|
exists: existsSync4,
|
|
20869
|
-
stat:
|
|
20870
|
-
readdir:
|
|
21814
|
+
stat: statSync5,
|
|
21815
|
+
readdir: readdirSync4
|
|
20871
21816
|
};
|
|
20872
21817
|
function createAbortGuard(signal) {
|
|
20873
21818
|
let aborted = signal?.aborted ?? false;
|
|
@@ -20952,8 +21897,8 @@ var DirectoryListRunner = class {
|
|
|
20952
21897
|
if (!await this.ops.exists(resolvedDir)) {
|
|
20953
21898
|
throw new Error(`No such path: ${resolvedDir}`);
|
|
20954
21899
|
}
|
|
20955
|
-
const
|
|
20956
|
-
if (!
|
|
21900
|
+
const stat3 = await this.ops.stat(resolvedDir);
|
|
21901
|
+
if (!stat3.isDirectory()) {
|
|
20957
21902
|
throw new Error(`Target is not a directory: ${resolvedDir}`);
|
|
20958
21903
|
}
|
|
20959
21904
|
}
|
|
@@ -21009,6 +21954,7 @@ function createLsTool(cwd, options) {
|
|
|
21009
21954
|
return {
|
|
21010
21955
|
name: "ls",
|
|
21011
21956
|
label: "ls",
|
|
21957
|
+
readOnly: true,
|
|
21012
21958
|
description: `List the contents of a directory. Names are returned sorted alphabetically with dotfiles kept in, and subdirectories carry a trailing '/'. The listing halts at ${DEFAULT_LIMIT3} entries or ${DEFAULT_MAX_BYTES / 1024}KB of output, whichever it reaches first.`,
|
|
21013
21959
|
parameters: lsSchema,
|
|
21014
21960
|
execute: async (_toolCallId, { path: path9, limit }, signal) => {
|
|
@@ -22435,24 +23381,25 @@ function withAbort2(signal, task) {
|
|
|
22435
23381
|
});
|
|
22436
23382
|
}
|
|
22437
23383
|
var ReadExecutionPipeline = class {
|
|
22438
|
-
constructor(cwd, ops, autoResizeImages) {
|
|
23384
|
+
constructor(cwd, ops, autoResizeImages, readState) {
|
|
22439
23385
|
this.cwd = cwd;
|
|
22440
23386
|
this.ops = ops;
|
|
22441
23387
|
this.autoResizeImages = autoResizeImages;
|
|
23388
|
+
this.readState = readState;
|
|
22442
23389
|
}
|
|
22443
23390
|
cwd;
|
|
22444
23391
|
ops;
|
|
22445
23392
|
autoResizeImages;
|
|
23393
|
+
readState;
|
|
22446
23394
|
async run(input, throwIfAborted2) {
|
|
22447
23395
|
const absolutePath = resolveReadPath(input.path, this.cwd);
|
|
22448
23396
|
await this.ops.access(absolutePath);
|
|
22449
23397
|
throwIfAborted2();
|
|
22450
23398
|
const mimeType = this.ops.detectImageMimeType ? await this.ops.detectImageMimeType(absolutePath) : void 0;
|
|
22451
23399
|
throwIfAborted2();
|
|
22452
|
-
|
|
22453
|
-
|
|
22454
|
-
|
|
22455
|
-
return this.readTextVariant(absolutePath, input.path, input.offset, input.limit, throwIfAborted2);
|
|
23400
|
+
const result = mimeType ? await this.readImageVariant(absolutePath, mimeType, throwIfAborted2) : await this.readTextVariant(absolutePath, input.path, input.offset, input.limit, throwIfAborted2);
|
|
23401
|
+
recordReadState(this.readState, absolutePath);
|
|
23402
|
+
return result;
|
|
22456
23403
|
}
|
|
22457
23404
|
async readImageVariant(absolutePath, mimeType, throwIfAborted2) {
|
|
22458
23405
|
const buffer = await this.ops.readFile(absolutePath);
|
|
@@ -22500,12 +23447,14 @@ ${dimensionNote}`;
|
|
|
22500
23447
|
renderedText = `[Line ${startLineDisplay} weighs ${firstLineSize}, over the ${formatSize(DEFAULT_MAX_BYTES)} cap. Pull a byte slice via bash: sed -n '${startLineDisplay}p' ${requestedPath} | head -c ${DEFAULT_MAX_BYTES}]`;
|
|
22501
23448
|
details = { truncation };
|
|
22502
23449
|
} else if (truncation.truncated) {
|
|
22503
|
-
|
|
23450
|
+
const gutteredBody = this.addGutter(truncation.content, startLineDisplay);
|
|
23451
|
+
renderedText = this.formatTruncationNotice(gutteredBody, truncation, totalFileLines, startLineDisplay);
|
|
22504
23452
|
details = { truncation };
|
|
22505
23453
|
} else if (requestedLineCount !== void 0 && startLine + requestedLineCount < fileLines.length) {
|
|
22506
|
-
|
|
23454
|
+
const gutteredBody = this.addGutter(truncation.content, startLineDisplay);
|
|
23455
|
+
renderedText = this.formatUserLimitNotice(gutteredBody, fileLines.length, startLine, requestedLineCount);
|
|
22507
23456
|
} else {
|
|
22508
|
-
renderedText = truncation.content;
|
|
23457
|
+
renderedText = this.addGutter(truncation.content, startLineDisplay);
|
|
22509
23458
|
}
|
|
22510
23459
|
throwIfAborted2();
|
|
22511
23460
|
return {
|
|
@@ -22533,14 +23482,30 @@ ${dimensionNote}`;
|
|
|
22533
23482
|
requestedLineCount: void 0
|
|
22534
23483
|
};
|
|
22535
23484
|
}
|
|
22536
|
-
formatTruncationNotice(truncation, totalFileLines, startLineDisplay) {
|
|
23485
|
+
formatTruncationNotice(body, truncation, totalFileLines, startLineDisplay) {
|
|
22537
23486
|
const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
|
|
22538
23487
|
const nextOffset = endLineDisplay + 1;
|
|
22539
23488
|
const continuationHint = this.buildContinuationHint(truncation, totalFileLines, startLineDisplay, endLineDisplay, nextOffset);
|
|
22540
|
-
return `${
|
|
23489
|
+
return `${body}
|
|
22541
23490
|
|
|
22542
23491
|
${continuationHint}`;
|
|
22543
23492
|
}
|
|
23493
|
+
/**
|
|
23494
|
+
* Prefix each line with a right-aligned 1-based absolute line number in the
|
|
23495
|
+
* cat -n style, giving the model stable edit/diff coordinates. `content` is
|
|
23496
|
+
* already windowed; `startLineDisplay` is the whole-file number of its first
|
|
23497
|
+
* line so the gutter stays correct under offset/limit paging. Empty content
|
|
23498
|
+
* (e.g. binary or an over-budget first line yielding "") passes through
|
|
23499
|
+
* unchanged so non-text output stays stable.
|
|
23500
|
+
*/
|
|
23501
|
+
addGutter(content, startLineDisplay) {
|
|
23502
|
+
if (content.length === 0) return content;
|
|
23503
|
+
return content.split(/\r?\n/).map((line, i) => {
|
|
23504
|
+
const numStr = String(startLineDisplay + i);
|
|
23505
|
+
const label = numStr.length >= 6 ? numStr : numStr.padStart(6, " ");
|
|
23506
|
+
return `${label}\u2192${line}`;
|
|
23507
|
+
}).join("\n");
|
|
23508
|
+
}
|
|
22544
23509
|
formatUserLimitNotice(baseText, totalLines, startLine, requestedLineCount) {
|
|
22545
23510
|
const remaining = totalLines - (startLine + requestedLineCount);
|
|
22546
23511
|
const nextOffset = startLine + requestedLineCount + 1;
|
|
@@ -22558,11 +23523,12 @@ ${continuationHint}`;
|
|
|
22558
23523
|
function createReadTool(cwd, options) {
|
|
22559
23524
|
const autoResizeImages = options?.autoResizeImages ?? true;
|
|
22560
23525
|
const ops = options?.operations ?? defaultReadOperations;
|
|
22561
|
-
const pipeline = new ReadExecutionPipeline(cwd, ops, autoResizeImages);
|
|
23526
|
+
const pipeline = new ReadExecutionPipeline(cwd, ops, autoResizeImages, options?.readState);
|
|
22562
23527
|
return {
|
|
22563
23528
|
name: "read",
|
|
22564
23529
|
label: "read",
|
|
22565
|
-
|
|
23530
|
+
readOnly: true,
|
|
23531
|
+
description: `Open a file and return its contents. Both text and images (jpg, png, gif, webp) work; images come back as attachments. Output uses cat -n format - each line is prefixed with its 1-based line number. Text output is capped at ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB, whichever comes first. For big files, page through with offset and limit, advancing offset until you have read everything you need.`,
|
|
22566
23532
|
parameters: readSchema,
|
|
22567
23533
|
execute: async (_toolCallId, { path: path9, offset, limit }, signal) => {
|
|
22568
23534
|
return withAbort2(signal, async ({ throwIfAborted: throwIfAborted2 }) => {
|
|
@@ -22579,9 +23545,9 @@ import { Type as Type15 } from "@sinclair/typebox";
|
|
|
22579
23545
|
// src/facade/bot/actions/process-manager.ts
|
|
22580
23546
|
import { spawn as spawn3 } from "node:child_process";
|
|
22581
23547
|
import { EventEmitter } from "node:events";
|
|
22582
|
-
import { appendFileSync, existsSync as existsSync5, mkdirSync, readFileSync as
|
|
22583
|
-
import { tmpdir as
|
|
22584
|
-
import { isAbsolute as isAbsolute2, join as
|
|
23548
|
+
import { appendFileSync, existsSync as existsSync5, mkdirSync, readFileSync as readFileSync3, rmSync, statSync as statSync6 } from "node:fs";
|
|
23549
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
23550
|
+
import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
|
|
22585
23551
|
|
|
22586
23552
|
// src/facade/bot/actions/process-types.ts
|
|
22587
23553
|
var MESSAGE_TYPE_PROCESS_UPDATE = "ad-process:update";
|
|
@@ -22646,7 +23612,7 @@ var ProcessManager = class {
|
|
|
22646
23612
|
watcher = null;
|
|
22647
23613
|
getConfiguredShellPath;
|
|
22648
23614
|
constructor(options) {
|
|
22649
|
-
this.logDir =
|
|
23615
|
+
this.logDir = join3(tmpdir3(), `indusagi-processes-${Date.now()}`);
|
|
22650
23616
|
mkdirSync(this.logDir, { recursive: true });
|
|
22651
23617
|
this.getConfiguredShellPath = options?.getConfiguredShellPath ?? (() => void 0);
|
|
22652
23618
|
}
|
|
@@ -22715,9 +23681,9 @@ var ProcessManager = class {
|
|
|
22715
23681
|
}
|
|
22716
23682
|
start(name, command, cwd, options) {
|
|
22717
23683
|
const id = `proc_${++this.counter}`;
|
|
22718
|
-
const stdoutFile =
|
|
22719
|
-
const stderrFile =
|
|
22720
|
-
const combinedFile =
|
|
23684
|
+
const stdoutFile = join3(this.logDir, `${id}-stdout.log`);
|
|
23685
|
+
const stderrFile = join3(this.logDir, `${id}-stderr.log`);
|
|
23686
|
+
const combinedFile = join3(this.logDir, `${id}-combined.log`);
|
|
22721
23687
|
appendFileSync(stdoutFile, "");
|
|
22722
23688
|
appendFileSync(stderrFile, "");
|
|
22723
23689
|
appendFileSync(combinedFile, "");
|
|
@@ -22867,8 +23833,8 @@ var ProcessManager = class {
|
|
|
22867
23833
|
}
|
|
22868
23834
|
try {
|
|
22869
23835
|
return {
|
|
22870
|
-
stdout:
|
|
22871
|
-
stderr:
|
|
23836
|
+
stdout: readFileSync3(managed.stdoutFile, "utf-8"),
|
|
23837
|
+
stderr: readFileSync3(managed.stderrFile, "utf-8")
|
|
22872
23838
|
};
|
|
22873
23839
|
} catch {
|
|
22874
23840
|
return { stdout: "", stderr: "" };
|
|
@@ -23024,8 +23990,8 @@ var ProcessManager = class {
|
|
|
23024
23990
|
}
|
|
23025
23991
|
try {
|
|
23026
23992
|
return {
|
|
23027
|
-
stdout:
|
|
23028
|
-
stderr:
|
|
23993
|
+
stdout: statSync6(managed.stdoutFile).size,
|
|
23994
|
+
stderr: statSync6(managed.stderrFile).size
|
|
23029
23995
|
};
|
|
23030
23996
|
} catch {
|
|
23031
23997
|
return { stdout: 0, stderr: 0 };
|
|
@@ -23033,7 +23999,7 @@ var ProcessManager = class {
|
|
|
23033
23999
|
}
|
|
23034
24000
|
readTailLines(filePath, lines) {
|
|
23035
24001
|
try {
|
|
23036
|
-
const content =
|
|
24002
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
23037
24003
|
const allLines = content.split("\n");
|
|
23038
24004
|
if (allLines.length > 0 && allLines[allLines.length - 1] === "") {
|
|
23039
24005
|
allLines.pop();
|
|
@@ -23596,6 +24562,7 @@ function createTodoReadTool(store) {
|
|
|
23596
24562
|
return {
|
|
23597
24563
|
name: "todoread",
|
|
23598
24564
|
label: "todoread",
|
|
24565
|
+
readOnly: true,
|
|
23599
24566
|
description: "Read the current todo list. Returns items with content, status, and priority.",
|
|
23600
24567
|
parameters: TodoReadSchema,
|
|
23601
24568
|
execute: async () => {
|
|
@@ -23736,6 +24703,7 @@ function createWebFetchTool(options) {
|
|
|
23736
24703
|
return {
|
|
23737
24704
|
name: "webfetch",
|
|
23738
24705
|
label: "webfetch",
|
|
24706
|
+
readOnly: true,
|
|
23739
24707
|
description: "Fetches content from a specified URL. Takes a URL and optional format as input. Fetches URL content, converts to requested format (markdown by default). Returns content in the specified format. Use this tool when you need to retrieve and analyze web content. The URL must be a fully-formed valid URL starting with http:// or https://. Format options: 'markdown' (default), 'text', or 'html'. This tool is read-only and does not modify any files.",
|
|
23740
24708
|
parameters: webFetchSchema,
|
|
23741
24709
|
execute: async (_toolCallId, params, signal) => {
|
|
@@ -23832,6 +24800,62 @@ var webFetchTool = createWebFetchTool();
|
|
|
23832
24800
|
|
|
23833
24801
|
// src/facade/bot/actions/websearch.ts
|
|
23834
24802
|
import { Type as Type18 } from "@sinclair/typebox";
|
|
24803
|
+
var SEARCH_ENDPOINT = "https://html.duckduckgo.com/html/";
|
|
24804
|
+
var USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36";
|
|
24805
|
+
function toPlainText(html) {
|
|
24806
|
+
const withoutTags = html.replace(/<[^>]*>/g, "");
|
|
24807
|
+
const decoded = withoutTags.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'").replace(/ /g, " ");
|
|
24808
|
+
return decoded.replace(/\s+/g, " ").trim();
|
|
24809
|
+
}
|
|
24810
|
+
function unwrapTarget(href) {
|
|
24811
|
+
let candidate = href;
|
|
24812
|
+
const redirectMatch = /[?&]uddg=([^&]+)/.exec(candidate);
|
|
24813
|
+
if (redirectMatch) {
|
|
24814
|
+
try {
|
|
24815
|
+
candidate = decodeURIComponent(redirectMatch[1]);
|
|
24816
|
+
} catch {
|
|
24817
|
+
}
|
|
24818
|
+
}
|
|
24819
|
+
if (candidate.startsWith("//")) {
|
|
24820
|
+
candidate = `https:${candidate}`;
|
|
24821
|
+
}
|
|
24822
|
+
return candidate;
|
|
24823
|
+
}
|
|
24824
|
+
function extractHits(body, limit) {
|
|
24825
|
+
const hits = [];
|
|
24826
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24827
|
+
const titleAnchor = /<a\b[^>]*\bclass="[^"]*\bresult__a\b[^"]*"[^>]*\bhref="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
24828
|
+
const snippetBlock = /\bclass="[^"]*\bresult__snippet\b[^"]*"[^>]*>([\s\S]*?)<\/[a-z]+>/i;
|
|
24829
|
+
let match;
|
|
24830
|
+
while ((match = titleAnchor.exec(body)) !== null) {
|
|
24831
|
+
if (hits.length >= limit) break;
|
|
24832
|
+
const url = unwrapTarget(match[1]);
|
|
24833
|
+
const title = toPlainText(match[2]);
|
|
24834
|
+
if (url.length === 0 || title.length === 0) continue;
|
|
24835
|
+
if (seen.has(url)) continue;
|
|
24836
|
+
const tail = body.slice(titleAnchor.lastIndex);
|
|
24837
|
+
const snippetMatch = snippetBlock.exec(tail);
|
|
24838
|
+
const snippet = snippetMatch ? toPlainText(snippetMatch[1]) : "";
|
|
24839
|
+
seen.add(url);
|
|
24840
|
+
hits.push({ title, url, snippet });
|
|
24841
|
+
}
|
|
24842
|
+
return hits;
|
|
24843
|
+
}
|
|
24844
|
+
function renderHits(query, hits) {
|
|
24845
|
+
const lines = [
|
|
24846
|
+
`${hits.length} result${hits.length === 1 ? "" : "s"} for "${query}":`,
|
|
24847
|
+
""
|
|
24848
|
+
];
|
|
24849
|
+
hits.forEach((hit, index) => {
|
|
24850
|
+
lines.push(`${index + 1}. ${hit.title}`);
|
|
24851
|
+
lines.push(` ${hit.url}`);
|
|
24852
|
+
if (hit.snippet.length > 0) {
|
|
24853
|
+
lines.push(` ${hit.snippet}`);
|
|
24854
|
+
}
|
|
24855
|
+
lines.push("");
|
|
24856
|
+
});
|
|
24857
|
+
return lines.join("\n").trimEnd();
|
|
24858
|
+
}
|
|
23835
24859
|
var webSearchSchema = Type18.Object({
|
|
23836
24860
|
query: Type18.String({ description: "Web search query" }),
|
|
23837
24861
|
numResults: Type18.Optional(
|
|
@@ -23843,48 +24867,31 @@ var requestCount = 0;
|
|
|
23843
24867
|
async function defaultWebSearch(query, numResults, signal) {
|
|
23844
24868
|
const count = Math.min(numResults ?? 8, 10);
|
|
23845
24869
|
try {
|
|
23846
|
-
const
|
|
23847
|
-
const response = await fetch(
|
|
24870
|
+
const form = new URLSearchParams({ q: query });
|
|
24871
|
+
const response = await fetch(SEARCH_ENDPOINT, {
|
|
24872
|
+
method: "POST",
|
|
23848
24873
|
signal,
|
|
23849
24874
|
headers: {
|
|
23850
|
-
"
|
|
23851
|
-
|
|
24875
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
24876
|
+
"User-Agent": USER_AGENT,
|
|
24877
|
+
Accept: "text/html"
|
|
24878
|
+
},
|
|
24879
|
+
body: form.toString()
|
|
23852
24880
|
});
|
|
23853
24881
|
if (!response.ok) {
|
|
23854
24882
|
throw new Error(`Search request failed: ${response.status}`);
|
|
23855
24883
|
}
|
|
23856
|
-
const
|
|
23857
|
-
const
|
|
23858
|
-
if (
|
|
23859
|
-
results.push(`**Summary:** ${data.AbstractText}`);
|
|
23860
|
-
if (data.AbstractURL) {
|
|
23861
|
-
results.push(`Source: ${data.AbstractURL}`);
|
|
23862
|
-
}
|
|
23863
|
-
}
|
|
23864
|
-
if (data.RelatedTopics && Array.isArray(data.RelatedTopics)) {
|
|
23865
|
-
const topics = data.RelatedTopics.filter((t) => t.Text && t.FirstURL).slice(0, count);
|
|
23866
|
-
if (topics.length > 0) {
|
|
23867
|
-
results.push(`
|
|
23868
|
-
**Related Results:**`);
|
|
23869
|
-
for (const topic of topics) {
|
|
23870
|
-
results.push(`- ${topic.Text}`);
|
|
23871
|
-
results.push(` URL: ${topic.FirstURL}`);
|
|
23872
|
-
}
|
|
23873
|
-
}
|
|
23874
|
-
}
|
|
23875
|
-
if (results.length === 0) {
|
|
24884
|
+
const body = await response.text();
|
|
24885
|
+
const hits = extractHits(body, count);
|
|
24886
|
+
if (hits.length === 0) {
|
|
23876
24887
|
return `No results found for: "${query}". Try a different search query.`;
|
|
23877
24888
|
}
|
|
23878
|
-
return
|
|
23879
|
-
|
|
23880
|
-
${results.join("\n")}`;
|
|
24889
|
+
return renderHits(query, hits);
|
|
23881
24890
|
} catch (error) {
|
|
23882
|
-
if (error
|
|
24891
|
+
if (error?.name === "AbortError") {
|
|
23883
24892
|
throw new Error("Web search request timed out or was aborted");
|
|
23884
24893
|
}
|
|
23885
|
-
|
|
23886
|
-
|
|
23887
|
-
Unable to perform live search. Please try again or use a different query.`;
|
|
24894
|
+
throw error;
|
|
23888
24895
|
}
|
|
23889
24896
|
}
|
|
23890
24897
|
function createWebSearchTool(options) {
|
|
@@ -23892,6 +24899,7 @@ function createWebSearchTool(options) {
|
|
|
23892
24899
|
return {
|
|
23893
24900
|
name: "websearch",
|
|
23894
24901
|
label: "websearch",
|
|
24902
|
+
readOnly: true,
|
|
23895
24903
|
description: "Search the web - performs real-time web searches and can scrape content from specific URLs. Provides up-to-date information for current events and recent data. Use this tool for accessing information beyond knowledge cutoff. The current year is 2026. You MUST use this year when searching for recent information or current events (e.g., search for 'AI news 2026', NOT 'AI news 2025').",
|
|
23896
24904
|
parameters: webSearchSchema,
|
|
23897
24905
|
execute: async (_toolCallId, { query, numResults }, signal) => {
|
|
@@ -23950,8 +24958,9 @@ var webSearchTool = createWebSearchTool();
|
|
|
23950
24958
|
|
|
23951
24959
|
// src/facade/bot/actions/write.ts
|
|
23952
24960
|
import { Type as Type19 } from "@sinclair/typebox";
|
|
24961
|
+
import { existsSync as existsSync6 } from "fs";
|
|
23953
24962
|
import { copyFile as fsCopyFile, mkdir as fsMkdir, writeFile as fsWriteFile2 } from "fs/promises";
|
|
23954
|
-
import { dirname } from "path";
|
|
24963
|
+
import { dirname as dirname2 } from "path";
|
|
23955
24964
|
var writeSchema = Type19.Object({
|
|
23956
24965
|
path: Type19.String({ description: "Destination file, given as a relative or absolute path" }),
|
|
23957
24966
|
content: Type19.String({ description: "The text that will become the file's contents" })
|
|
@@ -23970,22 +24979,48 @@ var FileValidator = class {
|
|
|
23970
24979
|
}
|
|
23971
24980
|
};
|
|
23972
24981
|
var WriteSession = class {
|
|
23973
|
-
constructor(cwd, relativePath, fileContent, operations, createBackup) {
|
|
24982
|
+
constructor(cwd, relativePath, fileContent, operations, createBackup, readState, checkpoint) {
|
|
23974
24983
|
this.cwd = cwd;
|
|
23975
24984
|
this.relativePath = relativePath;
|
|
23976
24985
|
this.fileContent = fileContent;
|
|
23977
24986
|
this.operations = operations;
|
|
23978
24987
|
this.createBackup = createBackup;
|
|
24988
|
+
this.readState = readState;
|
|
24989
|
+
this.checkpoint = checkpoint;
|
|
23979
24990
|
this.absolutePath = resolveToCwd(relativePath, cwd);
|
|
23980
|
-
this.targetDir =
|
|
24991
|
+
this.targetDir = dirname2(this.absolutePath);
|
|
23981
24992
|
}
|
|
23982
24993
|
cwd;
|
|
23983
24994
|
relativePath;
|
|
23984
24995
|
fileContent;
|
|
23985
24996
|
operations;
|
|
23986
24997
|
createBackup;
|
|
24998
|
+
readState;
|
|
24999
|
+
checkpoint;
|
|
23987
25000
|
absolutePath;
|
|
23988
25001
|
targetDir;
|
|
25002
|
+
/**
|
|
25003
|
+
* Enforce the read-before-edit + staleness gate ahead of an OVERWRITE.
|
|
25004
|
+
* Brand-new files (those not yet on disk) are exempt — there is nothing the
|
|
25005
|
+
* model could have clobbered. No-op without a handle.
|
|
25006
|
+
*/
|
|
25007
|
+
enforceGate() {
|
|
25008
|
+
if (!this.readState) return;
|
|
25009
|
+
if (!existsSync6(this.absolutePath)) return;
|
|
25010
|
+
const gate = enforceReadGate(this.readState, this.absolutePath);
|
|
25011
|
+
if (!gate.ok) {
|
|
25012
|
+
throw new Error(gate.message);
|
|
25013
|
+
}
|
|
25014
|
+
}
|
|
25015
|
+
/**
|
|
25016
|
+
* Snapshot the file's pre-mutation on-disk content for rewind. Runs AFTER the
|
|
25017
|
+
* read-state gate and BEFORE any mutation (mkdir/write), so the captured bytes
|
|
25018
|
+
* are the OLD content; a brand-new file is recorded as `null`. No-op without a
|
|
25019
|
+
* handle.
|
|
25020
|
+
*/
|
|
25021
|
+
recordCheckpoint() {
|
|
25022
|
+
recordCheckpoint(this.checkpoint, this.absolutePath);
|
|
25023
|
+
}
|
|
23989
25024
|
async prepareDirectories() {
|
|
23990
25025
|
await this.operations.mkdir(this.targetDir);
|
|
23991
25026
|
}
|
|
@@ -24000,6 +25035,7 @@ var WriteSession = class {
|
|
|
24000
25035
|
}
|
|
24001
25036
|
async persistContent() {
|
|
24002
25037
|
await this.operations.writeFile(this.absolutePath, this.fileContent);
|
|
25038
|
+
recordReadState(this.readState, this.absolutePath);
|
|
24003
25039
|
}
|
|
24004
25040
|
buildResponse() {
|
|
24005
25041
|
return {
|
|
@@ -24064,8 +25100,20 @@ function createWriteTool(cwd, options) {
|
|
|
24064
25100
|
parameters: writeSchema,
|
|
24065
25101
|
execute: async (_toolCallId, { path: path9, content }, signal) => {
|
|
24066
25102
|
validator.validate(path9);
|
|
24067
|
-
const writeSession = new WriteSession(
|
|
25103
|
+
const writeSession = new WriteSession(
|
|
25104
|
+
cwd,
|
|
25105
|
+
path9,
|
|
25106
|
+
content,
|
|
25107
|
+
operations,
|
|
25108
|
+
Boolean(options?.createBackup),
|
|
25109
|
+
options?.readState,
|
|
25110
|
+
options?.checkpoint
|
|
25111
|
+
);
|
|
24068
25112
|
return withAbort3(signal, async (isAborted) => {
|
|
25113
|
+
writeSession.enforceGate();
|
|
25114
|
+
throwIfAborted(isAborted);
|
|
25115
|
+
writeSession.recordCheckpoint();
|
|
25116
|
+
throwIfAborted(isAborted);
|
|
24069
25117
|
await writeSession.prepareDirectories();
|
|
24070
25118
|
throwIfAborted(isAborted);
|
|
24071
25119
|
await writeSession.backupIfEnabled();
|
|
@@ -24689,13 +25737,13 @@ async function gcStaleTeamDirs(opts) {
|
|
|
24689
25737
|
continue;
|
|
24690
25738
|
}
|
|
24691
25739
|
const teamDir = path4.join(teamsRootAbs, teamId);
|
|
24692
|
-
let
|
|
25740
|
+
let stat3;
|
|
24693
25741
|
try {
|
|
24694
|
-
|
|
25742
|
+
stat3 = await fs2.promises.stat(teamDir);
|
|
24695
25743
|
} catch {
|
|
24696
25744
|
continue;
|
|
24697
25745
|
}
|
|
24698
|
-
if (!
|
|
25746
|
+
if (!stat3.isDirectory()) continue;
|
|
24699
25747
|
let ageMs;
|
|
24700
25748
|
try {
|
|
24701
25749
|
const configPath = path4.join(teamDir, "config.json");
|
|
@@ -24704,12 +25752,12 @@ async function gcStaleTeamDirs(opts) {
|
|
|
24704
25752
|
const createdAt = typeof config === "object" && config !== null && "createdAt" in config ? config.createdAt : void 0;
|
|
24705
25753
|
if (typeof createdAt === "string") {
|
|
24706
25754
|
const ts = Date.parse(createdAt);
|
|
24707
|
-
ageMs = Number.isFinite(ts) ? now - ts : now -
|
|
25755
|
+
ageMs = Number.isFinite(ts) ? now - ts : now - stat3.mtimeMs;
|
|
24708
25756
|
} else {
|
|
24709
|
-
ageMs = now -
|
|
25757
|
+
ageMs = now - stat3.mtimeMs;
|
|
24710
25758
|
}
|
|
24711
25759
|
} catch {
|
|
24712
|
-
ageMs = now -
|
|
25760
|
+
ageMs = now - stat3.mtimeMs;
|
|
24713
25761
|
}
|
|
24714
25762
|
if (ageMs < maxAgeMs) {
|
|
24715
25763
|
skipped.push({ teamId, reason: "too recent" });
|
|
@@ -26352,17 +27400,17 @@ import { homedir as homedir2 } from "os";
|
|
|
26352
27400
|
import {
|
|
26353
27401
|
appendFileSync as appendFileSync2,
|
|
26354
27402
|
closeSync as closeSync2,
|
|
26355
|
-
existsSync as
|
|
27403
|
+
existsSync as existsSync8,
|
|
26356
27404
|
mkdirSync as mkdirSync2,
|
|
26357
27405
|
openSync as openSync2,
|
|
26358
|
-
readdirSync as
|
|
26359
|
-
readFileSync as
|
|
27406
|
+
readdirSync as readdirSync5,
|
|
27407
|
+
readFileSync as readFileSync4,
|
|
26360
27408
|
readSync,
|
|
26361
|
-
statSync as
|
|
27409
|
+
statSync as statSync8,
|
|
26362
27410
|
writeFileSync as writeFileSync2
|
|
26363
27411
|
} from "fs";
|
|
26364
|
-
import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
26365
|
-
import { join as
|
|
27412
|
+
import { readdir, readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
27413
|
+
import { join as join10, resolve as resolve4 } from "path";
|
|
26366
27414
|
var CURRENT_SESSION_VERSION = 3;
|
|
26367
27415
|
function generateId(byId) {
|
|
26368
27416
|
let attempt = 0;
|
|
@@ -26542,11 +27590,11 @@ function getDefaultAgentDir() {
|
|
|
26542
27590
|
return expandHomePath(explicitAgentDir);
|
|
26543
27591
|
}
|
|
26544
27592
|
const rawBase = process.env[ENV_BASE_DIR];
|
|
26545
|
-
const baseDir = rawBase ? expandHomePath(rawBase) :
|
|
26546
|
-
return
|
|
27593
|
+
const baseDir = rawBase ? expandHomePath(rawBase) : join10(homedir2(), DEFAULT_CONFIG_DIR);
|
|
27594
|
+
return join10(baseDir, "agent");
|
|
26547
27595
|
}
|
|
26548
27596
|
function getSessionsDir() {
|
|
26549
|
-
return
|
|
27597
|
+
return join10(getDefaultAgentDir(), "sessions");
|
|
26550
27598
|
}
|
|
26551
27599
|
function encodeCwdForDir(cwd) {
|
|
26552
27600
|
const trimmedLeadingSlash = cwd.replace(/^[/\\]/, "");
|
|
@@ -26554,8 +27602,8 @@ function encodeCwdForDir(cwd) {
|
|
|
26554
27602
|
return `--${sanitized}--`;
|
|
26555
27603
|
}
|
|
26556
27604
|
function getDefaultSessionDir(cwd) {
|
|
26557
|
-
const sessionDir =
|
|
26558
|
-
if (!
|
|
27605
|
+
const sessionDir = join10(getDefaultAgentDir(), "sessions", encodeCwdForDir(cwd));
|
|
27606
|
+
if (!existsSync8(sessionDir)) {
|
|
26559
27607
|
mkdirSync2(sessionDir, { recursive: true });
|
|
26560
27608
|
}
|
|
26561
27609
|
return sessionDir;
|
|
@@ -26565,10 +27613,10 @@ function hasValidSessionHeader(entries) {
|
|
|
26565
27613
|
return first !== void 0 && first.type === "session" && typeof first.id === "string";
|
|
26566
27614
|
}
|
|
26567
27615
|
function loadEntriesFromFile(filePath) {
|
|
26568
|
-
if (!
|
|
27616
|
+
if (!existsSync8(filePath)) {
|
|
26569
27617
|
return [];
|
|
26570
27618
|
}
|
|
26571
|
-
const entries = decodeJsonlLines(
|
|
27619
|
+
const entries = decodeJsonlLines(readFileSync4(filePath, "utf8"));
|
|
26572
27620
|
if (entries.length === 0) {
|
|
26573
27621
|
return entries;
|
|
26574
27622
|
}
|
|
@@ -26596,7 +27644,7 @@ function isValidSessionFile(filePath) {
|
|
|
26596
27644
|
}
|
|
26597
27645
|
function findMostRecentSession(sessionDir) {
|
|
26598
27646
|
try {
|
|
26599
|
-
const ranked =
|
|
27647
|
+
const ranked = readdirSync5(sessionDir).filter((f) => f.endsWith(".jsonl")).map((f) => join10(sessionDir, f)).filter(isValidSessionFile).map((path9) => ({ path: path9, mtime: statSync8(path9).mtime })).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
26600
27648
|
return ranked[0]?.path || null;
|
|
26601
27649
|
} catch {
|
|
26602
27650
|
return null;
|
|
@@ -26665,7 +27713,7 @@ async function buildSessionInfo(filePath) {
|
|
|
26665
27713
|
return null;
|
|
26666
27714
|
}
|
|
26667
27715
|
const sessionHeader = header;
|
|
26668
|
-
const stats = await
|
|
27716
|
+
const stats = await stat2(filePath);
|
|
26669
27717
|
let messageCount = 0;
|
|
26670
27718
|
let firstMessage = "";
|
|
26671
27719
|
let name;
|
|
@@ -26710,11 +27758,11 @@ async function buildSessionInfo(filePath) {
|
|
|
26710
27758
|
}
|
|
26711
27759
|
}
|
|
26712
27760
|
async function listSessionsFromDir(dir, onProgress, progressOffset = 0, progressTotal) {
|
|
26713
|
-
if (!
|
|
27761
|
+
if (!existsSync8(dir)) {
|
|
26714
27762
|
return [];
|
|
26715
27763
|
}
|
|
26716
27764
|
try {
|
|
26717
|
-
const files = (await readdir(dir)).filter((f) => f.endsWith(".jsonl")).map((f) =>
|
|
27765
|
+
const files = (await readdir(dir)).filter((f) => f.endsWith(".jsonl")).map((f) => join10(dir, f));
|
|
26718
27766
|
const total = progressTotal ?? files.length;
|
|
26719
27767
|
let loaded = 0;
|
|
26720
27768
|
const results = await Promise.all(
|
|
@@ -26745,7 +27793,7 @@ var SessionManager = class _SessionManager {
|
|
|
26745
27793
|
this.cwd = cwd;
|
|
26746
27794
|
this.sessionDir = sessionDir;
|
|
26747
27795
|
this.persist = persist;
|
|
26748
|
-
if (persist && sessionDir && !
|
|
27796
|
+
if (persist && sessionDir && !existsSync8(sessionDir)) {
|
|
26749
27797
|
mkdirSync2(sessionDir, { recursive: true });
|
|
26750
27798
|
}
|
|
26751
27799
|
if (sessionFile) {
|
|
@@ -26758,7 +27806,7 @@ var SessionManager = class _SessionManager {
|
|
|
26758
27806
|
setSessionFile(sessionFile) {
|
|
26759
27807
|
const resolved = resolve4(sessionFile);
|
|
26760
27808
|
this.sessionFile = resolved;
|
|
26761
|
-
if (!
|
|
27809
|
+
if (!existsSync8(resolved)) {
|
|
26762
27810
|
this.newSession();
|
|
26763
27811
|
this.sessionFile = resolved;
|
|
26764
27812
|
return;
|
|
@@ -26797,7 +27845,7 @@ var SessionManager = class _SessionManager {
|
|
|
26797
27845
|
this.flushed = false;
|
|
26798
27846
|
if (this.persist) {
|
|
26799
27847
|
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
26800
|
-
this.sessionFile =
|
|
27848
|
+
this.sessionFile = join10(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`);
|
|
26801
27849
|
}
|
|
26802
27850
|
return this.sessionFile;
|
|
26803
27851
|
}
|
|
@@ -27153,7 +28201,7 @@ var SessionManager = class _SessionManager {
|
|
|
27153
28201
|
const newSessionId = randomUUID();
|
|
27154
28202
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
27155
28203
|
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
27156
|
-
const newSessionFile =
|
|
28204
|
+
const newSessionFile = join10(this.getSessionDir(), `${fileTimestamp}_${newSessionId}.jsonl`);
|
|
27157
28205
|
const header = {
|
|
27158
28206
|
type: "session",
|
|
27159
28207
|
version: CURRENT_SESSION_VERSION,
|
|
@@ -27270,13 +28318,13 @@ var SessionManager = class _SessionManager {
|
|
|
27270
28318
|
throw new Error(`Fork aborted \u2014 source session ${sourcePath} is missing its header record`);
|
|
27271
28319
|
}
|
|
27272
28320
|
const dir = sessionDir ?? getDefaultSessionDir(targetCwd);
|
|
27273
|
-
if (!
|
|
28321
|
+
if (!existsSync8(dir)) {
|
|
27274
28322
|
mkdirSync2(dir, { recursive: true });
|
|
27275
28323
|
}
|
|
27276
28324
|
const newSessionId = randomUUID();
|
|
27277
28325
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
27278
28326
|
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
27279
|
-
const newSessionFile =
|
|
28327
|
+
const newSessionFile = join10(dir, `${fileTimestamp}_${newSessionId}.jsonl`);
|
|
27280
28328
|
const newHeader = {
|
|
27281
28329
|
type: "session",
|
|
27282
28330
|
version: CURRENT_SESSION_VERSION,
|
|
@@ -27314,16 +28362,16 @@ var SessionManager = class _SessionManager {
|
|
|
27314
28362
|
static async listAll(onProgress) {
|
|
27315
28363
|
const sessionsDir = getSessionsDir();
|
|
27316
28364
|
try {
|
|
27317
|
-
if (!
|
|
28365
|
+
if (!existsSync8(sessionsDir)) {
|
|
27318
28366
|
return [];
|
|
27319
28367
|
}
|
|
27320
28368
|
const rootEntries = await readdir(sessionsDir, { withFileTypes: true });
|
|
27321
|
-
const dirs = rootEntries.filter((e) => e.isDirectory()).map((e) =>
|
|
28369
|
+
const dirs = rootEntries.filter((e) => e.isDirectory()).map((e) => join10(sessionsDir, e.name));
|
|
27322
28370
|
const perDirFiles = [];
|
|
27323
28371
|
let totalFiles = 0;
|
|
27324
28372
|
for (const dir of dirs) {
|
|
27325
28373
|
try {
|
|
27326
|
-
const jsonlFiles = (await readdir(dir)).filter((f) => f.endsWith(".jsonl")).map((f) =>
|
|
28374
|
+
const jsonlFiles = (await readdir(dir)).filter((f) => f.endsWith(".jsonl")).map((f) => join10(dir, f));
|
|
27327
28375
|
perDirFiles.push(jsonlFiles);
|
|
27328
28376
|
totalFiles += jsonlFiles.length;
|
|
27329
28377
|
} catch {
|
|
@@ -27361,11 +28409,14 @@ export {
|
|
|
27361
28409
|
ComposioService,
|
|
27362
28410
|
DEFAULT_MAX_BYTES,
|
|
27363
28411
|
DEFAULT_MAX_LINES,
|
|
28412
|
+
DESANITIZATIONS,
|
|
28413
|
+
FILE_NOT_FOUND_CWD_NOTE,
|
|
27364
28414
|
IndusagiComposioProvider,
|
|
27365
28415
|
LIVE_STATUSES,
|
|
27366
28416
|
MESSAGE_TYPE_PROCESS_UPDATE,
|
|
27367
28417
|
PIRATE_NAME_POOL,
|
|
27368
28418
|
ProcessManager,
|
|
28419
|
+
READ_ONLY_TOOL_NAMES,
|
|
27369
28420
|
SessionManager,
|
|
27370
28421
|
TEAM_ATTACH_CLAIM_FILE,
|
|
27371
28422
|
TEAM_ATTACH_CLAIM_STALE_MS,
|
|
@@ -27384,10 +28435,15 @@ export {
|
|
|
27384
28435
|
agentLoop,
|
|
27385
28436
|
agentLoopContinue,
|
|
27386
28437
|
allTools,
|
|
28438
|
+
argvToCommandString,
|
|
27387
28439
|
assertTeamDirWithinTeamsRoot,
|
|
27388
28440
|
assessAttachClaimFreshness,
|
|
27389
28441
|
bashExecutionToText,
|
|
27390
28442
|
bashTool,
|
|
28443
|
+
buildBwrapArgv,
|
|
28444
|
+
buildSandboxArgv,
|
|
28445
|
+
buildSeatbeltArgv,
|
|
28446
|
+
buildSeatbeltProfile,
|
|
27391
28447
|
buildSessionContext,
|
|
27392
28448
|
claimNextAvailableTask,
|
|
27393
28449
|
claimTask,
|
|
@@ -27424,6 +28480,7 @@ export {
|
|
|
27424
28480
|
createProcessTool,
|
|
27425
28481
|
createReadOnlyTools,
|
|
27426
28482
|
createReadTool,
|
|
28483
|
+
createSandboxedBashOperations,
|
|
27427
28484
|
createTask,
|
|
27428
28485
|
createTodoReadTool,
|
|
27429
28486
|
createTodoWriteTool,
|
|
@@ -27431,11 +28488,13 @@ export {
|
|
|
27431
28488
|
createWebFetchTool,
|
|
27432
28489
|
createWebSearchTool,
|
|
27433
28490
|
createWriteTool,
|
|
28491
|
+
desanitizeMatchString,
|
|
27434
28492
|
editTool,
|
|
27435
28493
|
ensureTeamConfig,
|
|
27436
28494
|
ensureWorktreeCwd,
|
|
27437
28495
|
expandPath,
|
|
27438
28496
|
findMostRecentSession,
|
|
28497
|
+
findSimilarFile,
|
|
27439
28498
|
findTool,
|
|
27440
28499
|
formatProviderModel,
|
|
27441
28500
|
formatSize,
|
|
@@ -27477,21 +28536,25 @@ export {
|
|
|
27477
28536
|
pickNamesFromPool,
|
|
27478
28537
|
pickPirateNames,
|
|
27479
28538
|
popUnreadMessages,
|
|
28539
|
+
preserveQuoteStyle,
|
|
27480
28540
|
processTool,
|
|
27481
28541
|
readOnlyTools,
|
|
27482
28542
|
readTeamAttachClaim,
|
|
27483
28543
|
readTool,
|
|
27484
28544
|
releaseTeamAttachClaim,
|
|
27485
28545
|
removeTaskDependency,
|
|
28546
|
+
replaceAllLiteral,
|
|
27486
28547
|
resolveReadPath,
|
|
27487
28548
|
resolveTeammateModelSelection,
|
|
27488
28549
|
resolveToCwd,
|
|
28550
|
+
sandboxAvailability,
|
|
27489
28551
|
sanitizeName,
|
|
27490
28552
|
setMemberStatus,
|
|
27491
28553
|
setTeamStyle,
|
|
27492
28554
|
shortTaskId,
|
|
27493
28555
|
startAssignedTask,
|
|
27494
28556
|
streamProxy,
|
|
28557
|
+
suggestPathUnderCwd,
|
|
27495
28558
|
taskAssignmentPayload,
|
|
27496
28559
|
todoReadTool,
|
|
27497
28560
|
todoWriteTool,
|