@bubblebrain-ai/bubble 0.0.17 → 0.0.19
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/tool-intent.js +0 -1
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +54 -21
- package/dist/context/prune.d.ts +1 -0
- package/dist/context/prune.js +32 -0
- package/dist/feishu/agent-host/run-driver.js +2 -2
- package/dist/feishu/card/run-state.js +1 -0
- package/dist/main.js +11 -9
- package/dist/model-pricing.js +2 -1
- package/dist/model-selection.d.ts +7 -0
- package/dist/model-selection.js +9 -0
- package/dist/network/chatgpt-transport.d.ts +1 -0
- package/dist/network/chatgpt-transport.js +123 -16
- package/dist/orchestrator/default-hooks.js +1 -1
- package/dist/prompt/environment.js +1 -3
- package/dist/prompt/runtime.js +1 -1
- package/dist/provider-anthropic.d.ts +15 -3
- package/dist/provider-anthropic.js +55 -2
- package/dist/provider-openai-codex.js +3 -1
- package/dist/provider.js +1 -1
- package/dist/session-title.js +3 -6
- package/dist/slash-commands/commands.js +4 -0
- package/dist/stats/usage.d.ts +1 -0
- package/dist/stats/usage.js +28 -3
- package/dist/tools/edit.js +75 -1
- package/dist/tools/glob.js +77 -12
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +1 -3
- package/dist/tools/prompt-metadata.d.ts +3 -0
- package/dist/tools/prompt-metadata.js +17 -0
- package/dist/tools/write.js +14 -0
- package/dist/tui/paste-placeholder.d.ts +10 -0
- package/dist/tui/paste-placeholder.js +45 -0
- package/dist/tui/run.js +23 -0
- package/dist/tui-ink/app.js +2 -0
- package/dist/tui-ink/input-box.d.ts +1 -8
- package/dist/tui-ink/input-box.js +8 -38
- package/dist/tui-opentui/app.js +2 -0
- package/dist/tui-opentui/input-box.d.ts +1 -3
- package/dist/tui-opentui/input-box.js +17 -26
- package/dist/types.d.ts +9 -0
- package/package.json +7 -3
- package/dist/tools/apply-patch.d.ts +0 -9
- package/dist/tools/apply-patch.js +0 -330
- package/dist/tools/patch-apply.d.ts +0 -41
- package/dist/tools/patch-apply.js +0 -312
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { Provider, ProviderMessage, StreamChunk, ThinkingLevel, ToolDefinition } from "./types.js";
|
|
1
|
+
import type { Provider, ProviderMessage, StreamChunk, ThinkingLevel, ToolChoiceMode, ToolDefinition } from "./types.js";
|
|
2
|
+
declare const ANTHROPIC_PROMPT_CACHE_CONTROL: {
|
|
3
|
+
readonly type: "ephemeral";
|
|
4
|
+
};
|
|
2
5
|
export interface AnthropicProviderOptions {
|
|
3
6
|
providerId?: string;
|
|
4
7
|
apiKey: string;
|
|
@@ -9,10 +12,10 @@ interface AnthropicRequest {
|
|
|
9
12
|
model: string;
|
|
10
13
|
max_tokens: number;
|
|
11
14
|
messages: AnthropicMessage[];
|
|
12
|
-
system?: string;
|
|
15
|
+
system?: string | AnthropicSystemBlock[];
|
|
13
16
|
tools?: AnthropicTool[];
|
|
14
17
|
tool_choice?: {
|
|
15
|
-
type: "auto" | "any";
|
|
18
|
+
type: "auto" | "any" | "none";
|
|
16
19
|
};
|
|
17
20
|
stream?: boolean;
|
|
18
21
|
temperature?: number;
|
|
@@ -20,6 +23,12 @@ interface AnthropicRequest {
|
|
|
20
23
|
type: "adaptive";
|
|
21
24
|
};
|
|
22
25
|
}
|
|
26
|
+
type AnthropicCacheControl = typeof ANTHROPIC_PROMPT_CACHE_CONTROL;
|
|
27
|
+
interface AnthropicSystemBlock {
|
|
28
|
+
type: "text";
|
|
29
|
+
text: string;
|
|
30
|
+
cache_control?: AnthropicCacheControl;
|
|
31
|
+
}
|
|
23
32
|
type AnthropicContentBlock = {
|
|
24
33
|
type: "text";
|
|
25
34
|
text: string;
|
|
@@ -59,15 +68,18 @@ interface AnthropicTool {
|
|
|
59
68
|
name: string;
|
|
60
69
|
description: string;
|
|
61
70
|
input_schema: ToolDefinition["parameters"];
|
|
71
|
+
cache_control?: AnthropicCacheControl;
|
|
62
72
|
}
|
|
63
73
|
export declare function createAnthropicMessagesProvider(options: AnthropicProviderOptions): Provider;
|
|
64
74
|
export declare function buildAnthropicRequest(options: AnthropicProviderOptions, messages: ProviderMessage[], chatOptions: {
|
|
65
75
|
model: string;
|
|
66
76
|
tools?: ToolDefinition[];
|
|
77
|
+
toolChoice?: ToolChoiceMode;
|
|
67
78
|
temperature?: number;
|
|
68
79
|
thinkingLevel?: ThinkingLevel;
|
|
69
80
|
stream?: boolean;
|
|
70
81
|
}): AnthropicRequest;
|
|
82
|
+
export declare function supportsAnthropicPromptCache(options: AnthropicProviderOptions, model: string): boolean;
|
|
71
83
|
export declare function toAnthropicMessages(messages: ProviderMessage[], echoThinking?: boolean): {
|
|
72
84
|
system: string;
|
|
73
85
|
messages: AnthropicMessage[];
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { getAvailableThinkingLevels, normalizeThinkingLevel } from "./provider-transform.js";
|
|
2
2
|
const ANTHROPIC_VERSION = "2023-06-01";
|
|
3
3
|
const DEFAULT_MAX_TOKENS = 8192;
|
|
4
|
+
const ANTHROPIC_PROMPT_CACHE_CONTROL = { type: "ephemeral" };
|
|
5
|
+
const MINIMAX_PROMPT_CACHE_MODELS = new Set([
|
|
6
|
+
"minimax-m2.7",
|
|
7
|
+
"minimax-m2.7-highspeed",
|
|
8
|
+
"minimax-m2.5",
|
|
9
|
+
"minimax-m2.5-highspeed",
|
|
10
|
+
"minimax-m2.1",
|
|
11
|
+
"minimax-m2.1-highspeed",
|
|
12
|
+
"minimax-m2",
|
|
13
|
+
"m2-her",
|
|
14
|
+
]);
|
|
4
15
|
export function createAnthropicMessagesProvider(options) {
|
|
5
16
|
async function* streamChat(messages, chatOptions) {
|
|
6
17
|
const body = buildAnthropicRequest(options, messages, {
|
|
7
18
|
model: chatOptions.model,
|
|
8
19
|
tools: chatOptions.tools,
|
|
20
|
+
toolChoice: chatOptions.toolChoice,
|
|
9
21
|
temperature: chatOptions.temperature,
|
|
10
22
|
thinkingLevel: chatOptions.thinkingLevel,
|
|
11
23
|
stream: true,
|
|
@@ -41,18 +53,25 @@ export function createAnthropicMessagesProvider(options) {
|
|
|
41
53
|
}
|
|
42
54
|
export function buildAnthropicRequest(options, messages, chatOptions) {
|
|
43
55
|
const { system, messages: anthropicMessages } = toAnthropicMessages(messages, shouldEchoThinking(options.providerId));
|
|
56
|
+
const enablePromptCache = supportsAnthropicPromptCache(options, chatOptions.model);
|
|
44
57
|
const tools = chatOptions.tools?.map((tool) => ({
|
|
45
58
|
name: tool.name,
|
|
46
59
|
description: tool.description,
|
|
47
60
|
input_schema: tool.parameters,
|
|
48
61
|
}));
|
|
62
|
+
if (enablePromptCache && tools && tools.length > 0) {
|
|
63
|
+
tools[tools.length - 1] = {
|
|
64
|
+
...tools[tools.length - 1],
|
|
65
|
+
cache_control: ANTHROPIC_PROMPT_CACHE_CONTROL,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
49
68
|
const body = {
|
|
50
69
|
model: chatOptions.model,
|
|
51
70
|
max_tokens: DEFAULT_MAX_TOKENS,
|
|
52
|
-
system: system
|
|
71
|
+
system: buildAnthropicSystem(system, enablePromptCache),
|
|
53
72
|
messages: anthropicMessages,
|
|
54
73
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
55
|
-
tool_choice: tools && tools.length > 0 ? { type: "auto" } : undefined,
|
|
74
|
+
tool_choice: tools && tools.length > 0 ? { type: chatOptions.toolChoice ?? "auto" } : undefined,
|
|
56
75
|
stream: chatOptions.stream || undefined,
|
|
57
76
|
};
|
|
58
77
|
if (typeof chatOptions.temperature === "number") {
|
|
@@ -64,6 +83,23 @@ export function buildAnthropicRequest(options, messages, chatOptions) {
|
|
|
64
83
|
}
|
|
65
84
|
return body;
|
|
66
85
|
}
|
|
86
|
+
function buildAnthropicSystem(system, enablePromptCache) {
|
|
87
|
+
if (!system)
|
|
88
|
+
return undefined;
|
|
89
|
+
if (!enablePromptCache)
|
|
90
|
+
return system;
|
|
91
|
+
return [{ type: "text", text: system, cache_control: ANTHROPIC_PROMPT_CACHE_CONTROL }];
|
|
92
|
+
}
|
|
93
|
+
export function supportsAnthropicPromptCache(options, model) {
|
|
94
|
+
const providerId = (options.providerId ?? "").toLowerCase();
|
|
95
|
+
if (providerId === "anthropic" || isOfficialAnthropicBaseUrl(options.baseURL)) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if (!isMiniMaxAnthropicEndpoint(options)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return MINIMAX_PROMPT_CACHE_MODELS.has(model.toLowerCase());
|
|
102
|
+
}
|
|
67
103
|
export function toAnthropicMessages(messages, echoThinking = false) {
|
|
68
104
|
const system = [];
|
|
69
105
|
const out = [];
|
|
@@ -512,6 +548,7 @@ function mergeAnthropicUsage(current, raw) {
|
|
|
512
548
|
let promptTokens = current?.promptTokens ?? 0;
|
|
513
549
|
let promptCacheHitTokens = current?.promptCacheHitTokens;
|
|
514
550
|
let promptCacheMissTokens = current?.promptCacheMissTokens;
|
|
551
|
+
let cacheCreationTokens = current?.cacheCreationTokens;
|
|
515
552
|
if (hasPromptUsage) {
|
|
516
553
|
const inputTokens = rawInput ?? promptCacheMissTokens ?? promptTokens;
|
|
517
554
|
const cacheRead = rawCacheRead ?? promptCacheHitTokens ?? 0;
|
|
@@ -519,12 +556,14 @@ function mergeAnthropicUsage(current, raw) {
|
|
|
519
556
|
promptTokens = inputTokens + cacheRead + cacheCreation;
|
|
520
557
|
promptCacheHitTokens = cacheRead;
|
|
521
558
|
promptCacheMissTokens = inputTokens + cacheCreation;
|
|
559
|
+
cacheCreationTokens = cacheCreation;
|
|
522
560
|
}
|
|
523
561
|
return {
|
|
524
562
|
promptTokens,
|
|
525
563
|
completionTokens: outputTokens,
|
|
526
564
|
promptCacheHitTokens,
|
|
527
565
|
promptCacheMissTokens,
|
|
566
|
+
cacheCreationTokens,
|
|
528
567
|
totalTokens: promptTokens + outputTokens,
|
|
529
568
|
};
|
|
530
569
|
}
|
|
@@ -534,6 +573,20 @@ function shouldEchoThinking(providerId) {
|
|
|
534
573
|
function shouldSendBearerAuth(options) {
|
|
535
574
|
return !isOfficialAnthropicBaseUrl(options.baseURL) || options.providerId?.startsWith("minimax") === true;
|
|
536
575
|
}
|
|
576
|
+
function isMiniMaxAnthropicEndpoint(options) {
|
|
577
|
+
const providerId = (options.providerId ?? "").toLowerCase();
|
|
578
|
+
if (providerId !== "minimax" && providerId !== "minimax-anthropic")
|
|
579
|
+
return false;
|
|
580
|
+
try {
|
|
581
|
+
const url = new URL(options.baseURL);
|
|
582
|
+
const host = url.hostname.toLowerCase();
|
|
583
|
+
const path = url.pathname.toLowerCase();
|
|
584
|
+
return (host === "api.minimax.io" || host === "api.minimaxi.com") && path.includes("/anthropic");
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
537
590
|
function isOfficialAnthropicBaseUrl(baseURL) {
|
|
538
591
|
try {
|
|
539
592
|
return new URL(baseURL).hostname === "api.anthropic.com";
|
|
@@ -72,6 +72,7 @@ export function createOpenAICodexProvider(options) {
|
|
|
72
72
|
const body = JSON.stringify(buildRequestBody(messages, {
|
|
73
73
|
model: chatOptions.model,
|
|
74
74
|
tools: chatOptions.tools,
|
|
75
|
+
toolChoice: chatOptions.toolChoice,
|
|
75
76
|
reasoningEffort: requestConfig.reasoningEffort,
|
|
76
77
|
sessionId,
|
|
77
78
|
providerId: options.providerId,
|
|
@@ -314,7 +315,7 @@ function buildRequestBody(messages, options) {
|
|
|
314
315
|
providerId: options.providerId,
|
|
315
316
|
model: options.model,
|
|
316
317
|
}),
|
|
317
|
-
tool_choice: "auto",
|
|
318
|
+
tool_choice: options.tools && options.tools.length > 0 ? options.toolChoice ?? "auto" : undefined,
|
|
318
319
|
parallel_tool_calls: true,
|
|
319
320
|
text: { verbosity: "medium" },
|
|
320
321
|
};
|
|
@@ -454,6 +455,7 @@ function isTransientCodexTransportError(error) {
|
|
|
454
455
|
/\bEPIPE\b/i,
|
|
455
456
|
/socket hang up/i,
|
|
456
457
|
/fetch failed/i,
|
|
458
|
+
/Unable to connect\. Is the computer able to access the url\?/i,
|
|
457
459
|
/unknown certificate verification error/i,
|
|
458
460
|
/certificate (?:verify|verification) (?:failed|error)/i,
|
|
459
461
|
/unable to verify (?:the )?(?:first )?certificate/i,
|
package/dist/provider.js
CHANGED
|
@@ -106,7 +106,7 @@ export function createProviderInstance(options) {
|
|
|
106
106
|
reasoningContentEcho: requestConfig.reasoningContentEcho ?? "tool_calls",
|
|
107
107
|
})),
|
|
108
108
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
109
|
-
tool_choice: tools && tools.length > 0 ? "auto" : undefined,
|
|
109
|
+
tool_choice: tools && tools.length > 0 ? chatOptions.toolChoice ?? "auto" : undefined,
|
|
110
110
|
stream: true,
|
|
111
111
|
};
|
|
112
112
|
// DeepSeek and MiniMax only emit final usage in streaming mode when this flag is set.
|
package/dist/session-title.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { normalizeSingleLine, truncateVisual } from "./text-display.js";
|
|
2
|
-
|
|
3
|
-
const LONG_PASTE_LINE_THRESHOLD = 20;
|
|
2
|
+
import { createPastedContentMarker, shouldCollapsePastedContent } from "./tui/paste-placeholder.js";
|
|
4
3
|
const TITLE_INPUT_MAX_CHARS = 4000;
|
|
5
4
|
const TITLE_MAX_WIDTH = 80;
|
|
6
5
|
const TITLE_SYSTEM_PROMPT = [
|
|
@@ -81,10 +80,8 @@ export function deterministicTitleFromUserContent(content) {
|
|
|
81
80
|
const text = userContentText(content);
|
|
82
81
|
if (!text)
|
|
83
82
|
return "User message";
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (charCount > LONG_PASTE_CHAR_THRESHOLD || lineCount > LONG_PASTE_LINE_THRESHOLD) {
|
|
87
|
-
return `[Pasted Content ${charCount} chars]`;
|
|
83
|
+
if (shouldCollapsePastedContent(text)) {
|
|
84
|
+
return createPastedContentMarker(text);
|
|
88
85
|
}
|
|
89
86
|
return truncateVisual(normalizeSingleLine(text), TITLE_MAX_WIDTH) || "User message";
|
|
90
87
|
}
|
|
@@ -61,6 +61,9 @@ function persistSelectedModel(model, ctx) {
|
|
|
61
61
|
}
|
|
62
62
|
function syncSystemPrompt(ctx, model) {
|
|
63
63
|
const { providerId, modelId } = decodeModel(model);
|
|
64
|
+
const toolPromptOptions = typeof ctx.agent.getSystemPromptToolOptions === "function"
|
|
65
|
+
? ctx.agent.getSystemPromptToolOptions()
|
|
66
|
+
: {};
|
|
64
67
|
ctx.agent.setSystemPrompt(buildSystemPrompt({
|
|
65
68
|
agentName: "Bubble",
|
|
66
69
|
configuredProvider: providerId,
|
|
@@ -68,6 +71,7 @@ function syncSystemPrompt(ctx, model) {
|
|
|
68
71
|
configuredModelId: model,
|
|
69
72
|
thinkingLevel: ctx.agent.thinking,
|
|
70
73
|
workingDir: ctx.cwd,
|
|
74
|
+
...toolPromptOptions,
|
|
71
75
|
memoryPrompt: buildMemoryPrompt(ctx.cwd),
|
|
72
76
|
}));
|
|
73
77
|
}
|
package/dist/stats/usage.d.ts
CHANGED
package/dist/stats/usage.js
CHANGED
|
@@ -187,6 +187,7 @@ function addModelUsage(accumulator, model, message, usage) {
|
|
|
187
187
|
completionTokens: 0,
|
|
188
188
|
promptCacheHitTokens: 0,
|
|
189
189
|
promptCacheMissTokens: 0,
|
|
190
|
+
cacheCreationTokens: 0,
|
|
190
191
|
reasoningTokens: 0,
|
|
191
192
|
totalTokens: 0,
|
|
192
193
|
};
|
|
@@ -195,6 +196,7 @@ function addModelUsage(accumulator, model, message, usage) {
|
|
|
195
196
|
existing.completionTokens += usage.completionTokens;
|
|
196
197
|
existing.promptCacheHitTokens += usage.promptCacheHitTokens ?? 0;
|
|
197
198
|
existing.promptCacheMissTokens += usage.promptCacheMissTokens ?? 0;
|
|
199
|
+
existing.cacheCreationTokens += usage.cacheCreationTokens ?? 0;
|
|
198
200
|
existing.reasoningTokens += usage.reasoningTokens ?? 0;
|
|
199
201
|
existing.totalTokens += tokenTotal(usage);
|
|
200
202
|
if (providerId && modelId) {
|
|
@@ -284,6 +286,9 @@ function formatSummaryLines(stats, width) {
|
|
|
284
286
|
if (favorite) {
|
|
285
287
|
lines.push(` Favorite model ${truncate(favorite, Math.max(12, width - 17))}`);
|
|
286
288
|
}
|
|
289
|
+
const cacheSummary = formatCacheSummary(stats.models);
|
|
290
|
+
if (cacheSummary)
|
|
291
|
+
lines.push(` ${cacheSummary}`);
|
|
287
292
|
const trackedCostText = formatTrackedCosts(stats);
|
|
288
293
|
if (trackedCostText)
|
|
289
294
|
lines.push(` Tracked cost ${trackedCostText}`);
|
|
@@ -293,6 +298,17 @@ function formatSummaryLines(stats, width) {
|
|
|
293
298
|
}
|
|
294
299
|
return lines;
|
|
295
300
|
}
|
|
301
|
+
function formatCacheSummary(models) {
|
|
302
|
+
const read = models.reduce((sum, model) => sum + model.promptCacheHitTokens, 0);
|
|
303
|
+
const create = models.reduce((sum, model) => sum + model.cacheCreationTokens, 0);
|
|
304
|
+
const missWithCreate = models.reduce((sum, model) => sum + model.promptCacheMissTokens, 0);
|
|
305
|
+
const miss = Math.max(0, missWithCreate - create);
|
|
306
|
+
const observed = read + create + miss;
|
|
307
|
+
if (observed === 0)
|
|
308
|
+
return undefined;
|
|
309
|
+
const hitRate = Math.round((read / observed) * 100);
|
|
310
|
+
return `Prompt cache ${formatCompactNumber(read)} read · ${formatCompactNumber(create)} create · ${formatCompactNumber(miss)} miss · ${hitRate}% hit`;
|
|
311
|
+
}
|
|
296
312
|
function aggregateCosts(models) {
|
|
297
313
|
const totals = {};
|
|
298
314
|
for (const model of models) {
|
|
@@ -355,15 +371,24 @@ function normalizeUsage(raw) {
|
|
|
355
371
|
if (!raw || typeof raw !== "object")
|
|
356
372
|
return undefined;
|
|
357
373
|
const value = raw;
|
|
358
|
-
const
|
|
374
|
+
const rawInputTokens = numberValue(value.input_tokens);
|
|
375
|
+
const cacheReadTokens = numberValue(value.promptCacheHitTokens) ?? numberValue(value.cache_read_input_tokens);
|
|
376
|
+
const cacheCreationTokens = numberValue(value.cacheCreationTokens) ?? numberValue(value.cache_creation_input_tokens);
|
|
377
|
+
const promptTokens = numberValue(value.promptTokens)
|
|
378
|
+
?? (rawInputTokens !== undefined
|
|
379
|
+
? rawInputTokens + (cacheReadTokens ?? 0) + (cacheCreationTokens ?? 0)
|
|
380
|
+
: undefined);
|
|
359
381
|
const completionTokens = numberValue(value.completionTokens) ?? numberValue(value.output_tokens);
|
|
360
382
|
if (promptTokens === undefined || completionTokens === undefined)
|
|
361
383
|
return undefined;
|
|
362
384
|
return {
|
|
363
385
|
promptTokens,
|
|
364
386
|
completionTokens,
|
|
365
|
-
promptCacheHitTokens:
|
|
366
|
-
promptCacheMissTokens: numberValue(value.promptCacheMissTokens)
|
|
387
|
+
promptCacheHitTokens: cacheReadTokens,
|
|
388
|
+
promptCacheMissTokens: numberValue(value.promptCacheMissTokens)
|
|
389
|
+
?? numberValue(value.cache_miss_input_tokens)
|
|
390
|
+
?? (rawInputTokens !== undefined ? rawInputTokens + (cacheCreationTokens ?? 0) : undefined),
|
|
391
|
+
cacheCreationTokens,
|
|
367
392
|
reasoningTokens: numberValue(value.reasoningTokens),
|
|
368
393
|
totalTokens: numberValue(value.totalTokens) ?? numberValue(value.total_tokens),
|
|
369
394
|
};
|
package/dist/tools/edit.js
CHANGED
|
@@ -13,12 +13,66 @@ import { applyEditsToContent, EditApplyError, formatEditMatchNotes } from "./edi
|
|
|
13
13
|
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
14
14
|
import { isWithinWorkspace } from "./file-state.js";
|
|
15
15
|
import { resolveToolPath } from "./path-utils.js";
|
|
16
|
+
function prepareEditArguments(input) {
|
|
17
|
+
if (!input || typeof input !== "object")
|
|
18
|
+
return input;
|
|
19
|
+
const args = { ...input };
|
|
20
|
+
if (typeof args.file_path === "string" && typeof args.path !== "string") {
|
|
21
|
+
args.path = args.file_path;
|
|
22
|
+
}
|
|
23
|
+
if (typeof args.edits === "string") {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(args.edits);
|
|
26
|
+
if (Array.isArray(parsed))
|
|
27
|
+
args.edits = parsed;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Keep the original value so validation surfaces the problem.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(args.edits)) {
|
|
34
|
+
args.edits = args.edits.map((edit) => {
|
|
35
|
+
if (!edit || typeof edit !== "object")
|
|
36
|
+
return edit;
|
|
37
|
+
const normalized = { ...edit };
|
|
38
|
+
if (typeof normalized.oldText !== "string") {
|
|
39
|
+
normalized.oldText = firstString(edit.old_text, edit.oldString, edit.old_string);
|
|
40
|
+
}
|
|
41
|
+
if (typeof normalized.newText !== "string") {
|
|
42
|
+
normalized.newText = firstString(edit.new_text, edit.newString, edit.new_string);
|
|
43
|
+
}
|
|
44
|
+
return normalized;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (!Array.isArray(args.edits)) {
|
|
48
|
+
const oldText = firstString(args.oldText, args.old_text, args.oldString, args.old_string);
|
|
49
|
+
const newText = firstString(args.newText, args.new_text, args.newString, args.new_string);
|
|
50
|
+
if (typeof oldText === "string" && typeof newText === "string") {
|
|
51
|
+
args.edits = [{ oldText, newText }];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return args;
|
|
55
|
+
}
|
|
56
|
+
function firstString(...values) {
|
|
57
|
+
for (const value of values) {
|
|
58
|
+
if (typeof value === "string")
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
16
63
|
export function createEditTool(cwd, approval, lsp, fileState) {
|
|
17
64
|
return {
|
|
18
65
|
name: "edit",
|
|
19
66
|
effect: "write_direct",
|
|
20
67
|
requiresApproval: true,
|
|
21
|
-
description: "
|
|
68
|
+
description: "Edit a single file using targeted text replacements. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.",
|
|
69
|
+
promptSnippet: "Make precise file edits with exact text replacement, including multiple disjoint edits in one call",
|
|
70
|
+
promptGuidelines: [
|
|
71
|
+
"Use edit for precise changes; edits[].oldText should be copied from a recent read and must identify a unique target.",
|
|
72
|
+
"When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls.",
|
|
73
|
+
"Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.",
|
|
74
|
+
"Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.",
|
|
75
|
+
],
|
|
22
76
|
parameters: {
|
|
23
77
|
type: "object",
|
|
24
78
|
properties: {
|
|
@@ -38,7 +92,27 @@ export function createEditTool(cwd, approval, lsp, fileState) {
|
|
|
38
92
|
},
|
|
39
93
|
required: ["path", "edits"],
|
|
40
94
|
},
|
|
95
|
+
prepareArguments: prepareEditArguments,
|
|
41
96
|
async execute(args) {
|
|
97
|
+
if (!Array.isArray(args.edits)) {
|
|
98
|
+
return {
|
|
99
|
+
content: "Error: edit requires edits to be an array of { oldText, newText } replacements.",
|
|
100
|
+
isError: true,
|
|
101
|
+
status: "blocked",
|
|
102
|
+
metadata: { kind: "edit", reason: "invalid_args" },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
for (let index = 0; index < args.edits.length; index++) {
|
|
106
|
+
const edit = args.edits[index];
|
|
107
|
+
if (!edit || typeof edit !== "object" || typeof edit.oldText !== "string" || typeof edit.newText !== "string") {
|
|
108
|
+
return {
|
|
109
|
+
content: `Error: edit requires edits[${index}] to contain string oldText and newText fields.`,
|
|
110
|
+
isError: true,
|
|
111
|
+
status: "blocked",
|
|
112
|
+
metadata: { kind: "edit", reason: "invalid_args", index },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
42
116
|
const filePath = resolveToolPath(cwd, args.path);
|
|
43
117
|
if (!isWithinWorkspace(cwd, filePath)) {
|
|
44
118
|
return {
|
package/dist/tools/glob.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Glob tool - discover files by path pattern without shell access.
|
|
3
3
|
*/
|
|
4
4
|
import { readdir, stat } from "node:fs/promises";
|
|
5
|
-
import { relative, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
6
6
|
import picomatch from "picomatch";
|
|
7
7
|
import { isSensitivePath } from "./sensitive-paths.js";
|
|
8
|
-
import { resolveToolPath } from "./path-utils.js";
|
|
8
|
+
import { expandHomePath, resolveToolPath } from "./path-utils.js";
|
|
9
9
|
const MAX_RESULTS = 100;
|
|
10
10
|
const DEFAULT_IGNORES = new Set([
|
|
11
11
|
".git",
|
|
@@ -32,11 +32,16 @@ export function createGlobTool(cwd) {
|
|
|
32
32
|
required: ["pattern"],
|
|
33
33
|
},
|
|
34
34
|
async execute(args, ctx) {
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
if (!
|
|
35
|
+
const requestedRoot = resolveToolPath(cwd, typeof args.path === "string" && args.path.trim() ? args.path : ".");
|
|
36
|
+
const originalPattern = String(args.pattern || "").trim();
|
|
37
|
+
if (!originalPattern) {
|
|
38
38
|
return { content: "Error: glob pattern is required", isError: true, status: "command_error" };
|
|
39
39
|
}
|
|
40
|
+
const normalized = normalizeGlobSearch(requestedRoot, originalPattern);
|
|
41
|
+
if (normalized.error) {
|
|
42
|
+
return normalized.error;
|
|
43
|
+
}
|
|
44
|
+
const { root, pattern, normalizedPattern } = normalized;
|
|
40
45
|
if (isSensitivePath(root)) {
|
|
41
46
|
return {
|
|
42
47
|
content: `Error: Glob blocked for sensitive credential storage: ${root}`,
|
|
@@ -45,7 +50,9 @@ export function createGlobTool(cwd) {
|
|
|
45
50
|
metadata: {
|
|
46
51
|
kind: "security",
|
|
47
52
|
path: root,
|
|
48
|
-
pattern,
|
|
53
|
+
pattern: normalizedPattern,
|
|
54
|
+
originalPattern,
|
|
55
|
+
normalizedPattern,
|
|
49
56
|
reason: "Sensitive credential storage is not searchable from general-purpose tasks.",
|
|
50
57
|
},
|
|
51
58
|
};
|
|
@@ -74,11 +81,13 @@ export function createGlobTool(cwd) {
|
|
|
74
81
|
metadata: {
|
|
75
82
|
kind: "search",
|
|
76
83
|
path: root,
|
|
77
|
-
pattern,
|
|
84
|
+
pattern: normalizedPattern,
|
|
85
|
+
originalPattern,
|
|
86
|
+
normalizedPattern,
|
|
78
87
|
matches: 0,
|
|
79
88
|
truncated: false,
|
|
80
|
-
searchSignature: `glob:${root}:${
|
|
81
|
-
searchFamily: `glob:${
|
|
89
|
+
searchSignature: `glob:${root}:${normalizedPattern}`,
|
|
90
|
+
searchFamily: `glob:${normalizedPattern}`,
|
|
82
91
|
paths: [],
|
|
83
92
|
},
|
|
84
93
|
};
|
|
@@ -89,17 +98,70 @@ export function createGlobTool(cwd) {
|
|
|
89
98
|
metadata: {
|
|
90
99
|
kind: "search",
|
|
91
100
|
path: root,
|
|
92
|
-
pattern,
|
|
101
|
+
pattern: normalizedPattern,
|
|
102
|
+
originalPattern,
|
|
103
|
+
normalizedPattern,
|
|
93
104
|
matches: matches.length,
|
|
94
105
|
truncated: wasTruncated,
|
|
95
|
-
searchSignature: `glob:${root}:${
|
|
96
|
-
searchFamily: `glob:${
|
|
106
|
+
searchSignature: `glob:${root}:${normalizedPattern}`,
|
|
107
|
+
searchFamily: `glob:${normalizedPattern}`,
|
|
97
108
|
paths: absoluteMatches,
|
|
98
109
|
},
|
|
99
110
|
};
|
|
100
111
|
},
|
|
101
112
|
};
|
|
102
113
|
}
|
|
114
|
+
function normalizeGlobSearch(requestedRoot, originalPattern) {
|
|
115
|
+
const expandedPattern = expandGlobPatternHome(originalPattern);
|
|
116
|
+
const scan = picomatch.scan(expandedPattern);
|
|
117
|
+
const prefix = scan.prefix ?? "";
|
|
118
|
+
if (!isAbsolute(scan.base)) {
|
|
119
|
+
if (escapesSearchRoot(scan.base)) {
|
|
120
|
+
return {
|
|
121
|
+
error: {
|
|
122
|
+
content: `Error: Glob pattern must stay within the search path: ${originalPattern}`,
|
|
123
|
+
isError: true,
|
|
124
|
+
status: "command_error",
|
|
125
|
+
metadata: {
|
|
126
|
+
kind: "search",
|
|
127
|
+
path: requestedRoot,
|
|
128
|
+
pattern: originalPattern,
|
|
129
|
+
originalPattern,
|
|
130
|
+
normalizedPattern: originalPattern,
|
|
131
|
+
reason: "pattern_outside_search_path",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return { root: requestedRoot, pattern: originalPattern, normalizedPattern: originalPattern };
|
|
137
|
+
}
|
|
138
|
+
const absoluteBase = resolve(scan.base);
|
|
139
|
+
const patternRoot = scan.isGlob ? absoluteBase : dirname(absoluteBase);
|
|
140
|
+
const patternBody = scan.isGlob ? scan.glob : basename(absoluteBase);
|
|
141
|
+
const normalizedRoot = isWithinSearchRoot(requestedRoot, patternRoot) ? requestedRoot : patternRoot;
|
|
142
|
+
const relativeBase = toPosix(relative(normalizedRoot, patternRoot));
|
|
143
|
+
const normalizedBody = [relativeBase, patternBody].filter(Boolean).join("/");
|
|
144
|
+
const normalizedPattern = `${prefix}${normalizedBody}`;
|
|
145
|
+
return {
|
|
146
|
+
root: normalizedRoot,
|
|
147
|
+
pattern: normalizedPattern,
|
|
148
|
+
normalizedPattern,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function expandGlobPatternHome(pattern) {
|
|
152
|
+
if (pattern.startsWith("!")) {
|
|
153
|
+
return `!${expandHomePath(pattern.slice(1))}`;
|
|
154
|
+
}
|
|
155
|
+
return expandHomePath(pattern);
|
|
156
|
+
}
|
|
157
|
+
function escapesSearchRoot(base) {
|
|
158
|
+
const normalized = toPosix(base);
|
|
159
|
+
return normalized === ".." || normalized.startsWith("../");
|
|
160
|
+
}
|
|
161
|
+
function isWithinSearchRoot(root, target) {
|
|
162
|
+
const rel = relative(root, target);
|
|
163
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
164
|
+
}
|
|
103
165
|
async function walk(root, dir, matcher, files, truncated, abortSignal) {
|
|
104
166
|
if (abortSignal?.aborted || files.length >= MAX_RESULTS) {
|
|
105
167
|
truncated.value = true;
|
|
@@ -115,6 +177,9 @@ async function walk(root, dir, matcher, files, truncated, abortSignal) {
|
|
|
115
177
|
continue;
|
|
116
178
|
}
|
|
117
179
|
const absolute = resolve(dir, entry.name);
|
|
180
|
+
if (isSensitivePath(absolute)) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
118
183
|
const rel = toPosix(relative(root, absolute));
|
|
119
184
|
if (entry.isDirectory()) {
|
|
120
185
|
await walk(root, absolute, matcher, files, truncated, abortSignal);
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { createBashTool } from "./bash.js";
|
|
|
6
6
|
export { createManagedServerTools } from "./server.js";
|
|
7
7
|
export { createWriteTool } from "./write.js";
|
|
8
8
|
export { createEditTool } from "./edit.js";
|
|
9
|
-
export {
|
|
9
|
+
export { buildToolPromptOptions } from "./prompt-metadata.js";
|
|
10
10
|
export { createGlobTool } from "./glob.js";
|
|
11
11
|
export { createGrepTool } from "./grep.js";
|
|
12
12
|
export { createLspTool } from "./lsp.js";
|
package/dist/tools/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export { createBashTool } from "./bash.js";
|
|
|
6
6
|
export { createManagedServerTools } from "./server.js";
|
|
7
7
|
export { createWriteTool } from "./write.js";
|
|
8
8
|
export { createEditTool } from "./edit.js";
|
|
9
|
-
export {
|
|
9
|
+
export { buildToolPromptOptions } from "./prompt-metadata.js";
|
|
10
10
|
export { createGlobTool } from "./glob.js";
|
|
11
11
|
export { createGrepTool } from "./grep.js";
|
|
12
12
|
export { createLspTool } from "./lsp.js";
|
|
@@ -23,7 +23,6 @@ export { createMemoryReadSummaryTool, createMemorySearchTool } from "./memory.js
|
|
|
23
23
|
import { createBashTool } from "./bash.js";
|
|
24
24
|
import { createManagedServerTools } from "./server.js";
|
|
25
25
|
import { createEditTool } from "./edit.js";
|
|
26
|
-
import { createApplyPatchTool } from "./apply-patch.js";
|
|
27
26
|
import { createExitPlanModeTool } from "./exit-plan-mode.js";
|
|
28
27
|
import { createGlobTool } from "./glob.js";
|
|
29
28
|
import { createGrepTool } from "./grep.js";
|
|
@@ -51,7 +50,6 @@ export function createAllTools(cwd, skillRegistry, options = {}) {
|
|
|
51
50
|
...createManagedServerTools(cwd, approval),
|
|
52
51
|
createWriteTool(cwd, {}, approval, lsp, fileState),
|
|
53
52
|
createEditTool(cwd, approval, lsp, fileState),
|
|
54
|
-
createApplyPatchTool(cwd, approval, lsp, fileState),
|
|
55
53
|
createGlobTool(cwd),
|
|
56
54
|
createGrepTool(cwd),
|
|
57
55
|
createLspTool(cwd, lsp, approval),
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function buildToolPromptOptions(tools) {
|
|
2
|
+
const toolSnippets = {};
|
|
3
|
+
const guidelines = [];
|
|
4
|
+
for (const tool of tools) {
|
|
5
|
+
if (tool.promptSnippet) {
|
|
6
|
+
toolSnippets[tool.name] = tool.promptSnippet;
|
|
7
|
+
}
|
|
8
|
+
for (const guideline of tool.promptGuidelines ?? []) {
|
|
9
|
+
guidelines.push(guideline);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
tools: tools.map((tool) => tool.name),
|
|
14
|
+
toolSnippets,
|
|
15
|
+
guidelines,
|
|
16
|
+
};
|
|
17
|
+
}
|
package/dist/tools/write.js
CHANGED
|
@@ -9,12 +9,25 @@ import { formatDiagnosticBlocks } from "../lsp/index.js";
|
|
|
9
9
|
import { isWithinWorkspace } from "./file-state.js";
|
|
10
10
|
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
11
11
|
import { resolveToolPath } from "./path-utils.js";
|
|
12
|
+
function prepareWriteArguments(input) {
|
|
13
|
+
if (!input || typeof input !== "object")
|
|
14
|
+
return input;
|
|
15
|
+
const args = { ...input };
|
|
16
|
+
if (typeof args.file_path === "string" && typeof args.path !== "string") {
|
|
17
|
+
args.path = args.file_path;
|
|
18
|
+
}
|
|
19
|
+
return args;
|
|
20
|
+
}
|
|
12
21
|
export function createWriteTool(cwd, _options = {}, approval, lsp, fileState) {
|
|
13
22
|
return {
|
|
14
23
|
name: "write",
|
|
15
24
|
effect: "write_direct",
|
|
16
25
|
requiresApproval: true,
|
|
17
26
|
description: "Write content to a file. Creates parent directories as needed. If the file already exists, this replaces the full file; use edit for small targeted changes.",
|
|
27
|
+
promptSnippet: "Create new files or intentionally rewrite complete files",
|
|
28
|
+
promptGuidelines: [
|
|
29
|
+
"Use write only for new files, generated files, or intentional complete rewrites. Use edit for targeted changes to existing files.",
|
|
30
|
+
],
|
|
18
31
|
parameters: {
|
|
19
32
|
type: "object",
|
|
20
33
|
properties: {
|
|
@@ -23,6 +36,7 @@ export function createWriteTool(cwd, _options = {}, approval, lsp, fileState) {
|
|
|
23
36
|
},
|
|
24
37
|
required: ["path", "content"],
|
|
25
38
|
},
|
|
39
|
+
prepareArguments: prepareWriteArguments,
|
|
26
40
|
async execute(args) {
|
|
27
41
|
const filePath = resolveToolPath(cwd, args.path);
|
|
28
42
|
if (!isWithinWorkspace(cwd, filePath)) {
|