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.
Files changed (180) hide show
  1. package/dist/agent.d.ts +32 -20
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +209 -24
  4. package/dist/constants/events.d.ts +28 -0
  5. package/dist/constants/events.d.ts.map +1 -0
  6. package/dist/constants/events.js +27 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +2 -0
  10. package/dist/managers/aiManager.d.ts +34 -1
  11. package/dist/managers/aiManager.d.ts.map +1 -1
  12. package/dist/managers/aiManager.js +248 -132
  13. package/dist/managers/backgroundBashManager.d.ts.map +1 -1
  14. package/dist/managers/backgroundBashManager.js +7 -6
  15. package/dist/managers/hookManager.d.ts +13 -16
  16. package/dist/managers/hookManager.d.ts.map +1 -1
  17. package/dist/managers/hookManager.js +81 -44
  18. package/dist/managers/liveConfigManager.d.ts +58 -0
  19. package/dist/managers/liveConfigManager.d.ts.map +1 -0
  20. package/dist/managers/liveConfigManager.js +160 -0
  21. package/dist/managers/messageManager.d.ts +41 -24
  22. package/dist/managers/messageManager.d.ts.map +1 -1
  23. package/dist/managers/messageManager.js +168 -49
  24. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  25. package/dist/managers/slashCommandManager.js +9 -3
  26. package/dist/managers/subagentManager.d.ts +51 -0
  27. package/dist/managers/subagentManager.d.ts.map +1 -1
  28. package/dist/managers/subagentManager.js +190 -19
  29. package/dist/services/aiService.d.ts +13 -5
  30. package/dist/services/aiService.d.ts.map +1 -1
  31. package/dist/services/aiService.js +350 -74
  32. package/dist/services/configurationWatcher.d.ts +120 -0
  33. package/dist/services/configurationWatcher.d.ts.map +1 -0
  34. package/dist/services/configurationWatcher.js +439 -0
  35. package/dist/services/fileWatcher.d.ts +69 -0
  36. package/dist/services/fileWatcher.d.ts.map +1 -0
  37. package/dist/services/fileWatcher.js +213 -0
  38. package/dist/services/hook.d.ts +91 -9
  39. package/dist/services/hook.d.ts.map +1 -1
  40. package/dist/services/hook.js +393 -43
  41. package/dist/services/jsonlHandler.d.ts +62 -0
  42. package/dist/services/jsonlHandler.d.ts.map +1 -0
  43. package/dist/services/jsonlHandler.js +257 -0
  44. package/dist/services/memory.d.ts +9 -0
  45. package/dist/services/memory.d.ts.map +1 -1
  46. package/dist/services/memory.js +81 -12
  47. package/dist/services/memoryStore.d.ts +81 -0
  48. package/dist/services/memoryStore.d.ts.map +1 -0
  49. package/dist/services/memoryStore.js +200 -0
  50. package/dist/services/session.d.ts +64 -49
  51. package/dist/services/session.d.ts.map +1 -1
  52. package/dist/services/session.js +310 -132
  53. package/dist/tools/bashTool.d.ts.map +1 -1
  54. package/dist/tools/bashTool.js +5 -4
  55. package/dist/tools/deleteFileTool.d.ts.map +1 -1
  56. package/dist/tools/deleteFileTool.js +2 -1
  57. package/dist/tools/editTool.d.ts.map +1 -1
  58. package/dist/tools/editTool.js +3 -2
  59. package/dist/tools/multiEditTool.d.ts.map +1 -1
  60. package/dist/tools/multiEditTool.js +4 -3
  61. package/dist/tools/readTool.d.ts.map +1 -1
  62. package/dist/tools/readTool.js +2 -1
  63. package/dist/tools/todoWriteTool.d.ts.map +1 -1
  64. package/dist/tools/todoWriteTool.js +3 -10
  65. package/dist/tools/writeTool.d.ts.map +1 -1
  66. package/dist/tools/writeTool.js +5 -6
  67. package/dist/types/commands.d.ts +4 -0
  68. package/dist/types/commands.d.ts.map +1 -1
  69. package/dist/types/core.d.ts +35 -0
  70. package/dist/types/core.d.ts.map +1 -1
  71. package/dist/types/environment.d.ts +42 -0
  72. package/dist/types/environment.d.ts.map +1 -0
  73. package/dist/types/environment.js +21 -0
  74. package/dist/types/hooks.d.ts +8 -2
  75. package/dist/types/hooks.d.ts.map +1 -1
  76. package/dist/types/hooks.js +8 -2
  77. package/dist/types/index.d.ts +2 -0
  78. package/dist/types/index.d.ts.map +1 -1
  79. package/dist/types/index.js +2 -0
  80. package/dist/types/memoryStore.d.ts +82 -0
  81. package/dist/types/memoryStore.d.ts.map +1 -0
  82. package/dist/types/memoryStore.js +7 -0
  83. package/dist/types/messaging.d.ts +21 -9
  84. package/dist/types/messaging.d.ts.map +1 -1
  85. package/dist/types/messaging.js +5 -1
  86. package/dist/types/session.d.ts +20 -0
  87. package/dist/types/session.d.ts.map +1 -0
  88. package/dist/types/session.js +7 -0
  89. package/dist/utils/bashHistory.d.ts.map +1 -1
  90. package/dist/utils/bashHistory.js +27 -26
  91. package/dist/utils/cacheControlUtils.d.ts +121 -0
  92. package/dist/utils/cacheControlUtils.d.ts.map +1 -0
  93. package/dist/utils/cacheControlUtils.js +367 -0
  94. package/dist/utils/commandPathResolver.d.ts +52 -0
  95. package/dist/utils/commandPathResolver.d.ts.map +1 -0
  96. package/dist/utils/commandPathResolver.js +145 -0
  97. package/dist/utils/configPaths.d.ts +85 -0
  98. package/dist/utils/configPaths.d.ts.map +1 -0
  99. package/dist/utils/configPaths.js +121 -0
  100. package/dist/utils/configResolver.d.ts +37 -10
  101. package/dist/utils/configResolver.d.ts.map +1 -1
  102. package/dist/utils/configResolver.js +127 -23
  103. package/dist/utils/constants.d.ts +1 -1
  104. package/dist/utils/constants.js +1 -1
  105. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  106. package/dist/utils/convertMessagesForAPI.js +8 -13
  107. package/dist/utils/customCommands.d.ts.map +1 -1
  108. package/dist/utils/customCommands.js +66 -21
  109. package/dist/utils/fileUtils.d.ts +15 -0
  110. package/dist/utils/fileUtils.d.ts.map +1 -0
  111. package/dist/utils/fileUtils.js +61 -0
  112. package/dist/utils/globalLogger.d.ts +102 -0
  113. package/dist/utils/globalLogger.d.ts.map +1 -0
  114. package/dist/utils/globalLogger.js +136 -0
  115. package/dist/utils/hookMatcher.d.ts +1 -6
  116. package/dist/utils/hookMatcher.d.ts.map +1 -1
  117. package/dist/utils/mcpUtils.d.ts.map +1 -1
  118. package/dist/utils/mcpUtils.js +25 -3
  119. package/dist/utils/messageOperations.d.ts +27 -27
  120. package/dist/utils/messageOperations.d.ts.map +1 -1
  121. package/dist/utils/messageOperations.js +46 -36
  122. package/dist/utils/pathEncoder.d.ts +104 -0
  123. package/dist/utils/pathEncoder.d.ts.map +1 -0
  124. package/dist/utils/pathEncoder.js +272 -0
  125. package/dist/utils/subagentParser.d.ts.map +1 -1
  126. package/dist/utils/subagentParser.js +2 -1
  127. package/dist/utils/tokenCalculation.d.ts +26 -0
  128. package/dist/utils/tokenCalculation.d.ts.map +1 -0
  129. package/dist/utils/tokenCalculation.js +36 -0
  130. package/package.json +6 -3
  131. package/src/agent.ts +301 -37
  132. package/src/constants/events.ts +38 -0
  133. package/src/index.ts +2 -0
  134. package/src/managers/aiManager.ts +325 -173
  135. package/src/managers/backgroundBashManager.ts +7 -6
  136. package/src/managers/hookManager.ts +106 -84
  137. package/src/managers/liveConfigManager.ts +248 -0
  138. package/src/managers/messageManager.ts +237 -100
  139. package/src/managers/slashCommandManager.ts +9 -7
  140. package/src/managers/subagentManager.ts +284 -22
  141. package/src/services/aiService.ts +474 -83
  142. package/src/services/configurationWatcher.ts +622 -0
  143. package/src/services/fileWatcher.ts +301 -0
  144. package/src/services/hook.ts +538 -47
  145. package/src/services/jsonlHandler.ts +319 -0
  146. package/src/services/memory.ts +92 -12
  147. package/src/services/memoryStore.ts +279 -0
  148. package/src/services/session.ts +381 -157
  149. package/src/tools/bashTool.ts +5 -4
  150. package/src/tools/deleteFileTool.ts +2 -1
  151. package/src/tools/editTool.ts +3 -2
  152. package/src/tools/multiEditTool.ts +4 -3
  153. package/src/tools/readTool.ts +2 -1
  154. package/src/tools/todoWriteTool.ts +3 -11
  155. package/src/tools/writeTool.ts +7 -6
  156. package/src/types/commands.ts +6 -0
  157. package/src/types/core.ts +44 -0
  158. package/src/types/environment.ts +60 -0
  159. package/src/types/hooks.ts +21 -8
  160. package/src/types/index.ts +2 -0
  161. package/src/types/memoryStore.ts +94 -0
  162. package/src/types/messaging.ts +21 -10
  163. package/src/types/session.ts +25 -0
  164. package/src/utils/bashHistory.ts +27 -27
  165. package/src/utils/cacheControlUtils.ts +540 -0
  166. package/src/utils/commandPathResolver.ts +189 -0
  167. package/src/utils/configPaths.ts +163 -0
  168. package/src/utils/configResolver.ts +182 -22
  169. package/src/utils/constants.ts +1 -1
  170. package/src/utils/convertMessagesForAPI.ts +8 -14
  171. package/src/utils/customCommands.ts +90 -22
  172. package/src/utils/fileUtils.ts +65 -0
  173. package/src/utils/globalLogger.ts +145 -0
  174. package/src/utils/hookMatcher.ts +1 -12
  175. package/src/utils/mcpUtils.ts +34 -3
  176. package/src/utils/messageOperations.ts +77 -60
  177. package/src/utils/pathEncoder.ts +379 -0
  178. package/src/utils/subagentParser.ts +2 -1
  179. package/src/utils/tokenCalculation.ts +43 -0
  180. 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
- // Parameter interfaces for message operations
7
- export interface AddUserMessageParams {
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
- customCommandBlock?: {
12
- type: "custom_command";
13
- commandName: string;
14
- content: string;
15
- originalInput?: string;
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
- isRunning?: boolean;
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 interface AgentToolBlockUpdateParams {
35
- toolId: string;
36
- args?: string;
37
- result?: string;
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 all text block content and merge
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
- // logger.error(`Failed to convert image to base64: ${imagePath}`, error);
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
- customCommandBlock,
153
+ customCommandContent,
154
+ source,
149
155
  }: AddUserMessageParams): Message[] => {
150
156
  const blocks: Message["blocks"] = [];
151
157
 
152
- // If there's a custom command block, use it instead of text content
153
- if (customCommandBlock) {
154
- blocks.push(customCommandBlock);
155
- } else {
156
- blocks.push({ type: "text", content });
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
- isRunning: false,
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
- isRunning,
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 (isRunning !== undefined) toolBlock.isRunning = isRunning;
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 if (result !== undefined) {
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
- isRunning: isRunning ?? false,
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
- // Find the last assistant message
298
- let assistantMessageFound = false;
299
- for (let i = newMessages.length - 1; i >= 0; i--) {
300
- if (newMessages[i].role === "assistant") {
301
- newMessages[i].blocks = [
302
- ...newMessages[i].blocks,
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
- assistantMessageFound = true;
309
- break;
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
- subagentMessages?: Message[];
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
- subagentMessages: Message[];
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
- subagentMessages = [],
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
- messages: subagentMessages,
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
- messages: Message[];
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.messages !== undefined) {
579
- block.messages = updates.messages;
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
- console.warn(
176
+ logger.warn(
176
177
  `Warning: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
177
178
  );
178
179
  }