wave-agent-sdk 0.10.1 → 0.10.2
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 +6 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +2 -0
- package/dist/core/session.d.ts +1 -1
- package/dist/core/session.d.ts.map +1 -1
- package/dist/core/session.js +1 -1
- package/dist/managers/backgroundTaskManager.d.ts +3 -0
- package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
- package/dist/managers/backgroundTaskManager.js +3 -0
- package/dist/managers/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +33 -4
- package/dist/managers/skillManager.d.ts +17 -2
- package/dist/managers/skillManager.d.ts.map +1 -1
- package/dist/managers/skillManager.js +93 -21
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +7 -0
- package/dist/prompts/index.d.ts +1 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +1 -0
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +1 -0
- package/dist/services/fileWatcher.d.ts.map +1 -1
- package/dist/services/fileWatcher.js +27 -15
- package/dist/services/session.d.ts +9 -2
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +77 -7
- package/dist/tools/bashTool.js +3 -3
- package/dist/tools/exitPlanMode.d.ts.map +1 -1
- package/dist/tools/exitPlanMode.js +3 -1
- package/dist/tools/grepTool.d.ts.map +1 -1
- package/dist/tools/grepTool.js +1 -1
- package/dist/tools/lspTool.d.ts.map +1 -1
- package/dist/tools/lspTool.js +47 -15
- package/dist/tools/taskOutputTool.d.ts.map +1 -1
- package/dist/tools/taskOutputTool.js +22 -5
- package/dist/tools/types.d.ts +1 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/types/agent.d.ts +2 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/configuration.d.ts +2 -0
- package/dist/types/configuration.d.ts.map +1 -1
- package/dist/types/skills.d.ts +4 -2
- package/dist/types/skills.d.ts.map +1 -1
- package/dist/utils/configPaths.d.ts +6 -0
- package/dist/utils/configPaths.d.ts.map +1 -1
- package/dist/utils/configPaths.js +23 -2
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +4 -1
- package/dist/utils/gitUtils.d.ts +6 -0
- package/dist/utils/gitUtils.d.ts.map +1 -1
- package/dist/utils/gitUtils.js +22 -0
- package/package.json +1 -1
- package/src/agent.ts +18 -2
- package/src/builtin-skills/settings/HOOKS.md +95 -0
- package/src/builtin-skills/settings/SKILL.md +86 -0
- package/src/core/session.ts +1 -0
- package/src/managers/backgroundTaskManager.ts +11 -1
- package/src/managers/permissionManager.ts +33 -4
- package/src/managers/skillManager.ts +111 -25
- package/src/managers/slashCommandManager.ts +8 -0
- package/src/prompts/index.ts +1 -0
- package/src/services/configurationService.ts +1 -0
- package/src/services/fileWatcher.ts +33 -17
- package/src/services/session.ts +83 -7
- package/src/tools/bashTool.ts +3 -3
- package/src/tools/exitPlanMode.ts +3 -1
- package/src/tools/grepTool.ts +2 -1
- package/src/tools/lspTool.ts +99 -9
- package/src/tools/taskOutputTool.ts +25 -6
- package/src/tools/types.ts +2 -0
- package/src/types/agent.ts +2 -0
- package/src/types/configuration.ts +2 -0
- package/src/types/skills.ts +4 -2
- package/src/utils/configPaths.ts +28 -2
- package/src/utils/containerSetup.ts +4 -1
- package/src/utils/gitUtils.ts +22 -0
|
@@ -95,7 +95,7 @@ export class FileWatcherService extends EventEmitter {
|
|
|
95
95
|
await this.initializeWatcher(entry);
|
|
96
96
|
} catch (error) {
|
|
97
97
|
this.logger?.error(
|
|
98
|
-
`
|
|
98
|
+
`FileWatcher: Failed to watch file ${path}: ${(error as Error).message}`,
|
|
99
99
|
);
|
|
100
100
|
throw error;
|
|
101
101
|
}
|
|
@@ -117,7 +117,7 @@ export class FileWatcherService extends EventEmitter {
|
|
|
117
117
|
this.watchers.delete(path);
|
|
118
118
|
} catch (error) {
|
|
119
119
|
this.logger?.warn(
|
|
120
|
-
`
|
|
120
|
+
`FileWatcher: Error unwatching file ${path}: ${(error as Error).message}`,
|
|
121
121
|
);
|
|
122
122
|
}
|
|
123
123
|
}
|
|
@@ -204,7 +204,7 @@ export class FileWatcherService extends EventEmitter {
|
|
|
204
204
|
entry.lastError = (error as Error).message;
|
|
205
205
|
|
|
206
206
|
this.logger?.error(
|
|
207
|
-
`
|
|
207
|
+
`FileWatcher: Failed to initialize watcher for ${entry.path}: ${(error as Error).message}`,
|
|
208
208
|
);
|
|
209
209
|
|
|
210
210
|
// Try fallback polling if not already using it
|
|
@@ -241,9 +241,17 @@ export class FileWatcherService extends EventEmitter {
|
|
|
241
241
|
this.handleFileEvent("delete", filePath);
|
|
242
242
|
});
|
|
243
243
|
|
|
244
|
+
this.globalWatcher.on("addDir", (dirPath: string) => {
|
|
245
|
+
this.handleFileEvent("create", dirPath);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
this.globalWatcher.on("unlinkDir", (dirPath: string) => {
|
|
249
|
+
this.handleFileEvent("delete", dirPath);
|
|
250
|
+
});
|
|
251
|
+
|
|
244
252
|
this.globalWatcher.on("error", (err: unknown) => {
|
|
245
253
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
246
|
-
this.logger?.error(`
|
|
254
|
+
this.logger?.error(`FileWatcher: File watcher error: ${error.message}`);
|
|
247
255
|
this.emit("watcherError", error);
|
|
248
256
|
});
|
|
249
257
|
}
|
|
@@ -253,9 +261,6 @@ export class FileWatcherService extends EventEmitter {
|
|
|
253
261
|
filePath: string,
|
|
254
262
|
stats?: { size?: number },
|
|
255
263
|
): void {
|
|
256
|
-
const entry = this.watchers.get(filePath);
|
|
257
|
-
if (!entry) return;
|
|
258
|
-
|
|
259
264
|
const event: FileWatchEvent = {
|
|
260
265
|
type,
|
|
261
266
|
path: filePath,
|
|
@@ -263,16 +268,27 @@ export class FileWatcherService extends EventEmitter {
|
|
|
263
268
|
size: stats?.size,
|
|
264
269
|
};
|
|
265
270
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
271
|
+
// Notify all watchers that match the path or are parents of the path
|
|
272
|
+
for (const [watchedPath, entry] of this.watchers.entries()) {
|
|
273
|
+
if (
|
|
274
|
+
filePath === watchedPath ||
|
|
275
|
+
filePath.startsWith(watchedPath + "/") ||
|
|
276
|
+
// Handle cases where the watched path might be a file and we get an event for it
|
|
277
|
+
// (already covered by filePath === watchedPath)
|
|
278
|
+
false
|
|
279
|
+
) {
|
|
280
|
+
entry.lastEvent = event.timestamp;
|
|
281
|
+
|
|
282
|
+
// Notify all callbacks for this watcher
|
|
283
|
+
for (const callback of entry.callbacks) {
|
|
284
|
+
try {
|
|
285
|
+
callback(event);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
this.logger?.error(
|
|
288
|
+
`FileWatcher: Error in file watch callback for ${watchedPath} (event on ${filePath}): ${(error as Error).message}`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
276
292
|
}
|
|
277
293
|
}
|
|
278
294
|
}
|
package/src/services/session.ts
CHANGED
|
@@ -423,11 +423,17 @@ export async function listSessionsFromJsonl(
|
|
|
423
423
|
|
|
424
424
|
// Try to read from index first
|
|
425
425
|
const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
|
|
426
|
+
const sevenDaysAgo = new Date();
|
|
427
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
428
|
+
|
|
426
429
|
try {
|
|
427
430
|
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
428
431
|
const index = JSON.parse(indexContent) as SessionIndex;
|
|
429
432
|
const sessions: SessionMetadata[] = Object.entries(index.sessions)
|
|
430
|
-
.filter(([, meta]) =>
|
|
433
|
+
.filter(([, meta]) => {
|
|
434
|
+
const lastActiveAt = new Date(meta.lastActiveAt);
|
|
435
|
+
return meta.sessionType === "main" && lastActiveAt >= sevenDaysAgo;
|
|
436
|
+
})
|
|
431
437
|
.map(([id, meta]) => ({
|
|
432
438
|
id,
|
|
433
439
|
...meta,
|
|
@@ -492,6 +498,10 @@ export async function listSessionsFromJsonl(
|
|
|
492
498
|
lastActiveAt = stats.mtime;
|
|
493
499
|
}
|
|
494
500
|
|
|
501
|
+
if (lastActiveAt < sevenDaysAgo) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
|
|
495
505
|
// Return inline object for performance (no interface instantiation overhead)
|
|
496
506
|
const sessionMeta: SessionMetadata = {
|
|
497
507
|
id: sessionId,
|
|
@@ -550,6 +560,68 @@ export async function listSessionsFromJsonl(
|
|
|
550
560
|
}
|
|
551
561
|
}
|
|
552
562
|
|
|
563
|
+
/**
|
|
564
|
+
* List all sessions across all project directories
|
|
565
|
+
*
|
|
566
|
+
* @returns Promise that resolves to array of session metadata objects
|
|
567
|
+
*/
|
|
568
|
+
export async function listAllSessions(): Promise<SessionMetadata[]> {
|
|
569
|
+
try {
|
|
570
|
+
const baseDir = SESSION_DIR;
|
|
571
|
+
let projectDirs: string[];
|
|
572
|
+
try {
|
|
573
|
+
projectDirs = await fs.readdir(baseDir);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const allSessions: SessionMetadata[] = [];
|
|
582
|
+
const sevenDaysAgo = new Date();
|
|
583
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
584
|
+
|
|
585
|
+
for (const projectDirName of projectDirs) {
|
|
586
|
+
const projectPath = join(baseDir, projectDirName);
|
|
587
|
+
try {
|
|
588
|
+
const stat = await fs.stat(projectPath);
|
|
589
|
+
if (!stat.isDirectory()) {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Try to read from index first
|
|
594
|
+
const indexPath = join(projectPath, SESSION_INDEX_FILENAME);
|
|
595
|
+
try {
|
|
596
|
+
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
597
|
+
const index = JSON.parse(indexContent) as SessionIndex;
|
|
598
|
+
for (const [id, meta] of Object.entries(index.sessions)) {
|
|
599
|
+
const lastActiveAt = new Date(meta.lastActiveAt);
|
|
600
|
+
if (meta.sessionType === "main" && lastActiveAt >= sevenDaysAgo) {
|
|
601
|
+
allSessions.push({
|
|
602
|
+
id,
|
|
603
|
+
...meta,
|
|
604
|
+
lastActiveAt,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
} catch {
|
|
609
|
+
// If index fails, we skip this project directory
|
|
610
|
+
// In the future, we could scan for .jsonl files here
|
|
611
|
+
}
|
|
612
|
+
} catch {
|
|
613
|
+
// Skip if stat fails
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return allSessions.sort(
|
|
618
|
+
(a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
|
|
619
|
+
);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
throw new Error(`Failed to list all sessions: ${error}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
553
625
|
/**
|
|
554
626
|
* Clean up expired sessions older than 14 days based on file modification time
|
|
555
627
|
*
|
|
@@ -840,19 +912,23 @@ export async function deleteSession(
|
|
|
840
912
|
}
|
|
841
913
|
|
|
842
914
|
/**
|
|
843
|
-
* Truncate content to a maximum length, adding ellipsis if truncated
|
|
915
|
+
* Truncate content to a maximum length, adding ellipsis if truncated.
|
|
916
|
+
* Also replaces real newlines with the literal string "\n".
|
|
844
917
|
* @param content - The content to truncate
|
|
845
|
-
* @param maxLength - Maximum length before truncation (default:
|
|
918
|
+
* @param maxLength - Maximum length before truncation (default: 100)
|
|
846
919
|
* @returns Truncated content with ellipsis if needed
|
|
847
920
|
*/
|
|
848
921
|
export function truncateContent(
|
|
849
922
|
content: string,
|
|
850
|
-
maxLength: number =
|
|
923
|
+
maxLength: number = 100,
|
|
851
924
|
): string {
|
|
852
|
-
|
|
853
|
-
|
|
925
|
+
// Replace real newlines with literal "\n"
|
|
926
|
+
const singleLineContent = content.replace(/\n/g, "\\n");
|
|
927
|
+
|
|
928
|
+
if (singleLineContent.length <= maxLength) {
|
|
929
|
+
return singleLineContent;
|
|
854
930
|
}
|
|
855
|
-
return
|
|
931
|
+
return singleLineContent.substring(0, maxLength) + "...";
|
|
856
932
|
}
|
|
857
933
|
|
|
858
934
|
/**
|
package/src/tools/bashTool.ts
CHANGED
|
@@ -77,10 +77,10 @@ Usage notes:
|
|
|
77
77
|
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
|
78
78
|
- If the output exceeds ${MAX_OUTPUT_LENGTH} characters, output will be truncated before being returned to you.
|
|
79
79
|
- You can use the \`run_in_background\` parameter to run the command in the background, which allows you to continue working while the command runs. You can monitor the output using the ${BASH_TOOL_NAME} tool as it becomes available. You do not need to use '&' at the end of the command when using this parameter.
|
|
80
|
-
- Avoid using ${BASH_TOOL_NAME} with the \`find\`, \`
|
|
80
|
+
- Avoid using ${BASH_TOOL_NAME} with the \`find\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
|
|
81
81
|
- File search: Use ${GLOB_TOOL_NAME} (NOT find or ls)
|
|
82
|
-
- Content search: Use ${GREP_TOOL_NAME}
|
|
83
|
-
- Read files: Use ${READ_TOOL_NAME}
|
|
82
|
+
- Content search: Use ${GREP_TOOL_NAME}
|
|
83
|
+
- Read files: Use ${READ_TOOL_NAME}
|
|
84
84
|
- Edit files: Use ${EDIT_TOOL_NAME} (NOT sed/awk)
|
|
85
85
|
- Write files: Use ${WRITE_TOOL_NAME} (NOT echo >/cat <<EOF)
|
|
86
86
|
- Communication: Output text directly (NOT echo/printf)
|
package/src/tools/grepTool.ts
CHANGED
|
@@ -212,7 +212,8 @@ export const grepTool: ToolPlugin = {
|
|
|
212
212
|
if (!output) {
|
|
213
213
|
return {
|
|
214
214
|
success: true,
|
|
215
|
-
content:
|
|
215
|
+
content:
|
|
216
|
+
"No matches found. Suggestion: specify the 'path' field to search in ignored or other directories (e.g., 'node_modules'), as the default search path is the current working directory and respects .gitignore.",
|
|
216
217
|
shortResult: "No matches found",
|
|
217
218
|
};
|
|
218
219
|
}
|
package/src/tools/lspTool.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
2
|
-
import { relative } from "path";
|
|
2
|
+
import { relative, join, isAbsolute } from "path";
|
|
3
|
+
import * as fs from "fs";
|
|
3
4
|
import { logger } from "../utils/globalLogger.js";
|
|
4
5
|
import { getDisplayPath } from "../utils/path.js";
|
|
5
6
|
import { LSP_TOOL_NAME } from "../constants/tools.js";
|
|
@@ -50,6 +51,43 @@ function formatUri(uri: string, workdir?: string): string {
|
|
|
50
51
|
return path;
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Formats an error message when no results are found, including context from the file
|
|
56
|
+
*/
|
|
57
|
+
function formatNoResultError(
|
|
58
|
+
message: string,
|
|
59
|
+
filePath: string,
|
|
60
|
+
line: number,
|
|
61
|
+
character: number,
|
|
62
|
+
workdir?: string,
|
|
63
|
+
): string {
|
|
64
|
+
let absolutePath = filePath;
|
|
65
|
+
if (!isAbsolute(filePath) && workdir) {
|
|
66
|
+
absolutePath = join(workdir, filePath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let lineContent = "";
|
|
70
|
+
try {
|
|
71
|
+
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
72
|
+
const lines = content.split("\n");
|
|
73
|
+
if (line > 0 && line <= lines.length) {
|
|
74
|
+
lineContent = lines[line - 1];
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Ignore file read errors
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const displayPath = getDisplayPath(filePath, workdir || "");
|
|
81
|
+
const baseMessage = `${message}\n\nPlease check if the character offset is correct and points to a valid symbol.`;
|
|
82
|
+
|
|
83
|
+
if (lineContent) {
|
|
84
|
+
const pointer = " ".repeat(Math.max(0, character - 1)) + "^";
|
|
85
|
+
return `${message}\n\nContext at ${displayPath}:${line}:${character}:\n${lineContent}\n${pointer}\n\nPlease check if the character offset is correct and points to a valid symbol.`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return baseMessage;
|
|
89
|
+
}
|
|
90
|
+
|
|
53
91
|
/**
|
|
54
92
|
* Groups items by their URI
|
|
55
93
|
*/
|
|
@@ -109,10 +147,19 @@ function isLocationLink(loc: Location | LocationLink): loc is LocationLink {
|
|
|
109
147
|
*/
|
|
110
148
|
function formatGoToDefinitionResult(
|
|
111
149
|
result: Location | Location[] | LocationLink | LocationLink[] | null,
|
|
150
|
+
filePath: string,
|
|
151
|
+
line: number,
|
|
152
|
+
character: number,
|
|
153
|
+
operation: string,
|
|
112
154
|
workdir?: string,
|
|
113
155
|
): string {
|
|
156
|
+
const message =
|
|
157
|
+
operation === "goToImplementation"
|
|
158
|
+
? "No implementation found. This may occur if the cursor is not on a symbol, or if the implementation is in an external library not indexed by the LSP server."
|
|
159
|
+
: "No definition found. This may occur if the cursor is not on a symbol, or if the definition is in an external library not indexed by the LSP server.";
|
|
160
|
+
|
|
114
161
|
if (!result) {
|
|
115
|
-
return
|
|
162
|
+
return formatNoResultError(message, filePath, line, character, workdir);
|
|
116
163
|
}
|
|
117
164
|
|
|
118
165
|
if (Array.isArray(result)) {
|
|
@@ -122,7 +169,7 @@ function formatGoToDefinitionResult(
|
|
|
122
169
|
const validLocations = locations.filter((loc) => loc && loc.uri);
|
|
123
170
|
|
|
124
171
|
if (validLocations.length === 0) {
|
|
125
|
-
return
|
|
172
|
+
return formatNoResultError(message, filePath, line, character, workdir);
|
|
126
173
|
}
|
|
127
174
|
|
|
128
175
|
if (validLocations.length === 1) {
|
|
@@ -150,15 +197,21 @@ function formatGoToDefinitionResult(
|
|
|
150
197
|
*/
|
|
151
198
|
function formatFindReferencesResult(
|
|
152
199
|
result: Location[] | null,
|
|
200
|
+
filePath: string,
|
|
201
|
+
line: number,
|
|
202
|
+
character: number,
|
|
153
203
|
workdir?: string,
|
|
154
204
|
): string {
|
|
205
|
+
const message =
|
|
206
|
+
"No references found. This may occur if the symbol has no usages, or if the LSP server has not fully indexed the workspace.";
|
|
207
|
+
|
|
155
208
|
if (!result || result.length === 0) {
|
|
156
|
-
return
|
|
209
|
+
return formatNoResultError(message, filePath, line, character, workdir);
|
|
157
210
|
}
|
|
158
211
|
|
|
159
212
|
const validLocations = result.filter((loc) => loc && loc.uri);
|
|
160
213
|
if (validLocations.length === 0) {
|
|
161
|
-
return
|
|
214
|
+
return formatNoResultError(message, filePath, line, character, workdir);
|
|
162
215
|
}
|
|
163
216
|
|
|
164
217
|
if (validLocations.length === 1) {
|
|
@@ -224,9 +277,21 @@ function formatHoverContents(contents: Hover["contents"]): string {
|
|
|
224
277
|
/**
|
|
225
278
|
* Formats the result of a hover operation
|
|
226
279
|
*/
|
|
227
|
-
function formatHoverResult(
|
|
280
|
+
function formatHoverResult(
|
|
281
|
+
result: Hover | null,
|
|
282
|
+
filePath: string,
|
|
283
|
+
line: number,
|
|
284
|
+
character: number,
|
|
285
|
+
workdir?: string,
|
|
286
|
+
): string {
|
|
228
287
|
if (!result) {
|
|
229
|
-
return
|
|
288
|
+
return formatNoResultError(
|
|
289
|
+
"No hover information available. This may occur if the cursor is not on a symbol, or if the LSP server has not fully indexed the file.",
|
|
290
|
+
filePath,
|
|
291
|
+
line,
|
|
292
|
+
character,
|
|
293
|
+
workdir,
|
|
294
|
+
);
|
|
230
295
|
}
|
|
231
296
|
|
|
232
297
|
const contents = formatHoverContents(result.contents);
|
|
@@ -428,10 +493,19 @@ function formatCallHierarchyItem(
|
|
|
428
493
|
*/
|
|
429
494
|
function formatPrepareCallHierarchyResult(
|
|
430
495
|
result: CallHierarchyItem[] | null,
|
|
496
|
+
filePath: string,
|
|
497
|
+
line: number,
|
|
498
|
+
character: number,
|
|
431
499
|
workdir?: string,
|
|
432
500
|
): string {
|
|
433
501
|
if (!result || result.length === 0) {
|
|
434
|
-
return
|
|
502
|
+
return formatNoResultError(
|
|
503
|
+
"No call hierarchy item found at this position",
|
|
504
|
+
filePath,
|
|
505
|
+
line,
|
|
506
|
+
character,
|
|
507
|
+
workdir,
|
|
508
|
+
);
|
|
435
509
|
}
|
|
436
510
|
|
|
437
511
|
if (result.length === 1) {
|
|
@@ -719,17 +793,30 @@ Note: LSP servers must be configured for the file type. If no server is availabl
|
|
|
719
793
|
| LocationLink
|
|
720
794
|
| LocationLink[]
|
|
721
795
|
| null,
|
|
796
|
+
filePath,
|
|
797
|
+
line,
|
|
798
|
+
character,
|
|
799
|
+
operation,
|
|
722
800
|
context.workdir,
|
|
723
801
|
);
|
|
724
802
|
break;
|
|
725
803
|
case "findReferences":
|
|
726
804
|
formattedContent = formatFindReferencesResult(
|
|
727
805
|
rawResult as Location[] | null,
|
|
806
|
+
filePath,
|
|
807
|
+
line,
|
|
808
|
+
character,
|
|
728
809
|
context.workdir,
|
|
729
810
|
);
|
|
730
811
|
break;
|
|
731
812
|
case "hover":
|
|
732
|
-
formattedContent = formatHoverResult(
|
|
813
|
+
formattedContent = formatHoverResult(
|
|
814
|
+
rawResult as Hover | null,
|
|
815
|
+
filePath,
|
|
816
|
+
line,
|
|
817
|
+
character,
|
|
818
|
+
context.workdir,
|
|
819
|
+
);
|
|
733
820
|
break;
|
|
734
821
|
case "documentSymbol":
|
|
735
822
|
formattedContent = formatDocumentSymbolResult(
|
|
@@ -746,6 +833,9 @@ Note: LSP servers must be configured for the file type. If no server is availabl
|
|
|
746
833
|
case "prepareCallHierarchy":
|
|
747
834
|
formattedContent = formatPrepareCallHierarchyResult(
|
|
748
835
|
rawResult as CallHierarchyItem[] | null,
|
|
836
|
+
filePath,
|
|
837
|
+
line,
|
|
838
|
+
character,
|
|
749
839
|
context.workdir,
|
|
750
840
|
);
|
|
751
841
|
break;
|
|
@@ -82,16 +82,35 @@ export const taskOutputTool: ToolPlugin = {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
const finalContent = content || "No output available";
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
let processedContent = finalContent;
|
|
86
|
+
let isTruncated = false;
|
|
87
|
+
|
|
88
|
+
if (finalContent.length > MAX_OUTPUT_LENGTH) {
|
|
89
|
+
processedContent =
|
|
90
|
+
finalContent.substring(0, MAX_OUTPUT_LENGTH) +
|
|
91
|
+
"\n\n... (output truncated)";
|
|
92
|
+
isTruncated = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isTruncated && output.outputPath) {
|
|
96
|
+
processedContent += `\n\nFull output available at: ${output.outputPath}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let shortResult = `${taskId}: ${output.status}`;
|
|
100
|
+
if (output.status === "failed" && output.exitCode !== undefined) {
|
|
101
|
+
shortResult += ` (exit code: ${output.exitCode})`;
|
|
102
|
+
}
|
|
90
103
|
|
|
91
104
|
return {
|
|
92
105
|
success: true,
|
|
93
106
|
content: processedContent,
|
|
94
|
-
shortResult
|
|
107
|
+
shortResult,
|
|
108
|
+
metadata: {
|
|
109
|
+
outputPath: output.outputPath,
|
|
110
|
+
status: output.status,
|
|
111
|
+
type: output.type,
|
|
112
|
+
exitCode: output.exitCode,
|
|
113
|
+
},
|
|
95
114
|
};
|
|
96
115
|
};
|
|
97
116
|
|
package/src/tools/types.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface ToolResult {
|
|
|
49
49
|
}>;
|
|
50
50
|
// Whether the tool was manually backgrounded by the user (e.g. via Ctrl-B)
|
|
51
51
|
isManuallyBackgrounded?: boolean;
|
|
52
|
+
// Optional metadata for the tool result
|
|
53
|
+
metadata?: Record<string, unknown>;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export interface ToolContext {
|
package/src/types/agent.ts
CHANGED
|
@@ -65,6 +65,8 @@ export interface AgentOptions {
|
|
|
65
65
|
worktreeName?: string;
|
|
66
66
|
/**Whether this is a newly created worktree */
|
|
67
67
|
isNewWorktree?: boolean;
|
|
68
|
+
/**Whether to watch for skill changes - defaults to true */
|
|
69
|
+
watchSkills?: boolean;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
export interface AgentCallbacks
|
|
@@ -91,6 +91,8 @@ export interface ConfigurationPaths {
|
|
|
91
91
|
userPaths: string[];
|
|
92
92
|
/** Project-specific configuration file paths in priority order */
|
|
93
93
|
projectPaths: string[];
|
|
94
|
+
/** Builtin configuration file paths */
|
|
95
|
+
builtinPaths: string[];
|
|
94
96
|
/** All configuration paths combined */
|
|
95
97
|
allPaths: string[];
|
|
96
98
|
/** Only the paths that actually exist on the filesystem */
|
package/src/types/skills.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
export interface SkillMetadata {
|
|
7
7
|
name: string;
|
|
8
8
|
description: string;
|
|
9
|
-
type: "personal" | "project";
|
|
9
|
+
type: "personal" | "project" | "builtin";
|
|
10
10
|
skillPath: string;
|
|
11
11
|
allowedTools?: string[];
|
|
12
12
|
context?: "fork";
|
|
@@ -37,7 +37,7 @@ export interface SkillFrontmatter {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export interface SkillCollection {
|
|
40
|
-
type: "personal" | "project";
|
|
40
|
+
type: "personal" | "project" | "builtin";
|
|
41
41
|
basePath: string;
|
|
42
42
|
skills: Map<string, SkillMetadata>;
|
|
43
43
|
errors: SkillError[];
|
|
@@ -57,6 +57,7 @@ export interface SkillValidationResult {
|
|
|
57
57
|
export interface SkillDiscoveryResult {
|
|
58
58
|
personalSkills: Map<string, SkillMetadata>;
|
|
59
59
|
projectSkills: Map<string, SkillMetadata>;
|
|
60
|
+
builtinSkills: Map<string, SkillMetadata>;
|
|
60
61
|
errors: SkillError[];
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -73,6 +74,7 @@ export interface SkillManagerOptions {
|
|
|
73
74
|
personalSkillsPath?: string;
|
|
74
75
|
scanTimeout?: number;
|
|
75
76
|
workdir?: string;
|
|
77
|
+
watch?: boolean;
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
export interface ParsedSkillFile {
|
package/src/utils/configPaths.ts
CHANGED
|
@@ -10,9 +10,29 @@
|
|
|
10
10
|
* - Project configs override user configs (existing behavior)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { join } from "path";
|
|
13
|
+
import { join, dirname } from "path";
|
|
14
14
|
import { homedir } from "os";
|
|
15
15
|
import { existsSync } from "fs";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the builtin skills directory path
|
|
23
|
+
*/
|
|
24
|
+
export function getBuiltinSkillsDir(): string {
|
|
25
|
+
// In development, it's in src/builtin-skills
|
|
26
|
+
// In production (dist), it should be in dist/builtin-skills
|
|
27
|
+
// We'll look for it relative to this file
|
|
28
|
+
const devPath = join(__dirname, "..", "builtin-skills");
|
|
29
|
+
const prodPath = join(__dirname, "builtin-skills");
|
|
30
|
+
|
|
31
|
+
if (existsSync(devPath)) {
|
|
32
|
+
return devPath;
|
|
33
|
+
}
|
|
34
|
+
return prodPath;
|
|
35
|
+
}
|
|
16
36
|
|
|
17
37
|
/**
|
|
18
38
|
* Get the user-specific configuration file path (legacy function)
|
|
@@ -62,15 +82,18 @@ export function getProjectConfigPaths(workdir: string): string[] {
|
|
|
62
82
|
export function getAllConfigPaths(workdir: string): {
|
|
63
83
|
userPaths: string[];
|
|
64
84
|
projectPaths: string[];
|
|
85
|
+
builtinPaths: string[];
|
|
65
86
|
allPaths: string[];
|
|
66
87
|
} {
|
|
67
88
|
const userPaths = getUserConfigPaths();
|
|
68
89
|
const projectPaths = getProjectConfigPaths(workdir);
|
|
90
|
+
const builtinPaths = [join(getBuiltinSkillsDir(), "settings", "SKILL.md")];
|
|
69
91
|
|
|
70
92
|
return {
|
|
71
93
|
userPaths,
|
|
72
94
|
projectPaths,
|
|
73
|
-
|
|
95
|
+
builtinPaths,
|
|
96
|
+
allPaths: [...userPaths, ...projectPaths, ...builtinPaths],
|
|
74
97
|
};
|
|
75
98
|
}
|
|
76
99
|
|
|
@@ -81,17 +104,20 @@ export function getAllConfigPaths(workdir: string): {
|
|
|
81
104
|
export function getExistingConfigPaths(workdir: string): {
|
|
82
105
|
userPaths: string[];
|
|
83
106
|
projectPaths: string[];
|
|
107
|
+
builtinPaths: string[];
|
|
84
108
|
existingPaths: string[];
|
|
85
109
|
} {
|
|
86
110
|
const allPaths = getAllConfigPaths(workdir);
|
|
87
111
|
|
|
88
112
|
const existingUserPaths = allPaths.userPaths.filter(existsSync);
|
|
89
113
|
const existingProjectPaths = allPaths.projectPaths.filter(existsSync);
|
|
114
|
+
const existingBuiltinPaths = allPaths.builtinPaths.filter(existsSync);
|
|
90
115
|
const allExistingPaths = allPaths.allPaths.filter(existsSync);
|
|
91
116
|
|
|
92
117
|
return {
|
|
93
118
|
userPaths: existingUserPaths,
|
|
94
119
|
projectPaths: existingProjectPaths,
|
|
120
|
+
builtinPaths: existingBuiltinPaths,
|
|
95
121
|
existingPaths: allExistingPaths,
|
|
96
122
|
};
|
|
97
123
|
}
|
|
@@ -147,7 +147,10 @@ export function setupAgentContainer(
|
|
|
147
147
|
const hookManager = new HookManager(container, workdir);
|
|
148
148
|
container.register("HookManager", hookManager);
|
|
149
149
|
|
|
150
|
-
const skillManager = new SkillManager(container, {
|
|
150
|
+
const skillManager = new SkillManager(container, {
|
|
151
|
+
workdir,
|
|
152
|
+
watch: options.watchSkills ?? true,
|
|
153
|
+
});
|
|
151
154
|
container.register("SkillManager", skillManager);
|
|
152
155
|
|
|
153
156
|
container.register(
|
package/src/utils/gitUtils.ts
CHANGED
|
@@ -59,6 +59,28 @@ export function getGitCommonDir(cwd: string): string {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Get the main repository root directory (the first worktree in the list)
|
|
64
|
+
* @param cwd Working directory
|
|
65
|
+
* @returns Main repository root path
|
|
66
|
+
*/
|
|
67
|
+
export function getGitMainRepoRoot(cwd: string): string {
|
|
68
|
+
try {
|
|
69
|
+
const output = execSync("git worktree list --porcelain", {
|
|
70
|
+
cwd,
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
73
|
+
}).trim();
|
|
74
|
+
const lines = output.split("\n");
|
|
75
|
+
if (lines.length > 0 && lines[0].startsWith("worktree ")) {
|
|
76
|
+
return lines[0].substring("worktree ".length).trim();
|
|
77
|
+
}
|
|
78
|
+
return getGitRepoRoot(cwd);
|
|
79
|
+
} catch {
|
|
80
|
+
return getGitRepoRoot(cwd);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
62
84
|
/**
|
|
63
85
|
* Get the default remote branch (e.g., origin/main)
|
|
64
86
|
* @param cwd Working directory
|