wave-agent-sdk 0.0.6 → 0.0.8
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 +32 -20
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +209 -24
- package/dist/constants/events.d.ts +28 -0
- package/dist/constants/events.d.ts.map +1 -0
- package/dist/constants/events.js +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/managers/aiManager.d.ts +34 -1
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +248 -132
- package/dist/managers/backgroundBashManager.d.ts.map +1 -1
- package/dist/managers/backgroundBashManager.js +7 -6
- package/dist/managers/hookManager.d.ts +13 -16
- package/dist/managers/hookManager.d.ts.map +1 -1
- package/dist/managers/hookManager.js +81 -44
- package/dist/managers/liveConfigManager.d.ts +58 -0
- package/dist/managers/liveConfigManager.d.ts.map +1 -0
- package/dist/managers/liveConfigManager.js +160 -0
- package/dist/managers/messageManager.d.ts +41 -24
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +168 -49
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +9 -3
- package/dist/managers/subagentManager.d.ts +51 -0
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +190 -19
- package/dist/services/aiService.d.ts +13 -5
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +350 -74
- package/dist/services/configurationWatcher.d.ts +120 -0
- package/dist/services/configurationWatcher.d.ts.map +1 -0
- package/dist/services/configurationWatcher.js +439 -0
- package/dist/services/fileWatcher.d.ts +69 -0
- package/dist/services/fileWatcher.d.ts.map +1 -0
- package/dist/services/fileWatcher.js +213 -0
- package/dist/services/hook.d.ts +91 -9
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +393 -43
- package/dist/services/jsonlHandler.d.ts +62 -0
- package/dist/services/jsonlHandler.d.ts.map +1 -0
- package/dist/services/jsonlHandler.js +257 -0
- package/dist/services/memory.d.ts +9 -0
- package/dist/services/memory.d.ts.map +1 -1
- package/dist/services/memory.js +81 -12
- package/dist/services/memoryStore.d.ts +81 -0
- package/dist/services/memoryStore.d.ts.map +1 -0
- package/dist/services/memoryStore.js +200 -0
- package/dist/services/session.d.ts +64 -49
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +310 -132
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +5 -4
- package/dist/tools/deleteFileTool.d.ts.map +1 -1
- package/dist/tools/deleteFileTool.js +2 -1
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +3 -2
- package/dist/tools/multiEditTool.d.ts.map +1 -1
- package/dist/tools/multiEditTool.js +4 -3
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +2 -1
- package/dist/tools/todoWriteTool.d.ts.map +1 -1
- package/dist/tools/todoWriteTool.js +3 -10
- package/dist/tools/writeTool.d.ts.map +1 -1
- package/dist/tools/writeTool.js +5 -6
- package/dist/types/commands.d.ts +4 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/types/core.d.ts +35 -0
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/environment.d.ts +42 -0
- package/dist/types/environment.d.ts.map +1 -0
- package/dist/types/environment.js +21 -0
- package/dist/types/hooks.d.ts +8 -2
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/hooks.js +8 -2
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/memoryStore.d.ts +82 -0
- package/dist/types/memoryStore.d.ts.map +1 -0
- package/dist/types/memoryStore.js +7 -0
- package/dist/types/messaging.d.ts +21 -9
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/messaging.js +5 -1
- package/dist/types/session.d.ts +20 -0
- package/dist/types/session.d.ts.map +1 -0
- package/dist/types/session.js +7 -0
- package/dist/utils/bashHistory.d.ts.map +1 -1
- package/dist/utils/bashHistory.js +27 -26
- package/dist/utils/cacheControlUtils.d.ts +121 -0
- package/dist/utils/cacheControlUtils.d.ts.map +1 -0
- package/dist/utils/cacheControlUtils.js +367 -0
- package/dist/utils/commandPathResolver.d.ts +52 -0
- package/dist/utils/commandPathResolver.d.ts.map +1 -0
- package/dist/utils/commandPathResolver.js +145 -0
- package/dist/utils/configPaths.d.ts +85 -0
- package/dist/utils/configPaths.d.ts.map +1 -0
- package/dist/utils/configPaths.js +121 -0
- package/dist/utils/configResolver.d.ts +37 -10
- package/dist/utils/configResolver.d.ts.map +1 -1
- package/dist/utils/configResolver.js +127 -23
- package/dist/utils/constants.d.ts +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.js +8 -13
- package/dist/utils/customCommands.d.ts.map +1 -1
- package/dist/utils/customCommands.js +66 -21
- package/dist/utils/fileUtils.d.ts +15 -0
- package/dist/utils/fileUtils.d.ts.map +1 -0
- package/dist/utils/fileUtils.js +61 -0
- package/dist/utils/globalLogger.d.ts +102 -0
- package/dist/utils/globalLogger.d.ts.map +1 -0
- package/dist/utils/globalLogger.js +136 -0
- package/dist/utils/hookMatcher.d.ts +1 -6
- package/dist/utils/hookMatcher.d.ts.map +1 -1
- package/dist/utils/mcpUtils.d.ts.map +1 -1
- package/dist/utils/mcpUtils.js +25 -3
- package/dist/utils/messageOperations.d.ts +27 -27
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/messageOperations.js +46 -36
- package/dist/utils/pathEncoder.d.ts +104 -0
- package/dist/utils/pathEncoder.d.ts.map +1 -0
- package/dist/utils/pathEncoder.js +272 -0
- package/dist/utils/subagentParser.d.ts.map +1 -1
- package/dist/utils/subagentParser.js +2 -1
- package/dist/utils/tokenCalculation.d.ts +26 -0
- package/dist/utils/tokenCalculation.d.ts.map +1 -0
- package/dist/utils/tokenCalculation.js +36 -0
- package/package.json +6 -3
- package/src/agent.ts +301 -37
- package/src/constants/events.ts +38 -0
- package/src/index.ts +2 -0
- package/src/managers/aiManager.ts +325 -173
- package/src/managers/backgroundBashManager.ts +7 -6
- package/src/managers/hookManager.ts +106 -84
- package/src/managers/liveConfigManager.ts +248 -0
- package/src/managers/messageManager.ts +237 -100
- package/src/managers/slashCommandManager.ts +9 -7
- package/src/managers/subagentManager.ts +284 -22
- package/src/services/aiService.ts +474 -83
- package/src/services/configurationWatcher.ts +622 -0
- package/src/services/fileWatcher.ts +301 -0
- package/src/services/hook.ts +538 -47
- package/src/services/jsonlHandler.ts +319 -0
- package/src/services/memory.ts +92 -12
- package/src/services/memoryStore.ts +279 -0
- package/src/services/session.ts +381 -157
- package/src/tools/bashTool.ts +5 -4
- package/src/tools/deleteFileTool.ts +2 -1
- package/src/tools/editTool.ts +3 -2
- package/src/tools/multiEditTool.ts +4 -3
- package/src/tools/readTool.ts +2 -1
- package/src/tools/todoWriteTool.ts +3 -11
- package/src/tools/writeTool.ts +7 -6
- package/src/types/commands.ts +6 -0
- package/src/types/core.ts +44 -0
- package/src/types/environment.ts +60 -0
- package/src/types/hooks.ts +21 -8
- package/src/types/index.ts +2 -0
- package/src/types/memoryStore.ts +94 -0
- package/src/types/messaging.ts +21 -10
- package/src/types/session.ts +25 -0
- package/src/utils/bashHistory.ts +27 -27
- package/src/utils/cacheControlUtils.ts +540 -0
- package/src/utils/commandPathResolver.ts +189 -0
- package/src/utils/configPaths.ts +163 -0
- package/src/utils/configResolver.ts +182 -22
- package/src/utils/constants.ts +1 -1
- package/src/utils/convertMessagesForAPI.ts +8 -14
- package/src/utils/customCommands.ts +90 -22
- package/src/utils/fileUtils.ts +65 -0
- package/src/utils/globalLogger.ts +145 -0
- package/src/utils/hookMatcher.ts +1 -12
- package/src/utils/mcpUtils.ts +34 -3
- package/src/utils/messageOperations.ts +77 -60
- package/src/utils/pathEncoder.ts +379 -0
- package/src/utils/subagentParser.ts +2 -1
- package/src/utils/tokenCalculation.ts +43 -0
- package/src/types/index.ts.backup +0 -357
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import type { Message, Usage } from "../types/index.js";
|
|
2
|
+
import { MessageSource } from "../types/index.js";
|
|
3
|
+
import type { SubagentConfiguration } from "./subagentParser.js";
|
|
2
4
|
import { readFileSync } from "fs";
|
|
3
5
|
import { extname } from "path";
|
|
4
6
|
import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
|
|
7
|
+
import { logger } from "./globalLogger.js";
|
|
5
8
|
|
|
6
|
-
//
|
|
7
|
-
export interface
|
|
8
|
-
messages: Message[];
|
|
9
|
+
// Base user message parameters interface
|
|
10
|
+
export interface UserMessageParams {
|
|
9
11
|
content: string;
|
|
10
12
|
images?: Array<{ path: string; mimeType: string }>;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
customCommandContent?: string;
|
|
14
|
+
source?: MessageSource;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Parameter interfaces for message operations
|
|
18
|
+
export interface AddUserMessageParams extends UserMessageParams {
|
|
19
|
+
messages: Message[];
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export interface UpdateToolBlockParams {
|
|
@@ -23,25 +26,26 @@ export interface UpdateToolBlockParams {
|
|
|
23
26
|
result?: string;
|
|
24
27
|
success?: boolean;
|
|
25
28
|
error?: string;
|
|
26
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Tool execution stage:
|
|
31
|
+
* - 'start': Tool call initiated during AI streaming
|
|
32
|
+
* - 'streaming': Tool parameters being received incrementally
|
|
33
|
+
* - 'running': Tool execution in progress
|
|
34
|
+
* - 'end': Tool execution completed with final result
|
|
35
|
+
*/
|
|
36
|
+
stage: "start" | "streaming" | "running" | "end";
|
|
27
37
|
name?: string;
|
|
28
38
|
shortResult?: string;
|
|
29
39
|
images?: Array<{ data: string; mediaType?: string }>;
|
|
30
40
|
compactParams?: string;
|
|
41
|
+
parametersChunk?: string; // Incremental parameter updates for streaming
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
// Agent specific interfaces (without messages parameter)
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
success?: boolean;
|
|
39
|
-
error?: string;
|
|
40
|
-
isRunning?: boolean;
|
|
41
|
-
name?: string;
|
|
42
|
-
shortResult?: string;
|
|
43
|
-
compactParams?: string;
|
|
44
|
-
}
|
|
45
|
+
export type AgentToolBlockUpdateParams = Omit<
|
|
46
|
+
UpdateToolBlockParams,
|
|
47
|
+
"messages"
|
|
48
|
+
>;
|
|
45
49
|
|
|
46
50
|
export interface AddDiffBlockParams {
|
|
47
51
|
messages: Message[];
|
|
@@ -81,17 +85,18 @@ export interface CompleteCommandParams {
|
|
|
81
85
|
|
|
82
86
|
/**
|
|
83
87
|
* Extract text content from user messages in the messages array
|
|
88
|
+
* Excludes messages with source HOOK to prevent hook-generated content from entering user history
|
|
84
89
|
*/
|
|
85
90
|
export const extractUserInputHistory = (messages: Message[]): string[] => {
|
|
86
91
|
return messages
|
|
87
92
|
.filter((message) => message.role === "user")
|
|
88
93
|
.map((message) => {
|
|
89
|
-
// Extract
|
|
94
|
+
// Extract text block content, excluding HOOK-sourced blocks
|
|
90
95
|
const textBlocks = message.blocks.filter(
|
|
91
|
-
(block) => block.type === "text",
|
|
96
|
+
(block) => block.type === "text" && block.source !== MessageSource.HOOK,
|
|
92
97
|
);
|
|
93
98
|
return textBlocks
|
|
94
|
-
.map((block) => block.content)
|
|
99
|
+
.map((block) => (block as { content: string }).content)
|
|
95
100
|
.join(" ")
|
|
96
101
|
.trim();
|
|
97
102
|
})
|
|
@@ -133,8 +138,8 @@ export const convertImageToBase64 = (imagePath: string): string => {
|
|
|
133
138
|
|
|
134
139
|
const base64String = imageBuffer.toString("base64");
|
|
135
140
|
return `data:${mimeType};base64,${base64String}`;
|
|
136
|
-
} catch {
|
|
137
|
-
|
|
141
|
+
} catch (error) {
|
|
142
|
+
logger.error(`Failed to convert image to base64: ${imagePath}`, error);
|
|
138
143
|
// Return an error placeholder or throw error
|
|
139
144
|
return `data:image/png;base64,`; // Empty base64, avoid program crash
|
|
140
145
|
}
|
|
@@ -145,16 +150,19 @@ export const addUserMessageToMessages = ({
|
|
|
145
150
|
messages,
|
|
146
151
|
content,
|
|
147
152
|
images,
|
|
148
|
-
|
|
153
|
+
customCommandContent,
|
|
154
|
+
source,
|
|
149
155
|
}: AddUserMessageParams): Message[] => {
|
|
150
156
|
const blocks: Message["blocks"] = [];
|
|
151
157
|
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
// Create text block with optional customCommandContent and source
|
|
159
|
+
const textBlock = {
|
|
160
|
+
type: "text" as const,
|
|
161
|
+
content,
|
|
162
|
+
...(customCommandContent && { customCommandContent }),
|
|
163
|
+
...(source && { source }),
|
|
164
|
+
};
|
|
165
|
+
blocks.push(textBlock);
|
|
158
166
|
|
|
159
167
|
// If there are images, add image block
|
|
160
168
|
if (images && images.length > 0) {
|
|
@@ -178,6 +186,7 @@ export const addAssistantMessageToMessages = (
|
|
|
178
186
|
content?: string,
|
|
179
187
|
toolCalls?: ChatCompletionMessageFunctionToolCall[],
|
|
180
188
|
usage?: Usage,
|
|
189
|
+
metadata?: Record<string, unknown>,
|
|
181
190
|
): Message[] => {
|
|
182
191
|
const blocks: Message["blocks"] = [];
|
|
183
192
|
|
|
@@ -195,7 +204,7 @@ export const addAssistantMessageToMessages = (
|
|
|
195
204
|
result: "",
|
|
196
205
|
id: toolCall.id || "",
|
|
197
206
|
name: toolCall.function?.name || "",
|
|
198
|
-
|
|
207
|
+
stage: "start",
|
|
199
208
|
});
|
|
200
209
|
});
|
|
201
210
|
}
|
|
@@ -204,6 +213,7 @@ export const addAssistantMessageToMessages = (
|
|
|
204
213
|
role: "assistant",
|
|
205
214
|
blocks,
|
|
206
215
|
usage, // Include usage data if provided
|
|
216
|
+
...(metadata ? { metadata: { ...metadata } } : {}),
|
|
207
217
|
};
|
|
208
218
|
|
|
209
219
|
return [...messages, initialAssistantMessage];
|
|
@@ -239,11 +249,12 @@ export const updateToolBlockInMessage = ({
|
|
|
239
249
|
result,
|
|
240
250
|
success,
|
|
241
251
|
error,
|
|
242
|
-
|
|
252
|
+
stage,
|
|
243
253
|
name,
|
|
244
254
|
shortResult,
|
|
245
255
|
images,
|
|
246
256
|
compactParams,
|
|
257
|
+
parametersChunk,
|
|
247
258
|
}: UpdateToolBlockParams): Message[] => {
|
|
248
259
|
const newMessages = [...messages];
|
|
249
260
|
// Find the last assistant message
|
|
@@ -262,24 +273,28 @@ export const updateToolBlockInMessage = ({
|
|
|
262
273
|
toolBlock.images = images; // Add image data update
|
|
263
274
|
if (success !== undefined) toolBlock.success = success;
|
|
264
275
|
if (error !== undefined) toolBlock.error = error;
|
|
265
|
-
if (
|
|
276
|
+
if (stage !== undefined) toolBlock.stage = stage;
|
|
266
277
|
if (compactParams !== undefined)
|
|
267
278
|
toolBlock.compactParams = compactParams;
|
|
279
|
+
if (parametersChunk !== undefined)
|
|
280
|
+
toolBlock.parametersChunk = parametersChunk;
|
|
268
281
|
}
|
|
269
|
-
} else
|
|
282
|
+
} else {
|
|
270
283
|
// If existing block not found, create new one
|
|
284
|
+
// This handles cases where we're streaming tool parameters before execution
|
|
271
285
|
newMessages[i].blocks.push({
|
|
272
286
|
type: "tool",
|
|
273
287
|
parameters: parameters,
|
|
274
|
-
result: result,
|
|
288
|
+
result: result || "",
|
|
275
289
|
shortResult: shortResult,
|
|
276
290
|
images: images, // Add image data
|
|
277
291
|
id: id,
|
|
278
292
|
name: name || "unknown",
|
|
279
293
|
success: success,
|
|
280
294
|
error: error,
|
|
281
|
-
|
|
295
|
+
stage: stage ?? "start",
|
|
282
296
|
compactParams: compactParams,
|
|
297
|
+
parametersChunk: parametersChunk,
|
|
283
298
|
});
|
|
284
299
|
}
|
|
285
300
|
break;
|
|
@@ -294,24 +309,23 @@ export const addErrorBlockToMessage = ({
|
|
|
294
309
|
error,
|
|
295
310
|
}: AddErrorBlockParams): Message[] => {
|
|
296
311
|
const newMessages = [...messages];
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
312
|
+
|
|
313
|
+
// Check if the last message is an assistant message
|
|
314
|
+
const lastMessage = newMessages[newMessages.length - 1];
|
|
315
|
+
if (lastMessage && lastMessage.role === "assistant") {
|
|
316
|
+
// Create a new message object with the error block added
|
|
317
|
+
newMessages[newMessages.length - 1] = {
|
|
318
|
+
...lastMessage,
|
|
319
|
+
blocks: [
|
|
320
|
+
...lastMessage.blocks,
|
|
303
321
|
{
|
|
304
322
|
type: "error",
|
|
305
323
|
content: error,
|
|
306
324
|
},
|
|
307
|
-
]
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// If no assistant message found, create a new assistant message with only error block
|
|
314
|
-
if (!assistantMessageFound) {
|
|
325
|
+
],
|
|
326
|
+
};
|
|
327
|
+
} else {
|
|
328
|
+
// If the last message is not an assistant message, create a new assistant message
|
|
315
329
|
newMessages.push({
|
|
316
330
|
role: "assistant",
|
|
317
331
|
blocks: [
|
|
@@ -513,14 +527,15 @@ export interface AddSubagentBlockParams {
|
|
|
513
527
|
subagentId: string;
|
|
514
528
|
subagentName: string;
|
|
515
529
|
status: "active" | "completed" | "error" | "aborted";
|
|
516
|
-
|
|
530
|
+
sessionId: string;
|
|
531
|
+
configuration: SubagentConfiguration;
|
|
517
532
|
}
|
|
518
533
|
|
|
519
534
|
export interface UpdateSubagentBlockParams {
|
|
520
535
|
messages: Message[];
|
|
521
536
|
subagentId: string;
|
|
522
537
|
status: "active" | "completed" | "error" | "aborted";
|
|
523
|
-
|
|
538
|
+
sessionId?: string;
|
|
524
539
|
}
|
|
525
540
|
|
|
526
541
|
export const addSubagentBlockToMessage = ({
|
|
@@ -528,7 +543,8 @@ export const addSubagentBlockToMessage = ({
|
|
|
528
543
|
subagentId,
|
|
529
544
|
subagentName,
|
|
530
545
|
status,
|
|
531
|
-
|
|
546
|
+
sessionId,
|
|
547
|
+
configuration,
|
|
532
548
|
}: AddSubagentBlockParams): Message[] => {
|
|
533
549
|
const newMessages = [...messages];
|
|
534
550
|
|
|
@@ -550,7 +566,8 @@ export const addSubagentBlockToMessage = ({
|
|
|
550
566
|
subagentId,
|
|
551
567
|
subagentName,
|
|
552
568
|
status,
|
|
553
|
-
|
|
569
|
+
sessionId,
|
|
570
|
+
configuration,
|
|
554
571
|
});
|
|
555
572
|
|
|
556
573
|
return newMessages;
|
|
@@ -561,7 +578,7 @@ export const updateSubagentBlockInMessage = (
|
|
|
561
578
|
subagentId: string,
|
|
562
579
|
updates: Partial<{
|
|
563
580
|
status: "active" | "completed" | "error" | "aborted";
|
|
564
|
-
|
|
581
|
+
sessionId: string;
|
|
565
582
|
}>,
|
|
566
583
|
): Message[] => {
|
|
567
584
|
const newMessages = [...messages];
|
|
@@ -575,8 +592,8 @@ export const updateSubagentBlockInMessage = (
|
|
|
575
592
|
if (updates.status !== undefined) {
|
|
576
593
|
block.status = updates.status;
|
|
577
594
|
}
|
|
578
|
-
if (updates.
|
|
579
|
-
block.
|
|
595
|
+
if (updates.sessionId !== undefined) {
|
|
596
|
+
block.sessionId = updates.sessionId;
|
|
580
597
|
}
|
|
581
598
|
return newMessages;
|
|
582
599
|
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path encoding utility for converting working directory paths to filesystem-safe names
|
|
3
|
+
* Handles cross-platform directory name encoding for project-based session organization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { resolve, join } from "path";
|
|
7
|
+
import { createHash } from "crypto";
|
|
8
|
+
import { realpath, mkdir } from "fs/promises";
|
|
9
|
+
import { homedir, platform } from "os";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Project directory information
|
|
13
|
+
*/
|
|
14
|
+
export interface ProjectDirectory {
|
|
15
|
+
readonly originalPath: string;
|
|
16
|
+
readonly encodedName: string;
|
|
17
|
+
readonly encodedPath: string;
|
|
18
|
+
readonly pathHash?: string; // For collision resolution
|
|
19
|
+
readonly isSymbolicLink: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Path encoding configuration options
|
|
24
|
+
*/
|
|
25
|
+
export interface PathEncodingOptions {
|
|
26
|
+
maxLength?: number; // Default: 200 characters
|
|
27
|
+
pathSeparatorReplacement?: string; // Default: '-'
|
|
28
|
+
spaceReplacement?: string; // Default: '_'
|
|
29
|
+
invalidCharReplacement?: string; // Default: '_'
|
|
30
|
+
preserveCase?: boolean; // Default: false (convert to lowercase)
|
|
31
|
+
hashLength?: number; // Default: 8 characters
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Platform-specific filesystem constraints
|
|
36
|
+
*/
|
|
37
|
+
export interface FilesystemConstraints {
|
|
38
|
+
readonly maxDirectoryNameLength: number;
|
|
39
|
+
readonly maxPathLength: number;
|
|
40
|
+
readonly invalidCharacters: string[];
|
|
41
|
+
readonly reservedNames: string[];
|
|
42
|
+
readonly caseSensitive: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Path validation result
|
|
47
|
+
*/
|
|
48
|
+
export interface PathValidationResult {
|
|
49
|
+
readonly isValid: boolean;
|
|
50
|
+
readonly errors: string[];
|
|
51
|
+
readonly warnings: string[];
|
|
52
|
+
readonly suggestedFix?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* PathEncoder class for converting working directory paths to filesystem-safe names
|
|
57
|
+
*/
|
|
58
|
+
export class PathEncoder {
|
|
59
|
+
private readonly options: Required<PathEncodingOptions>;
|
|
60
|
+
private readonly constraints: FilesystemConstraints;
|
|
61
|
+
|
|
62
|
+
constructor(options: PathEncodingOptions = {}) {
|
|
63
|
+
this.options = {
|
|
64
|
+
maxLength: options.maxLength ?? 200,
|
|
65
|
+
pathSeparatorReplacement: options.pathSeparatorReplacement ?? "-",
|
|
66
|
+
spaceReplacement: options.spaceReplacement ?? "_",
|
|
67
|
+
invalidCharReplacement: options.invalidCharReplacement ?? "_",
|
|
68
|
+
preserveCase: options.preserveCase ?? false,
|
|
69
|
+
hashLength: options.hashLength ?? 8,
|
|
70
|
+
};
|
|
71
|
+
this.constraints = this.getFilesystemConstraints();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Encode a working directory path to a filesystem-safe directory name
|
|
76
|
+
*/
|
|
77
|
+
async encode(originalPath: string): Promise<string> {
|
|
78
|
+
// Resolve symbolic links and normalize path
|
|
79
|
+
const resolvedPath = await this.resolvePath(originalPath);
|
|
80
|
+
return this.encodeSync(resolvedPath);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Synchronously encode a path to a filesystem-safe directory name
|
|
85
|
+
* Note: Does not resolve symbolic links - use encode() for full path resolution
|
|
86
|
+
*/
|
|
87
|
+
encodeSync(pathToEncode: string): string {
|
|
88
|
+
// Convert to safe directory name
|
|
89
|
+
let encoded = pathToEncode;
|
|
90
|
+
|
|
91
|
+
// Remove leading slash to avoid empty directory names
|
|
92
|
+
if (encoded.startsWith("/")) {
|
|
93
|
+
encoded = encoded.substring(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Replace path separators with hyphens
|
|
97
|
+
encoded = encoded.replace(/[/\\]/g, this.options.pathSeparatorReplacement);
|
|
98
|
+
|
|
99
|
+
// Replace spaces with underscores
|
|
100
|
+
encoded = encoded.replace(/\s+/g, this.options.spaceReplacement);
|
|
101
|
+
|
|
102
|
+
// Replace invalid characters with underscores
|
|
103
|
+
const escapedChars = this.constraints.invalidCharacters
|
|
104
|
+
.map((c) => `\\${c}`)
|
|
105
|
+
.join("");
|
|
106
|
+
const invalidChars = new RegExp(`[${escapedChars}]`, "g");
|
|
107
|
+
encoded = encoded.replace(
|
|
108
|
+
invalidChars,
|
|
109
|
+
this.options.invalidCharReplacement,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Convert to lowercase unless preserveCase is true
|
|
113
|
+
if (!this.options.preserveCase) {
|
|
114
|
+
encoded = encoded.toLowerCase();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Handle length limit with hash
|
|
118
|
+
if (encoded.length > this.options.maxLength) {
|
|
119
|
+
const hash = this.generateHash(pathToEncode, this.options.hashLength);
|
|
120
|
+
const maxBaseLength =
|
|
121
|
+
this.options.maxLength - this.options.hashLength - 1; // -1 for separator
|
|
122
|
+
encoded = `${encoded.substring(0, maxBaseLength)}-${hash}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return encoded;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Decode an encoded directory name back to original path (limited functionality)
|
|
130
|
+
* Note: This is best-effort as encoding is lossy
|
|
131
|
+
*/
|
|
132
|
+
async decode(encodedName: string): Promise<string | null> {
|
|
133
|
+
return this.decodeSync(encodedName);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Synchronously decode an encoded directory name back to original path (limited functionality)
|
|
138
|
+
* Note: This is best-effort as encoding is lossy
|
|
139
|
+
*/
|
|
140
|
+
decodeSync(encodedName: string): string | null {
|
|
141
|
+
// This is a simplified version - full reversal is not always possible
|
|
142
|
+
// due to lossy encoding (case changes, character replacements, hashing)
|
|
143
|
+
|
|
144
|
+
// Check if this has a hash suffix
|
|
145
|
+
const hashPattern = new RegExp(`-[a-f0-9]{${this.options.hashLength}}$`);
|
|
146
|
+
if (hashPattern.test(encodedName)) {
|
|
147
|
+
// Cannot reliably decode hashed paths
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Attempt basic reversal
|
|
152
|
+
let decoded = encodedName;
|
|
153
|
+
|
|
154
|
+
// Reverse path separator replacement
|
|
155
|
+
decoded = decoded.replace(
|
|
156
|
+
new RegExp(this.options.pathSeparatorReplacement, "g"),
|
|
157
|
+
"/",
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Reverse space replacement
|
|
161
|
+
decoded = decoded.replace(
|
|
162
|
+
new RegExp(this.options.spaceReplacement, "g"),
|
|
163
|
+
" ",
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Add leading slash
|
|
167
|
+
decoded = `/${decoded}`;
|
|
168
|
+
|
|
169
|
+
return decoded;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolve symbolic links and normalize path before encoding
|
|
174
|
+
*/
|
|
175
|
+
async resolvePath(path: string): Promise<string> {
|
|
176
|
+
try {
|
|
177
|
+
// Expand tilde to home directory
|
|
178
|
+
const expandedPath = this.expandTilde(path);
|
|
179
|
+
|
|
180
|
+
// Resolve to absolute path
|
|
181
|
+
const absolutePath = resolve(expandedPath);
|
|
182
|
+
|
|
183
|
+
// Resolve symbolic links
|
|
184
|
+
const resolvedPath = await realpath(absolutePath);
|
|
185
|
+
|
|
186
|
+
return resolvedPath;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw new Error(`Failed to resolve path "${path}": ${error}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create project directory entity from original path
|
|
194
|
+
*/
|
|
195
|
+
async createProjectDirectory(
|
|
196
|
+
originalPath: string,
|
|
197
|
+
baseSessionDir: string,
|
|
198
|
+
): Promise<ProjectDirectory> {
|
|
199
|
+
// Resolve the original path and check for symbolic links
|
|
200
|
+
const expandedPath = this.expandTilde(originalPath);
|
|
201
|
+
const absolutePath = resolve(expandedPath);
|
|
202
|
+
|
|
203
|
+
let resolvedPath: string;
|
|
204
|
+
let isSymbolicLink = false;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
resolvedPath = await realpath(absolutePath);
|
|
208
|
+
isSymbolicLink = resolvedPath !== absolutePath;
|
|
209
|
+
} catch {
|
|
210
|
+
// If realpath fails, use the absolute path
|
|
211
|
+
resolvedPath = absolutePath;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Encode the resolved path
|
|
215
|
+
const encodedName = await this.encode(resolvedPath);
|
|
216
|
+
const encodedPath = join(baseSessionDir, encodedName);
|
|
217
|
+
|
|
218
|
+
// Generate hash if encoding resulted in truncation
|
|
219
|
+
let pathHash: string | undefined;
|
|
220
|
+
if (resolvedPath.length > this.options.maxLength) {
|
|
221
|
+
pathHash = this.generateHash(resolvedPath, this.options.hashLength);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Ensure the encoded directory exists
|
|
225
|
+
try {
|
|
226
|
+
await mkdir(encodedPath, { recursive: true });
|
|
227
|
+
} catch {
|
|
228
|
+
// Ignore errors if directory already exists
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
originalPath: resolvedPath,
|
|
233
|
+
encodedName,
|
|
234
|
+
encodedPath,
|
|
235
|
+
pathHash,
|
|
236
|
+
isSymbolicLink,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Validate that an encoded name is filesystem-safe
|
|
242
|
+
*/
|
|
243
|
+
validateEncodedName(encodedName: string): boolean {
|
|
244
|
+
// Check length
|
|
245
|
+
if (encodedName.length > this.constraints.maxDirectoryNameLength) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Check for invalid characters
|
|
250
|
+
for (const char of this.constraints.invalidCharacters) {
|
|
251
|
+
if (encodedName.includes(char)) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check for reserved names
|
|
257
|
+
const lowerName = encodedName.toLowerCase();
|
|
258
|
+
if (
|
|
259
|
+
this.constraints.reservedNames.some(
|
|
260
|
+
(reserved) => reserved.toLowerCase() === lowerName,
|
|
261
|
+
)
|
|
262
|
+
) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Check for empty or dots-only names
|
|
267
|
+
if (!encodedName.trim() || /^\.+$/.test(encodedName)) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Handle encoding collisions by generating unique names
|
|
276
|
+
*/
|
|
277
|
+
resolveCollision(baseName: string, existingNames: Set<string>): string {
|
|
278
|
+
if (!existingNames.has(baseName)) {
|
|
279
|
+
return baseName;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Try numbered suffixes first
|
|
283
|
+
for (let i = 1; i <= 999; i++) {
|
|
284
|
+
const candidate = `${baseName}-${i}`;
|
|
285
|
+
if (!existingNames.has(candidate)) {
|
|
286
|
+
return candidate;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// If all numbered suffixes are taken, use hash
|
|
291
|
+
const hash = this.generateHash(
|
|
292
|
+
baseName + Date.now(),
|
|
293
|
+
this.options.hashLength,
|
|
294
|
+
);
|
|
295
|
+
return `${baseName}-${hash}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get platform-specific filesystem constraints
|
|
300
|
+
*/
|
|
301
|
+
private getFilesystemConstraints(): FilesystemConstraints {
|
|
302
|
+
const currentPlatform = platform();
|
|
303
|
+
|
|
304
|
+
switch (currentPlatform) {
|
|
305
|
+
case "win32":
|
|
306
|
+
return {
|
|
307
|
+
maxDirectoryNameLength: 255,
|
|
308
|
+
maxPathLength: 260,
|
|
309
|
+
invalidCharacters: ["<", ">", ":", '"', "|", "?", "*"],
|
|
310
|
+
reservedNames: [
|
|
311
|
+
"CON",
|
|
312
|
+
"PRN",
|
|
313
|
+
"AUX",
|
|
314
|
+
"NUL",
|
|
315
|
+
"COM1",
|
|
316
|
+
"COM2",
|
|
317
|
+
"COM3",
|
|
318
|
+
"COM4",
|
|
319
|
+
"COM5",
|
|
320
|
+
"COM6",
|
|
321
|
+
"COM7",
|
|
322
|
+
"COM8",
|
|
323
|
+
"COM9",
|
|
324
|
+
"LPT1",
|
|
325
|
+
"LPT2",
|
|
326
|
+
"LPT3",
|
|
327
|
+
"LPT4",
|
|
328
|
+
"LPT5",
|
|
329
|
+
"LPT6",
|
|
330
|
+
"LPT7",
|
|
331
|
+
"LPT8",
|
|
332
|
+
"LPT9",
|
|
333
|
+
],
|
|
334
|
+
caseSensitive: false,
|
|
335
|
+
};
|
|
336
|
+
case "darwin":
|
|
337
|
+
return {
|
|
338
|
+
maxDirectoryNameLength: 255,
|
|
339
|
+
maxPathLength: 1024,
|
|
340
|
+
invalidCharacters: [":"],
|
|
341
|
+
reservedNames: [],
|
|
342
|
+
caseSensitive: false, // HFS+ is case-insensitive by default
|
|
343
|
+
};
|
|
344
|
+
default: // Linux and other Unix-like systems
|
|
345
|
+
return {
|
|
346
|
+
maxDirectoryNameLength: 255,
|
|
347
|
+
maxPathLength: 4096,
|
|
348
|
+
invalidCharacters: ["\0"],
|
|
349
|
+
reservedNames: [],
|
|
350
|
+
caseSensitive: true,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Generate hash for collision resolution
|
|
357
|
+
*/
|
|
358
|
+
private generateHash(input: string, length: number): string {
|
|
359
|
+
return createHash("sha256")
|
|
360
|
+
.update(input)
|
|
361
|
+
.digest("hex")
|
|
362
|
+
.substring(0, length);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Expand tilde (~) to home directory
|
|
367
|
+
*/
|
|
368
|
+
private expandTilde(path: string): string {
|
|
369
|
+
if (path.startsWith("~/") || path === "~") {
|
|
370
|
+
return path.replace(/^~/, homedir());
|
|
371
|
+
}
|
|
372
|
+
return path;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Default PathEncoder instance
|
|
378
|
+
*/
|
|
379
|
+
export const pathEncoder = new PathEncoder();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, statSync } from "fs";
|
|
2
2
|
import { join, extname } from "path";
|
|
3
|
+
import { logger } from "./globalLogger.js";
|
|
3
4
|
|
|
4
5
|
export interface SubagentConfiguration {
|
|
5
6
|
name: string;
|
|
@@ -172,7 +173,7 @@ function scanSubagentDirectory(
|
|
|
172
173
|
configurations.push(config);
|
|
173
174
|
} catch (parseError) {
|
|
174
175
|
// Log error but continue with other files
|
|
175
|
-
|
|
176
|
+
logger.warn(
|
|
176
177
|
`Warning: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
|
177
178
|
);
|
|
178
179
|
}
|