octocode-cli 1.5.0 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +167 -90
- package/out/chunks/chunk-375ZNU3A.js +317 -0
- package/out/chunks/{chunk-V64AXUWB.js → chunk-AYILFPX4.js} +1 -1
- package/out/chunks/chunk-BRHVQB64.js +10 -0
- package/out/chunks/{command-help-specs-JZXVSLZ5.js → chunk-KWJUBVXJ.js} +1 -1
- package/out/chunks/chunk-OMXWZJBJ.js +100 -0
- package/out/chunks/command-help-specs-RVJK6V6M.js +8 -0
- package/out/chunks/commands-JFDCC4Q7.js +8 -0
- package/out/chunks/{help-P7TCOYAJ.js → help-H3WGUJO7.js} +1 -1
- package/out/chunks/main-help-BUTDD4IR.js +10 -0
- package/out/chunks/{spinner-URV2OX6O.js → spinner-DXD4JQIU.js} +1 -1
- package/out/chunks/tool-command-L6PVHUNS.js +8 -0
- package/out/octocode-cli.js +1 -1
- package/package.json +2 -2
- package/skills/octocode-brainstorming/scripts/tavily-search.mjs +1 -10
- package/skills/octocode-chrome-devtools/scripts/cdp-runner.mjs +0 -2
- package/skills/octocode-chrome-devtools/scripts/cdp-sandbox.mjs +1 -5
- package/skills/octocode-chrome-devtools/scripts/cdp-template.mjs +1 -10
- package/skills/octocode-chrome-devtools/scripts/open-browser.mjs +0 -3
- package/skills/octocode-chrome-devtools/scripts/sourcemap-resolver.mjs +1 -4
- package/skills/octocode-chrome-devtools/scripts/undercover.mjs +0 -2
- package/skills/octocode-engineer/SKILL.md +7 -33
- package/skills/octocode-engineer/references/cli-reference.md +5 -11
- package/skills/octocode-engineer/references/externals.md +0 -4
- package/skills/octocode-engineer/references/quality-indicators.md +10 -50
- package/skills/octocode-engineer/references/tool-workflows.md +10 -13
- package/skills/octocode-engineer/src/analysis/dependency-summary.test.ts +2 -2
- package/skills/octocode-engineer/src/ast/metrics.test.ts +2 -2
- package/skills/octocode-engineer/src/ast/search-main.ts +2 -15
- package/skills/octocode-engineer/src/ast/search.ts +1 -7
- package/skills/octocode-engineer/src/ast/tree-sitter.test.ts +4 -4
- package/skills/octocode-engineer/src/ast/tree-sitter.ts +2 -2
- package/skills/octocode-engineer/src/collectors/chains.ts +3 -10
- package/skills/octocode-engineer/src/collectors/prototype-pollution.ts +3 -3
- package/skills/octocode-engineer/src/collectors/security.ts +3 -3
- package/skills/octocode-engineer/src/common/ensure-deps.ts +3 -20
- package/skills/octocode-engineer/src/detectors/code-quality.ts +0 -1
- package/skills/octocode-engineer/src/index.test.ts +1 -1
- package/skills/octocode-engineer/src/index.ts +1 -11
- package/skills/octocode-engineer/src/pipeline/affected.ts +2 -7
- package/skills/octocode-engineer/src/pipeline/baseline.ts +2 -8
- package/skills/octocode-engineer/src/pipeline/cache.ts +2 -2
- package/skills/octocode-engineer/src/pipeline/config-loader.test.ts +1 -2
- package/skills/octocode-engineer/src/pipeline/config-loader.ts +0 -9
- package/skills/octocode-engineer/src/pipeline/create-options.ts +2 -15
- package/skills/octocode-engineer/src/pipeline/reporters.ts +2 -12
- package/skills/octocode-engineer/src/reporting/summary-md.test.ts +2 -2
- package/skills/octocode-engineer/src/run.ts +1 -7
- package/skills/octocode-engineer/src/types/constants.ts +0 -5
- package/skills/octocode-engineer/src/types/core.ts +0 -5
- package/skills/octocode-news/eslint.config.mjs +1 -6
- package/skills/octocode-news/src/rss-core.ts +2 -4
- package/skills/octocode-research/ecosystem.config.cjs +0 -31
- package/skills/octocode-research/package.json +1 -1
- package/skills/octocode-research/scripts/server.js +272 -230
- package/skills/octocode-research/src/__tests__/integration/circuitBreaker.test.ts +0 -24
- package/skills/octocode-research/src/__tests__/integration/routes.test.ts +0 -10
- package/skills/octocode-research/src/__tests__/integration/serverLifecycle.test.ts +5 -22
- package/skills/octocode-research/src/__tests__/integration/toolsRoutes.test.ts +0 -27
- package/skills/octocode-research/src/__tests__/unit/circuitBreaker.test.ts +0 -29
- package/skills/octocode-research/src/__tests__/unit/errorHandler.test.ts +1 -12
- package/skills/octocode-research/src/__tests__/unit/httpPreprocess.test.ts +1 -9
- package/skills/octocode-research/src/__tests__/unit/logger.test.ts +0 -7
- package/skills/octocode-research/src/__tests__/unit/queryParser.test.ts +1 -7
- package/skills/octocode-research/src/__tests__/unit/responseBuilder.test.ts +0 -11
- package/skills/octocode-research/src/__tests__/unit/retry.test.ts +1 -18
- package/skills/octocode-research/src/__tests__/unit/schemas.test.ts +2 -25
- package/skills/octocode-research/src/index.ts +0 -27
- package/skills/octocode-research/src/mcpCache.ts +3 -18
- package/skills/octocode-research/src/middleware/errorHandler.ts +1 -4
- package/skills/octocode-research/src/middleware/logger.ts +1 -6
- package/skills/octocode-research/src/middleware/queryParser.ts +3 -22
- package/skills/octocode-research/src/routes/github.ts +0 -11
- package/skills/octocode-research/src/routes/local.ts +0 -12
- package/skills/octocode-research/src/routes/lsp.ts +1 -10
- package/skills/octocode-research/src/routes/package.ts +0 -2
- package/skills/octocode-research/src/routes/prompts.ts +2 -32
- package/skills/octocode-research/src/routes/tools.ts +12 -177
- package/skills/octocode-research/src/server-init.ts +0 -32
- package/skills/octocode-research/src/server.ts +8 -37
- package/skills/octocode-research/src/types/errorGuards.ts +0 -15
- package/skills/octocode-research/src/types/guards.ts +6 -27
- package/skills/octocode-research/src/types/mcp.ts +1 -8
- package/skills/octocode-research/src/types/responses.ts +0 -15
- package/skills/octocode-research/src/utils/asyncTimeout.ts +1 -63
- package/skills/octocode-research/src/utils/circuitBreaker.ts +43 -166
- package/skills/octocode-research/src/utils/colors.ts +3 -17
- package/skills/octocode-research/src/utils/errorQueue.ts +6 -28
- package/skills/octocode-research/src/utils/logger.ts +11 -93
- package/skills/octocode-research/src/utils/resilience.ts +22 -77
- package/skills/octocode-research/src/utils/responseBuilder.ts +26 -78
- package/skills/octocode-research/src/utils/responseFactory.ts +0 -7
- package/skills/octocode-research/src/utils/responseParser.ts +11 -59
- package/skills/octocode-research/src/utils/retry.ts +12 -75
- package/skills/octocode-research/src/utils/routeFactory.ts +10 -61
- package/skills/octocode-research/src/utils/url.ts +0 -5
- package/skills/octocode-research/src/validation/httpPreprocess.ts +15 -61
- package/skills/octocode-research/src/validation/schemas.ts +14 -101
- package/skills/octocode-research/src/validation/toolCallSchema.ts +5 -65
- package/skills/octocode-research/tsdown.config.ts +2 -7
- package/skills/octocode-search-skill/SKILL.md +9 -2
- package/skills/octocode-search-skill/references/discovery-surfaces.md +2 -1
- package/skills/octocode-slides/scripts/navbridge.js +1 -14
- package/skills/octocode-slides/scripts/presenter.js +2 -61
- package/skills/octocode-stats/scripts/build_dashboard.mjs +1 -19
- package/assets/example.png +0 -0
- package/out/chunks/chunk-7476PETK.js +0 -309
- package/out/chunks/chunk-OQBJTZWK.js +0 -60
- package/out/chunks/chunk-UCZCF3BQ.js +0 -9
- package/out/chunks/commands-XBFPLHSQ.js +0 -8
- package/out/chunks/main-help-ULF5PAQY.js +0 -10
- package/out/chunks/tool-command-M6VA7P2F.js +0 -8
- package/skills/octocode-research/AGENTS.md +0 -349
|
@@ -1,27 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Type guard utilities for consistent runtime type checking.
|
|
3
|
-
* Use these instead of inline typeof checks across routes.
|
|
4
|
-
*
|
|
5
|
-
* @module types/guards
|
|
6
|
-
*/
|
|
7
1
|
|
|
8
|
-
/**
|
|
9
|
-
* Check if value is an array (of any type)
|
|
10
|
-
*/
|
|
11
2
|
export function isArray(value: unknown): value is unknown[] {
|
|
12
3
|
return Array.isArray(value);
|
|
13
4
|
}
|
|
14
5
|
|
|
15
|
-
|
|
16
|
-
* Check if value is a non-null object
|
|
17
|
-
*/
|
|
6
|
+
|
|
18
7
|
export function isObject(value: unknown): value is Record<string, unknown> {
|
|
19
8
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
20
9
|
}
|
|
21
10
|
|
|
22
|
-
|
|
23
|
-
* Check if object has a specific property
|
|
24
|
-
*/
|
|
11
|
+
|
|
25
12
|
export function hasProperty<K extends string>(
|
|
26
13
|
obj: unknown,
|
|
27
14
|
key: K
|
|
@@ -29,9 +16,7 @@ export function hasProperty<K extends string>(
|
|
|
29
16
|
return typeof obj === 'object' && obj !== null && key in obj;
|
|
30
17
|
}
|
|
31
18
|
|
|
32
|
-
|
|
33
|
-
* Check if object has a string property
|
|
34
|
-
*/
|
|
19
|
+
|
|
35
20
|
export function hasStringProperty<K extends string>(
|
|
36
21
|
obj: unknown,
|
|
37
22
|
key: K
|
|
@@ -39,9 +24,7 @@ export function hasStringProperty<K extends string>(
|
|
|
39
24
|
return hasProperty(obj, key) && typeof obj[key] === 'string';
|
|
40
25
|
}
|
|
41
26
|
|
|
42
|
-
|
|
43
|
-
* Check if object has a number property
|
|
44
|
-
*/
|
|
27
|
+
|
|
45
28
|
export function hasNumberProperty<K extends string>(
|
|
46
29
|
obj: unknown,
|
|
47
30
|
key: K
|
|
@@ -49,9 +32,7 @@ export function hasNumberProperty<K extends string>(
|
|
|
49
32
|
return hasProperty(obj, key) && typeof obj[key] === 'number';
|
|
50
33
|
}
|
|
51
34
|
|
|
52
|
-
|
|
53
|
-
* Check if object has a boolean property
|
|
54
|
-
*/
|
|
35
|
+
|
|
55
36
|
export function hasBooleanProperty<K extends string>(
|
|
56
37
|
obj: unknown,
|
|
57
38
|
key: K
|
|
@@ -59,9 +40,7 @@ export function hasBooleanProperty<K extends string>(
|
|
|
59
40
|
return hasProperty(obj, key) && typeof obj[key] === 'boolean';
|
|
60
41
|
}
|
|
61
42
|
|
|
62
|
-
|
|
63
|
-
* Check if object has an array property
|
|
64
|
-
*/
|
|
43
|
+
|
|
65
44
|
export function hasArrayProperty<K extends string>(
|
|
66
45
|
obj: unknown,
|
|
67
46
|
key: K
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Protocol utilities for /tools endpoints
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
1
|
interface JsonSchemaProperty {
|
|
6
2
|
type: string;
|
|
7
3
|
description?: string;
|
|
@@ -26,10 +22,7 @@ interface JsonSchema {
|
|
|
26
22
|
additionalProperties?: boolean;
|
|
27
23
|
}
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
* Transform description-based schema to JSON Schema format.
|
|
31
|
-
* Fallback transformer when full JSON schemas are not available.
|
|
32
|
-
*/
|
|
25
|
+
|
|
33
26
|
export function transformToJsonSchema(
|
|
34
27
|
schemaDescriptions: Record<string, string>,
|
|
35
28
|
toolName: string
|
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Response types for MCP tool results.
|
|
3
|
-
*
|
|
4
|
-
* @module types/responses
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
import { hasProperty, hasArrayProperty } from './guards.js';
|
|
8
2
|
|
|
9
|
-
// =============================================================================
|
|
10
|
-
// File and Search Types
|
|
11
|
-
// =============================================================================
|
|
12
3
|
|
|
13
4
|
export interface FileMatch {
|
|
14
5
|
path: string;
|
|
@@ -37,9 +28,6 @@ export interface PaginationInfo {
|
|
|
37
28
|
nextCursor?: string;
|
|
38
29
|
}
|
|
39
30
|
|
|
40
|
-
// =============================================================================
|
|
41
|
-
// Internal helpers
|
|
42
|
-
// =============================================================================
|
|
43
31
|
|
|
44
32
|
function isFileMatch(obj: unknown): obj is FileMatch {
|
|
45
33
|
return (
|
|
@@ -56,9 +44,6 @@ function hasValidPagination(obj: unknown): obj is { pagination: PaginationInfo }
|
|
|
56
44
|
return typeof p === 'object' && p !== null;
|
|
57
45
|
}
|
|
58
46
|
|
|
59
|
-
// =============================================================================
|
|
60
|
-
// Public extractors
|
|
61
|
-
// =============================================================================
|
|
62
47
|
|
|
63
48
|
export function extractFiles(data: unknown): FileMatch[] {
|
|
64
49
|
if (!hasArrayProperty(data, 'files')) return [];
|
|
@@ -1,45 +1,8 @@
|
|
|
1
1
|
import { errorQueue } from './errorQueue.js';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Timeout wrapper for fire-and-forget async operations.
|
|
5
|
-
*
|
|
6
|
-
* Prevents unbounded async operations from hanging indefinitely.
|
|
7
|
-
* Uses Promise.race with AbortController for clean cancellation.
|
|
8
|
-
*
|
|
9
|
-
* @module utils/asyncTimeout
|
|
10
|
-
*/
|
|
11
3
|
|
|
12
|
-
/**
|
|
13
|
-
* Default timeout for fire-and-forget operations (5 seconds).
|
|
14
|
-
* Tuned for logging/telemetry operations that should complete quickly.
|
|
15
|
-
*/
|
|
16
4
|
const DEFAULT_TIMEOUT_MS = 5000;
|
|
17
5
|
|
|
18
|
-
/**
|
|
19
|
-
* Execute an async operation with timeout protection.
|
|
20
|
-
*
|
|
21
|
-
* Fire-and-forget pattern with built-in timeout to prevent:
|
|
22
|
-
* - Memory leaks from unresolved promises
|
|
23
|
-
* - Resource exhaustion from hanging operations
|
|
24
|
-
* - Cascading failures from slow logging/telemetry
|
|
25
|
-
*
|
|
26
|
-
* @param operation - Async operation to execute
|
|
27
|
-
* @param timeoutMs - Maximum time to wait (default: 5000ms)
|
|
28
|
-
* @param context - Context string for error logging
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* ```typescript
|
|
32
|
-
* // Instead of:
|
|
33
|
-
* logSessionError(tool, code).catch(err => errorQueue.push(err, 'logSessionError'));
|
|
34
|
-
*
|
|
35
|
-
* // Use:
|
|
36
|
-
* fireAndForgetWithTimeout(
|
|
37
|
-
* () => logSessionError(tool, code),
|
|
38
|
-
* 5000,
|
|
39
|
-
* 'logSessionError'
|
|
40
|
-
* );
|
|
41
|
-
* ```
|
|
42
|
-
*/
|
|
43
6
|
export function fireAndForgetWithTimeout(
|
|
44
7
|
operation: () => Promise<unknown>,
|
|
45
8
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
@@ -48,21 +11,17 @@ export function fireAndForgetWithTimeout(
|
|
|
48
11
|
const controller = new AbortController();
|
|
49
12
|
const { signal } = controller;
|
|
50
13
|
|
|
51
|
-
// Create timeout promise
|
|
52
14
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
53
15
|
const timeoutId = setTimeout(() => {
|
|
54
16
|
controller.abort();
|
|
55
17
|
reject(new Error(`Operation timed out after ${timeoutMs}ms`));
|
|
56
18
|
}, timeoutMs);
|
|
57
19
|
|
|
58
|
-
// Clean up timeout if operation completes first
|
|
59
20
|
signal.addEventListener('abort', () => clearTimeout(timeoutId), { once: true });
|
|
60
21
|
});
|
|
61
22
|
|
|
62
|
-
// Race operation against timeout
|
|
63
23
|
Promise.race([operation(), timeoutPromise])
|
|
64
24
|
.catch((err: unknown) => {
|
|
65
|
-
// Log to error queue - never throw from fire-and-forget
|
|
66
25
|
errorQueue.push(
|
|
67
26
|
err instanceof Error ? err : new Error(String(err)),
|
|
68
27
|
context
|
|
@@ -70,27 +29,6 @@ export function fireAndForgetWithTimeout(
|
|
|
70
29
|
});
|
|
71
30
|
}
|
|
72
31
|
|
|
73
|
-
/**
|
|
74
|
-
* Execute an async operation with timeout and return result.
|
|
75
|
-
*
|
|
76
|
-
* Unlike fireAndForgetWithTimeout, this returns the result or throws on timeout.
|
|
77
|
-
* Use for operations where you need the result but want timeout protection.
|
|
78
|
-
*
|
|
79
|
-
* @param operation - Async operation to execute
|
|
80
|
-
* @param timeoutMs - Maximum time to wait
|
|
81
|
-
* @param context - Context string for timeout error message
|
|
82
|
-
* @returns Promise resolving to operation result
|
|
83
|
-
* @throws Error if operation times out
|
|
84
|
-
*
|
|
85
|
-
* @example
|
|
86
|
-
* ```typescript
|
|
87
|
-
* const result = await withTimeout(
|
|
88
|
-
* () => fetchData(),
|
|
89
|
-
* 3000,
|
|
90
|
-
* 'fetchData'
|
|
91
|
-
* );
|
|
92
|
-
* ```
|
|
93
|
-
*/
|
|
94
32
|
export async function withTimeout<T>(
|
|
95
33
|
operation: () => Promise<T>,
|
|
96
34
|
timeoutMs: number,
|
|
@@ -111,6 +49,6 @@ export async function withTimeout<T>(
|
|
|
111
49
|
try {
|
|
112
50
|
return await Promise.race([operation(), timeoutPromise]);
|
|
113
51
|
} finally {
|
|
114
|
-
controller.abort();
|
|
52
|
+
controller.abort();
|
|
115
53
|
}
|
|
116
54
|
}
|
|
@@ -1,25 +1,14 @@
|
|
|
1
1
|
import { agentLog, warnLog, successLog, errorLog } from './colors.js';
|
|
2
2
|
import { logRateLimit } from '../index.js';
|
|
3
3
|
import { fireAndForgetWithTimeout } from './asyncTimeout.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
* Prevents cascading failures by temporarily stopping calls to failing services.
|
|
8
|
-
*
|
|
9
|
-
* @module utils/circuitBreaker
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Circuit breaker states
|
|
14
|
-
*/
|
|
4
|
+
|
|
5
|
+
|
|
15
6
|
type CircuitState = 'closed' | 'open' | 'half-open';
|
|
16
7
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const CIRCUIT_TTL_MS = 3600000; // 1 hour TTL for stale circuits
|
|
22
|
-
const CLEANUP_INTERVAL_MS = 600000; // 10 minutes between cleanup runs
|
|
8
|
+
|
|
9
|
+
const MAX_CIRCUITS = 100;
|
|
10
|
+
const CIRCUIT_TTL_MS = 3600000;
|
|
11
|
+
const CLEANUP_INTERVAL_MS = 600000;
|
|
23
12
|
|
|
24
13
|
interface CircuitRecord {
|
|
25
14
|
failures: number;
|
|
@@ -27,55 +16,37 @@ interface CircuitRecord {
|
|
|
27
16
|
lastFailure: number;
|
|
28
17
|
lastAttempt: number;
|
|
29
18
|
state: CircuitState;
|
|
30
|
-
createdAt: number;
|
|
19
|
+
createdAt: number;
|
|
31
20
|
}
|
|
32
21
|
|
|
33
|
-
|
|
34
|
-
* Circuit breaker configuration
|
|
35
|
-
*/
|
|
22
|
+
|
|
36
23
|
export interface CircuitBreakerConfig {
|
|
37
|
-
|
|
24
|
+
|
|
38
25
|
failureThreshold: number;
|
|
39
|
-
|
|
26
|
+
|
|
40
27
|
successThreshold: number;
|
|
41
|
-
|
|
28
|
+
|
|
42
29
|
resetTimeoutMs: number;
|
|
43
30
|
}
|
|
44
31
|
|
|
45
|
-
|
|
46
|
-
* Default circuit breaker configuration.
|
|
47
|
-
* Tuned for balance between fault tolerance and quick recovery.
|
|
48
|
-
*/
|
|
32
|
+
|
|
49
33
|
const DEFAULT_CONFIG: CircuitBreakerConfig = {
|
|
50
|
-
// 3 failures: Quick to detect persistent issues,
|
|
51
|
-
// but tolerant of occasional transient errors.
|
|
52
34
|
failureThreshold: 3,
|
|
53
35
|
|
|
54
|
-
// 2 successes: Requires service to prove stability
|
|
55
|
-
// before fully resuming (prevents flapping).
|
|
56
36
|
successThreshold: 2,
|
|
57
37
|
|
|
58
|
-
// 30s timeout: Allows services time to recover from
|
|
59
|
-
// rate limits or temporary outages before retrying.
|
|
60
38
|
resetTimeoutMs: 30000,
|
|
61
39
|
};
|
|
62
40
|
|
|
63
|
-
|
|
64
|
-
* Circuit breaker registry
|
|
65
|
-
*/
|
|
41
|
+
|
|
66
42
|
const circuits = new Map<string, CircuitRecord>();
|
|
67
43
|
const configs = new Map<string, CircuitBreakerConfig>();
|
|
68
44
|
|
|
69
|
-
|
|
70
|
-
* Get or create circuit breaker for a named service.
|
|
71
|
-
* Enforces MAX_CIRCUITS limit by cleaning up stale circuits when limit is reached.
|
|
72
|
-
*/
|
|
45
|
+
|
|
73
46
|
function getCircuit(name: string): CircuitRecord {
|
|
74
47
|
if (!circuits.has(name)) {
|
|
75
|
-
// Enforce circuit limit by cleaning up stale circuits
|
|
76
48
|
if (circuits.size >= MAX_CIRCUITS) {
|
|
77
49
|
cleanupStaleCircuits();
|
|
78
|
-
// If still at limit after cleanup, remove oldest circuit
|
|
79
50
|
if (circuits.size >= MAX_CIRCUITS) {
|
|
80
51
|
const oldestKey = findOldestCircuit();
|
|
81
52
|
if (oldestKey) {
|
|
@@ -98,9 +69,7 @@ function getCircuit(name: string): CircuitRecord {
|
|
|
98
69
|
return circuits.get(name)!;
|
|
99
70
|
}
|
|
100
71
|
|
|
101
|
-
|
|
102
|
-
* Find the oldest circuit by lastAttempt time
|
|
103
|
-
*/
|
|
72
|
+
|
|
104
73
|
function findOldestCircuit(): string | null {
|
|
105
74
|
let oldest: string | null = null;
|
|
106
75
|
let oldestTime = Infinity;
|
|
@@ -115,16 +84,12 @@ function findOldestCircuit(): string | null {
|
|
|
115
84
|
return oldest;
|
|
116
85
|
}
|
|
117
86
|
|
|
118
|
-
|
|
119
|
-
* Get configuration for a circuit
|
|
120
|
-
*/
|
|
87
|
+
|
|
121
88
|
function getConfig(name: string): CircuitBreakerConfig {
|
|
122
89
|
return configs.get(name) || DEFAULT_CONFIG;
|
|
123
90
|
}
|
|
124
91
|
|
|
125
|
-
|
|
126
|
-
* Configure a specific circuit breaker
|
|
127
|
-
*/
|
|
92
|
+
|
|
128
93
|
export function configureCircuit(
|
|
129
94
|
name: string,
|
|
130
95
|
config: Partial<CircuitBreakerConfig>
|
|
@@ -132,24 +97,6 @@ export function configureCircuit(
|
|
|
132
97
|
configs.set(name, { ...DEFAULT_CONFIG, ...config });
|
|
133
98
|
}
|
|
134
99
|
|
|
135
|
-
/**
|
|
136
|
-
* Execute operation with circuit breaker protection.
|
|
137
|
-
*
|
|
138
|
-
* @param name - Circuit breaker name (e.g., 'lsp', 'github')
|
|
139
|
-
* @param operation - Async operation to execute
|
|
140
|
-
* @param fallback - Optional fallback when circuit is open
|
|
141
|
-
* @returns Operation result or fallback
|
|
142
|
-
* @throws CircuitOpenError if circuit is open and no fallback provided
|
|
143
|
-
*
|
|
144
|
-
* @example
|
|
145
|
-
* ```typescript
|
|
146
|
-
* const result = await withCircuitBreaker(
|
|
147
|
-
* 'lsp',
|
|
148
|
-
* () => lspGotoDefinition({ queries }),
|
|
149
|
-
* () => ({ fallback: true, locations: [] })
|
|
150
|
-
* );
|
|
151
|
-
* ```
|
|
152
|
-
*/
|
|
153
100
|
export async function withCircuitBreaker<T>(
|
|
154
101
|
name: string,
|
|
155
102
|
operation: () => Promise<T>,
|
|
@@ -159,17 +106,13 @@ export async function withCircuitBreaker<T>(
|
|
|
159
106
|
const config = getConfig(name);
|
|
160
107
|
const now = Date.now();
|
|
161
108
|
|
|
162
|
-
// Update last attempt time
|
|
163
109
|
circuit.lastAttempt = now;
|
|
164
110
|
|
|
165
|
-
// Check circuit state
|
|
166
111
|
if (circuit.state === 'open') {
|
|
167
|
-
// Check if we should try half-open
|
|
168
112
|
if (now - circuit.lastFailure > config.resetTimeoutMs) {
|
|
169
113
|
circuit.state = 'half-open';
|
|
170
114
|
console.log(warnLog(`🟡 Circuit ${name} entering half-open state`));
|
|
171
115
|
} else {
|
|
172
|
-
// Circuit is open - use fallback or throw
|
|
173
116
|
console.log(
|
|
174
117
|
`🔴 Circuit ${name} is OPEN - ${Math.ceil((circuit.lastFailure + config.resetTimeoutMs - now) / 1000)}s until retry`
|
|
175
118
|
);
|
|
@@ -183,7 +126,6 @@ export async function withCircuitBreaker<T>(
|
|
|
183
126
|
try {
|
|
184
127
|
const result = await operation();
|
|
185
128
|
|
|
186
|
-
// Success - handle state transition
|
|
187
129
|
if (circuit.state === 'half-open') {
|
|
188
130
|
circuit.successes++;
|
|
189
131
|
if (circuit.successes >= config.successThreshold) {
|
|
@@ -193,22 +135,18 @@ export async function withCircuitBreaker<T>(
|
|
|
193
135
|
console.log(successLog(`🟢 Circuit ${name} CLOSED after recovery`));
|
|
194
136
|
}
|
|
195
137
|
} else {
|
|
196
|
-
// Reset failures on success in closed state
|
|
197
138
|
circuit.failures = 0;
|
|
198
139
|
}
|
|
199
140
|
|
|
200
141
|
return result;
|
|
201
142
|
} catch (error) {
|
|
202
|
-
// Failure - handle state transition
|
|
203
143
|
circuit.failures++;
|
|
204
144
|
circuit.lastFailure = now;
|
|
205
145
|
circuit.successes = 0;
|
|
206
146
|
|
|
207
147
|
if (circuit.state === 'half-open') {
|
|
208
|
-
// Failed in half-open - back to open
|
|
209
148
|
circuit.state = 'open';
|
|
210
149
|
console.log(errorLog(`🔴 Circuit ${name} back to OPEN after half-open failure`));
|
|
211
|
-
// Log rate limit/circuit open event to session telemetry
|
|
212
150
|
fireAndForgetWithTimeout(
|
|
213
151
|
() => logRateLimit({
|
|
214
152
|
limit_type: 'secondary',
|
|
@@ -220,12 +158,10 @@ export async function withCircuitBreaker<T>(
|
|
|
220
158
|
'logRateLimit'
|
|
221
159
|
);
|
|
222
160
|
} else if (circuit.failures >= config.failureThreshold) {
|
|
223
|
-
// Too many failures - open circuit
|
|
224
161
|
circuit.state = 'open';
|
|
225
162
|
console.log(
|
|
226
163
|
`🔴 Circuit ${name} OPENED after ${circuit.failures} failures`
|
|
227
164
|
);
|
|
228
|
-
// Log rate limit/circuit open event to session telemetry
|
|
229
165
|
fireAndForgetWithTimeout(
|
|
230
166
|
() => logRateLimit({
|
|
231
167
|
limit_type: 'secondary',
|
|
@@ -242,9 +178,7 @@ export async function withCircuitBreaker<T>(
|
|
|
242
178
|
}
|
|
243
179
|
}
|
|
244
180
|
|
|
245
|
-
|
|
246
|
-
* Get current state of a circuit breaker
|
|
247
|
-
*/
|
|
181
|
+
|
|
248
182
|
export function getCircuitState(name: string): {
|
|
249
183
|
state: CircuitState;
|
|
250
184
|
failures: number;
|
|
@@ -260,9 +194,7 @@ export function getCircuitState(name: string): {
|
|
|
260
194
|
};
|
|
261
195
|
}
|
|
262
196
|
|
|
263
|
-
|
|
264
|
-
* Reset a circuit breaker to closed state
|
|
265
|
-
*/
|
|
197
|
+
|
|
266
198
|
export function resetCircuit(name: string): void {
|
|
267
199
|
const circuit = getCircuit(name);
|
|
268
200
|
circuit.state = 'closed';
|
|
@@ -272,9 +204,7 @@ export function resetCircuit(name: string): void {
|
|
|
272
204
|
console.log(agentLog(`🔄 Circuit ${name} manually reset to CLOSED`));
|
|
273
205
|
}
|
|
274
206
|
|
|
275
|
-
|
|
276
|
-
* Get all circuit states (for health endpoint)
|
|
277
|
-
*/
|
|
207
|
+
|
|
278
208
|
export function getAllCircuitStates(): Record<
|
|
279
209
|
string,
|
|
280
210
|
{ state: CircuitState; failures: number; isHealthy: boolean }
|
|
@@ -295,9 +225,7 @@ export function getAllCircuitStates(): Record<
|
|
|
295
225
|
return states;
|
|
296
226
|
}
|
|
297
227
|
|
|
298
|
-
|
|
299
|
-
* Error thrown when circuit is open
|
|
300
|
-
*/
|
|
228
|
+
|
|
301
229
|
export class CircuitOpenError extends Error {
|
|
302
230
|
readonly circuitName: string;
|
|
303
231
|
readonly retryAfterMs: number;
|
|
@@ -310,107 +238,71 @@ export class CircuitOpenError extends Error {
|
|
|
310
238
|
}
|
|
311
239
|
}
|
|
312
240
|
|
|
313
|
-
// =============================================================================
|
|
314
|
-
// Pre-configured circuits (per-tool granularity)
|
|
315
|
-
// =============================================================================
|
|
316
241
|
|
|
317
|
-
// -----------------------------------------------------------------------------
|
|
318
|
-
// GitHub circuits - split by API endpoint (different rate limits)
|
|
319
|
-
// -----------------------------------------------------------------------------
|
|
320
|
-
|
|
321
|
-
// GitHub Search API - aggressive rate limiting (30 req/min)
|
|
322
242
|
configureCircuit('github:search', {
|
|
323
|
-
failureThreshold: 2,
|
|
324
|
-
successThreshold: 1,
|
|
325
|
-
resetTimeoutMs: 60000,
|
|
243
|
+
failureThreshold: 2,
|
|
244
|
+
successThreshold: 1,
|
|
245
|
+
resetTimeoutMs: 60000,
|
|
326
246
|
});
|
|
327
247
|
|
|
328
|
-
// GitHub Content API - higher limits, separate quota
|
|
329
248
|
configureCircuit('github:content', {
|
|
330
|
-
failureThreshold: 3,
|
|
249
|
+
failureThreshold: 3,
|
|
331
250
|
successThreshold: 1,
|
|
332
|
-
resetTimeoutMs: 30000,
|
|
251
|
+
resetTimeoutMs: 30000,
|
|
333
252
|
});
|
|
334
253
|
|
|
335
|
-
// GitHub PR API - separate quota from search
|
|
336
254
|
configureCircuit('github:pulls', {
|
|
337
|
-
failureThreshold: 2,
|
|
255
|
+
failureThreshold: 2,
|
|
338
256
|
successThreshold: 1,
|
|
339
|
-
resetTimeoutMs: 60000,
|
|
257
|
+
resetTimeoutMs: 60000,
|
|
340
258
|
});
|
|
341
259
|
|
|
342
|
-
// Legacy fallback for any uncategorized github tools
|
|
343
260
|
configureCircuit('github', {
|
|
344
261
|
failureThreshold: 2,
|
|
345
262
|
successThreshold: 1,
|
|
346
263
|
resetTimeoutMs: 60000,
|
|
347
264
|
});
|
|
348
265
|
|
|
349
|
-
// -----------------------------------------------------------------------------
|
|
350
|
-
// LSP circuits - split by operation weight
|
|
351
|
-
// -----------------------------------------------------------------------------
|
|
352
|
-
|
|
353
|
-
// LSP Navigation (definition, references) - fast, single lookup
|
|
354
266
|
configureCircuit('lsp:navigation', {
|
|
355
|
-
failureThreshold: 3,
|
|
356
|
-
successThreshold: 1,
|
|
357
|
-
resetTimeoutMs: 10000,
|
|
267
|
+
failureThreshold: 3,
|
|
268
|
+
successThreshold: 1,
|
|
269
|
+
resetTimeoutMs: 10000,
|
|
358
270
|
});
|
|
359
271
|
|
|
360
|
-
// LSP Call Hierarchy - heavier recursive operation
|
|
361
272
|
configureCircuit('lsp:hierarchy', {
|
|
362
|
-
failureThreshold: 2,
|
|
273
|
+
failureThreshold: 2,
|
|
363
274
|
successThreshold: 1,
|
|
364
|
-
resetTimeoutMs: 15000,
|
|
275
|
+
resetTimeoutMs: 15000,
|
|
365
276
|
});
|
|
366
277
|
|
|
367
|
-
// Legacy fallback
|
|
368
278
|
configureCircuit('lsp', {
|
|
369
279
|
failureThreshold: 3,
|
|
370
280
|
successThreshold: 1,
|
|
371
281
|
resetTimeoutMs: 10000,
|
|
372
282
|
});
|
|
373
283
|
|
|
374
|
-
// -----------------------------------------------------------------------------
|
|
375
|
-
// Local circuits - unified (same failure mode: filesystem)
|
|
376
|
-
// -----------------------------------------------------------------------------
|
|
377
|
-
|
|
378
284
|
configureCircuit('local', {
|
|
379
|
-
failureThreshold: 5,
|
|
380
|
-
successThreshold: 1,
|
|
381
|
-
resetTimeoutMs: 5000,
|
|
285
|
+
failureThreshold: 5,
|
|
286
|
+
successThreshold: 1,
|
|
287
|
+
resetTimeoutMs: 5000,
|
|
382
288
|
});
|
|
383
289
|
|
|
384
|
-
// -----------------------------------------------------------------------------
|
|
385
|
-
// Package circuits - unified (npm/PyPI)
|
|
386
|
-
// -----------------------------------------------------------------------------
|
|
387
|
-
|
|
388
290
|
configureCircuit('package', {
|
|
389
|
-
failureThreshold: 3,
|
|
390
|
-
successThreshold: 1,
|
|
391
|
-
resetTimeoutMs: 45000,
|
|
291
|
+
failureThreshold: 3,
|
|
292
|
+
successThreshold: 1,
|
|
293
|
+
resetTimeoutMs: 45000,
|
|
392
294
|
});
|
|
393
295
|
|
|
394
|
-
// =============================================================================
|
|
395
|
-
// Automatic Cleanup (Memory Leak Prevention)
|
|
396
|
-
// =============================================================================
|
|
397
296
|
|
|
398
|
-
/**
|
|
399
|
-
* Cleanup interval reference (for graceful shutdown)
|
|
400
|
-
*/
|
|
401
297
|
let cleanupIntervalId: ReturnType<typeof setInterval> | null = null;
|
|
402
298
|
|
|
403
|
-
|
|
404
|
-
* Remove stale circuits that haven't been used recently.
|
|
405
|
-
* A circuit is stale if it hasn't been attempted in CIRCUIT_TTL_MS.
|
|
406
|
-
*/
|
|
299
|
+
|
|
407
300
|
function cleanupStaleCircuits(): void {
|
|
408
301
|
const now = Date.now();
|
|
409
302
|
const staleCutoff = now - CIRCUIT_TTL_MS;
|
|
410
303
|
let removedCount = 0;
|
|
411
304
|
|
|
412
305
|
for (const [name, circuit] of circuits) {
|
|
413
|
-
// Don't remove circuits that are currently open (still tracking failure)
|
|
414
306
|
if (circuit.state !== 'open' && circuit.lastAttempt < staleCutoff) {
|
|
415
307
|
circuits.delete(name);
|
|
416
308
|
configs.delete(name);
|
|
@@ -423,27 +315,20 @@ function cleanupStaleCircuits(): void {
|
|
|
423
315
|
}
|
|
424
316
|
}
|
|
425
317
|
|
|
426
|
-
|
|
427
|
-
* Start the periodic cleanup interval.
|
|
428
|
-
* Called automatically on module load.
|
|
429
|
-
*/
|
|
318
|
+
|
|
430
319
|
function startPeriodicCleanup(): void {
|
|
431
|
-
if (cleanupIntervalId) return;
|
|
320
|
+
if (cleanupIntervalId) return;
|
|
432
321
|
|
|
433
322
|
cleanupIntervalId = setInterval(() => {
|
|
434
323
|
cleanupStaleCircuits();
|
|
435
324
|
}, CLEANUP_INTERVAL_MS);
|
|
436
325
|
|
|
437
|
-
// Unref the interval so it doesn't prevent process exit
|
|
438
326
|
if (cleanupIntervalId.unref) {
|
|
439
327
|
cleanupIntervalId.unref();
|
|
440
328
|
}
|
|
441
329
|
}
|
|
442
330
|
|
|
443
|
-
|
|
444
|
-
* Stop the periodic cleanup interval.
|
|
445
|
-
* Call during graceful shutdown.
|
|
446
|
-
*/
|
|
331
|
+
|
|
447
332
|
export function stopCircuitCleanup(): void {
|
|
448
333
|
if (cleanupIntervalId) {
|
|
449
334
|
clearInterval(cleanupIntervalId);
|
|
@@ -451,17 +336,9 @@ export function stopCircuitCleanup(): void {
|
|
|
451
336
|
}
|
|
452
337
|
}
|
|
453
338
|
|
|
454
|
-
// Start periodic cleanup on module load
|
|
455
339
|
startPeriodicCleanup();
|
|
456
340
|
|
|
457
|
-
// =============================================================================
|
|
458
|
-
// Manual Cleanup Functions
|
|
459
|
-
// =============================================================================
|
|
460
341
|
|
|
461
|
-
/**
|
|
462
|
-
* Clear all circuit breakers.
|
|
463
|
-
* Use for testing cleanup or server shutdown.
|
|
464
|
-
*/
|
|
465
342
|
export function clearAllCircuits(): void {
|
|
466
343
|
const count = circuits.size;
|
|
467
344
|
circuits.clear();
|
|
@@ -1,33 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ANSI color codes for terminal output
|
|
3
|
-
*
|
|
4
|
-
* Color scheme:
|
|
5
|
-
* - AGENT (purple): Server/agent messages, startup, shutdown
|
|
6
|
-
* - RESULT (blue): Tool output, skill results
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
1
|
const colors = {
|
|
10
|
-
// Reset
|
|
11
2
|
reset: '\x1b[0m',
|
|
12
3
|
|
|
13
|
-
// Agent messages - Purple/Magenta
|
|
14
4
|
agent: '\x1b[35m',
|
|
15
5
|
agentBright: '\x1b[95m',
|
|
16
6
|
|
|
17
|
-
// Results/Tool output - Blue
|
|
18
7
|
result: '\x1b[34m',
|
|
19
8
|
resultBright: '\x1b[94m',
|
|
20
9
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
warn: '\x1b[33m', // Yellow
|
|
10
|
+
success: '\x1b[32m',
|
|
11
|
+
error: '\x1b[31m',
|
|
12
|
+
warn: '\x1b[33m',
|
|
25
13
|
|
|
26
|
-
// Dim for secondary info
|
|
27
14
|
dim: '\x1b[2m',
|
|
28
15
|
};
|
|
29
16
|
|
|
30
|
-
// Helper functions for consistent formatting
|
|
31
17
|
export function agentLog(message: string): string {
|
|
32
18
|
return `${colors.agentBright}${message}${colors.reset}`;
|
|
33
19
|
}
|