@sylphx/flow 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/assets/output-styles/silent.md +145 -8
  3. package/assets/rules/core.md +19 -2
  4. package/package.json +2 -12
  5. package/src/commands/flow/execute.ts +470 -0
  6. package/src/commands/flow/index.ts +11 -0
  7. package/src/commands/flow/prompt.ts +35 -0
  8. package/src/commands/flow/setup.ts +312 -0
  9. package/src/commands/flow/targets.ts +18 -0
  10. package/src/commands/flow/types.ts +47 -0
  11. package/src/commands/flow-command.ts +18 -967
  12. package/src/commands/flow-orchestrator.ts +14 -5
  13. package/src/commands/hook-command.ts +1 -1
  14. package/src/commands/init-core.ts +12 -3
  15. package/src/commands/run-command.ts +1 -1
  16. package/src/config/rules.ts +1 -1
  17. package/src/core/error-handling.ts +1 -1
  18. package/src/core/loop-controller.ts +1 -1
  19. package/src/core/state-detector.ts +1 -1
  20. package/src/core/target-manager.ts +1 -1
  21. package/src/index.ts +1 -1
  22. package/src/shared/files/index.ts +1 -1
  23. package/src/shared/processing/index.ts +1 -1
  24. package/src/targets/claude-code.ts +3 -3
  25. package/src/targets/opencode.ts +3 -3
  26. package/src/utils/agent-enhancer.ts +2 -2
  27. package/src/utils/{mcp-config.ts → config/mcp-config.ts} +4 -4
  28. package/src/utils/{paths.ts → config/paths.ts} +1 -1
  29. package/src/utils/{settings.ts → config/settings.ts} +1 -1
  30. package/src/utils/{target-config.ts → config/target-config.ts} +5 -5
  31. package/src/utils/{target-utils.ts → config/target-utils.ts} +3 -3
  32. package/src/utils/display/banner.ts +25 -0
  33. package/src/utils/display/status.ts +55 -0
  34. package/src/utils/{file-operations.ts → files/file-operations.ts} +2 -2
  35. package/src/utils/files/jsonc.ts +36 -0
  36. package/src/utils/{sync-utils.ts → files/sync-utils.ts} +3 -3
  37. package/src/utils/index.ts +42 -61
  38. package/src/utils/version.ts +47 -0
  39. package/src/components/benchmark-monitor.tsx +0 -331
  40. package/src/components/reindex-progress.tsx +0 -261
  41. package/src/composables/functional/index.ts +0 -14
  42. package/src/composables/functional/useEnvironment.ts +0 -171
  43. package/src/composables/functional/useFileSystem.ts +0 -139
  44. package/src/composables/index.ts +0 -4
  45. package/src/composables/useEnv.ts +0 -13
  46. package/src/composables/useRuntimeConfig.ts +0 -27
  47. package/src/core/ai-sdk.ts +0 -603
  48. package/src/core/app-factory.ts +0 -381
  49. package/src/core/builtin-agents.ts +0 -9
  50. package/src/core/command-system.ts +0 -550
  51. package/src/core/config-system.ts +0 -550
  52. package/src/core/connection-pool.ts +0 -390
  53. package/src/core/di-container.ts +0 -155
  54. package/src/core/headless-display.ts +0 -96
  55. package/src/core/interfaces/index.ts +0 -22
  56. package/src/core/interfaces/repository.interface.ts +0 -91
  57. package/src/core/interfaces/service.interface.ts +0 -133
  58. package/src/core/interfaces.ts +0 -96
  59. package/src/core/result.ts +0 -351
  60. package/src/core/service-config.ts +0 -252
  61. package/src/core/session-service.ts +0 -121
  62. package/src/core/storage-factory.ts +0 -115
  63. package/src/core/stream-handler.ts +0 -288
  64. package/src/core/type-utils.ts +0 -427
  65. package/src/core/unified-storage.ts +0 -456
  66. package/src/core/validation/limit.ts +0 -46
  67. package/src/core/validation/query.ts +0 -20
  68. package/src/db/auto-migrate.ts +0 -322
  69. package/src/db/base-database-client.ts +0 -144
  70. package/src/db/cache-db.ts +0 -218
  71. package/src/db/cache-schema.ts +0 -75
  72. package/src/db/database.ts +0 -70
  73. package/src/db/index.ts +0 -252
  74. package/src/db/memory-db.ts +0 -153
  75. package/src/db/memory-schema.ts +0 -29
  76. package/src/db/schema.ts +0 -289
  77. package/src/db/session-repository.ts +0 -733
  78. package/src/domains/index.ts +0 -6
  79. package/src/domains/utilities/index.ts +0 -6
  80. package/src/domains/utilities/time/index.ts +0 -5
  81. package/src/domains/utilities/time/tools.ts +0 -291
  82. package/src/services/agent-service.ts +0 -273
  83. package/src/services/evaluation-service.ts +0 -271
  84. package/src/services/functional/evaluation-logic.ts +0 -296
  85. package/src/services/functional/file-processor.ts +0 -273
  86. package/src/services/functional/index.ts +0 -12
  87. package/src/services/memory.service.ts +0 -476
  88. package/src/types/api/batch.ts +0 -108
  89. package/src/types/api/errors.ts +0 -118
  90. package/src/types/api/index.ts +0 -55
  91. package/src/types/api/requests.ts +0 -76
  92. package/src/types/api/responses.ts +0 -180
  93. package/src/types/api/websockets.ts +0 -85
  94. package/src/types/benchmark.ts +0 -49
  95. package/src/types/database.types.ts +0 -510
  96. package/src/types/memory-types.ts +0 -63
  97. package/src/utils/advanced-tokenizer.ts +0 -191
  98. package/src/utils/ai-model-fetcher.ts +0 -19
  99. package/src/utils/async-file-operations.ts +0 -516
  100. package/src/utils/audio-player.ts +0 -345
  101. package/src/utils/codebase-helpers.ts +0 -211
  102. package/src/utils/console-ui.ts +0 -79
  103. package/src/utils/database-errors.ts +0 -140
  104. package/src/utils/debug-logger.ts +0 -49
  105. package/src/utils/file-scanner.ts +0 -259
  106. package/src/utils/help.ts +0 -20
  107. package/src/utils/immutable-cache.ts +0 -106
  108. package/src/utils/jsonc.ts +0 -158
  109. package/src/utils/memory-tui.ts +0 -414
  110. package/src/utils/models-dev.ts +0 -91
  111. package/src/utils/parallel-operations.ts +0 -487
  112. package/src/utils/process-manager.ts +0 -155
  113. package/src/utils/prompts.ts +0 -120
  114. package/src/utils/search-tool-builder.ts +0 -214
  115. package/src/utils/session-manager.ts +0 -168
  116. package/src/utils/session-title.ts +0 -87
  117. package/src/utils/simplified-errors.ts +0 -410
  118. package/src/utils/template-engine.ts +0 -94
  119. package/src/utils/test-audio.ts +0 -71
  120. package/src/utils/todo-context.ts +0 -46
  121. package/src/utils/token-counter.ts +0 -288
  122. /package/src/utils/{cli-output.ts → display/cli-output.ts} +0 -0
  123. /package/src/utils/{logger.ts → display/logger.ts} +0 -0
  124. /package/src/utils/{notifications.ts → display/notifications.ts} +0 -0
  125. /package/src/utils/{secret-utils.ts → security/secret-utils.ts} +0 -0
  126. /package/src/utils/{security.ts → security/security.ts} +0 -0
@@ -1,288 +0,0 @@
1
- /**
2
- * Stream Handler
3
- * Unified stream processing for both headless and TUI modes
4
- */
5
-
6
- import type { StreamChunk } from './ai-sdk.js';
7
- import type { MessagePart, TokenUsage } from '../types/session.types.js';
8
-
9
- /**
10
- * Callbacks for stream events
11
- */
12
- export interface StreamCallbacks {
13
- onTextStart?: () => void;
14
- onTextDelta?: (text: string) => void;
15
- onTextEnd?: () => void;
16
- onReasoningStart?: () => void;
17
- onReasoningDelta?: (text: string) => void;
18
- onReasoningEnd?: (duration: number) => void;
19
- onToolCall?: (toolCallId: string, toolName: string, args: unknown) => void;
20
- onToolInputStart?: (toolCallId: string, toolName: string) => void;
21
- onToolInputDelta?: (toolCallId: string, toolName: string, argsTextDelta: string) => void;
22
- onToolInputEnd?: (toolCallId: string, toolName: string, args: unknown) => void;
23
- onToolResult?: (toolCallId: string, toolName: string, result: unknown, duration: number) => void;
24
- onToolError?: (toolCallId: string, toolName: string, error: string, duration: number) => void;
25
- onAbort?: () => void;
26
- onError?: (error: string) => void;
27
- onFinish?: (usage: TokenUsage, finishReason: string) => void;
28
- onComplete?: () => void;
29
- }
30
-
31
- /**
32
- * Stream processing result
33
- */
34
- export interface StreamResult {
35
- fullResponse: string;
36
- messageParts: MessagePart[];
37
- usage?: TokenUsage;
38
- finishReason?: string;
39
- }
40
-
41
- /**
42
- * Process AI stream and collect response with parts
43
- */
44
- export async function processStream(
45
- stream: AsyncIterable<StreamChunk>,
46
- callbacks: StreamCallbacks = {}
47
- ): Promise<StreamResult> {
48
- const { onTextStart, onTextDelta, onTextEnd, onReasoningStart, onReasoningDelta, onReasoningEnd, onToolCall, onToolInputStart, onToolInputDelta, onToolInputEnd, onToolResult, onToolError, onAbort, onError, onFinish, onComplete } = callbacks;
49
-
50
- let fullResponse = '';
51
- const messageParts: MessagePart[] = [];
52
- const activeTools = new Map<string, { name: string; startTime: number; args: unknown }>();
53
- let currentTextContent = '';
54
- let currentReasoningContent = '';
55
- let reasoningStartTime: number | null = null;
56
- let usage: TokenUsage | undefined;
57
- let finishReason: string | undefined;
58
-
59
- for await (const chunk of stream) {
60
- switch (chunk.type) {
61
- case 'text-start': {
62
- // Text generation started - notify immediately
63
- onTextStart?.();
64
- break;
65
- }
66
-
67
- case 'text-delta': {
68
- fullResponse += chunk.textDelta;
69
- currentTextContent += chunk.textDelta;
70
- onTextDelta?.(chunk.textDelta);
71
- break;
72
- }
73
-
74
- case 'text-end': {
75
- // Text generation finished - save text part if any
76
- if (currentTextContent) {
77
- messageParts.push({ type: 'text', content: currentTextContent, status: 'completed' });
78
- currentTextContent = '';
79
- }
80
- onTextEnd?.();
81
- break;
82
- }
83
-
84
- case 'reasoning-start': {
85
- // Save current text part if any
86
- if (currentTextContent) {
87
- messageParts.push({ type: 'text', content: currentTextContent, status: 'completed' });
88
- currentTextContent = '';
89
- }
90
- reasoningStartTime = Date.now();
91
- onReasoningStart?.();
92
- break;
93
- }
94
-
95
- case 'reasoning-delta': {
96
- currentReasoningContent += chunk.textDelta;
97
- onReasoningDelta?.(chunk.textDelta);
98
- break;
99
- }
100
-
101
- case 'reasoning-end': {
102
- // Save reasoning part with duration
103
- const duration = reasoningStartTime ? Date.now() - reasoningStartTime : 0;
104
- if (currentReasoningContent || reasoningStartTime) {
105
- messageParts.push({
106
- type: 'reasoning',
107
- content: currentReasoningContent,
108
- status: 'completed', // All saved parts are completed
109
- duration
110
- });
111
- currentReasoningContent = '';
112
- reasoningStartTime = null;
113
- }
114
- // Pass duration to callback so UI can display it
115
- onReasoningEnd?.(duration);
116
- break;
117
- }
118
-
119
- case 'tool-call': {
120
- // Save current text part if any
121
- if (currentTextContent) {
122
- messageParts.push({ type: 'text', content: currentTextContent, status: 'completed' });
123
- currentTextContent = '';
124
- }
125
-
126
- // Add tool part (may not have complete args yet if streaming)
127
- messageParts.push({
128
- type: 'tool',
129
- toolId: chunk.toolCallId,
130
- name: chunk.toolName,
131
- status: 'active', // Match MessagePart type
132
- args: chunk.args,
133
- });
134
-
135
- // Track tool start time
136
- activeTools.set(chunk.toolCallId, {
137
- name: chunk.toolName,
138
- startTime: Date.now(),
139
- args: chunk.args,
140
- });
141
-
142
- onToolCall?.(chunk.toolCallId, chunk.toolName, chunk.args);
143
- break;
144
- }
145
-
146
- case 'tool-input-start': {
147
- // Tool input streaming started - notify callback
148
- onToolInputStart?.(chunk.toolCallId, chunk.toolName);
149
- break;
150
- }
151
-
152
- case 'tool-input-delta': {
153
- // Update tool args as they stream in
154
- // Find the active tool part and update its args
155
- const toolPart = messageParts.find(
156
- (p) => p.type === 'tool' && p.name === chunk.toolName && p.status === 'active'
157
- );
158
-
159
- if (toolPart && toolPart.type === 'tool') {
160
- // Append args delta (args are streaming as JSON text)
161
- const currentArgsText = typeof toolPart.args === 'string' ? toolPart.args : '';
162
- toolPart.args = currentArgsText + chunk.argsTextDelta;
163
- }
164
-
165
- // Notify callback for real-time UI update
166
- onToolInputDelta?.(chunk.toolCallId, chunk.toolName, chunk.argsTextDelta);
167
- break;
168
- }
169
-
170
- case 'tool-input-end': {
171
- // Tool input streaming complete - args are ready
172
- // Find tool part to get final args
173
- const toolPart = messageParts.find(
174
- (p) => p.type === 'tool' && p.name === chunk.toolName && p.status === 'active'
175
- );
176
-
177
- if (toolPart && toolPart.type === 'tool') {
178
- onToolInputEnd?.(chunk.toolCallId, chunk.toolName, toolPart.args);
179
- }
180
- break;
181
- }
182
-
183
- case 'tool-result': {
184
- const tool = activeTools.get(chunk.toolCallId);
185
- if (tool) {
186
- const duration = Date.now() - tool.startTime;
187
- activeTools.delete(chunk.toolCallId);
188
-
189
- // Update tool part status and result
190
- const toolPart = messageParts.find(
191
- (p) => p.type === 'tool' && p.name === chunk.toolName && p.status === 'active'
192
- );
193
-
194
- if (toolPart && toolPart.type === 'tool') {
195
- toolPart.status = 'completed';
196
- toolPart.duration = duration;
197
- toolPart.result = chunk.result;
198
- }
199
-
200
- onToolResult?.(chunk.toolCallId, chunk.toolName, chunk.result, duration);
201
- }
202
- break;
203
- }
204
-
205
- case 'tool-error': {
206
- // Save current text part if any
207
- if (currentTextContent) {
208
- messageParts.push({ type: 'text', content: currentTextContent, status: 'completed' });
209
- currentTextContent = '';
210
- }
211
-
212
- const tool = activeTools.get(chunk.toolCallId);
213
- if (tool) {
214
- const duration = Date.now() - tool.startTime;
215
- activeTools.delete(chunk.toolCallId);
216
-
217
- // Update tool part status and error
218
- const toolPart = messageParts.find(
219
- (p) => p.type === 'tool' && p.name === chunk.toolName && p.status === 'active'
220
- );
221
-
222
- if (toolPart && toolPart.type === 'tool') {
223
- toolPart.status = 'error';
224
- toolPart.duration = duration;
225
- toolPart.error = chunk.error;
226
- }
227
-
228
- // Notify callback
229
- onToolError?.(chunk.toolCallId, chunk.toolName, chunk.error, duration);
230
- }
231
- break;
232
- }
233
-
234
- case 'abort': {
235
- // Save current text part if any
236
- if (currentTextContent) {
237
- messageParts.push({ type: 'text', content: currentTextContent, status: 'completed' });
238
- currentTextContent = '';
239
- }
240
-
241
- // Mark all active parts as 'abort'
242
- messageParts.forEach(part => {
243
- if (part.status === 'active') {
244
- part.status = 'abort';
245
- }
246
- });
247
-
248
- // Notify callback
249
- onAbort?.();
250
- break;
251
- }
252
-
253
- case 'error': {
254
- // Save current text part if any
255
- if (currentTextContent) {
256
- messageParts.push({ type: 'text', content: currentTextContent, status: 'completed' });
257
- currentTextContent = '';
258
- }
259
-
260
- // Add error part
261
- messageParts.push({ type: 'error', error: chunk.error, status: 'completed' });
262
-
263
- // Notify callback
264
- onError?.(chunk.error);
265
- break;
266
- }
267
-
268
- case 'finish': {
269
- usage = chunk.usage;
270
- finishReason = chunk.finishReason;
271
- onFinish?.(chunk.usage, chunk.finishReason);
272
- break;
273
- }
274
- }
275
- }
276
-
277
- // Save final text part if any
278
- if (currentTextContent) {
279
- messageParts.push({ type: 'text', content: currentTextContent, status: 'completed' });
280
- }
281
-
282
- return {
283
- fullResponse,
284
- messageParts,
285
- usage,
286
- finishReason,
287
- };
288
- }
@@ -1,427 +0,0 @@
1
- /**
2
- * Type Utilities - 類型安全工具
3
- * Functional approach to type safety and validation
4
- */
5
-
6
- import { z } from 'zod';
7
-
8
- // Re-export unified Result type and utilities
9
- export {
10
- Result,
11
- AsyncResult,
12
- ok,
13
- err,
14
- isOk,
15
- isErr,
16
- map,
17
- flatMap,
18
- mapError,
19
- getOrElse,
20
- getOrElseLazy,
21
- match,
22
- unwrap,
23
- tryCatch,
24
- tryCatchAsync,
25
- safeAsync,
26
- safeSync,
27
- all,
28
- allAsync,
29
- tap,
30
- tapError,
31
- type SuccessType,
32
- type ErrorType,
33
- type SafeResult,
34
- } from './result.js';
35
-
36
- /**
37
- * Common validation schemas
38
- */
39
- export const Schemas = {
40
- /** Non-empty string validation */
41
- nonEmptyString: z.string().min(1, 'String cannot be empty'),
42
-
43
- /** Optional non-empty string */
44
- optionalNonEmptyString: z.string().min(1).optional(),
45
-
46
- /** Positive number validation */
47
- positiveNumber: z.number().positive('Number must be positive'),
48
-
49
- /** Optional positive number */
50
- optionalPositiveNumber: z.number().positive().optional(),
51
-
52
- /** Array validation */
53
- nonEmptyArray: z.array(z.any()).min(1, 'Array cannot be empty'),
54
-
55
- /** Email validation */
56
- email: z.string().email('Invalid email format'),
57
-
58
- /** URL validation */
59
- url: z.string().url('Invalid URL format'),
60
-
61
- /** Object validation */
62
- object: z.object({}).passthrough(),
63
-
64
- /** Storage configuration */
65
- storageConfig: z.object({
66
- type: z.enum(['memory', 'cache', 'vector', 'drizzle']),
67
- connectionString: z.string().optional(),
68
- defaultTTL: z.number().positive().optional(),
69
- maxCacheSize: z.number().positive().optional(),
70
- vectorDimensions: z.number().positive().optional(),
71
- storageDir: z.string().optional(),
72
- }),
73
-
74
- /** CLI options */
75
- cliOptions: z.object({
76
- target: z.string().optional(),
77
- verbose: z.boolean().default(false),
78
- dryRun: z.boolean().default(false),
79
- clear: z.boolean().default(false),
80
- mcp: z.union([z.array(z.string()), z.boolean(), z.null()]).default(null),
81
- quiet: z.boolean().default(false),
82
- agent: z.string().optional(),
83
- }),
84
- } as const;
85
-
86
- /**
87
- * Type guards
88
- */
89
- export const TypeGuards = {
90
- /** Check if value is a non-empty string */
91
- isNonEmptyString: (value: unknown): value is string => {
92
- return typeof value === 'string' && value.length > 0;
93
- },
94
-
95
- /** Check if value is a positive number */
96
- isPositiveNumber: (value: unknown): value is number => {
97
- return typeof value === 'number' && value > 0;
98
- },
99
-
100
- /** Check if value is a non-empty array */
101
- isNonEmptyArray: (value: unknown): value is unknown[] => {
102
- return Array.isArray(value) && value.length > 0;
103
- },
104
-
105
- /** Check if value is an object */
106
- isObject: (value: unknown): value is Record<string, unknown> => {
107
- return value !== null && typeof value === 'object' && !Array.isArray(value);
108
- },
109
-
110
- /** Check if value is a function */
111
- isFunction: (value: unknown): value is Function => {
112
- return typeof value === 'function';
113
- },
114
-
115
- /** Check if value is a Date */
116
- isDate: (value: unknown): value is Date => {
117
- return value instanceof Date && !isNaN(value.getTime());
118
- },
119
-
120
- /** Check if value is a Buffer */
121
- isBuffer: (value: unknown): value is Buffer => {
122
- return Buffer.isBuffer(value);
123
- },
124
- } as const;
125
-
126
- /**
127
- * Safe parsing utilities
128
- */
129
- export const SafeParse = {
130
- /** Parse JSON safely */
131
- json: <T = unknown>(str: string): Result<T> => {
132
- return safeSync(() => JSON.parse(str) as T,
133
- error => new Error(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`));
134
- },
135
-
136
- /** Parse number safely */
137
- number: (str: string, radix = 10): Result<number> => {
138
- return safeSync(() => {
139
- const num = parseInt(str, radix);
140
- if (isNaN(num)) throw new Error(`Invalid number: ${str}`);
141
- return num;
142
- }, error => new Error(`Failed to parse number: ${error instanceof Error ? error.message : String(error)}`));
143
- },
144
-
145
- /** Parse float safely */
146
- float: (str: string): Result<number> => {
147
- return safeSync(() => {
148
- const num = parseFloat(str);
149
- if (isNaN(num)) throw new Error(`Invalid float: ${str}`);
150
- return num;
151
- }, error => new Error(`Failed to parse float: ${error instanceof Error ? error.message : String(error)}`));
152
- },
153
-
154
- /** Parse boolean safely */
155
- boolean: (str: string): Result<boolean> => {
156
- const lower = str.toLowerCase();
157
- if (['true', '1', 'yes', 'on'].includes(lower)) return ok(true);
158
- if (['false', '0', 'no', 'off'].includes(lower)) return ok(false);
159
- return err(new Error(`Invalid boolean: ${str}`));
160
- },
161
- } as const;
162
-
163
- /**
164
- * String utilities
165
- */
166
- export const StringUtils = {
167
- /** Truncate string to max length */
168
- truncate: (str: string, maxLength: number, suffix = '...'): string => {
169
- if (str.length <= maxLength) return str;
170
- return str.slice(0, maxLength - suffix.length) + suffix;
171
- },
172
-
173
- /** Convert to kebab-case */
174
- kebabCase: (str: string): string => {
175
- return str
176
- .replace(/([a-z])([A-Z])/g, '$1-$2')
177
- .replace(/[\s_]+/g, '-')
178
- .toLowerCase();
179
- },
180
-
181
- /** Convert to camelCase */
182
- camelCase: (str: string): string => {
183
- return str
184
- .replace(/[-_\s]+(.)?/g, (_, char) => (char ? char.toUpperCase() : ''))
185
- .replace(/^[A-Z]/, char => char.toLowerCase());
186
- },
187
-
188
- /** Convert to PascalCase */
189
- pascalCase: (str: string): string => {
190
- const camel = StringUtils.camelCase(str);
191
- return camel.charAt(0).toUpperCase() + camel.slice(1);
192
- },
193
-
194
- /** Capitalize first letter */
195
- capitalize: (str: string): string => {
196
- return str.charAt(0).toUpperCase() + str.slice(1);
197
- },
198
-
199
- /** Check if string is empty or whitespace */
200
- isEmpty: (str: string): boolean => {
201
- return str.trim().length === 0;
202
- },
203
-
204
- /** Generate random string */
205
- random: (length = 8): string => {
206
- const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
207
- let result = '';
208
- for (let i = 0; i < length; i++) {
209
- result += chars.charAt(Math.floor(Math.random() * chars.length));
210
- }
211
- return result;
212
- },
213
-
214
- /** Escape regex special characters */
215
- escapeRegex: (str: string): string => {
216
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
217
- },
218
- } as const;
219
-
220
- /**
221
- * Array utilities
222
- */
223
- export const ArrayUtils = {
224
- /** Check if array is empty */
225
- isEmpty: <T>(arr: T[]): boolean => arr.length === 0,
226
-
227
- /** Remove duplicates */
228
- unique: <T>(arr: T[]): T[] => [...new Set(arr)],
229
-
230
- /** Group array by key */
231
- groupBy: <T, K extends string | number>(
232
- arr: T[],
233
- keyFn: (item: T) => K
234
- ): Record<K, T[]> => {
235
- return arr.reduce((groups, item) => {
236
- const key = keyFn(item);
237
- if (!groups[key]) groups[key] = [];
238
- groups[key].push(item);
239
- return groups;
240
- }, {} as Record<K, T[]>);
241
- },
242
-
243
- /** Chunk array into smaller arrays */
244
- chunk: <T>(arr: T[], size: number): T[][] => {
245
- const chunks: T[][] = [];
246
- for (let i = 0; i < arr.length; i += size) {
247
- chunks.push(arr.slice(i, i + size));
248
- }
249
- return chunks;
250
- },
251
-
252
- /** Flatten nested arrays */
253
- flatten: <T>(arr: (T | T[])[]): T[] => {
254
- return arr.reduce<T[]>((flat, item) => {
255
- return flat.concat(Array.isArray(item) ? ArrayUtils.flatten(item) : item);
256
- }, []);
257
- },
258
-
259
- /** Pick random element */
260
- sample: <T>(arr: T[]): T | undefined => {
261
- return arr[Math.floor(Math.random() * arr.length)];
262
- },
263
-
264
- /** Shuffle array */
265
- shuffle: <T>(arr: T[]): T[] => {
266
- const shuffled = [...arr];
267
- for (let i = shuffled.length - 1; i > 0; i--) {
268
- const j = Math.floor(Math.random() * (i + 1));
269
- [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
270
- }
271
- return shuffled;
272
- },
273
- } as const;
274
-
275
- /**
276
- * Object utilities
277
- */
278
- export const ObjectUtils = {
279
- /** Check if object is empty */
280
- isEmpty: (obj: Record<string, unknown>): boolean => {
281
- return Object.keys(obj).length === 0;
282
- },
283
-
284
- /** Pick specific keys from object */
285
- pick: <T extends Record<string, unknown>, K extends keyof T>(
286
- obj: T,
287
- keys: K[]
288
- ): Pick<T, K> => {
289
- return keys.reduce((picked, key) => {
290
- if (key in obj) {
291
- picked[key] = obj[key];
292
- }
293
- return picked;
294
- }, {} as Pick<T, K>);
295
- },
296
-
297
- /** Omit specific keys from object */
298
- omit: <T extends Record<string, unknown>, K extends keyof T>(
299
- obj: T,
300
- keys: K[]
301
- ): Omit<T, K> => {
302
- const result = { ...obj };
303
- keys.forEach(key => delete result[key]);
304
- return result as Omit<T, K>;
305
- },
306
-
307
- /** Deep clone object */
308
- deepClone: <T>(obj: T): T => {
309
- return JSON.parse(JSON.stringify(obj));
310
- },
311
-
312
- /** Merge objects */
313
- merge: <T extends Record<string, unknown>>(
314
- ...objects: Partial<T>[]
315
- ): T => {
316
- return objects.reduce((merged, obj) => ({ ...merged, ...obj }), {} as T);
317
- },
318
-
319
- /** Get nested value from object */
320
- get: (obj: any, path: string, defaultValue?: unknown): unknown => {
321
- const keys = path.split('.');
322
- let current = obj;
323
-
324
- for (const key of keys) {
325
- if (current == null || typeof current !== 'object') {
326
- return defaultValue;
327
- }
328
- current = current[key];
329
- }
330
-
331
- return current !== undefined ? current : defaultValue;
332
- },
333
-
334
- /** Set nested value in object */
335
- set: (obj: any, path: string, value: unknown): void => {
336
- const keys = path.split('.');
337
- let current = obj;
338
-
339
- for (let i = 0; i < keys.length - 1; i++) {
340
- const key = keys[i];
341
- if (!(key in current) || typeof current[key] !== 'object') {
342
- current[key] = {};
343
- }
344
- current = current[key];
345
- }
346
-
347
- current[keys[keys.length - 1]] = value;
348
- },
349
- } as const;
350
-
351
- /**
352
- * Function utilities
353
- */
354
- export const FunctionUtils = {
355
- /** Debounce function */
356
- debounce: <T extends (...args: any[]) => any>(
357
- fn: T,
358
- delay: number
359
- ): ((...args: Parameters<T>) => void) => {
360
- let timeoutId: NodeJS.Timeout;
361
- return (...args: Parameters<T>) => {
362
- clearTimeout(timeoutId);
363
- timeoutId = setTimeout(() => fn(...args), delay);
364
- };
365
- },
366
-
367
- /** Throttle function */
368
- throttle: <T extends (...args: any[]) => any>(
369
- fn: T,
370
- delay: number
371
- ): ((...args: Parameters<T>) => void) => {
372
- let lastCall = 0;
373
- return (...args: Parameters<T>) => {
374
- const now = Date.now();
375
- if (now - lastCall >= delay) {
376
- lastCall = now;
377
- fn(...args);
378
- }
379
- };
380
- },
381
-
382
- /** Memoize function */
383
- memoize: <T extends (...args: any[]) => any>(
384
- fn: T,
385
- keyFn?: (...args: Parameters<T>) => string
386
- ): T => {
387
- const cache = new Map<string, ReturnType<T>>();
388
-
389
- return ((...args: Parameters<T>) => {
390
- const key = keyFn ? keyFn(...args) : JSON.stringify(args);
391
-
392
- if (cache.has(key)) {
393
- return cache.get(key);
394
- }
395
-
396
- const result = fn(...args);
397
- cache.set(key, result);
398
- return result;
399
- }) as T;
400
- },
401
-
402
- /** Retry function */
403
- retry: async <T>(
404
- fn: () => Promise<T>,
405
- maxAttempts = 3,
406
- delay = 1000
407
- ): Promise<T> => {
408
- let lastError: Error;
409
-
410
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
411
- try {
412
- return await fn();
413
- } catch (error) {
414
- lastError = error instanceof Error ? error : new Error(String(error));
415
-
416
- if (attempt === maxAttempts) {
417
- throw lastError;
418
- }
419
-
420
- // Exponential backoff
421
- await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, attempt - 1)));
422
- }
423
- }
424
-
425
- throw lastError!;
426
- },
427
- } as const;