wave-agent-sdk 0.0.7 → 0.0.10

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 (240) hide show
  1. package/dist/agent.d.ts +105 -24
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +438 -53
  4. package/dist/index.d.ts +4 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +4 -0
  7. package/dist/managers/aiManager.d.ts +18 -7
  8. package/dist/managers/aiManager.d.ts.map +1 -1
  9. package/dist/managers/aiManager.js +254 -142
  10. package/dist/managers/backgroundBashManager.d.ts.map +1 -1
  11. package/dist/managers/backgroundBashManager.js +11 -9
  12. package/dist/managers/hookManager.d.ts +6 -6
  13. package/dist/managers/hookManager.d.ts.map +1 -1
  14. package/dist/managers/hookManager.js +81 -39
  15. package/dist/managers/liveConfigManager.d.ts +95 -0
  16. package/dist/managers/liveConfigManager.d.ts.map +1 -0
  17. package/dist/managers/liveConfigManager.js +442 -0
  18. package/dist/managers/lspManager.d.ts +43 -0
  19. package/dist/managers/lspManager.d.ts.map +1 -0
  20. package/dist/managers/lspManager.js +326 -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 +184 -73
  24. package/dist/managers/permissionManager.d.ts +66 -0
  25. package/dist/managers/permissionManager.d.ts.map +1 -0
  26. package/dist/managers/permissionManager.js +208 -0
  27. package/dist/managers/skillManager.d.ts +1 -0
  28. package/dist/managers/skillManager.d.ts.map +1 -1
  29. package/dist/managers/skillManager.js +2 -1
  30. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  31. package/dist/managers/slashCommandManager.js +4 -2
  32. package/dist/managers/subagentManager.d.ts +42 -6
  33. package/dist/managers/subagentManager.d.ts.map +1 -1
  34. package/dist/managers/subagentManager.js +213 -62
  35. package/dist/managers/toolManager.d.ts +38 -1
  36. package/dist/managers/toolManager.d.ts.map +1 -1
  37. package/dist/managers/toolManager.js +66 -2
  38. package/dist/services/aiService.d.ts +15 -5
  39. package/dist/services/aiService.d.ts.map +1 -1
  40. package/dist/services/aiService.js +446 -77
  41. package/dist/services/configurationService.d.ts +116 -0
  42. package/dist/services/configurationService.d.ts.map +1 -0
  43. package/dist/services/configurationService.js +585 -0
  44. package/dist/services/fileWatcher.d.ts +69 -0
  45. package/dist/services/fileWatcher.d.ts.map +1 -0
  46. package/dist/services/fileWatcher.js +212 -0
  47. package/dist/services/hook.d.ts +5 -40
  48. package/dist/services/hook.d.ts.map +1 -1
  49. package/dist/services/hook.js +47 -109
  50. package/dist/services/jsonlHandler.d.ts +71 -0
  51. package/dist/services/jsonlHandler.d.ts.map +1 -0
  52. package/dist/services/jsonlHandler.js +236 -0
  53. package/dist/services/memory.d.ts.map +1 -1
  54. package/dist/services/memory.js +33 -11
  55. package/dist/services/session.d.ts +116 -52
  56. package/dist/services/session.d.ts.map +1 -1
  57. package/dist/services/session.js +415 -143
  58. package/dist/tools/bashTool.d.ts.map +1 -1
  59. package/dist/tools/bashTool.js +77 -17
  60. package/dist/tools/deleteFileTool.d.ts.map +1 -1
  61. package/dist/tools/deleteFileTool.js +27 -1
  62. package/dist/tools/editTool.d.ts.map +1 -1
  63. package/dist/tools/editTool.js +33 -8
  64. package/dist/tools/lspTool.d.ts +6 -0
  65. package/dist/tools/lspTool.d.ts.map +1 -0
  66. package/dist/tools/lspTool.js +589 -0
  67. package/dist/tools/multiEditTool.d.ts.map +1 -1
  68. package/dist/tools/multiEditTool.js +30 -10
  69. package/dist/tools/readTool.d.ts.map +1 -1
  70. package/dist/tools/readTool.js +113 -3
  71. package/dist/tools/skillTool.js +2 -2
  72. package/dist/tools/todoWriteTool.d.ts.map +1 -1
  73. package/dist/tools/todoWriteTool.js +23 -0
  74. package/dist/tools/types.d.ts +11 -8
  75. package/dist/tools/types.d.ts.map +1 -1
  76. package/dist/tools/writeTool.d.ts.map +1 -1
  77. package/dist/tools/writeTool.js +30 -15
  78. package/dist/types/commands.d.ts +4 -1
  79. package/dist/types/commands.d.ts.map +1 -1
  80. package/dist/types/config.d.ts +4 -0
  81. package/dist/types/config.d.ts.map +1 -1
  82. package/dist/types/configuration.d.ts +69 -0
  83. package/dist/types/configuration.d.ts.map +1 -0
  84. package/dist/types/configuration.js +8 -0
  85. package/dist/types/core.d.ts +45 -0
  86. package/dist/types/core.d.ts.map +1 -1
  87. package/dist/types/environment.d.ts +83 -0
  88. package/dist/types/environment.d.ts.map +1 -0
  89. package/dist/types/environment.js +21 -0
  90. package/dist/types/fileSearch.d.ts +5 -0
  91. package/dist/types/fileSearch.d.ts.map +1 -0
  92. package/dist/types/fileSearch.js +1 -0
  93. package/dist/types/hooks.d.ts +18 -3
  94. package/dist/types/hooks.d.ts.map +1 -1
  95. package/dist/types/hooks.js +8 -8
  96. package/dist/types/index.d.ts +7 -0
  97. package/dist/types/index.d.ts.map +1 -1
  98. package/dist/types/index.js +7 -0
  99. package/dist/types/lsp.d.ts +90 -0
  100. package/dist/types/lsp.d.ts.map +1 -0
  101. package/dist/types/lsp.js +4 -0
  102. package/dist/types/messaging.d.ts +19 -12
  103. package/dist/types/messaging.d.ts.map +1 -1
  104. package/dist/types/permissions.d.ts +35 -0
  105. package/dist/types/permissions.d.ts.map +1 -0
  106. package/dist/types/permissions.js +12 -0
  107. package/dist/types/session.d.ts +15 -0
  108. package/dist/types/session.d.ts.map +1 -0
  109. package/dist/types/session.js +7 -0
  110. package/dist/types/skills.d.ts +1 -0
  111. package/dist/types/skills.d.ts.map +1 -1
  112. package/dist/types/tools.d.ts +35 -0
  113. package/dist/types/tools.d.ts.map +1 -0
  114. package/dist/types/tools.js +4 -0
  115. package/dist/utils/abortUtils.d.ts +34 -0
  116. package/dist/utils/abortUtils.d.ts.map +1 -0
  117. package/dist/utils/abortUtils.js +92 -0
  118. package/dist/utils/bashHistory.d.ts +4 -0
  119. package/dist/utils/bashHistory.d.ts.map +1 -1
  120. package/dist/utils/bashHistory.js +48 -30
  121. package/dist/utils/builtinSubagents.d.ts +7 -0
  122. package/dist/utils/builtinSubagents.d.ts.map +1 -0
  123. package/dist/utils/builtinSubagents.js +65 -0
  124. package/dist/utils/cacheControlUtils.d.ts +96 -0
  125. package/dist/utils/cacheControlUtils.d.ts.map +1 -0
  126. package/dist/utils/cacheControlUtils.js +324 -0
  127. package/dist/utils/commandPathResolver.d.ts +52 -0
  128. package/dist/utils/commandPathResolver.d.ts.map +1 -0
  129. package/dist/utils/commandPathResolver.js +145 -0
  130. package/dist/utils/configPaths.d.ts +85 -0
  131. package/dist/utils/configPaths.d.ts.map +1 -0
  132. package/dist/utils/configPaths.js +121 -0
  133. package/dist/utils/constants.d.ts +1 -13
  134. package/dist/utils/constants.d.ts.map +1 -1
  135. package/dist/utils/constants.js +2 -14
  136. package/dist/utils/convertMessagesForAPI.d.ts +2 -1
  137. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  138. package/dist/utils/convertMessagesForAPI.js +39 -18
  139. package/dist/utils/customCommands.d.ts.map +1 -1
  140. package/dist/utils/customCommands.js +66 -21
  141. package/dist/utils/fileSearch.d.ts +14 -0
  142. package/dist/utils/fileSearch.d.ts.map +1 -0
  143. package/dist/utils/fileSearch.js +88 -0
  144. package/dist/utils/fileUtils.d.ts +27 -0
  145. package/dist/utils/fileUtils.d.ts.map +1 -0
  146. package/dist/utils/fileUtils.js +145 -0
  147. package/dist/utils/globalLogger.d.ts +88 -0
  148. package/dist/utils/globalLogger.d.ts.map +1 -0
  149. package/dist/utils/globalLogger.js +120 -0
  150. package/dist/utils/largeOutputHandler.d.ts +15 -0
  151. package/dist/utils/largeOutputHandler.d.ts.map +1 -0
  152. package/dist/utils/largeOutputHandler.js +40 -0
  153. package/dist/utils/markdownParser.d.ts.map +1 -1
  154. package/dist/utils/markdownParser.js +1 -17
  155. package/dist/utils/mcpUtils.d.ts.map +1 -1
  156. package/dist/utils/mcpUtils.js +25 -3
  157. package/dist/utils/messageOperations.d.ts +20 -18
  158. package/dist/utils/messageOperations.d.ts.map +1 -1
  159. package/dist/utils/messageOperations.js +30 -38
  160. package/dist/utils/pathEncoder.d.ts +108 -0
  161. package/dist/utils/pathEncoder.d.ts.map +1 -0
  162. package/dist/utils/pathEncoder.js +279 -0
  163. package/dist/utils/subagentParser.d.ts +2 -2
  164. package/dist/utils/subagentParser.d.ts.map +1 -1
  165. package/dist/utils/subagentParser.js +12 -8
  166. package/dist/utils/tokenCalculation.d.ts +26 -0
  167. package/dist/utils/tokenCalculation.d.ts.map +1 -0
  168. package/dist/utils/tokenCalculation.js +36 -0
  169. package/dist/utils/tokenEstimator.d.ts +39 -0
  170. package/dist/utils/tokenEstimator.d.ts.map +1 -0
  171. package/dist/utils/tokenEstimator.js +55 -0
  172. package/package.json +6 -6
  173. package/src/agent.ts +586 -78
  174. package/src/index.ts +4 -0
  175. package/src/managers/aiManager.ts +341 -192
  176. package/src/managers/backgroundBashManager.ts +11 -9
  177. package/src/managers/hookManager.ts +102 -54
  178. package/src/managers/liveConfigManager.ts +634 -0
  179. package/src/managers/lspManager.ts +434 -0
  180. package/src/managers/messageManager.ts +258 -121
  181. package/src/managers/permissionManager.ts +276 -0
  182. package/src/managers/skillManager.ts +3 -1
  183. package/src/managers/slashCommandManager.ts +5 -3
  184. package/src/managers/subagentManager.ts +295 -76
  185. package/src/managers/toolManager.ts +95 -3
  186. package/src/services/aiService.ts +656 -84
  187. package/src/services/configurationService.ts +762 -0
  188. package/src/services/fileWatcher.ts +300 -0
  189. package/src/services/hook.ts +54 -144
  190. package/src/services/jsonlHandler.ts +303 -0
  191. package/src/services/memory.ts +34 -11
  192. package/src/services/session.ts +522 -173
  193. package/src/tools/bashTool.ts +94 -20
  194. package/src/tools/deleteFileTool.ts +38 -1
  195. package/src/tools/editTool.ts +44 -9
  196. package/src/tools/lspTool.ts +760 -0
  197. package/src/tools/multiEditTool.ts +41 -11
  198. package/src/tools/readTool.ts +127 -3
  199. package/src/tools/skillTool.ts +2 -2
  200. package/src/tools/todoWriteTool.ts +33 -1
  201. package/src/tools/types.ts +15 -9
  202. package/src/tools/writeTool.ts +43 -16
  203. package/src/types/commands.ts +6 -1
  204. package/src/types/config.ts +5 -0
  205. package/src/types/configuration.ts +73 -0
  206. package/src/types/core.ts +55 -0
  207. package/src/types/environment.ts +104 -0
  208. package/src/types/fileSearch.ts +4 -0
  209. package/src/types/hooks.ts +32 -16
  210. package/src/types/index.ts +7 -0
  211. package/src/types/lsp.ts +96 -0
  212. package/src/types/messaging.ts +21 -14
  213. package/src/types/permissions.ts +48 -0
  214. package/src/types/session.ts +20 -0
  215. package/src/types/skills.ts +1 -0
  216. package/src/types/tools.ts +38 -0
  217. package/src/utils/abortUtils.ts +118 -0
  218. package/src/utils/bashHistory.ts +55 -31
  219. package/src/utils/builtinSubagents.ts +71 -0
  220. package/src/utils/cacheControlUtils.ts +475 -0
  221. package/src/utils/commandPathResolver.ts +189 -0
  222. package/src/utils/configPaths.ts +163 -0
  223. package/src/utils/constants.ts +2 -17
  224. package/src/utils/convertMessagesForAPI.ts +44 -18
  225. package/src/utils/customCommands.ts +90 -22
  226. package/src/utils/fileSearch.ts +107 -0
  227. package/src/utils/fileUtils.ts +160 -0
  228. package/src/utils/globalLogger.ts +128 -0
  229. package/src/utils/largeOutputHandler.ts +55 -0
  230. package/src/utils/markdownParser.ts +1 -19
  231. package/src/utils/mcpUtils.ts +34 -3
  232. package/src/utils/messageOperations.ts +47 -53
  233. package/src/utils/pathEncoder.ts +394 -0
  234. package/src/utils/subagentParser.ts +13 -9
  235. package/src/utils/tokenCalculation.ts +43 -0
  236. package/src/utils/tokenEstimator.ts +68 -0
  237. package/dist/utils/configResolver.d.ts +0 -38
  238. package/dist/utils/configResolver.d.ts.map +0 -1
  239. package/dist/utils/configResolver.js +0 -106
  240. package/src/utils/configResolver.ts +0 -142
@@ -0,0 +1,475 @@
1
+ /**
2
+ * Cache Control Utilities for Claude Models
3
+ *
4
+ * This module provides utilities for adding cache_control markers to Claude models
5
+ * to optimize token usage and reduce costs. Cache control is only applied to Claude
6
+ * models and preserves backward compatibility with existing message formats.
7
+ */
8
+
9
+ import type {
10
+ ChatCompletionMessageParam,
11
+ ChatCompletionContentPart,
12
+ ChatCompletionContentPartText,
13
+ ChatCompletionFunctionTool,
14
+ ChatCompletionMessageToolCall,
15
+ CompletionUsage,
16
+ } from "openai/resources";
17
+ import { logger } from "./globalLogger.js";
18
+
19
+ // ============================================================================
20
+ // Core Types
21
+ // ============================================================================
22
+
23
+ /**
24
+ * Cache control directive for Claude models
25
+ */
26
+ export interface CacheControl {
27
+ type: "ephemeral";
28
+ }
29
+
30
+ /**
31
+ * Extended text content part with cache control support
32
+ */
33
+ export interface ClaudeChatCompletionContentPartText
34
+ extends ChatCompletionContentPartText {
35
+ type: "text";
36
+ text: string;
37
+ cache_control?: CacheControl;
38
+ }
39
+
40
+ /**
41
+ * Extended tool definition with cache control support
42
+ */
43
+ export interface ClaudeChatCompletionFunctionTool
44
+ extends ChatCompletionFunctionTool {
45
+ type: "function";
46
+ function: ChatCompletionFunctionTool["function"];
47
+ cache_control?: CacheControl;
48
+ }
49
+
50
+ /**
51
+ * Enhanced usage metrics including Claude cache information
52
+ */
53
+ export interface ClaudeUsage extends CompletionUsage {
54
+ prompt_tokens: number;
55
+ completion_tokens: number;
56
+ total_tokens: number;
57
+
58
+ // Claude cache extensions
59
+ cache_read_input_tokens?: number;
60
+ cache_creation_input_tokens?: number;
61
+ cache_creation?: {
62
+ ephemeral_5m_input_tokens: number;
63
+ ephemeral_1h_input_tokens: number;
64
+ };
65
+ }
66
+
67
+ // ============================================================================
68
+ // Default Configuration
69
+ // ============================================================================
70
+
71
+ // ============================================================================
72
+ // Utility Functions (Basic Structure - to be implemented)
73
+ // ============================================================================
74
+
75
+ /**
76
+ * Determines if a model supports cache control
77
+ * @param modelName - Model identifier
78
+ * @returns True if model name contains 'claude' (case-insensitive)
79
+ */
80
+ export function isClaudeModel(modelName: string): boolean {
81
+ // Handle null, undefined, and non-string inputs
82
+ if (!modelName || typeof modelName !== "string") {
83
+ return false;
84
+ }
85
+
86
+ // Handle empty strings and whitespace-only strings
87
+ const trimmed = modelName.trim();
88
+ if (trimmed.length === 0) {
89
+ return false;
90
+ }
91
+
92
+ return trimmed.toLowerCase().includes("claude");
93
+ }
94
+
95
+ /**
96
+ * Validates cache control structure
97
+ * @param control - Object to validate
98
+ * @returns True if valid cache control object
99
+ */
100
+ export function isValidCacheControl(control: unknown): control is CacheControl {
101
+ return (
102
+ control !== null &&
103
+ typeof control === "object" &&
104
+ control !== undefined &&
105
+ "type" in control &&
106
+ (control as { type: unknown }).type === "ephemeral"
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Adds cache control to the last tool call in an array
112
+ * @param toolCalls - Array of tool calls
113
+ * @returns Tool calls array with cache control on the last tool call
114
+ */
115
+ function addCacheControlToLastToolCall(
116
+ toolCalls: ChatCompletionMessageToolCall[],
117
+ ): ChatCompletionMessageToolCall[] {
118
+ if (!toolCalls || toolCalls.length === 0) {
119
+ return toolCalls;
120
+ }
121
+
122
+ const result = [...toolCalls];
123
+ const lastIndex = result.length - 1;
124
+
125
+ // Add cache control to the last tool call
126
+ result[lastIndex] = {
127
+ ...result[lastIndex],
128
+ cache_control: { type: "ephemeral" },
129
+ } as ChatCompletionMessageToolCall & { cache_control: CacheControl };
130
+
131
+ return result;
132
+ }
133
+
134
+ /**
135
+ * Adds cache control markers to message content
136
+ * @param content - Original content (string or structured)
137
+ * @param shouldCache - Whether to add cache control
138
+ * @returns Structured content with cache control markers
139
+ */
140
+ export function addCacheControlToContent(
141
+ content: string | ChatCompletionContentPart[],
142
+ shouldCache: boolean,
143
+ ): ClaudeChatCompletionContentPartText[] {
144
+ // Handle null/undefined content
145
+ if (content == null) {
146
+ return [];
147
+ }
148
+
149
+ // If shouldCache is false, return content as text parts without cache control
150
+ if (!shouldCache) {
151
+ if (typeof content === "string") {
152
+ return [{ type: "text", text: content }];
153
+ }
154
+
155
+ // Validate array input
156
+ if (!Array.isArray(content)) {
157
+ logger.warn(
158
+ "Invalid content type for cache control transformation:",
159
+ typeof content,
160
+ );
161
+ return [];
162
+ }
163
+
164
+ // Filter and convert only text parts with validation
165
+ return content
166
+ .filter((part): part is ChatCompletionContentPartText => {
167
+ if (!part || typeof part !== "object") {
168
+ return false;
169
+ }
170
+ return part.type === "text" && typeof part.text === "string";
171
+ })
172
+ .map((part) => ({ type: "text", text: part.text }));
173
+ }
174
+
175
+ // shouldCache is true - add cache control markers
176
+ if (typeof content === "string") {
177
+ // Transform string content to structured array with cache control
178
+ return [
179
+ {
180
+ type: "text",
181
+ text: content,
182
+ cache_control: { type: "ephemeral" },
183
+ },
184
+ ];
185
+ }
186
+
187
+ // Validate array input
188
+ if (!Array.isArray(content)) {
189
+ logger.warn(
190
+ "Invalid content type for cache control transformation:",
191
+ typeof content,
192
+ );
193
+ return [];
194
+ }
195
+
196
+ // Handle structured content - preserve existing structure, add cache control to text parts
197
+ return content
198
+ .filter((part): part is ChatCompletionContentPartText => {
199
+ if (!part || typeof part !== "object") {
200
+ return false;
201
+ }
202
+ return part.type === "text" && typeof part.text === "string";
203
+ })
204
+ .map((part) => ({
205
+ type: "text",
206
+ text: part.text,
207
+ cache_control: { type: "ephemeral" },
208
+ }));
209
+ }
210
+
211
+ /**
212
+ * Adds cache control to the last tool in tools array
213
+ * @param tools - Array of tool definitions
214
+ * @returns Tools array with cache control on last tool
215
+ */
216
+ export function addCacheControlToLastTool(
217
+ tools: ChatCompletionFunctionTool[],
218
+ ): ClaudeChatCompletionFunctionTool[] {
219
+ // Handle null, undefined, or empty arrays
220
+ if (!tools || !Array.isArray(tools) || tools.length === 0) {
221
+ return [];
222
+ }
223
+
224
+ // Validate tools structure
225
+ const validTools = tools.filter((tool) => {
226
+ if (!tool || typeof tool !== "object") {
227
+ logger.warn("Invalid tool structure detected, skipping:", tool);
228
+ return false;
229
+ }
230
+ if (tool.type !== "function" || !tool.function) {
231
+ logger.warn(
232
+ "Tool is not a function type or missing function property:",
233
+ tool,
234
+ );
235
+ return false;
236
+ }
237
+ return true;
238
+ });
239
+
240
+ if (validTools.length === 0) {
241
+ logger.warn("No valid tools found for cache control");
242
+ return [];
243
+ }
244
+
245
+ // Create a copy of the valid tools array
246
+ const result = validTools.map((tool) => ({
247
+ ...tool,
248
+ })) as ClaudeChatCompletionFunctionTool[];
249
+
250
+ // Add cache control to the last tool only
251
+ const lastIndex = result.length - 1;
252
+ result[lastIndex] = {
253
+ ...result[lastIndex],
254
+ cache_control: { type: "ephemeral" },
255
+ };
256
+
257
+ return result;
258
+ }
259
+
260
+ /**
261
+ * Finds the latest message index at 20-message intervals (sliding window approach)
262
+ * @param messages - Array of chat completion messages
263
+ * @returns Index of the latest interval message (20th, 40th, 60th, etc.) or -1 if none
264
+ */
265
+ export function findIntervalMessageIndex(
266
+ messages: ChatCompletionMessageParam[],
267
+ ): number {
268
+ if (!Array.isArray(messages) || messages.length === 0) {
269
+ return -1;
270
+ }
271
+
272
+ const interval = 20; // Hardcoded interval
273
+ const messageCount = messages.length;
274
+
275
+ // Find the largest interval that fits within the message count
276
+ // Math.floor(messageCount / interval) gives us how many complete intervals we have
277
+ // Multiply by interval to get the position of the latest interval message
278
+ const latestIntervalPosition = Math.floor(messageCount / interval) * interval;
279
+
280
+ // If no complete intervals exist, return -1
281
+ if (latestIntervalPosition === 0) {
282
+ return -1;
283
+ }
284
+
285
+ // Convert from 1-based position to 0-based index
286
+ return latestIntervalPosition - 1;
287
+ }
288
+
289
+ /**
290
+ * Transforms messages for Claude cache control with hardcoded strategy
291
+ * @param messages - Original OpenAI message array
292
+ * @param modelName - Model name for cache detection
293
+ * @returns Messages with cache control markers applied
294
+ */
295
+ export function transformMessagesForClaudeCache(
296
+ messages: ChatCompletionMessageParam[],
297
+ modelName: string,
298
+ ): ChatCompletionMessageParam[] {
299
+ // Validate inputs
300
+ if (!messages || !Array.isArray(messages)) {
301
+ logger.warn(
302
+ "Invalid messages array provided to transformMessagesForClaudeCache",
303
+ );
304
+ return [];
305
+ }
306
+
307
+ if (messages.length === 0) {
308
+ return [];
309
+ }
310
+
311
+ // Only apply cache control for Claude models
312
+ if (!isClaudeModel(modelName)) {
313
+ return messages;
314
+ }
315
+
316
+ // Find the latest interval message index (20th, 40th, 60th, etc.)
317
+ const intervalMessageIndex = findIntervalMessageIndex(messages);
318
+
319
+ // Find last system message index
320
+ let lastSystemIndex = -1;
321
+ for (let i = messages.length - 1; i >= 0; i--) {
322
+ if (messages[i].role === "system") {
323
+ lastSystemIndex = i;
324
+ break;
325
+ }
326
+ }
327
+
328
+ const result = messages.map((message, index) => {
329
+ // Validate message structure
330
+ if (!message || typeof message !== "object" || !message.role) {
331
+ logger.warn("Invalid message structure at index", index, ":", message);
332
+ return message; // Return as-is to avoid breaking the flow
333
+ }
334
+
335
+ // Last system message: always cached (hardcoded)
336
+ if (message.role === "system" && index === lastSystemIndex) {
337
+ return {
338
+ ...message,
339
+ content: addCacheControlToContent(
340
+ (message.content as string | ChatCompletionContentPart[]) || "",
341
+ true,
342
+ ),
343
+ } as ChatCompletionMessageParam;
344
+ }
345
+
346
+ // Interval-based message caching: cache message at latest interval position (sliding window)
347
+ if (index === intervalMessageIndex) {
348
+ // If the message is a tool role, add cache control directly to the message
349
+ if (message.role === "tool") {
350
+ return {
351
+ ...message,
352
+ cache_control: { type: "ephemeral" },
353
+ } as ChatCompletionMessageParam;
354
+ }
355
+ // If the message has tool calls, cache the last tool call instead of content
356
+ else if (
357
+ message.role === "assistant" &&
358
+ message.tool_calls &&
359
+ message.tool_calls.length > 0
360
+ ) {
361
+ return {
362
+ ...message,
363
+ tool_calls: addCacheControlToLastToolCall(message.tool_calls),
364
+ } as ChatCompletionMessageParam;
365
+ } else {
366
+ // For other message types without tool calls, cache the content
367
+ return {
368
+ ...message,
369
+ content: addCacheControlToContent(
370
+ (message.content as string | ChatCompletionContentPart[]) || "",
371
+ true,
372
+ ),
373
+ } as ChatCompletionMessageParam;
374
+ }
375
+ }
376
+
377
+ // Return message unchanged
378
+ return message;
379
+ });
380
+
381
+ return result;
382
+ }
383
+
384
+ /**
385
+ * Extends standard usage with cache metrics
386
+ * @param standardUsage - OpenAI usage response
387
+ * @param cacheMetrics - Additional cache metrics from Claude
388
+ * @returns Extended usage with cache information
389
+ */
390
+ export function extendUsageWithCacheMetrics(
391
+ standardUsage: CompletionUsage,
392
+ cacheMetrics?: Partial<ClaudeUsage>,
393
+ ): ClaudeUsage {
394
+ const baseUsage: ClaudeUsage = {
395
+ prompt_tokens: standardUsage.prompt_tokens,
396
+ completion_tokens: standardUsage.completion_tokens,
397
+ total_tokens: standardUsage.total_tokens,
398
+ };
399
+
400
+ // Add cache metrics if provided
401
+ if (cacheMetrics) {
402
+ if (typeof cacheMetrics.cache_read_input_tokens === "number") {
403
+ baseUsage.cache_read_input_tokens = cacheMetrics.cache_read_input_tokens;
404
+ }
405
+
406
+ if (typeof cacheMetrics.cache_creation_input_tokens === "number") {
407
+ baseUsage.cache_creation_input_tokens =
408
+ cacheMetrics.cache_creation_input_tokens;
409
+ }
410
+
411
+ if (
412
+ cacheMetrics.cache_creation &&
413
+ typeof cacheMetrics.cache_creation.ephemeral_5m_input_tokens ===
414
+ "number" &&
415
+ typeof cacheMetrics.cache_creation.ephemeral_1h_input_tokens === "number"
416
+ ) {
417
+ baseUsage.cache_creation = {
418
+ ephemeral_5m_input_tokens:
419
+ cacheMetrics.cache_creation.ephemeral_5m_input_tokens,
420
+ ephemeral_1h_input_tokens:
421
+ cacheMetrics.cache_creation.ephemeral_1h_input_tokens,
422
+ };
423
+ }
424
+ }
425
+
426
+ return baseUsage;
427
+ }
428
+
429
+ /**
430
+ * Validates Claude usage structure
431
+ * @param usage - Usage object to validate
432
+ * @returns True if usage structure is valid
433
+ */
434
+ export function isValidClaudeUsage(usage: unknown): usage is ClaudeUsage {
435
+ if (!usage || typeof usage !== "object") {
436
+ return false;
437
+ }
438
+
439
+ const usageObj = usage as Record<string, unknown>;
440
+
441
+ // Check required standard fields
442
+ const hasStandardFields =
443
+ typeof usageObj.prompt_tokens === "number" &&
444
+ typeof usageObj.completion_tokens === "number" &&
445
+ typeof usageObj.total_tokens === "number";
446
+
447
+ if (!hasStandardFields) {
448
+ return false;
449
+ }
450
+
451
+ // Check optional cache fields
452
+ const hasCacheFields =
453
+ (usageObj.cache_read_input_tokens === undefined ||
454
+ typeof usageObj.cache_read_input_tokens === "number") &&
455
+ (usageObj.cache_creation_input_tokens === undefined ||
456
+ typeof usageObj.cache_creation_input_tokens === "number");
457
+
458
+ if (!hasCacheFields) {
459
+ return false;
460
+ }
461
+
462
+ // Check cache_creation object if present
463
+ if (usageObj.cache_creation !== undefined) {
464
+ const cacheCreation = usageObj.cache_creation as Record<string, unknown>;
465
+ if (
466
+ typeof cacheCreation !== "object" ||
467
+ typeof cacheCreation.ephemeral_5m_input_tokens !== "number" ||
468
+ typeof cacheCreation.ephemeral_1h_input_tokens !== "number"
469
+ ) {
470
+ return false;
471
+ }
472
+ }
473
+
474
+ return true;
475
+ }
@@ -0,0 +1,189 @@
1
+ import { relative, basename } from "path";
2
+
3
+ /**
4
+ * Command path resolver utilities for nested command discovery
5
+ * Handles conversion between file paths and command IDs with colon syntax
6
+ */
7
+
8
+ export interface CommandIdParts {
9
+ namespace?: string; // e.g., "openspec" for "openspec:apply"
10
+ commandName: string; // e.g., "apply" for "openspec:apply"
11
+ isNested: boolean; // true if command has namespace
12
+ depth: number; // 0 for root, 1 for nested
13
+ segments: string[]; // Path components array
14
+ }
15
+
16
+ /**
17
+ * Generate command ID from file path
18
+ * @param filePath - Absolute path to markdown file
19
+ * @param rootDir - Root commands directory path
20
+ * @returns Command identifier string (e.g., "openspec:apply")
21
+ * @throws Error on invalid path structure
22
+ */
23
+ export function generateCommandId(filePath: string, rootDir: string): string {
24
+ // Handle null/undefined inputs
25
+ if (filePath == null || rootDir == null) {
26
+ throw new Error("File path and root directory must be provided");
27
+ }
28
+
29
+ // Handle empty root directory (for root level commands)
30
+ const relativePath = rootDir === "" ? filePath : relative(rootDir, filePath);
31
+
32
+ // Handle edge cases
33
+ if (!relativePath || relativePath === ".") {
34
+ throw new Error("Command filename cannot be empty");
35
+ }
36
+
37
+ const segments = relativePath.split("/").filter((segment) => segment !== "");
38
+
39
+ // Remove .md extension from the last segment
40
+ const lastSegment = segments[segments.length - 1];
41
+ if (!lastSegment.endsWith(".md")) {
42
+ throw new Error(`Command files must have .md extension`);
43
+ }
44
+
45
+ segments[segments.length - 1] = basename(lastSegment, ".md");
46
+
47
+ // Handle empty filename after removing extension
48
+ if (segments[segments.length - 1] === "") {
49
+ throw new Error("Command filename cannot be empty");
50
+ }
51
+
52
+ // Validate depth (max 1 level of nesting)
53
+ if (segments.length > 2) {
54
+ throw new Error(
55
+ `Command nesting too deep: ${relativePath}. Maximum depth is 1 level.`,
56
+ );
57
+ }
58
+
59
+ // Validate segments
60
+ for (const segment of segments) {
61
+ if (!validateSegment(segment)) {
62
+ throw new Error(
63
+ `Invalid command path segment: "${segment}" in ${relativePath}. Must match pattern /^[a-zA-Z][a-zA-Z0-9_.-]*$/`,
64
+ );
65
+ }
66
+ }
67
+
68
+ // Generate command ID
69
+ if (segments.length === 1) {
70
+ return segments[0]; // Flat command
71
+ } else {
72
+ return segments.join(":"); // Nested command with colon syntax
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Parse command ID into components
78
+ * @param commandId - Command identifier (e.g., "openspec:apply")
79
+ * @returns Object with namespace and command name
80
+ * @throws Error on malformed command ID
81
+ */
82
+ export function parseCommandId(commandId: string): CommandIdParts {
83
+ // Handle null/undefined inputs
84
+ if (commandId == null) {
85
+ throw new Error("Command ID cannot be null or undefined");
86
+ }
87
+
88
+ if (commandId === "") {
89
+ throw new Error("Command ID cannot be empty");
90
+ }
91
+
92
+ if (!validateCommandId(commandId)) {
93
+ throw new Error(
94
+ `Invalid command ID format: "${commandId}". Must match pattern /^[a-zA-Z0-9_-]+(?::[a-zA-Z0-9_-]+)?$/`,
95
+ );
96
+ }
97
+
98
+ const parts = commandId.split(":");
99
+
100
+ if (parts.length === 1) {
101
+ // Flat command
102
+ return {
103
+ namespace: undefined,
104
+ commandName: parts[0],
105
+ isNested: false,
106
+ depth: 0,
107
+ segments: [parts[0]],
108
+ };
109
+ } else if (parts.length === 2) {
110
+ // Nested command
111
+ return {
112
+ namespace: parts[0],
113
+ commandName: parts[1],
114
+ isNested: true,
115
+ depth: 1,
116
+ segments: [parts[0], parts[1]],
117
+ };
118
+ } else {
119
+ throw new Error(
120
+ `Invalid command ID format: "${commandId}". Too many colon separators.`,
121
+ );
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Validate command ID format
127
+ * @param commandId - Command identifier to validate
128
+ * @returns Boolean indicating validity
129
+ */
130
+ export function validateCommandId(commandId: string): boolean {
131
+ // Handle null/undefined inputs
132
+ if (commandId == null) {
133
+ return false;
134
+ }
135
+
136
+ // Command ID can have multiple colons (though generateCommandId enforces max 1 level)
137
+ // This validates the format but doesn't enforce depth limits
138
+ const pattern = /^[a-zA-Z][a-zA-Z0-9_-]*(?::[a-zA-Z][a-zA-Z0-9_-]*)*$/;
139
+ return pattern.test(commandId);
140
+ }
141
+
142
+ /**
143
+ * Validate individual path segment
144
+ * @param segment - Path segment to validate
145
+ * @returns Boolean indicating validity
146
+ */
147
+ function validateSegment(segment: string): boolean {
148
+ // Segments should start with letters and can contain letters, numbers, dashes, underscores, dots
149
+ const pattern = /^[a-zA-Z][a-zA-Z0-9_.-]*$/;
150
+ return pattern.test(segment);
151
+ }
152
+
153
+ /**
154
+ * Convert file path to command segments array
155
+ * @param filePath - Absolute path to markdown file
156
+ * @param rootDir - Root commands directory path
157
+ * @returns Array of path segments
158
+ */
159
+ export function getCommandSegments(
160
+ filePath: string,
161
+ rootDir: string,
162
+ ): string[] {
163
+ const relativePath = relative(rootDir, filePath);
164
+ const segments = relativePath.split("/").filter((segment) => segment !== "");
165
+
166
+ // Remove .md extension from the last segment
167
+ const lastSegment = segments[segments.length - 1];
168
+ segments[segments.length - 1] = basename(lastSegment, ".md");
169
+
170
+ return segments;
171
+ }
172
+
173
+ /**
174
+ * Get namespace from command segments
175
+ * @param segments - Command path segments
176
+ * @returns Namespace string or undefined for flat commands
177
+ */
178
+ export function getNamespace(segments: string[]): string | undefined {
179
+ return segments.length > 1 ? segments[0] : undefined;
180
+ }
181
+
182
+ /**
183
+ * Get command depth from segments
184
+ * @param segments - Command path segments
185
+ * @returns Depth number (0 for root, 1 for nested)
186
+ */
187
+ export function getDepth(segments: string[]): number {
188
+ return Math.max(0, segments.length - 1);
189
+ }