wave-agent-sdk 1.0.9 → 1.1.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.d.ts +9 -6
- package/dist/agent.js +35 -33
- package/dist/builtin/index.d.ts +1 -0
- package/dist/builtin/index.js +20 -0
- package/dist/builtin/plugins.d.ts +1 -0
- package/dist/builtin/plugins.js +225 -0
- package/dist/builtin/skills/artifact.d.ts +1 -0
- package/dist/builtin/skills/artifact.js +18 -0
- package/dist/builtin/skills/code-review.d.ts +1 -0
- package/{builtin/skills/code-review/SKILL.md → dist/builtin/skills/code-review.js} +23 -19
- package/dist/builtin/skills/deep-research.d.ts +1 -0
- package/{builtin/skills/deep-research/SKILL.md → dist/builtin/skills/deep-research.js} +18 -14
- package/dist/builtin/skills/init.d.ts +1 -0
- package/{builtin/skills/init/SKILL.md → dist/builtin/skills/init.js} +6 -3
- package/dist/builtin/skills/loop.d.ts +1 -0
- package/dist/builtin/skills/loop.js +83 -0
- package/dist/builtin/skills/settings.d.ts +1 -0
- package/dist/builtin/skills/settings.js +1249 -0
- package/dist/builtin/skills/simplify.d.ts +1 -0
- package/{builtin/skills/simplify/SKILL.md → dist/builtin/skills/simplify.js} +7 -3
- package/dist/builtin/subagents.d.ts +1 -0
- package/dist/builtin/subagents.js +164 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/managers/aiManager.d.ts +10 -0
- package/dist/managers/aiManager.js +49 -5
- package/dist/managers/mcpManager.js +1 -1
- package/dist/managers/messageManager.d.ts +6 -0
- package/dist/managers/messageManager.js +33 -0
- package/dist/managers/subagentManager.d.ts +9 -0
- package/dist/managers/subagentManager.js +62 -3
- package/dist/services/hook.js +41 -7
- package/dist/services/initializationService.js +0 -21
- package/dist/services/jsonlHandler.d.ts +37 -2
- package/dist/services/jsonlHandler.js +55 -6
- package/dist/services/session.d.ts +35 -4
- package/dist/services/session.js +233 -36
- package/dist/services/worktreeHooks.d.ts +45 -0
- package/dist/services/worktreeHooks.js +133 -0
- package/dist/tools/agentTool.js +36 -1
- package/dist/tools/bashTool.js +120 -57
- package/dist/tools/editTool.js +1 -0
- package/dist/tools/enterWorktreeTool.d.ts +1 -1
- package/dist/tools/enterWorktreeTool.js +44 -31
- package/dist/tools/exitWorktreeTool.js +21 -26
- package/dist/tools/readTool.js +6 -3
- package/dist/tools/types.d.ts +1 -0
- package/dist/tools/writeTool.js +1 -0
- package/dist/types/agent.d.ts +5 -0
- package/dist/types/hooks.d.ts +3 -2
- package/dist/types/messaging.d.ts +1 -0
- package/dist/types/skills.d.ts +0 -1
- package/dist/types/skills.js +0 -1
- package/dist/utils/asyncWorkRegistry.d.ts +32 -0
- package/dist/utils/asyncWorkRegistry.js +81 -0
- package/dist/utils/builtinEmbed.d.ts +21 -0
- package/dist/utils/builtinEmbed.js +53 -0
- package/dist/utils/configPaths.d.ts +0 -1
- package/dist/utils/configPaths.js +5 -26
- package/dist/utils/containerSetup.js +5 -0
- package/dist/utils/convertMessagesForAPI.js +21 -2
- package/dist/utils/messageOperations.d.ts +1 -0
- package/dist/utils/skillParser.js +3 -6
- package/dist/utils/toolImagePersistence.d.ts +25 -0
- package/dist/utils/toolImagePersistence.js +56 -0
- package/dist/utils/windowsPaths.d.ts +28 -0
- package/dist/utils/windowsPaths.js +47 -0
- package/dist/utils/worktreeSession.d.ts +6 -0
- package/dist/utils/worktreeUtils.d.ts +7 -0
- package/dist/utils/worktreeUtils.js +88 -40
- package/package.json +5 -5
- package/builtin/plugins/sdd/.wave-plugin/plugin.json +0 -8
- package/builtin/plugins/sdd/hooks/hooks.json +0 -14
- package/builtin/plugins/sdd/scripts/session-start.js +0 -24
- package/builtin/plugins/sdd/scripts/spec-count.js +0 -77
- package/builtin/plugins/sdd/skills/specify/SKILL.md +0 -47
- package/builtin/plugins/sdd/skills/specify/templates/spec-template.md +0 -47
- package/builtin/skills/artifact/SKILL.md +0 -14
- package/builtin/skills/loop/SKILL.md +0 -79
- package/builtin/skills/settings/ENV.md +0 -78
- package/builtin/skills/settings/HOOKS.md +0 -227
- package/builtin/skills/settings/MCP.md +0 -137
- package/builtin/skills/settings/MEMORY.md +0 -76
- package/builtin/skills/settings/MODELS.md +0 -119
- package/builtin/skills/settings/PERMISSIONS.md +0 -88
- package/builtin/skills/settings/PLUGINS.md +0 -171
- package/builtin/skills/settings/SKILL.md +0 -132
- package/builtin/skills/settings/SKILLS.md +0 -107
- package/builtin/skills/settings/SUBAGENTS.md +0 -77
- package/builtin/subagents/bash.md +0 -19
- package/builtin/subagents/explore.md +0 -43
- package/builtin/subagents/general-purpose.md +0 -20
- package/builtin/subagents/plan.md +0 -56
- package/builtin/subagents/vision.md +0 -18
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builtin content embedding.
|
|
3
|
+
*
|
|
4
|
+
* Builtin skills/subagents/plugins ship as TS source (src/builtin/*.ts) and are
|
|
5
|
+
* compiled into the bundle. At runtime they are lazily materialized to a
|
|
6
|
+
* content-hashed cache directory under the OS temp dir, so consumers that
|
|
7
|
+
* readdirSync real directories keep working unchanged — including inside
|
|
8
|
+
* esbuild bundles that never ship a `builtin/` dir on disk.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from "crypto";
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
12
|
+
import { tmpdir } from "os";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
import { BUILTIN_CONTENT } from "../builtin/index.js";
|
|
15
|
+
const BUILTIN_CACHE_ROOT = "wave-builtin";
|
|
16
|
+
const BUILTIN_CACHE_COMPLETE = ".wave-builtin-complete";
|
|
17
|
+
/**
|
|
18
|
+
* Content-hash key: sha256 of the sorted `rel\0content` entries, truncated.
|
|
19
|
+
* Any change to builtin content yields a new key, auto-invalidating stale caches.
|
|
20
|
+
*/
|
|
21
|
+
export function getBuiltinCacheKey() {
|
|
22
|
+
const entries = Object.keys(BUILTIN_CONTENT)
|
|
23
|
+
.sort()
|
|
24
|
+
.map((rel) => `${rel}\0${BUILTIN_CONTENT[rel]}`);
|
|
25
|
+
return createHash("sha256")
|
|
26
|
+
.update(entries.join("\0"))
|
|
27
|
+
.digest("hex")
|
|
28
|
+
.slice(0, 16);
|
|
29
|
+
}
|
|
30
|
+
let _builtinDir;
|
|
31
|
+
/**
|
|
32
|
+
* Materialize builtin content to `{tmpdir}/wave-builtin/{contentHash}/` and
|
|
33
|
+
* return the directory. Memoized per process; idempotent across processes via
|
|
34
|
+
* the `.wave-builtin-complete` marker written last (a torn write leaves no
|
|
35
|
+
* marker, so the next run rewrites).
|
|
36
|
+
*/
|
|
37
|
+
export function ensureBuiltinMaterialized() {
|
|
38
|
+
if (_builtinDir)
|
|
39
|
+
return _builtinDir;
|
|
40
|
+
const dir = join(tmpdir(), BUILTIN_CACHE_ROOT, getBuiltinCacheKey());
|
|
41
|
+
const marker = join(dir, BUILTIN_CACHE_COMPLETE);
|
|
42
|
+
if (!existsSync(marker)) {
|
|
43
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
44
|
+
for (const [rel, content] of Object.entries(BUILTIN_CONTENT)) {
|
|
45
|
+
const filePath = join(dir, rel);
|
|
46
|
+
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
47
|
+
writeFileSync(filePath, content);
|
|
48
|
+
}
|
|
49
|
+
writeFileSync(marker, "");
|
|
50
|
+
}
|
|
51
|
+
_builtinDir = dir;
|
|
52
|
+
return dir;
|
|
53
|
+
}
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* - Local configs: {workdir}/.wave/settings.local.json > {workdir}/.wave/settings.json
|
|
10
10
|
* - Project configs override user configs (existing behavior)
|
|
11
11
|
*/
|
|
12
|
-
export declare function getPackageRoot(): string;
|
|
13
12
|
/**
|
|
14
13
|
* Get the builtin skills directory path
|
|
15
14
|
*/
|
|
@@ -9,48 +9,27 @@
|
|
|
9
9
|
* - Local configs: {workdir}/.wave/settings.local.json > {workdir}/.wave/settings.json
|
|
10
10
|
* - Project configs override user configs (existing behavior)
|
|
11
11
|
*/
|
|
12
|
-
import { join
|
|
12
|
+
import { join } from "path";
|
|
13
13
|
import { homedir } from "os";
|
|
14
14
|
import { existsSync } from "fs";
|
|
15
|
-
import {
|
|
16
|
-
import { findUpSync } from "find-up";
|
|
17
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
-
const __dirname = dirname(__filename);
|
|
19
|
-
/**
|
|
20
|
-
* Resolve the package root directory by finding the nearest package.json.
|
|
21
|
-
* Works correctly even when bundled (e.g. esbuild into a VS Code extension),
|
|
22
|
-
* as long as vendor/ and builtin/ remain alongside package.json on disk.
|
|
23
|
-
*/
|
|
24
|
-
let _packageRoot;
|
|
25
|
-
export function getPackageRoot() {
|
|
26
|
-
if (_packageRoot)
|
|
27
|
-
return _packageRoot;
|
|
28
|
-
const pkgPath = findUpSync("package.json", { cwd: __dirname });
|
|
29
|
-
if (pkgPath) {
|
|
30
|
-
_packageRoot = dirname(pkgPath);
|
|
31
|
-
return _packageRoot;
|
|
32
|
-
}
|
|
33
|
-
// Fallback: relative to this file (works during development)
|
|
34
|
-
_packageRoot = join(__dirname, "..", "..");
|
|
35
|
-
return _packageRoot;
|
|
36
|
-
}
|
|
15
|
+
import { ensureBuiltinMaterialized } from "./builtinEmbed.js";
|
|
37
16
|
/**
|
|
38
17
|
* Get the builtin skills directory path
|
|
39
18
|
*/
|
|
40
19
|
export function getBuiltinSkillsDir() {
|
|
41
|
-
return join(
|
|
20
|
+
return join(ensureBuiltinMaterialized(), "skills");
|
|
42
21
|
}
|
|
43
22
|
/**
|
|
44
23
|
* Get the builtin subagents directory path
|
|
45
24
|
*/
|
|
46
25
|
export function getBuiltinSubagentsDir() {
|
|
47
|
-
return join(
|
|
26
|
+
return join(ensureBuiltinMaterialized(), "subagents");
|
|
48
27
|
}
|
|
49
28
|
/**
|
|
50
29
|
* Get the builtin plugins directory path
|
|
51
30
|
*/
|
|
52
31
|
export function getBuiltinPluginsDir() {
|
|
53
|
-
return join(
|
|
32
|
+
return join(ensureBuiltinMaterialized(), "plugins");
|
|
54
33
|
}
|
|
55
34
|
/**
|
|
56
35
|
* Get the user-specific configuration file path (legacy function)
|
|
@@ -27,6 +27,7 @@ import { MemoryService } from "../services/memory.js";
|
|
|
27
27
|
import { AutoMemoryService } from "../services/autoMemoryService.js";
|
|
28
28
|
import { USER_MEMORY_FILE } from "./constants.js";
|
|
29
29
|
import { getGitMainRepoRoot } from "./gitUtils.js";
|
|
30
|
+
import { AsyncWorkRegistry } from "./asyncWorkRegistry.js";
|
|
30
31
|
import { logger } from "./globalLogger.js";
|
|
31
32
|
import { authService } from "../services/authService.js";
|
|
32
33
|
import { remoteSettingsService } from "../services/remoteSettingsService.js";
|
|
@@ -45,6 +46,10 @@ export function setupAgentContainer(setupOptions) {
|
|
|
45
46
|
container.register("WorktreeSession", null);
|
|
46
47
|
const messageQueue = new MessageQueue();
|
|
47
48
|
container.register("MessageQueue", messageQueue);
|
|
49
|
+
// Registry of live async work that must drain before the agent is destroyed
|
|
50
|
+
// (dispatch, background subagents, fork subagents — the fire-and-forget work
|
|
51
|
+
// sites). destroy() awaits its drain() so no async work can outlive the agent.
|
|
52
|
+
container.register("AsyncWorkRegistry", new AsyncWorkRegistry());
|
|
48
53
|
const foregroundTaskManager = new ForegroundTaskManager(container);
|
|
49
54
|
container.register("ForegroundTaskManager", foregroundTaskManager);
|
|
50
55
|
container.register("ConfigurationService", configurationService);
|
|
@@ -102,10 +102,29 @@ export function convertMessagesForAPI(messages, options) {
|
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
else {
|
|
105
|
-
// Non-vision model: replace images with a text placeholder
|
|
105
|
+
// Non-vision model: replace images with a text placeholder and
|
|
106
|
+
// append [Image source: <path>] metadata for MCP images that
|
|
107
|
+
// were persisted to temp files, so the main model can delegate
|
|
108
|
+
// recognition to a vision subagent (which reads the path with
|
|
109
|
+
// the Read tool). Images without a persisted path (e.g. legacy
|
|
110
|
+
// history) fall back to the placeholder only.
|
|
111
|
+
const contentParts = [
|
|
112
|
+
{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: "[Tool returned an image, but the current model does not support image recognition]",
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
toolBlock.images.forEach((image) => {
|
|
118
|
+
if (image.path) {
|
|
119
|
+
contentParts.push({
|
|
120
|
+
type: "text",
|
|
121
|
+
text: `[Image source: ${image.path}]`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
106
125
|
imageUserMessages.push({
|
|
107
126
|
role: "user",
|
|
108
|
-
content:
|
|
127
|
+
content: contentParts,
|
|
109
128
|
});
|
|
110
129
|
}
|
|
111
130
|
}
|
|
@@ -147,7 +147,6 @@ export function validateSkillMetadata(metadata) {
|
|
|
147
147
|
// Import SKILL_DEFAULTS dynamically to avoid circular imports
|
|
148
148
|
const NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
149
149
|
const MAX_NAME_LENGTH = 64;
|
|
150
|
-
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
151
150
|
const MIN_DESCRIPTION_LENGTH = 1;
|
|
152
151
|
// Validate name
|
|
153
152
|
if (!metadata.name) {
|
|
@@ -161,7 +160,8 @@ export function validateSkillMetadata(metadata) {
|
|
|
161
160
|
errors.push("Skill name must contain only lowercase letters, numbers, and hyphens");
|
|
162
161
|
}
|
|
163
162
|
}
|
|
164
|
-
// Validate description
|
|
163
|
+
// Validate description (no length limit, aligned with Claude Code which
|
|
164
|
+
// truncates long descriptions at render time instead of rejecting the skill)
|
|
165
165
|
if (!metadata.description) {
|
|
166
166
|
errors.push("Skill description is required");
|
|
167
167
|
}
|
|
@@ -169,9 +169,6 @@ export function validateSkillMetadata(metadata) {
|
|
|
169
169
|
if (metadata.description.length < MIN_DESCRIPTION_LENGTH) {
|
|
170
170
|
errors.push(`Skill description must be at least ${MIN_DESCRIPTION_LENGTH} character`);
|
|
171
171
|
}
|
|
172
|
-
if (metadata.description.length > MAX_DESCRIPTION_LENGTH) {
|
|
173
|
-
errors.push(`Skill description must be ${MAX_DESCRIPTION_LENGTH} characters or less`);
|
|
174
|
-
}
|
|
175
172
|
}
|
|
176
173
|
return errors;
|
|
177
174
|
}
|
|
@@ -194,7 +191,7 @@ export function formatSkillError(skillPath, errors) {
|
|
|
194
191
|
" 1. Ensure SKILL.md has valid YAML frontmatter (---...---)",
|
|
195
192
|
" 2. Include required fields: name and description",
|
|
196
193
|
" 3. Use lowercase letters, numbers, and hyphens only for name",
|
|
197
|
-
" 4. Keep name under 64 characters
|
|
194
|
+
" 4. Keep name under 64 characters",
|
|
198
195
|
].join("\n");
|
|
199
196
|
return `${header}\n${errorList}\n\n${suggestions}`;
|
|
200
197
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for MCP tool-returned images.
|
|
3
|
+
*
|
|
4
|
+
* MCP tools can return image content blocks as in-memory base64. When the
|
|
5
|
+
* agent's model does not support vision, the image is written to a temp file
|
|
6
|
+
* so convertMessagesForAPI can attach `[Image source: <path>]` metadata and
|
|
7
|
+
* the main model can delegate recognition to the vision subagent (which reads
|
|
8
|
+
* the path with the Read tool). Vision-capable models keep the inline base64
|
|
9
|
+
* and never write to disk.
|
|
10
|
+
*/
|
|
11
|
+
export interface PersistedToolImage {
|
|
12
|
+
data: string;
|
|
13
|
+
mediaType?: string;
|
|
14
|
+
path?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Persist base64 images to temp files under /tmp/wave-mcp-images/.
|
|
18
|
+
* Uses the OS tmpdir for simplicity and automatic OS cleanup (same convention
|
|
19
|
+
* as /tmp/wave-tool-results/). Returns each image unchanged (no path) when the
|
|
20
|
+
* write fails, so the caller degrades to the placeholder-only behavior.
|
|
21
|
+
*/
|
|
22
|
+
export declare function persistToolImages(images: Array<{
|
|
23
|
+
data: string;
|
|
24
|
+
mediaType?: string;
|
|
25
|
+
}>): PersistedToolImage[];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for MCP tool-returned images.
|
|
3
|
+
*
|
|
4
|
+
* MCP tools can return image content blocks as in-memory base64. When the
|
|
5
|
+
* agent's model does not support vision, the image is written to a temp file
|
|
6
|
+
* so convertMessagesForAPI can attach `[Image source: <path>]` metadata and
|
|
7
|
+
* the main model can delegate recognition to the vision subagent (which reads
|
|
8
|
+
* the path with the Read tool). Vision-capable models keep the inline base64
|
|
9
|
+
* and never write to disk.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
import * as os from "os";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import { logger } from "./globalLogger.js";
|
|
15
|
+
const MCP_IMAGES_DIR = path.join(os.tmpdir(), "wave-mcp-images");
|
|
16
|
+
/** Map MCP mimeType to a file extension. Unknown types fall back to .png. */
|
|
17
|
+
const MIME_TO_EXT = {
|
|
18
|
+
"image/png": ".png",
|
|
19
|
+
"image/jpeg": ".jpg",
|
|
20
|
+
"image/webp": ".webp",
|
|
21
|
+
"image/gif": ".gif",
|
|
22
|
+
"image/bmp": ".bmp",
|
|
23
|
+
"image/svg+xml": ".svg",
|
|
24
|
+
"image/avif": ".avif",
|
|
25
|
+
"image/x-icon": ".ico",
|
|
26
|
+
"image/tiff": ".tiff",
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Persist base64 images to temp files under /tmp/wave-mcp-images/.
|
|
30
|
+
* Uses the OS tmpdir for simplicity and automatic OS cleanup (same convention
|
|
31
|
+
* as /tmp/wave-tool-results/). Returns each image unchanged (no path) when the
|
|
32
|
+
* write fails, so the caller degrades to the placeholder-only behavior.
|
|
33
|
+
*/
|
|
34
|
+
export function persistToolImages(images) {
|
|
35
|
+
return images.map((image) => {
|
|
36
|
+
try {
|
|
37
|
+
fs.mkdirSync(MCP_IMAGES_DIR, { recursive: true });
|
|
38
|
+
// Strip a data: URL prefix if present (MCP spec carries raw base64, but
|
|
39
|
+
// be defensive about dataURL-formatted data).
|
|
40
|
+
let data = image.data;
|
|
41
|
+
if (data.startsWith("data:")) {
|
|
42
|
+
const commaIndex = data.indexOf(",");
|
|
43
|
+
data = commaIndex >= 0 ? data.slice(commaIndex + 1) : data;
|
|
44
|
+
}
|
|
45
|
+
const ext = MIME_TO_EXT[image.mediaType || ""] || ".png";
|
|
46
|
+
const id = `${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
47
|
+
const filePath = path.join(MCP_IMAGES_DIR, `mcp-image_${id}${ext}`);
|
|
48
|
+
fs.writeFileSync(filePath, Buffer.from(data, "base64"));
|
|
49
|
+
return { ...image, path: filePath };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
logger?.error("Failed to persist MCP tool image:", error);
|
|
53
|
+
return image;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows path conversion utilities for Git Bash (POSIX shell) execution.
|
|
3
|
+
*
|
|
4
|
+
* Hook commands on Windows are executed via Git Bash instead of cmd.exe
|
|
5
|
+
* (cmd.exe does not strip quotes from arguments after the first token, so
|
|
6
|
+
* `node "C:\path\script.js"` silently fails — see issue #1773). Git Bash
|
|
7
|
+
* cannot resolve Windows paths like `C:\Users\foo`: backslashes are treated
|
|
8
|
+
* as escape characters and the drive letter breaks POSIX semantics. These
|
|
9
|
+
* helpers convert Windows paths to the POSIX form Git Bash expects
|
|
10
|
+
* (`C:\Users\foo` → `/c/Users/foo`), matching Claude Code's
|
|
11
|
+
* `windowsPathToPosixPath`.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Convert a single Windows path to POSIX form for Git Bash:
|
|
15
|
+
* - `C:\Users\foo` → `/c/Users/foo`
|
|
16
|
+
* - `C:/Users/foo` → `/c/Users/foo`
|
|
17
|
+
* - `\\server\share` → `//server/share` (UNC preserved)
|
|
18
|
+
* - Already POSIX or relative paths are returned with slashes flipped.
|
|
19
|
+
*/
|
|
20
|
+
export declare function windowsPathToPosixPath(p: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Convert every Windows absolute path inside a shell command string to POSIX
|
|
23
|
+
* form so Git Bash can parse it. Handles both quoted paths (which may contain
|
|
24
|
+
* spaces, e.g. `node "C:\Program Files\script.js"`) and bare paths
|
|
25
|
+
* (`cd C:\path\x`). Drive letters preceded by another alphanumeric character
|
|
26
|
+
* (e.g. URLs like `http://`) are left untouched.
|
|
27
|
+
*/
|
|
28
|
+
export declare function toPosixCommand(command: string): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows path conversion utilities for Git Bash (POSIX shell) execution.
|
|
3
|
+
*
|
|
4
|
+
* Hook commands on Windows are executed via Git Bash instead of cmd.exe
|
|
5
|
+
* (cmd.exe does not strip quotes from arguments after the first token, so
|
|
6
|
+
* `node "C:\path\script.js"` silently fails — see issue #1773). Git Bash
|
|
7
|
+
* cannot resolve Windows paths like `C:\Users\foo`: backslashes are treated
|
|
8
|
+
* as escape characters and the drive letter breaks POSIX semantics. These
|
|
9
|
+
* helpers convert Windows paths to the POSIX form Git Bash expects
|
|
10
|
+
* (`C:\Users\foo` → `/c/Users/foo`), matching Claude Code's
|
|
11
|
+
* `windowsPathToPosixPath`.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Convert a single Windows path to POSIX form for Git Bash:
|
|
15
|
+
* - `C:\Users\foo` → `/c/Users/foo`
|
|
16
|
+
* - `C:/Users/foo` → `/c/Users/foo`
|
|
17
|
+
* - `\\server\share` → `//server/share` (UNC preserved)
|
|
18
|
+
* - Already POSIX or relative paths are returned with slashes flipped.
|
|
19
|
+
*/
|
|
20
|
+
export function windowsPathToPosixPath(p) {
|
|
21
|
+
// UNC paths: \\server\share -> //server/share
|
|
22
|
+
if (p.startsWith("\\\\")) {
|
|
23
|
+
return p.replace(/\\/g, "/");
|
|
24
|
+
}
|
|
25
|
+
// Drive letter paths: C:\Users\foo -> /c/Users/foo
|
|
26
|
+
const match = p.match(/^([A-Za-z]):[\\/]/);
|
|
27
|
+
if (match) {
|
|
28
|
+
const driveLetter = match[1].toLowerCase();
|
|
29
|
+
return `/${driveLetter}${p.slice(2).replace(/\\/g, "/")}`;
|
|
30
|
+
}
|
|
31
|
+
// Already POSIX or relative — just flip slashes
|
|
32
|
+
return p.replace(/\\/g, "/");
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Convert every Windows absolute path inside a shell command string to POSIX
|
|
36
|
+
* form so Git Bash can parse it. Handles both quoted paths (which may contain
|
|
37
|
+
* spaces, e.g. `node "C:\Program Files\script.js"`) and bare paths
|
|
38
|
+
* (`cd C:\path\x`). Drive letters preceded by another alphanumeric character
|
|
39
|
+
* (e.g. URLs like `http://`) are left untouched.
|
|
40
|
+
*/
|
|
41
|
+
export function toPosixCommand(command) {
|
|
42
|
+
// Quoted paths first (may contain spaces) — keep the surrounding quotes.
|
|
43
|
+
let converted = command.replace(/"([A-Za-z]):[\\/][^"]*"/g, (quoted) => `"${windowsPathToPosixPath(quoted.slice(1, -1))}"`);
|
|
44
|
+
// Bare (unquoted) paths.
|
|
45
|
+
converted = converted.replace(/(?<![A-Za-z0-9])([A-Za-z]):[\\/][^\s"']*/g, (match) => windowsPathToPosixPath(match));
|
|
46
|
+
return converted;
|
|
47
|
+
}
|
|
@@ -22,4 +22,10 @@ export interface WorktreeSession {
|
|
|
22
22
|
repoRoot: string;
|
|
23
23
|
/** The HEAD commit of the original branch at worktree creation time */
|
|
24
24
|
originalHeadCommit?: string;
|
|
25
|
+
/**
|
|
26
|
+
* True when this worktree was created by a WorktreeCreate hook (not by git).
|
|
27
|
+
* Deletion delegates to the WorktreeRemove hook; wave never runs
|
|
28
|
+
* `git worktree remove` for hook-based worktrees (aligned with Claude Code).
|
|
29
|
+
*/
|
|
30
|
+
hookBased?: boolean;
|
|
25
31
|
}
|
|
@@ -41,6 +41,13 @@ export declare function createWorktree(name: string, cwd: string, options?: {
|
|
|
41
41
|
export declare function performPostCreationSetup(worktreePath: string, repoRoot: string): Promise<void>;
|
|
42
42
|
/**
|
|
43
43
|
* Remove a git worktree and its branch.
|
|
44
|
+
*
|
|
45
|
+
* Removal is best-effort: `git worktree remove --force` deletes the worktree
|
|
46
|
+
* metadata before the working directory, and on Windows its recursive deletion
|
|
47
|
+
* is MAX_PATH-limited — deep paths (e.g. node_modules) can fail with "Filename
|
|
48
|
+
* too long", leaving an orphan directory. When git fails we fall back to
|
|
49
|
+
* fs.rmSync with an extended-length path (bypasses MAX_PATH) and prune stale
|
|
50
|
+
* metadata. Failures are logged but never block branch deletion.
|
|
44
51
|
*/
|
|
45
52
|
export declare function removeWorktree(info: WorktreeInfo): void;
|
|
46
53
|
/**
|
|
@@ -498,32 +498,77 @@ export async function performPostCreationSetup(worktreePath, repoRoot) {
|
|
|
498
498
|
await copyLocalSettingsToWorktree(repoRoot, worktreePath);
|
|
499
499
|
await copyWorktreeIncludeFiles(repoRoot, worktreePath);
|
|
500
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* On Windows, prefix an absolute path with the extended-length marker (`\\?\`)
|
|
503
|
+
* so recursive removal bypasses the 260-char MAX_PATH limit. POSIX paths are
|
|
504
|
+
* returned unchanged.
|
|
505
|
+
*/
|
|
506
|
+
function toExtendedLengthPath(worktreePath) {
|
|
507
|
+
if (process.platform !== "win32") {
|
|
508
|
+
return worktreePath;
|
|
509
|
+
}
|
|
510
|
+
const absolute = path.win32.resolve(worktreePath);
|
|
511
|
+
if (absolute.startsWith("\\\\?\\")) {
|
|
512
|
+
return absolute;
|
|
513
|
+
}
|
|
514
|
+
if (absolute.startsWith("\\\\")) {
|
|
515
|
+
// UNC path: \\server\share -> \\?\UNC\server\share
|
|
516
|
+
return `\\\\?\\UNC\\${absolute.slice(2)}`;
|
|
517
|
+
}
|
|
518
|
+
return `\\\\?\\${absolute}`;
|
|
519
|
+
}
|
|
501
520
|
/**
|
|
502
521
|
* Remove a git worktree and its branch.
|
|
522
|
+
*
|
|
523
|
+
* Removal is best-effort: `git worktree remove --force` deletes the worktree
|
|
524
|
+
* metadata before the working directory, and on Windows its recursive deletion
|
|
525
|
+
* is MAX_PATH-limited — deep paths (e.g. node_modules) can fail with "Filename
|
|
526
|
+
* too long", leaving an orphan directory. When git fails we fall back to
|
|
527
|
+
* fs.rmSync with an extended-length path (bypasses MAX_PATH) and prune stale
|
|
528
|
+
* metadata. Failures are logged but never block branch deletion.
|
|
503
529
|
*/
|
|
504
530
|
export function removeWorktree(info) {
|
|
505
531
|
const repoRoot = info.repoRoot;
|
|
532
|
+
// Get current branch in worktree before removing
|
|
533
|
+
let currentBranch;
|
|
534
|
+
try {
|
|
535
|
+
currentBranch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
536
|
+
cwd: info.path,
|
|
537
|
+
encoding: "utf8",
|
|
538
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
539
|
+
}).trim();
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
// Ignore errors
|
|
543
|
+
}
|
|
544
|
+
// Remove worktree
|
|
506
545
|
try {
|
|
507
|
-
// Get current branch in worktree before removing
|
|
508
|
-
let currentBranch;
|
|
509
|
-
try {
|
|
510
|
-
currentBranch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
511
|
-
cwd: info.path,
|
|
512
|
-
encoding: "utf8",
|
|
513
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
514
|
-
}).trim();
|
|
515
|
-
}
|
|
516
|
-
catch {
|
|
517
|
-
// Ignore errors
|
|
518
|
-
}
|
|
519
|
-
// Remove worktree
|
|
520
546
|
execFileSync("git", ["worktree", "remove", "--force", info.path], {
|
|
521
547
|
cwd: repoRoot,
|
|
522
548
|
stdio: ["ignore", "pipe", "pipe"],
|
|
523
549
|
});
|
|
524
|
-
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
logger.warn("git worktree remove failed, falling back to fs.rmSync:", {
|
|
553
|
+
error: error instanceof Error ? error.message : String(error),
|
|
554
|
+
worktreePath: info.path,
|
|
555
|
+
});
|
|
556
|
+
try {
|
|
557
|
+
fs.rmSync(toExtendedLengthPath(info.path), {
|
|
558
|
+
recursive: true,
|
|
559
|
+
force: true,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
catch (rmError) {
|
|
563
|
+
logger.error("Failed to remove worktree or branch:", {
|
|
564
|
+
error: rmError instanceof Error ? rmError.message : String(rmError),
|
|
565
|
+
worktreePath: info.path,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
// git removes worktree metadata before the working directory; prune any
|
|
569
|
+
// leftovers in case git failed before deleting them.
|
|
525
570
|
try {
|
|
526
|
-
execFileSync("git", ["
|
|
571
|
+
execFileSync("git", ["worktree", "prune"], {
|
|
527
572
|
cwd: repoRoot,
|
|
528
573
|
stdio: ["ignore", "pipe", "pipe"],
|
|
529
574
|
});
|
|
@@ -531,33 +576,36 @@ export function removeWorktree(info) {
|
|
|
531
576
|
catch {
|
|
532
577
|
// Ignore errors
|
|
533
578
|
}
|
|
534
|
-
// Delete current branch if different and not protected
|
|
535
|
-
if (currentBranch &&
|
|
536
|
-
currentBranch !== info.branch &&
|
|
537
|
-
currentBranch !== "HEAD") {
|
|
538
|
-
const defaultRemoteBranch = getDefaultRemoteBranch(repoRoot);
|
|
539
|
-
const defaultBranchName = defaultRemoteBranch.split("/").pop();
|
|
540
|
-
if (currentBranch !== defaultBranchName &&
|
|
541
|
-
currentBranch !== "main" &&
|
|
542
|
-
currentBranch !== "master") {
|
|
543
|
-
try {
|
|
544
|
-
execFileSync("git", ["branch", "-D", currentBranch], {
|
|
545
|
-
cwd: repoRoot,
|
|
546
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
catch {
|
|
550
|
-
// Ignore errors
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
579
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
580
|
+
// Delete worktree branch
|
|
581
|
+
try {
|
|
582
|
+
execFileSync("git", ["branch", "-D", info.branch], {
|
|
583
|
+
cwd: repoRoot,
|
|
584
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
559
585
|
});
|
|
560
|
-
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
// Ignore errors
|
|
589
|
+
}
|
|
590
|
+
// Delete current branch if different and not protected
|
|
591
|
+
if (currentBranch &&
|
|
592
|
+
currentBranch !== info.branch &&
|
|
593
|
+
currentBranch !== "HEAD") {
|
|
594
|
+
const defaultRemoteBranch = getDefaultRemoteBranch(repoRoot);
|
|
595
|
+
const defaultBranchName = defaultRemoteBranch.split("/").pop();
|
|
596
|
+
if (currentBranch !== defaultBranchName &&
|
|
597
|
+
currentBranch !== "main" &&
|
|
598
|
+
currentBranch !== "master") {
|
|
599
|
+
try {
|
|
600
|
+
execFileSync("git", ["branch", "-D", currentBranch], {
|
|
601
|
+
cwd: repoRoot,
|
|
602
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
// Ignore errors
|
|
607
|
+
}
|
|
608
|
+
}
|
|
561
609
|
}
|
|
562
610
|
}
|
|
563
611
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-agent-sdk",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "SDK for building AI-powered development tools and agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -36,7 +36,6 @@
|
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
"dist",
|
|
39
|
-
"builtin",
|
|
40
39
|
"README.md"
|
|
41
40
|
],
|
|
42
41
|
"dependencies": {
|
|
@@ -51,7 +50,6 @@
|
|
|
51
50
|
"@vscode/ripgrep": "^1.18.0",
|
|
52
51
|
"chokidar": "^4.0.3",
|
|
53
52
|
"cron-parser": "^5.5.0",
|
|
54
|
-
"find-up": "^8.0.0",
|
|
55
53
|
"fuzzysort": "^3.1.0",
|
|
56
54
|
"glob": "^13.0.0",
|
|
57
55
|
"lru-cache": "^11.3.5",
|
|
@@ -65,11 +63,10 @@
|
|
|
65
63
|
"@vitest/coverage-v8": "^4.1.7",
|
|
66
64
|
"rimraf": "^6.1.2",
|
|
67
65
|
"tsc-alias": "^1.8.16",
|
|
68
|
-
"tsx": "^4.20.4",
|
|
69
66
|
"vitest": "^4.1.7"
|
|
70
67
|
},
|
|
71
68
|
"engines": {
|
|
72
|
-
"node": ">=
|
|
69
|
+
"node": ">=22"
|
|
73
70
|
},
|
|
74
71
|
"license": "MIT",
|
|
75
72
|
"scripts": {
|
|
@@ -78,6 +75,9 @@
|
|
|
78
75
|
"watch": "tsc -p tsconfig.build.json --watch & tsc-alias -p tsconfig.build.json --watch",
|
|
79
76
|
"test": "vitest run --reporter=dot",
|
|
80
77
|
"test:coverage": "vitest run --coverage --reporter=dot",
|
|
78
|
+
"test:unit": "vitest run --reporter=dot --exclude 'tests/integration/**' --exclude '**/*.integration.test.ts'",
|
|
79
|
+
"test:unit:coverage": "vitest run --coverage --reporter=dot --exclude 'tests/integration/**' --exclude '**/*.integration.test.ts'",
|
|
80
|
+
"test:integration": "vitest run --reporter=dot tests/integration .integration.test",
|
|
81
81
|
"lint": "eslint --cache",
|
|
82
82
|
"format": "prettier --write ."
|
|
83
83
|
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SessionStart hook for the sdd built-in plugin.
|
|
3
|
-
// Emits the spec-first workflow guidance as additionalContext (JSON form),
|
|
4
|
-
// resolving the absolute path to the plugin's spec-count validator so the
|
|
5
|
-
// agent can run it from its bash tool (which does not carry WAVE_PLUGIN_ROOT).
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
|
|
8
|
-
const root =
|
|
9
|
-
process.env.WAVE_PLUGIN_ROOT ||
|
|
10
|
-
path.dirname(new URL("..", import.meta.url).pathname);
|
|
11
|
-
const specCount = `node ${JSON.stringify(path.join(root, "scripts", "spec-count.js"))}`;
|
|
12
|
-
|
|
13
|
-
const guidance = [
|
|
14
|
-
"Spec-First Workflow(规格优先工作流):",
|
|
15
|
-
"- 需求增加或变更时,优先更新 spec:先更新对应规格说明(新增用户故事、验收场景),待用户确认 spec 后再实现代码。spec 是功能设计的权威来源,不是 changelog。",
|
|
16
|
-
"- 边界模糊时也先写 spec 草稿请用户确认,不要直接改代码。",
|
|
17
|
-
"- 规格编写技能(specify)由 AI 自动触发:对话中涉及新需求或需求变更时主动创建或更新规格文件,不需要用户手动调用(不出现在斜杠命令列表中)。",
|
|
18
|
-
`- 新增或修改 spec 后运行校验:${specCount}(自动检测 docs/specs/,否则 specs/,否则退出)。`,
|
|
19
|
-
].join("\n");
|
|
20
|
-
|
|
21
|
-
// JSON form → parsed as hookSpecificOutput.additionalContext by the hook manager.
|
|
22
|
-
console.log(
|
|
23
|
-
JSON.stringify({ hookSpecificOutput: { additionalContext: guidance } }),
|
|
24
|
-
);
|