@cjhyy/code-shell-core 0.6.0-rc.8 → 0.7.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/automation/scheduler.d.ts +13 -7
- package/dist/automation/scheduler.js +116 -37
- package/dist/capability-control/service.d.ts +2 -0
- package/dist/capability-control/service.js +4 -2
- package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +7 -1
- package/dist/cc-orchestrator/codex-session-history.js +1 -1
- package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
- package/dist/cc-orchestrator/cwd-normalize.js +19 -0
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
- package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
- package/dist/cc-orchestrator/external-agent-driver.js +102 -51
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
- package/dist/cc-orchestrator/session-history.js +2 -2
- package/dist/cli/agent-server-stdio.js +9 -2
- package/dist/context/compaction.d.ts +38 -1
- package/dist/context/compaction.js +138 -0
- package/dist/context/manager.d.ts +34 -2
- package/dist/context/manager.js +254 -58
- package/dist/context/token-counter.js +13 -0
- package/dist/credentials/access.d.ts +56 -0
- package/dist/credentials/access.js +183 -0
- package/dist/credentials/index.d.ts +1 -0
- package/dist/credentials/index.js +1 -0
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +30 -11
- package/dist/credentials/types.d.ts +2 -2
- package/dist/credentials/use-credential-tool.d.ts +9 -1
- package/dist/credentials/use-credential-tool.js +58 -45
- package/dist/engine/engine.d.ts +38 -12
- package/dist/engine/engine.js +477 -163
- package/dist/engine/image-policy.d.ts +6 -0
- package/dist/engine/image-policy.js +17 -6
- package/dist/engine/input-attachments.d.ts +13 -0
- package/dist/engine/input-attachments.js +255 -0
- package/dist/engine/model-facade.d.ts +5 -2
- package/dist/engine/model-facade.js +6 -16
- package/dist/engine/parse-task.d.ts +10 -0
- package/dist/engine/parse-task.js +5 -0
- package/dist/engine/query.js +2 -0
- package/dist/engine/session-usage.d.ts +12 -0
- package/dist/engine/session-usage.js +56 -0
- package/dist/engine/steer-queue.d.ts +2 -1
- package/dist/engine/steer-queue.js +2 -2
- package/dist/engine/streaming-tool-queue.d.ts +11 -7
- package/dist/engine/streaming-tool-queue.js +11 -7
- package/dist/engine/turn-loop.d.ts +34 -2
- package/dist/engine/turn-loop.js +269 -52
- package/dist/engine/types.d.ts +8 -0
- package/dist/git/worktree/crud.d.ts +69 -0
- package/dist/git/worktree/crud.js +206 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +82 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +51 -0
- package/dist/git/worktree/index.d.ts +5 -0
- package/dist/git/worktree/index.js +5 -0
- package/dist/git/worktree/query.d.ts +42 -0
- package/dist/git/worktree/query.js +121 -0
- package/dist/git/worktree/slug.d.ts +11 -0
- package/dist/git/worktree/slug.js +58 -0
- package/dist/git/worktree.d.ts +1 -84
- package/dist/git/worktree.js +5 -230
- package/dist/index.d.ts +31 -28
- package/dist/index.js +27 -25
- package/dist/logging/logger.js +6 -6
- package/dist/logging/sanitize-messages.d.ts +10 -2
- package/dist/logging/sanitize-messages.js +21 -6
- package/dist/plugins/installer/checkUpdate.d.ts +4 -1
- package/dist/plugins/installer/checkUpdate.js +4 -2
- package/dist/plugins/installer/install.js +2 -0
- package/dist/plugins/installer/installFromSource.js +9 -1
- package/dist/plugins/installer/parseSource.d.ts +4 -1
- package/dist/plugins/installer/parseSource.js +28 -10
- package/dist/plugins/installer/sourcePath.d.ts +9 -0
- package/dist/plugins/installer/sourcePath.js +50 -0
- package/dist/plugins/installer/update.d.ts +4 -1
- package/dist/plugins/installer/update.js +5 -3
- package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
- package/dist/plugins/parseMarketplaceInput.js +4 -3
- package/dist/plugins/pluginInstaller.js +24 -22
- package/dist/preset/index.d.ts +1 -0
- package/dist/preset/index.js +16 -9
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/chat-session-manager.d.ts +2 -0
- package/dist/protocol/chat-session-manager.js +38 -2
- package/dist/protocol/chat-session.d.ts +5 -0
- package/dist/protocol/chat-session.js +2 -0
- package/dist/protocol/client.d.ts +13 -5
- package/dist/protocol/client.js +26 -3
- package/dist/protocol/server.d.ts +43 -12
- package/dist/protocol/server.js +326 -104
- package/dist/protocol/types.d.ts +51 -0
- package/dist/protocol/types.js +6 -0
- package/dist/run/FileRunStore.js +10 -1
- package/dist/run/Heartbeat.js +12 -0
- package/dist/run/RunApprovalBackend.d.ts +3 -0
- package/dist/run/RunApprovalBackend.js +41 -6
- package/dist/run/RunLock.js +2 -0
- package/dist/run/RunManager.d.ts +2 -0
- package/dist/run/RunManager.js +64 -24
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +23 -0
- package/dist/runtime/background-shell.d.ts +1 -0
- package/dist/runtime/background-shell.js +23 -13
- package/dist/runtime/safe-spawn.js +74 -11
- package/dist/runtime/spawn-common.js +10 -0
- package/dist/services/auto-dream.d.ts +15 -4
- package/dist/services/auto-dream.js +20 -20
- package/dist/services/dream-consolidation.d.ts +2 -3
- package/dist/services/dream-consolidation.js +65 -15
- package/dist/services/extract-memories.d.ts +14 -5
- package/dist/services/extract-memories.js +20 -3
- package/dist/services/global-dream-promotion.d.ts +23 -0
- package/dist/services/global-dream-promotion.js +112 -0
- package/dist/services/memory-orchestrator.js +347 -34
- package/dist/session/memory.d.ts +61 -18
- package/dist/session/memory.js +342 -79
- package/dist/session/session-manager.d.ts +35 -1
- package/dist/session/session-manager.js +197 -4
- package/dist/session/transcript.d.ts +5 -1
- package/dist/session/transcript.js +38 -5
- package/dist/settings/manager.d.ts +1 -0
- package/dist/settings/manager.js +87 -37
- package/dist/settings/schema-export.d.ts +2 -3
- package/dist/settings/schema-export.js +2 -3
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +12 -0
- package/dist/skills/scanner.d.ts +3 -2
- package/dist/skills/scanner.js +12 -9
- package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
- package/dist/tool-system/builtin/background-jobs.js +12 -1
- package/dist/tool-system/builtin/background-work.d.ts +17 -18
- package/dist/tool-system/builtin/background-work.js +51 -5
- package/dist/tool-system/builtin/config.d.ts +2 -1
- package/dist/tool-system/builtin/config.js +16 -11
- package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
- package/dist/tool-system/builtin/drive-claude-code.js +301 -47
- package/dist/tool-system/builtin/edit.js +5 -2
- package/dist/tool-system/builtin/generate-video.d.ts +1 -0
- package/dist/tool-system/builtin/generate-video.js +13 -4
- package/dist/tool-system/builtin/index.d.ts +8 -3
- package/dist/tool-system/builtin/index.js +37 -21
- package/dist/tool-system/builtin/lsp.d.ts +2 -1
- package/dist/tool-system/builtin/lsp.js +6 -3
- package/dist/tool-system/builtin/memory.js +40 -8
- package/dist/tool-system/builtin/notebook-edit.js +5 -2
- package/dist/tool-system/builtin/powershell.d.ts +5 -2
- package/dist/tool-system/builtin/powershell.js +11 -7
- package/dist/tool-system/builtin/read.js +118 -6
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/view-image.js +109 -19
- package/dist/tool-system/builtin/worktree.d.ts +4 -4
- package/dist/tool-system/builtin/worktree.js +297 -74
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +23 -1
- package/dist/tool-system/executor.d.ts +1 -5
- package/dist/tool-system/executor.js +94 -115
- package/dist/tool-system/mcp-manager.d.ts +18 -5
- package/dist/tool-system/mcp-manager.js +157 -74
- package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
- package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
- package/dist/tool-system/path-policy.d.ts +2 -0
- package/dist/tool-system/path-policy.js +41 -12
- package/dist/tool-system/permission.d.ts +28 -7
- package/dist/tool-system/permission.js +130 -49
- package/dist/tool-system/registry.js +11 -4
- package/dist/tool-system/tool-result-redaction.d.ts +7 -0
- package/dist/tool-system/tool-result-redaction.js +48 -0
- package/dist/tool-system/workspace-bridge.d.ts +11 -0
- package/dist/tool-system/workspace-bridge.js +1 -0
- package/dist/types.d.ts +86 -5
- package/dist/utils/toolDisplay.js +1 -1
- package/package.json +4 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in view_image tool —
|
|
3
|
-
* 回传进上下文,让 vision 模型「看」它(对照 codex 的 view_image)。
|
|
2
|
+
* Built-in view_image tool — 把一个本地图片文件或历史图片以 base64 image
|
|
3
|
+
* ContentBlock 回传进上下文,让 vision 模型「看」它(对照 codex 的 view_image)。
|
|
4
4
|
*
|
|
5
5
|
* 典型用法:模型先写 SVG/Mermaid 并用 shell 转成 PNG,再调 view_image(png)
|
|
6
6
|
* 检查图画对没有(标签是否重叠、文字是否溢出),不对就改源再重转。
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { readFile, stat } from "node:fs/promises";
|
|
15
15
|
import { extname, isAbsolute, resolve } from "node:path";
|
|
16
16
|
import { capabilitiesFor } from "../../llm/capabilities/index.js";
|
|
17
|
+
import { collectBase64Images, findImageByNumber } from "../../context/compaction.js";
|
|
17
18
|
const MAX_BYTES = 5 * 1024 * 1024; // 5MB —— vision 模型按 tile 计 token,大图无益且撑大请求
|
|
18
19
|
const MEDIA_TYPES = {
|
|
19
20
|
".png": "image/png",
|
|
@@ -24,11 +25,11 @@ const MEDIA_TYPES = {
|
|
|
24
25
|
};
|
|
25
26
|
export const viewImageToolDef = {
|
|
26
27
|
name: "view_image",
|
|
27
|
-
description: "Load
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
28
|
+
description: "Load an image into the conversation so you can SEE it (vision). Pass path to view a " +
|
|
29
|
+
"workspace image file, or pass imageNumber to retrieve the original image behind an " +
|
|
30
|
+
"earlier [image #N, already provided] history placeholder. Use exactly one of path or " +
|
|
31
|
+
"imageNumber. File paths support PNG/JPEG/GIF/WebP only; convert SVG/PDF to PNG first. " +
|
|
32
|
+
"Requires a vision-capable model; otherwise the image is skipped.",
|
|
32
33
|
inputSchema: {
|
|
33
34
|
type: "object",
|
|
34
35
|
properties: {
|
|
@@ -36,14 +37,40 @@ export const viewImageToolDef = {
|
|
|
36
37
|
type: "string",
|
|
37
38
|
description: "Path to the image file (absolute, or relative to the working directory).",
|
|
38
39
|
},
|
|
40
|
+
imageNumber: {
|
|
41
|
+
type: "number",
|
|
42
|
+
description: "Image history number N from an earlier [image #N, already provided] placeholder.",
|
|
43
|
+
},
|
|
44
|
+
detail: {
|
|
45
|
+
type: "string",
|
|
46
|
+
enum: ["low", "standard", "high"],
|
|
47
|
+
description: "Optional image detail preference. Accepted for compatibility; current providers use the runtime image detail default.",
|
|
48
|
+
},
|
|
39
49
|
},
|
|
40
|
-
required: ["path"],
|
|
41
50
|
},
|
|
42
51
|
};
|
|
43
52
|
export async function viewImageTool(args, ctx) {
|
|
44
53
|
const rawPath = args.path;
|
|
54
|
+
const rawImageNumber = args.imageNumber;
|
|
55
|
+
const hasPath = typeof rawPath === "string" && rawPath.trim().length > 0;
|
|
56
|
+
const hasImageNumber = rawImageNumber !== undefined && rawImageNumber !== null;
|
|
57
|
+
if (hasPath === hasImageNumber) {
|
|
58
|
+
return "Error: provide exactly one of path or imageNumber";
|
|
59
|
+
}
|
|
60
|
+
const detail = args.detail;
|
|
61
|
+
if (detail !== undefined && detail !== "low" && detail !== "standard" && detail !== "high") {
|
|
62
|
+
return "Error: detail must be one of low, standard, high";
|
|
63
|
+
}
|
|
64
|
+
if (hasImageNumber) {
|
|
65
|
+
if (typeof rawImageNumber !== "number" ||
|
|
66
|
+
!Number.isSafeInteger(rawImageNumber) ||
|
|
67
|
+
rawImageNumber <= 0) {
|
|
68
|
+
return "Error: imageNumber must be a positive integer";
|
|
69
|
+
}
|
|
70
|
+
return viewHistoricalImage(rawImageNumber, ctx);
|
|
71
|
+
}
|
|
45
72
|
if (typeof rawPath !== "string" || !rawPath.trim()) {
|
|
46
|
-
return "Error: path
|
|
73
|
+
return "Error: path must be a non-empty string";
|
|
47
74
|
}
|
|
48
75
|
const cwd = ctx?.cwd ?? process.cwd();
|
|
49
76
|
const abs = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
@@ -51,13 +78,7 @@ export async function viewImageTool(args, ctx) {
|
|
|
51
78
|
// 缺 llmConfig 时按「非视觉」处理(fail closed),对齐 DEFAULT_CAPABILITY
|
|
52
79
|
// 的保守姿态:运行时 ToolContext.llmConfig 必有,缺失只发生在测试 / 异常
|
|
53
80
|
// 装配下,此时绝不把 base64 读进上下文。
|
|
54
|
-
|
|
55
|
-
if (!ctx?.llmConfig)
|
|
56
|
-
return false;
|
|
57
|
-
const kind = (ctx.llmConfig.providerKind ?? ctx.llmConfig.provider);
|
|
58
|
-
return capabilitiesFor(kind, ctx.llmConfig.model).supportsVision;
|
|
59
|
-
})();
|
|
60
|
-
if (!supportsVision) {
|
|
81
|
+
if (!supportsVision(ctx)) {
|
|
61
82
|
return `[图片未加载: ${abs} —— 当前模型不支持视觉输入,已跳过。切换到 vision 模型后再 view_image。]`;
|
|
62
83
|
}
|
|
63
84
|
// 闸门 2:格式 gate
|
|
@@ -88,9 +109,78 @@ export async function viewImageTool(args, ctx) {
|
|
|
88
109
|
const data = buf.toString("base64");
|
|
89
110
|
const kb = Math.round(buf.length / 1024);
|
|
90
111
|
return {
|
|
91
|
-
contentBlocks: [
|
|
92
|
-
{ type: "image", source: { type: "base64", media_type: mediaType, data } },
|
|
93
|
-
],
|
|
112
|
+
contentBlocks: [{ type: "image", source: { type: "base64", media_type: mediaType, data } }],
|
|
94
113
|
result: `[已加载图片: ${abs} (${mediaType}, ${kb} KB)]`,
|
|
95
114
|
};
|
|
96
115
|
}
|
|
116
|
+
function supportsVision(ctx) {
|
|
117
|
+
if (!ctx?.llmConfig)
|
|
118
|
+
return false;
|
|
119
|
+
const kind = (ctx.llmConfig.providerKind ?? ctx.llmConfig.provider);
|
|
120
|
+
return capabilitiesFor(kind, ctx.llmConfig.model).supportsVision;
|
|
121
|
+
}
|
|
122
|
+
async function viewHistoricalImage(imageNumber, ctx) {
|
|
123
|
+
if (!supportsVision(ctx)) {
|
|
124
|
+
return `[图片未取回: image #${imageNumber} —— 当前模型不支持视觉输入,已跳过。切换到 vision 模型后再 view_image。]`;
|
|
125
|
+
}
|
|
126
|
+
const sessionManager = ctx?.engine?.getSessionManager?.();
|
|
127
|
+
const sessionId = ctx?.sessionId;
|
|
128
|
+
if (!sessionManager || !sessionId) {
|
|
129
|
+
return `Error: 无法取回 image #${imageNumber}: 当前工具上下文没有可读取的 session 历史。`;
|
|
130
|
+
}
|
|
131
|
+
let messages;
|
|
132
|
+
try {
|
|
133
|
+
messages = sessionManager.resume(sessionId).transcript.toMessages();
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
return `Error: 无法读取 session 历史以取回 image #${imageNumber}: ${err.message}`;
|
|
137
|
+
}
|
|
138
|
+
const images = collectBase64Images(messages);
|
|
139
|
+
const found = findImageByNumber(messages, imageNumber);
|
|
140
|
+
if (!found) {
|
|
141
|
+
return `Error: 未找到 image #${imageNumber}; 当前 session 历史中共有 ${images.length} 张可取回图片。`;
|
|
142
|
+
}
|
|
143
|
+
const block = normalizeImageBlockForReturn(found.block);
|
|
144
|
+
const size = decodedImageBlockBytes(block);
|
|
145
|
+
if (size !== undefined && size > MAX_BYTES) {
|
|
146
|
+
const mb = (size / 1024 / 1024).toFixed(1);
|
|
147
|
+
return `[图片未取回: image #${imageNumber} —— 图片过大 (${mb} MB > 5 MB),请先压缩或缩放原图。]`;
|
|
148
|
+
}
|
|
149
|
+
const kb = size === undefined ? "unknown" : String(Math.round(size / 1024));
|
|
150
|
+
const mediaType = mediaTypeOfImageBlock(block) ?? "base64 image";
|
|
151
|
+
return {
|
|
152
|
+
contentBlocks: [block],
|
|
153
|
+
result: `[已取回 image #${imageNumber}: ${mediaType}, ${kb} KB]`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function normalizeImageBlockForReturn(block) {
|
|
157
|
+
if (block.type === "image" && block.source?.type === "base64")
|
|
158
|
+
return block;
|
|
159
|
+
const source = openAIDataUrlImageSource(block);
|
|
160
|
+
return source ? { type: "image", source } : block;
|
|
161
|
+
}
|
|
162
|
+
function decodedImageBlockBytes(block) {
|
|
163
|
+
const data = base64DataOfImageBlock(block);
|
|
164
|
+
if (!data)
|
|
165
|
+
return undefined;
|
|
166
|
+
return Buffer.byteLength(data, "base64");
|
|
167
|
+
}
|
|
168
|
+
function base64DataOfImageBlock(block) {
|
|
169
|
+
if (block.type === "image" && block.source?.type === "base64") {
|
|
170
|
+
return block.source.data;
|
|
171
|
+
}
|
|
172
|
+
return openAIDataUrlImageSource(block)?.data;
|
|
173
|
+
}
|
|
174
|
+
function mediaTypeOfImageBlock(block) {
|
|
175
|
+
if (block.type === "image" && block.source?.type === "base64") {
|
|
176
|
+
return block.source.media_type;
|
|
177
|
+
}
|
|
178
|
+
return openAIDataUrlImageSource(block)?.media_type;
|
|
179
|
+
}
|
|
180
|
+
function openAIDataUrlImageSource(block) {
|
|
181
|
+
const maybeOpenAI = block;
|
|
182
|
+
const match = maybeOpenAI.image_url?.url?.match(/^data:(image\/[^;,]+);base64,(.+)$/i);
|
|
183
|
+
if (maybeOpenAI.type !== "image_url" || !match?.[1] || !match[2])
|
|
184
|
+
return undefined;
|
|
185
|
+
return { type: "base64", media_type: match[1], data: match[2] };
|
|
186
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* EnterWorktree / ExitWorktree tools —
|
|
2
|
+
* EnterWorktree / ExitWorktree tools — switch a session between git workspaces.
|
|
3
3
|
*/
|
|
4
4
|
import type { ToolDefinition } from "../../types.js";
|
|
5
5
|
import type { ToolContext } from "../context.js";
|
|
6
|
-
|
|
7
|
-
export declare function
|
|
6
|
+
export declare const switchSessionWorkspaceToolDef: ToolDefinition;
|
|
7
|
+
export declare function switchSessionWorkspaceTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
8
8
|
export declare const enterWorktreeToolDef: ToolDefinition;
|
|
9
9
|
export declare function enterWorktreeTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
10
10
|
export declare const exitWorktreeToolDef: ToolDefinition;
|
|
11
|
-
export declare function exitWorktreeTool(args: Record<string, unknown
|
|
11
|
+
export declare function exitWorktreeTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
@@ -1,122 +1,345 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* EnterWorktree / ExitWorktree tools —
|
|
2
|
+
* EnterWorktree / ExitWorktree tools — switch a session between git workspaces.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
"Changes made in the worktree do not affect the main working directory. " +
|
|
15
|
-
"Use this when you need to make experimental changes or work in isolation.",
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { isAbsolute, resolve } from "node:path";
|
|
6
|
+
import { createWorktree, removeWorktree, listWorktrees, validateWorktreeSlug, selectPlatformScript, runWorktreeSetup, worktreeHasUncommittedOrAheadChanges, currentBranch, } from "../../git/worktree.js";
|
|
7
|
+
export const switchSessionWorkspaceToolDef = {
|
|
8
|
+
name: "SwitchSessionWorkspace",
|
|
9
|
+
description: "Switch this current conversation session into or out of a workspace through the host UI path. " +
|
|
10
|
+
"Use this when you need isolated or parallel work in a git worktree, when you need to move " +
|
|
11
|
+
"this conversation to an existing worktree path/branch, or when you are done and should return " +
|
|
12
|
+
"this current conversation to main. This is the correct way to move THIS conversation's session " +
|
|
13
|
+
"into or out of a worktree on desktop.",
|
|
16
14
|
inputSchema: {
|
|
17
15
|
type: "object",
|
|
18
16
|
properties: {
|
|
19
|
-
|
|
17
|
+
target: {
|
|
20
18
|
type: "string",
|
|
21
|
-
description: "
|
|
22
|
-
"Example: 'fix-auth-bug', 'refactor-api'",
|
|
19
|
+
description: "Workspace target: 'main', a new slug, an existing worktree path, or an existing branch name.",
|
|
23
20
|
},
|
|
24
21
|
},
|
|
25
|
-
required: ["
|
|
22
|
+
required: ["target"],
|
|
26
23
|
},
|
|
27
24
|
};
|
|
28
|
-
export async function
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
31
|
-
return "Error:
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
export async function switchSessionWorkspaceTool(args, ctx) {
|
|
26
|
+
const target = stringArg(args.target);
|
|
27
|
+
if (!target)
|
|
28
|
+
return "Error: target is required";
|
|
29
|
+
const bridge = ctx?.workspace;
|
|
30
|
+
if (!bridge) {
|
|
31
|
+
return ("SwitchSessionWorkspace is not available in this host. " +
|
|
32
|
+
"Use the host's supported workspace controls instead.");
|
|
34
33
|
}
|
|
35
34
|
try {
|
|
36
|
-
|
|
35
|
+
const workspace = await bridge.switch(target);
|
|
36
|
+
ctx?.setSessionWorkspace?.(workspace);
|
|
37
|
+
const details = workspace.kind === "worktree" && workspace.worktree
|
|
38
|
+
? `\n Branch: ${workspace.worktree.branch}`
|
|
39
|
+
: "";
|
|
40
|
+
return (`Switched session workspace:\n` +
|
|
41
|
+
` Path: ${workspace.root}` +
|
|
42
|
+
details +
|
|
43
|
+
`\n\n` +
|
|
44
|
+
nextTurnNotice(workspace.root, ctx?.cwd ?? workspace.root));
|
|
37
45
|
}
|
|
38
46
|
catch (err) {
|
|
39
|
-
return `Error: ${err.message}`;
|
|
47
|
+
return `Error switching workspace: ${err.message}`;
|
|
40
48
|
}
|
|
49
|
+
}
|
|
50
|
+
export const enterWorktreeToolDef = {
|
|
51
|
+
name: "EnterWorktree",
|
|
52
|
+
description: "Switch the current session workspace. Target can be a new worktree slug, " +
|
|
53
|
+
"an existing worktree path or branch, or 'main' to return to the main repository. " +
|
|
54
|
+
"Switching leaves the previous worktree on disk.",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
target: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Workspace target: 'main', a new slug (alphanumeric, dots, dashes, underscores), " +
|
|
61
|
+
"or an existing worktree path/branch to switch to.",
|
|
62
|
+
},
|
|
63
|
+
slug: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "Deprecated alias for target. Prefer target.",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
required: [],
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
export async function enterWorktreeTool(args, ctx) {
|
|
72
|
+
const target = stringArg(args.target) ?? stringArg(args.slug);
|
|
73
|
+
if (!target)
|
|
74
|
+
return "Error: target is required";
|
|
75
|
+
const resolved = sessionServices(ctx);
|
|
76
|
+
if (!resolved.ok)
|
|
77
|
+
return resolved.error;
|
|
78
|
+
const { sessionId, sessionManager } = resolved;
|
|
79
|
+
const mainRoot = sessionManager.readCwd(sessionId) ?? ctx?.cwd ?? process.cwd();
|
|
80
|
+
const fromWorkspace = sessionManager.getSessionWorkspace(sessionId) ?? {
|
|
81
|
+
root: mainRoot,
|
|
82
|
+
kind: "main",
|
|
83
|
+
};
|
|
84
|
+
const fromRoot = fromWorkspace.root;
|
|
85
|
+
const currentTurnRoot = ctx?.cwd ?? fromRoot;
|
|
86
|
+
const branchPrefix = ctx?.engine?.readWorktreeBranchPrefix?.(mainRoot);
|
|
41
87
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let setupNote = "";
|
|
51
|
-
const setupScripts = ctx?.engine?.readWorktreeSetupScripts(cwd);
|
|
52
|
-
const script = selectPlatformScript(setupScripts);
|
|
53
|
-
if (script) {
|
|
54
|
-
const setup = await runWorktreeSetup(_activeWorktree.worktreePath, script, {
|
|
55
|
-
sandbox: ctx?.sandbox,
|
|
56
|
-
shellEnv: ctx?.shellEnv,
|
|
57
|
-
signal: ctx?.signal,
|
|
58
|
-
});
|
|
59
|
-
if (setup.ok) {
|
|
60
|
-
setupNote = `\n\nRan setup script (exit 0).${setup.output ? `\n${truncate(setup.output)}` : ""}`;
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
setupNote =
|
|
64
|
-
`\n\n⚠️ Setup script failed (exit ${setup.exitCode ?? "?"}) — continuing anyway. ` +
|
|
65
|
-
`You may need to run setup manually.${setup.output ? `\n${truncate(setup.output)}` : ""}`;
|
|
66
|
-
}
|
|
88
|
+
if (target === "main") {
|
|
89
|
+
const workspace = { root: mainRoot, kind: "main" };
|
|
90
|
+
persistSessionWorkspace(sessionManager, sessionId, workspace, ctx);
|
|
91
|
+
sessionManager.recordWorkspaceHandoff(sessionId, fromWorkspace, workspace);
|
|
92
|
+
return (`Switched to main workspace:\n` +
|
|
93
|
+
` Path: ${mainRoot}\n` +
|
|
94
|
+
` From: ${fromRoot}\n\n` +
|
|
95
|
+
nextTurnNotice(mainRoot, currentTurnRoot));
|
|
67
96
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
97
|
+
const selected = await resolveWorktreeTarget({
|
|
98
|
+
target,
|
|
99
|
+
cwd: ctx?.cwd ?? mainRoot,
|
|
100
|
+
mainRoot,
|
|
101
|
+
sessionId,
|
|
102
|
+
currentWorkspace: fromWorkspace,
|
|
103
|
+
branchPrefix,
|
|
104
|
+
});
|
|
105
|
+
const workspace = toSessionWorkspace(selected, fromWorkspace);
|
|
106
|
+
persistSessionWorkspace(sessionManager, sessionId, workspace, ctx);
|
|
107
|
+
sessionManager.recordWorkspaceHandoff(sessionId, fromWorkspace, workspace);
|
|
108
|
+
const setupNote = selected.created
|
|
109
|
+
? await runSetupIfConfigured(selected.session.worktreePath, mainRoot, ctx)
|
|
110
|
+
: "";
|
|
111
|
+
const verb = selected.created ? "Worktree created and switched" : "Switched to worktree";
|
|
112
|
+
return (`${verb}:\n` +
|
|
113
|
+
` Path: ${workspace.worktree.path}\n` +
|
|
114
|
+
` Branch: ${workspace.worktree.branch}\n` +
|
|
115
|
+
` From: ${selected.from}\n` +
|
|
116
|
+
` Previous: ${fromRoot}\n\n` +
|
|
117
|
+
nextTurnNotice(workspace.root, currentTurnRoot) +
|
|
73
118
|
setupNote);
|
|
74
119
|
}
|
|
75
120
|
catch (err) {
|
|
76
|
-
return `Error
|
|
121
|
+
return `Error switching worktree: ${err.message}`;
|
|
77
122
|
}
|
|
78
123
|
}
|
|
79
124
|
/** Keep setup output from bloating the tool result — head+tail-ish trim. */
|
|
80
125
|
function truncate(s, max = 2000) {
|
|
81
126
|
return s.length <= max ? s : `${s.slice(0, max)}\n…(${s.length - max} more chars)`;
|
|
82
127
|
}
|
|
128
|
+
function persistSessionWorkspace(sessionManager, sessionId, workspace, ctx) {
|
|
129
|
+
sessionManager.setSessionWorkspace(sessionId, workspace);
|
|
130
|
+
ctx?.setSessionWorkspace?.(workspace);
|
|
131
|
+
}
|
|
132
|
+
function nextTurnNotice(nextRoot, currentTurnRoot) {
|
|
133
|
+
return (`Workspace switched to ${nextRoot}. This takes effect on the next turn - ` +
|
|
134
|
+
`file/shell/sandbox tools in the CURRENT turn still target ${currentTurnRoot}.`);
|
|
135
|
+
}
|
|
83
136
|
export const exitWorktreeToolDef = {
|
|
84
137
|
name: "ExitWorktree",
|
|
85
|
-
description: "
|
|
86
|
-
"Choose whether to keep
|
|
138
|
+
description: "Switch the current session back to the main working directory. " +
|
|
139
|
+
"Choose whether to keep the worktree, detach it while preserving the branch, " +
|
|
140
|
+
"or discard it entirely.",
|
|
87
141
|
inputSchema: {
|
|
88
142
|
type: "object",
|
|
89
143
|
properties: {
|
|
90
144
|
action: {
|
|
91
145
|
type: "string",
|
|
92
|
-
enum: ["keep", "discard"],
|
|
93
|
-
description: "'keep' preserves the worktree branch for later
|
|
94
|
-
"'
|
|
146
|
+
enum: ["keep", "detach", "discard"],
|
|
147
|
+
description: "'keep' preserves the worktree directory and branch for later. " +
|
|
148
|
+
"'detach' removes the directory but keeps the branch. " +
|
|
149
|
+
"'discard' removes the directory and its branch entirely. " +
|
|
150
|
+
"If omitted, a clean worktree is detached automatically; a dirty worktree requires keep/discard.",
|
|
95
151
|
},
|
|
96
152
|
},
|
|
97
|
-
required: [
|
|
153
|
+
required: [],
|
|
98
154
|
},
|
|
99
155
|
};
|
|
100
|
-
export async function exitWorktreeTool(args) {
|
|
101
|
-
|
|
156
|
+
export async function exitWorktreeTool(args, ctx) {
|
|
157
|
+
const resolved = sessionServices(ctx);
|
|
158
|
+
if (!resolved.ok)
|
|
159
|
+
return resolved.error;
|
|
160
|
+
const { sessionId, sessionManager } = resolved;
|
|
161
|
+
const workspace = sessionManager.getSessionWorkspace(sessionId);
|
|
162
|
+
if (!workspace || workspace.kind !== "worktree" || !workspace.worktree) {
|
|
102
163
|
return "Not currently in a worktree.";
|
|
103
164
|
}
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
165
|
+
const requested = args.action;
|
|
166
|
+
if (requested !== undefined &&
|
|
167
|
+
requested !== "keep" &&
|
|
168
|
+
requested !== "detach" &&
|
|
169
|
+
requested !== "discard") {
|
|
170
|
+
return `Error: unknown action "${requested}" (expected keep, detach, or discard).`;
|
|
171
|
+
}
|
|
172
|
+
const hasPendingChanges = existsSync(workspace.worktree.path) &&
|
|
173
|
+
(await worktreeHasUncommittedOrAheadChanges(workspace.worktree.path, workspace.worktree.baseRef));
|
|
174
|
+
const action = requested ?? (hasPendingChanges ? undefined : "detach");
|
|
175
|
+
if (!action) {
|
|
176
|
+
return (`Error: worktree has uncommitted changes or new commits. Choose action "keep" to preserve ` +
|
|
177
|
+
`the directory or "discard" to delete the worktree and branch.`);
|
|
178
|
+
}
|
|
179
|
+
if (action === "detach" && hasPendingChanges) {
|
|
180
|
+
return (`Error: detach would drop uncommitted changes or new commits. Choose action "keep" to preserve ` +
|
|
181
|
+
`the directory or "discard" to delete the worktree and branch.`);
|
|
182
|
+
}
|
|
183
|
+
const mainRoot = sessionManager.readCwd(sessionId) ?? workspace.root;
|
|
184
|
+
const currentTurnRoot = ctx?.cwd ?? workspace.root;
|
|
185
|
+
const branchPrefix = ctx?.engine?.readWorktreeBranchPrefix?.(mainRoot);
|
|
107
186
|
try {
|
|
187
|
+
let removal;
|
|
188
|
+
if (action === "discard" || action === "detach") {
|
|
189
|
+
const otherOwners = await otherSessionOwnersForWorktree(sessionManager, sessionId, mainRoot, workspace.worktree.path);
|
|
190
|
+
if (otherOwners.length > 0) {
|
|
191
|
+
const mainWorkspace = { root: mainRoot, kind: "main" };
|
|
192
|
+
persistSessionWorkspace(sessionManager, sessionId, mainWorkspace, ctx);
|
|
193
|
+
sessionManager.recordWorkspaceHandoff(sessionId, workspace, mainWorkspace);
|
|
194
|
+
return sharedWorktreeRemovalSkippedMessage(otherOwners, mainRoot, currentTurnRoot);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
108
197
|
if (action === "discard") {
|
|
109
|
-
removeWorktree(
|
|
110
|
-
|
|
198
|
+
removal = removeWorktree(workspace.worktree.path, true, { prefix: branchPrefix });
|
|
199
|
+
}
|
|
200
|
+
else if (action === "detach") {
|
|
201
|
+
removal = removeWorktree(workspace.worktree.path, false);
|
|
202
|
+
}
|
|
203
|
+
const mainWorkspace = { root: mainRoot, kind: "main" };
|
|
204
|
+
persistSessionWorkspace(sessionManager, sessionId, mainWorkspace, ctx);
|
|
205
|
+
sessionManager.recordWorkspaceHandoff(sessionId, workspace, mainWorkspace);
|
|
206
|
+
if (action === "keep") {
|
|
207
|
+
return (`Worktree preserved at ${workspace.worktree.path}. ` +
|
|
208
|
+
`Branch ${workspace.worktree.branch} preserved.\n` +
|
|
209
|
+
`Back to ${mainRoot} starting next turn.\n` +
|
|
210
|
+
nextTurnNotice(mainRoot, currentTurnRoot));
|
|
111
211
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
`
|
|
116
|
-
|
|
212
|
+
if (action === "discard") {
|
|
213
|
+
if (removal?.branchDeleted === false) {
|
|
214
|
+
const branch = removal.branch ?? workspace.worktree.branch;
|
|
215
|
+
return (`WARNING: worktree removed; branch ${branch} could not be deleted: ` +
|
|
216
|
+
`${removal.branchError ?? "unknown error"}\n` +
|
|
217
|
+
`Delete it manually with git branch -D ${branch}.\n` +
|
|
218
|
+
`Back to ${mainRoot} starting next turn.\n` +
|
|
219
|
+
nextTurnNotice(mainRoot, currentTurnRoot));
|
|
220
|
+
}
|
|
221
|
+
return (`Worktree removed and branch ${workspace.worktree.branch} deleted. ` +
|
|
222
|
+
`Back to ${mainRoot} starting next turn.\n` +
|
|
223
|
+
nextTurnNotice(mainRoot, currentTurnRoot));
|
|
117
224
|
}
|
|
225
|
+
return (`Worktree removed${requested ? "" : " (auto-detached clean worktree)"}. ` +
|
|
226
|
+
`Branch ${workspace.worktree.branch} preserved.\n` +
|
|
227
|
+
`To merge: git merge ${workspace.worktree.branch}\n` +
|
|
228
|
+
`Back to ${mainRoot} starting next turn.\n` +
|
|
229
|
+
nextTurnNotice(mainRoot, currentTurnRoot));
|
|
118
230
|
}
|
|
119
231
|
catch (err) {
|
|
120
232
|
return `Error exiting worktree: ${err.message}`;
|
|
121
233
|
}
|
|
122
234
|
}
|
|
235
|
+
function sessionServices(ctx) {
|
|
236
|
+
const sessionId = ctx?.sessionId;
|
|
237
|
+
if (!sessionId)
|
|
238
|
+
return { ok: false, error: "Error: worktree tools require a sessionId." };
|
|
239
|
+
const sessionManager = ctx?.engine?.getSessionManager?.();
|
|
240
|
+
if (!sessionManager) {
|
|
241
|
+
return { ok: false, error: "Error: worktree tools require a session manager." };
|
|
242
|
+
}
|
|
243
|
+
return { ok: true, sessionId, sessionManager };
|
|
244
|
+
}
|
|
245
|
+
async function otherSessionOwnersForWorktree(sessionManager, sessionId, mainRoot, worktreePath) {
|
|
246
|
+
const workspaceOwners = sessionManager
|
|
247
|
+
.list(Number.MAX_SAFE_INTEGER)
|
|
248
|
+
.map((session) => ({ sessionId: session.sessionId, workspace: session.workspace }))
|
|
249
|
+
.filter((owner) => owner.workspace !== undefined);
|
|
250
|
+
const entry = (await listWorktrees(mainRoot, {
|
|
251
|
+
currentSessionId: sessionId,
|
|
252
|
+
workspaceOwners,
|
|
253
|
+
})).find((worktree) => resolve(worktree.path) === resolve(worktreePath));
|
|
254
|
+
return (entry?.occupiedBySessionIds ?? []).filter((owner) => owner !== sessionId);
|
|
255
|
+
}
|
|
256
|
+
function sharedWorktreeRemovalSkippedMessage(otherOwners, mainRoot, currentTurnRoot) {
|
|
257
|
+
return (`Error: this worktree is also in use by session(s) ${otherOwners.join(", ")}; ` +
|
|
258
|
+
`switching to main, but removal has been skipped.\n` +
|
|
259
|
+
`Back to ${mainRoot} starting next turn.\n` +
|
|
260
|
+
nextTurnNotice(mainRoot, currentTurnRoot));
|
|
261
|
+
}
|
|
262
|
+
async function resolveWorktreeTarget(opts) {
|
|
263
|
+
const entries = await listWorktrees(opts.mainRoot);
|
|
264
|
+
const pathTarget = pathLike(opts.target) ? resolvePathTarget(opts.target, opts.cwd) : undefined;
|
|
265
|
+
const branchTarget = normalizeBranchName(opts.target);
|
|
266
|
+
const match = entries.find((entry) => {
|
|
267
|
+
if (pathTarget && resolve(entry.path) === pathTarget)
|
|
268
|
+
return true;
|
|
269
|
+
return entry.branch === branchTarget;
|
|
270
|
+
});
|
|
271
|
+
if (match) {
|
|
272
|
+
return {
|
|
273
|
+
created: false,
|
|
274
|
+
session: {
|
|
275
|
+
originalCwd: opts.mainRoot,
|
|
276
|
+
worktreePath: match.path,
|
|
277
|
+
worktreeName: match.path.split(/[\\/]/).pop() ?? match.branch,
|
|
278
|
+
worktreeBranch: match.branch,
|
|
279
|
+
originalBranch: await currentBranch(opts.mainRoot),
|
|
280
|
+
sessionId: opts.sessionId,
|
|
281
|
+
createdAt: Date.now(),
|
|
282
|
+
},
|
|
283
|
+
from: opts.currentWorkspace.root,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (pathTarget) {
|
|
287
|
+
throw new Error(`no existing worktree found at ${opts.target}`);
|
|
288
|
+
}
|
|
289
|
+
validateWorktreeSlug(opts.target);
|
|
290
|
+
const created = await createWorktree(opts.mainRoot, opts.target, opts.sessionId, {
|
|
291
|
+
prefix: opts.branchPrefix,
|
|
292
|
+
});
|
|
293
|
+
return { created: true, session: created, from: created.originalBranch ?? "HEAD" };
|
|
294
|
+
}
|
|
295
|
+
function toSessionWorkspace(selected, currentWorkspace) {
|
|
296
|
+
const previous = currentWorkspace.kind === "worktree" &&
|
|
297
|
+
currentWorkspace.worktree &&
|
|
298
|
+
resolve(currentWorkspace.worktree.path) === resolve(selected.session.worktreePath)
|
|
299
|
+
? currentWorkspace.worktree
|
|
300
|
+
: undefined;
|
|
301
|
+
return {
|
|
302
|
+
root: selected.session.worktreePath,
|
|
303
|
+
kind: "worktree",
|
|
304
|
+
worktree: {
|
|
305
|
+
path: selected.session.worktreePath,
|
|
306
|
+
branch: selected.session.worktreeBranch,
|
|
307
|
+
baseRef: previous?.baseRef ?? selected.session.originalBranch ?? "HEAD",
|
|
308
|
+
createdBy: "codeshell",
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
async function runSetupIfConfigured(worktreePath, mainRoot, ctx) {
|
|
313
|
+
const setupScripts = ctx?.engine?.readWorktreeSetupScripts(mainRoot);
|
|
314
|
+
const script = selectPlatformScript(setupScripts);
|
|
315
|
+
if (!script)
|
|
316
|
+
return "";
|
|
317
|
+
const setupSandbox = ctx?.engine?.resolveWorktreeSetupSandbox
|
|
318
|
+
? await ctx.engine.resolveWorktreeSetupSandbox(worktreePath)
|
|
319
|
+
: ctx?.sandbox;
|
|
320
|
+
const setupShellEnv = ctx?.engine?.readWorktreeSetupShellEnv
|
|
321
|
+
? ctx.engine.readWorktreeSetupShellEnv(worktreePath)
|
|
322
|
+
: ctx?.shellEnv;
|
|
323
|
+
const setup = await runWorktreeSetup(worktreePath, script, {
|
|
324
|
+
sandbox: setupSandbox,
|
|
325
|
+
shellEnv: setupShellEnv,
|
|
326
|
+
signal: ctx?.signal,
|
|
327
|
+
});
|
|
328
|
+
if (setup.ok) {
|
|
329
|
+
return `\n\nRan setup script (exit 0).${setup.output ? `\n${truncate(setup.output)}` : ""}`;
|
|
330
|
+
}
|
|
331
|
+
return (`\n\n⚠️ Setup script failed (exit ${setup.exitCode ?? "?"}) — continuing anyway. ` +
|
|
332
|
+
`You may need to run setup manually.${setup.output ? `\n${truncate(setup.output)}` : ""}`);
|
|
333
|
+
}
|
|
334
|
+
function stringArg(value) {
|
|
335
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
336
|
+
}
|
|
337
|
+
function pathLike(target) {
|
|
338
|
+
return (isAbsolute(target) || target.startsWith(".") || target.includes("/") || target.includes("\\"));
|
|
339
|
+
}
|
|
340
|
+
function resolvePathTarget(target, cwd) {
|
|
341
|
+
return resolve(cwd, target);
|
|
342
|
+
}
|
|
343
|
+
function normalizeBranchName(branch) {
|
|
344
|
+
return branch.replace(/^refs\/heads\//, "");
|
|
345
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Built-in Write file tool.
|
|
3
3
|
*/
|
|
4
4
|
import { writeFile, mkdir } from "node:fs/promises";
|
|
5
|
-
import { dirname } from "node:path";
|
|
5
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
6
6
|
import { fileCache } from "./file-cache.js";
|
|
7
7
|
export const writeToolDef = {
|
|
8
8
|
name: "Write",
|
|
@@ -18,12 +18,14 @@ export const writeToolDef = {
|
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
20
|
export async function writeTool(args, ctx) {
|
|
21
|
-
const
|
|
21
|
+
const rawPath = args.file_path;
|
|
22
22
|
const content = args.content;
|
|
23
|
-
if (!
|
|
23
|
+
if (!rawPath)
|
|
24
24
|
return "Error: file_path is required";
|
|
25
25
|
if (content === undefined)
|
|
26
26
|
return "Error: content is required";
|
|
27
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
28
|
+
const filePath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
27
29
|
try {
|
|
28
30
|
await mkdir(dirname(filePath), { recursive: true });
|
|
29
31
|
await writeFile(filePath, content, "utf-8");
|