awesome-slash 2.4.3 → 2.5.0
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/.claude-plugin/marketplace.json +6 -6
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +99 -1
- package/README.md +173 -161
- package/SECURITY.md +25 -81
- package/adapters/codex/install.sh +58 -16
- package/adapters/opencode/install.sh +92 -23
- package/lib/index.js +47 -4
- package/lib/patterns/review-patterns.js +58 -11
- package/lib/patterns/slop-patterns.js +154 -147
- package/lib/platform/detect-platform.js +99 -350
- package/lib/platform/detection-configs.js +93 -0
- package/lib/platform/verify-tools.js +10 -78
- package/lib/schemas/README.md +195 -0
- package/lib/schemas/validator.js +247 -0
- package/lib/sources/custom-handler.js +199 -0
- package/lib/sources/policy-questions.js +239 -0
- package/lib/sources/source-cache.js +149 -0
- package/lib/state/workflow-state.js +363 -665
- package/lib/types/README.md +292 -0
- package/lib/types/agent-frontmatter.d.ts +134 -0
- package/lib/types/command-frontmatter.d.ts +107 -0
- package/lib/types/hook-frontmatter.d.ts +115 -0
- package/lib/types/index.d.ts +84 -0
- package/lib/types/plugin-manifest.d.ts +102 -0
- package/lib/types/skill-frontmatter.d.ts +89 -0
- package/lib/utils/cache-manager.js +154 -0
- package/lib/utils/context-optimizer.js +5 -36
- package/lib/utils/deprecation.js +37 -0
- package/lib/utils/shell-escape.js +88 -0
- package/mcp-server/index.js +513 -18
- package/package.json +6 -2
- package/plugins/deslop-around/.claude-plugin/plugin.json +1 -1
- package/plugins/deslop-around/lib/index.js +170 -0
- package/plugins/deslop-around/lib/patterns/review-patterns.js +58 -11
- package/plugins/deslop-around/lib/patterns/slop-patterns.js +170 -129
- package/plugins/deslop-around/lib/platform/detect-platform.js +212 -123
- package/plugins/deslop-around/lib/platform/detection-configs.js +93 -0
- package/plugins/deslop-around/lib/platform/verify-tools.js +10 -1
- package/plugins/deslop-around/lib/schemas/README.md +195 -0
- package/plugins/deslop-around/lib/schemas/validator.js +205 -0
- package/plugins/deslop-around/lib/sources/custom-handler.js +199 -0
- package/plugins/deslop-around/lib/sources/policy-questions.js +239 -0
- package/plugins/deslop-around/lib/sources/source-cache.js +149 -0
- package/plugins/deslop-around/lib/state/workflow-state.js +382 -484
- package/plugins/deslop-around/lib/types/README.md +292 -0
- package/plugins/deslop-around/lib/types/agent-frontmatter.d.ts +134 -0
- package/plugins/deslop-around/lib/types/command-frontmatter.d.ts +107 -0
- package/plugins/deslop-around/lib/types/hook-frontmatter.d.ts +115 -0
- package/plugins/deslop-around/lib/types/index.d.ts +84 -0
- package/plugins/deslop-around/lib/types/plugin-manifest.d.ts +102 -0
- package/plugins/deslop-around/lib/types/skill-frontmatter.d.ts +89 -0
- package/plugins/deslop-around/lib/utils/cache-manager.js +154 -0
- package/plugins/deslop-around/lib/utils/context-optimizer.js +115 -37
- package/plugins/deslop-around/lib/utils/deprecation.js +37 -0
- package/plugins/deslop-around/lib/utils/shell-escape.js +88 -0
- package/plugins/next-task/.claude-plugin/plugin.json +1 -1
- package/plugins/next-task/agents/ci-monitor.md +19 -0
- package/plugins/next-task/agents/delivery-validator.md +2 -2
- package/plugins/next-task/agents/implementation-agent.md +3 -4
- package/plugins/next-task/agents/planning-agent.md +77 -19
- package/plugins/next-task/agents/review-orchestrator.md +21 -122
- package/plugins/next-task/agents/task-discoverer.md +164 -23
- package/plugins/next-task/commands/next-task.md +180 -14
- package/plugins/next-task/lib/index.js +170 -0
- package/plugins/next-task/lib/patterns/review-patterns.js +58 -11
- package/plugins/next-task/lib/patterns/slop-patterns.js +170 -129
- package/plugins/next-task/lib/platform/detect-platform.js +212 -123
- package/plugins/next-task/lib/platform/detection-configs.js +93 -0
- package/plugins/next-task/lib/platform/verify-tools.js +10 -1
- package/plugins/next-task/lib/schemas/README.md +195 -0
- package/plugins/next-task/lib/schemas/validator.js +205 -0
- package/plugins/next-task/lib/sources/custom-handler.js +199 -0
- package/plugins/next-task/lib/sources/policy-questions.js +239 -0
- package/plugins/next-task/lib/sources/source-cache.js +149 -0
- package/plugins/next-task/lib/state/workflow-state.js +382 -484
- package/plugins/next-task/lib/types/README.md +292 -0
- package/plugins/next-task/lib/types/agent-frontmatter.d.ts +134 -0
- package/plugins/next-task/lib/types/command-frontmatter.d.ts +107 -0
- package/plugins/next-task/lib/types/hook-frontmatter.d.ts +115 -0
- package/plugins/next-task/lib/types/index.d.ts +84 -0
- package/plugins/next-task/lib/types/plugin-manifest.d.ts +102 -0
- package/plugins/next-task/lib/types/skill-frontmatter.d.ts +89 -0
- package/plugins/next-task/lib/utils/cache-manager.js +154 -0
- package/plugins/next-task/lib/utils/context-optimizer.js +115 -37
- package/plugins/next-task/lib/utils/deprecation.js +37 -0
- package/plugins/next-task/lib/utils/shell-escape.js +88 -0
- package/plugins/project-review/.claude-plugin/plugin.json +1 -1
- package/plugins/project-review/lib/index.js +170 -0
- package/plugins/project-review/lib/patterns/review-patterns.js +58 -11
- package/plugins/project-review/lib/patterns/slop-patterns.js +170 -129
- package/plugins/project-review/lib/platform/detect-platform.js +212 -123
- package/plugins/project-review/lib/platform/detection-configs.js +93 -0
- package/plugins/project-review/lib/platform/verify-tools.js +10 -1
- package/plugins/project-review/lib/schemas/README.md +195 -0
- package/plugins/project-review/lib/schemas/validator.js +205 -0
- package/plugins/project-review/lib/sources/custom-handler.js +199 -0
- package/plugins/project-review/lib/sources/policy-questions.js +239 -0
- package/plugins/project-review/lib/sources/source-cache.js +149 -0
- package/plugins/project-review/lib/state/workflow-state.js +382 -484
- package/plugins/project-review/lib/types/README.md +292 -0
- package/plugins/project-review/lib/types/agent-frontmatter.d.ts +134 -0
- package/plugins/project-review/lib/types/command-frontmatter.d.ts +107 -0
- package/plugins/project-review/lib/types/hook-frontmatter.d.ts +115 -0
- package/plugins/project-review/lib/types/index.d.ts +84 -0
- package/plugins/project-review/lib/types/plugin-manifest.d.ts +102 -0
- package/plugins/project-review/lib/types/skill-frontmatter.d.ts +89 -0
- package/plugins/project-review/lib/utils/cache-manager.js +154 -0
- package/plugins/project-review/lib/utils/context-optimizer.js +115 -37
- package/plugins/project-review/lib/utils/deprecation.js +37 -0
- package/plugins/project-review/lib/utils/shell-escape.js +88 -0
- package/plugins/reality-check/.claude-plugin/plugin.json +1 -1
- package/plugins/reality-check/agents/code-explorer.md +1 -1
- package/plugins/ship/.claude-plugin/plugin.json +1 -1
- package/plugins/ship/commands/ship-ci-review-loop.md +19 -0
- package/plugins/ship/lib/index.js +170 -0
- package/plugins/ship/lib/patterns/review-patterns.js +58 -11
- package/plugins/ship/lib/patterns/slop-patterns.js +170 -129
- package/plugins/ship/lib/platform/detect-platform.js +212 -123
- package/plugins/ship/lib/platform/detection-configs.js +93 -0
- package/plugins/ship/lib/platform/verify-tools.js +10 -1
- package/plugins/ship/lib/schemas/README.md +195 -0
- package/plugins/ship/lib/schemas/validator.js +205 -0
- package/plugins/ship/lib/sources/custom-handler.js +199 -0
- package/plugins/ship/lib/sources/policy-questions.js +239 -0
- package/plugins/ship/lib/sources/source-cache.js +149 -0
- package/plugins/ship/lib/state/workflow-state.js +382 -484
- package/plugins/ship/lib/types/README.md +292 -0
- package/plugins/ship/lib/types/agent-frontmatter.d.ts +134 -0
- package/plugins/ship/lib/types/command-frontmatter.d.ts +107 -0
- package/plugins/ship/lib/types/hook-frontmatter.d.ts +115 -0
- package/plugins/ship/lib/types/index.d.ts +84 -0
- package/plugins/ship/lib/types/plugin-manifest.d.ts +102 -0
- package/plugins/ship/lib/types/skill-frontmatter.d.ts +89 -0
- package/plugins/ship/lib/utils/cache-manager.js +154 -0
- package/plugins/ship/lib/utils/context-optimizer.js +115 -37
- package/plugins/ship/lib/utils/deprecation.js +37 -0
- package/plugins/ship/lib/utils/shell-escape.js +88 -0
- package/lib/state/workflow-state.schema.json +0 -282
- package/plugins/deslop-around/lib/state/workflow-state.schema.json +0 -282
- package/plugins/next-task/agents/policy-selector.md +0 -248
- package/plugins/next-task/lib/state/tasks-registry.schema.json +0 -85
- package/plugins/next-task/lib/state/workflow-state.schema.json +0 -282
- package/plugins/next-task/lib/state/worktree-status.schema.json +0 -219
- package/plugins/project-review/lib/state/workflow-state.schema.json +0 -282
- package/plugins/ship/lib/state/workflow-state.schema.json +0 -282
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Interface Type Definitions
|
|
3
|
+
* Centralized type definitions for all plugin components
|
|
4
|
+
*
|
|
5
|
+
* @module lib/types
|
|
6
|
+
* @author Avi Fenesh
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Re-export all types
|
|
11
|
+
export * from './plugin-manifest';
|
|
12
|
+
export * from './command-frontmatter';
|
|
13
|
+
export * from './agent-frontmatter';
|
|
14
|
+
export * from './skill-frontmatter';
|
|
15
|
+
export * from './hook-frontmatter';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Plugin component types union
|
|
19
|
+
*/
|
|
20
|
+
export type PluginComponentType = 'command' | 'agent' | 'skill' | 'hook';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Plugin directory structure
|
|
24
|
+
*/
|
|
25
|
+
export interface PluginStructure {
|
|
26
|
+
/** Plugin root directory */
|
|
27
|
+
root: string;
|
|
28
|
+
|
|
29
|
+
/** Plugin manifest (plugin.json) */
|
|
30
|
+
manifest: string;
|
|
31
|
+
|
|
32
|
+
/** Commands directory */
|
|
33
|
+
commands?: string;
|
|
34
|
+
|
|
35
|
+
/** Agents directory */
|
|
36
|
+
agents?: string;
|
|
37
|
+
|
|
38
|
+
/** Skills directory */
|
|
39
|
+
skills?: string;
|
|
40
|
+
|
|
41
|
+
/** Hooks directory */
|
|
42
|
+
hooks?: string;
|
|
43
|
+
|
|
44
|
+
/** Shared library directory */
|
|
45
|
+
lib?: string;
|
|
46
|
+
|
|
47
|
+
/** Tests directory */
|
|
48
|
+
tests?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Plugin validation result
|
|
53
|
+
*/
|
|
54
|
+
export interface PluginValidationResult {
|
|
55
|
+
/** Whether plugin is valid */
|
|
56
|
+
valid: boolean;
|
|
57
|
+
|
|
58
|
+
/** Validation errors (if any) */
|
|
59
|
+
errors: string[];
|
|
60
|
+
|
|
61
|
+
/** Validation warnings (if any) */
|
|
62
|
+
warnings: string[];
|
|
63
|
+
|
|
64
|
+
/** Detected components */
|
|
65
|
+
components: {
|
|
66
|
+
commands: number;
|
|
67
|
+
agents: number;
|
|
68
|
+
skills: number;
|
|
69
|
+
hooks: number;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Standard plugin directory structure
|
|
75
|
+
*/
|
|
76
|
+
export const PLUGIN_STRUCTURE: Readonly<Record<string, string>> = {
|
|
77
|
+
MANIFEST: '.claude-plugin/plugin.json',
|
|
78
|
+
COMMANDS: 'commands',
|
|
79
|
+
AGENTS: 'agents',
|
|
80
|
+
SKILLS: 'skills',
|
|
81
|
+
HOOKS: 'hooks',
|
|
82
|
+
LIB: 'lib',
|
|
83
|
+
TESTS: 'tests'
|
|
84
|
+
} as const;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Manifest Type Definitions
|
|
3
|
+
* Defines the structure of plugin.json files
|
|
4
|
+
*
|
|
5
|
+
* @module lib/types/plugin-manifest
|
|
6
|
+
* @author Avi Fenesh
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Author information for plugin manifest
|
|
12
|
+
*/
|
|
13
|
+
export interface PluginAuthor {
|
|
14
|
+
/** Author's full name */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Author's email address (optional) */
|
|
17
|
+
email?: string;
|
|
18
|
+
/** Author's website or GitHub profile URL (optional) */
|
|
19
|
+
url?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Plugin manifest structure
|
|
24
|
+
* Required fields for all Claude Code plugins
|
|
25
|
+
*/
|
|
26
|
+
export interface PluginManifest {
|
|
27
|
+
/** Unique plugin identifier (kebab-case, lowercase) */
|
|
28
|
+
name: string;
|
|
29
|
+
|
|
30
|
+
/** Semantic version (MAJOR.MINOR.PATCH) */
|
|
31
|
+
version: string;
|
|
32
|
+
|
|
33
|
+
/** Short description of plugin functionality */
|
|
34
|
+
description: string;
|
|
35
|
+
|
|
36
|
+
/** Author information */
|
|
37
|
+
author: PluginAuthor;
|
|
38
|
+
|
|
39
|
+
/** Plugin homepage URL (optional) */
|
|
40
|
+
homepage?: string;
|
|
41
|
+
|
|
42
|
+
/** Repository URL (optional) */
|
|
43
|
+
repository?: string;
|
|
44
|
+
|
|
45
|
+
/** License identifier (SPDX format, e.g., "MIT", "Apache-2.0") */
|
|
46
|
+
license: string;
|
|
47
|
+
|
|
48
|
+
/** Search keywords for discoverability (optional) */
|
|
49
|
+
keywords?: string[];
|
|
50
|
+
|
|
51
|
+
/** Minimum Claude Code version required (optional) */
|
|
52
|
+
minClaudeVersion?: string;
|
|
53
|
+
|
|
54
|
+
/** Plugin dependencies (optional) */
|
|
55
|
+
dependencies?: {
|
|
56
|
+
[pluginName: string]: string; // version constraint
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Plugin configuration schema (optional) */
|
|
60
|
+
config?: {
|
|
61
|
+
[key: string]: unknown;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Type guard to check if an object is a valid PluginManifest
|
|
67
|
+
*/
|
|
68
|
+
export function isPluginManifest(obj: unknown): obj is PluginManifest {
|
|
69
|
+
if (typeof obj !== 'object' || obj === null) return false;
|
|
70
|
+
const manifest = obj as Partial<PluginManifest>;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
typeof manifest.name === 'string' &&
|
|
74
|
+
/^[a-z0-9-]+$/.test(manifest.name) &&
|
|
75
|
+
typeof manifest.version === 'string' &&
|
|
76
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/.test(manifest.version) &&
|
|
77
|
+
typeof manifest.description === 'string' &&
|
|
78
|
+
typeof manifest.author === 'object' &&
|
|
79
|
+
manifest.author !== null &&
|
|
80
|
+
typeof manifest.author.name === 'string' &&
|
|
81
|
+
typeof manifest.license === 'string'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validates a plugin manifest against the schema
|
|
87
|
+
* @throws {Error} If manifest is invalid
|
|
88
|
+
*/
|
|
89
|
+
export function validatePluginManifest(manifest: unknown): asserts manifest is PluginManifest {
|
|
90
|
+
if (!isPluginManifest(manifest)) {
|
|
91
|
+
throw new Error('Invalid plugin manifest: missing required fields or invalid format');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Additional validations
|
|
95
|
+
if (manifest.keywords && !Array.isArray(manifest.keywords)) {
|
|
96
|
+
throw new Error('Invalid plugin manifest: keywords must be an array');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (manifest.dependencies && typeof manifest.dependencies !== 'object') {
|
|
100
|
+
throw new Error('Invalid plugin manifest: dependencies must be an object');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Frontmatter Type Definitions
|
|
3
|
+
* Defines the structure of YAML frontmatter in skill markdown files
|
|
4
|
+
*
|
|
5
|
+
* @module lib/types/skill-frontmatter
|
|
6
|
+
* @author Avi Fenesh
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Skill frontmatter structure
|
|
12
|
+
* YAML metadata at the top of skill markdown files
|
|
13
|
+
*/
|
|
14
|
+
export interface SkillFrontmatter {
|
|
15
|
+
/** Skill unique identifier (kebab-case) */
|
|
16
|
+
skill: string;
|
|
17
|
+
|
|
18
|
+
/** Short description of skill purpose */
|
|
19
|
+
description: string;
|
|
20
|
+
|
|
21
|
+
/** Skill category for organization */
|
|
22
|
+
category?: string;
|
|
23
|
+
|
|
24
|
+
/** When this skill should be invoked (triggering conditions) */
|
|
25
|
+
'when-to-use'?: string[];
|
|
26
|
+
|
|
27
|
+
/** Example usage scenarios */
|
|
28
|
+
examples?: string[];
|
|
29
|
+
|
|
30
|
+
/** Preferred model for this skill (sonnet, opus, haiku) */
|
|
31
|
+
model?: 'sonnet' | 'opus' | 'haiku';
|
|
32
|
+
|
|
33
|
+
/** Whether skill requires user approval before running */
|
|
34
|
+
requiresApproval?: boolean;
|
|
35
|
+
|
|
36
|
+
/** Tags for searchability */
|
|
37
|
+
tags?: string[];
|
|
38
|
+
|
|
39
|
+
/** Related commands or skills */
|
|
40
|
+
related?: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Type guard to check if an object is valid SkillFrontmatter
|
|
45
|
+
*/
|
|
46
|
+
export function isSkillFrontmatter(obj: unknown): obj is SkillFrontmatter {
|
|
47
|
+
if (typeof obj !== 'object' || obj === null) return false;
|
|
48
|
+
const fm = obj as Partial<SkillFrontmatter>;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
typeof fm.skill === 'string' &&
|
|
52
|
+
fm.skill.length > 0 &&
|
|
53
|
+
typeof fm.description === 'string' &&
|
|
54
|
+
fm.description.length > 0
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validates skill frontmatter
|
|
60
|
+
* @throws {Error} If frontmatter is invalid
|
|
61
|
+
*/
|
|
62
|
+
export function validateSkillFrontmatter(
|
|
63
|
+
frontmatter: unknown
|
|
64
|
+
): asserts frontmatter is SkillFrontmatter {
|
|
65
|
+
if (!isSkillFrontmatter(frontmatter)) {
|
|
66
|
+
throw new Error('Invalid skill frontmatter: missing required fields');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Additional validations
|
|
70
|
+
if (frontmatter.model && !['sonnet', 'opus', 'haiku'].includes(frontmatter.model)) {
|
|
71
|
+
throw new Error('Invalid skill frontmatter: model must be sonnet, opus, or haiku');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (frontmatter['when-to-use'] && !Array.isArray(frontmatter['when-to-use'])) {
|
|
75
|
+
throw new Error('Invalid skill frontmatter: when-to-use must be an array');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (frontmatter.examples && !Array.isArray(frontmatter.examples)) {
|
|
79
|
+
throw new Error('Invalid skill frontmatter: examples must be an array');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (frontmatter.tags && !Array.isArray(frontmatter.tags)) {
|
|
83
|
+
throw new Error('Invalid skill frontmatter: tags must be an array');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (frontmatter.related && !Array.isArray(frontmatter.related)) {
|
|
87
|
+
throw new Error('Invalid skill frontmatter: related must be an array');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Manager
|
|
3
|
+
* Centralized caching abstraction with TTL and size limits
|
|
4
|
+
*
|
|
5
|
+
* @module lib/utils/cache-manager
|
|
6
|
+
* @author Avi Fenesh
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Cache manager with TTL and size limits
|
|
12
|
+
*/
|
|
13
|
+
class CacheManager {
|
|
14
|
+
/**
|
|
15
|
+
* Create a new cache manager
|
|
16
|
+
* @param {Object} options - Cache configuration
|
|
17
|
+
* @param {number} options.maxSize - Maximum number of entries (default: 100)
|
|
18
|
+
* @param {number} options.ttl - Time-to-live in milliseconds (default: 60000)
|
|
19
|
+
* @param {number} options.maxValueSize - Maximum size per value in bytes (default: null - unlimited)
|
|
20
|
+
*/
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.maxSize = options.maxSize || 100;
|
|
23
|
+
this.ttl = options.ttl || 60000; // 1 minute default
|
|
24
|
+
this.maxValueSize = options.maxValueSize || null;
|
|
25
|
+
|
|
26
|
+
// Use Map for insertion-order guarantee (FIFO eviction)
|
|
27
|
+
this._cache = new Map();
|
|
28
|
+
this._timestamps = new Map();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get a value from cache
|
|
33
|
+
* @param {string} key - Cache key
|
|
34
|
+
* @returns {*} Cached value or undefined if not found/expired
|
|
35
|
+
*/
|
|
36
|
+
get(key) {
|
|
37
|
+
if (!this._cache.has(key)) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check if expired
|
|
42
|
+
const timestamp = this._timestamps.get(key);
|
|
43
|
+
if (Date.now() - timestamp > this.ttl) {
|
|
44
|
+
this.delete(key);
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return this._cache.get(key);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Set a value in cache
|
|
53
|
+
* @param {string} key - Cache key
|
|
54
|
+
* @param {*} value - Value to cache
|
|
55
|
+
* @returns {boolean} True if cached, false if value too large
|
|
56
|
+
*/
|
|
57
|
+
set(key, value) {
|
|
58
|
+
// Check value size if limit set
|
|
59
|
+
if (this.maxValueSize !== null && typeof value === 'string') {
|
|
60
|
+
if (value.length > this.maxValueSize) {
|
|
61
|
+
return false; // Value too large, don't cache
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Update or add entry
|
|
66
|
+
this._cache.set(key, value);
|
|
67
|
+
this._timestamps.set(key, Date.now());
|
|
68
|
+
|
|
69
|
+
// Enforce size limit with FIFO eviction
|
|
70
|
+
this._enforceMaxSize();
|
|
71
|
+
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if key exists and is not expired
|
|
77
|
+
* @param {string} key - Cache key
|
|
78
|
+
* @returns {boolean} True if key exists and is valid
|
|
79
|
+
*/
|
|
80
|
+
has(key) {
|
|
81
|
+
return this.get(key) !== undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Delete a key from cache
|
|
86
|
+
* @param {string} key - Cache key
|
|
87
|
+
* @returns {boolean} True if key existed
|
|
88
|
+
*/
|
|
89
|
+
delete(key) {
|
|
90
|
+
this._timestamps.delete(key);
|
|
91
|
+
return this._cache.delete(key);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Clear all cache entries
|
|
96
|
+
*/
|
|
97
|
+
clear() {
|
|
98
|
+
this._cache.clear();
|
|
99
|
+
this._timestamps.clear();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get current cache size
|
|
104
|
+
* @returns {number} Number of entries
|
|
105
|
+
*/
|
|
106
|
+
get size() {
|
|
107
|
+
return this._cache.size;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get cache statistics
|
|
112
|
+
* @returns {Object} Stats object with size, ttl, maxSize
|
|
113
|
+
*/
|
|
114
|
+
getStats() {
|
|
115
|
+
return {
|
|
116
|
+
size: this._cache.size,
|
|
117
|
+
maxSize: this.maxSize,
|
|
118
|
+
ttl: this.ttl,
|
|
119
|
+
maxValueSize: this.maxValueSize
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Enforce maximum cache size using FIFO eviction
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
_enforceMaxSize() {
|
|
128
|
+
// Map maintains insertion order - first key is oldest
|
|
129
|
+
while (this._cache.size > this.maxSize) {
|
|
130
|
+
const firstKey = this._cache.keys().next().value;
|
|
131
|
+
this.delete(firstKey);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Remove expired entries (useful for long-running processes)
|
|
137
|
+
* @returns {number} Number of entries removed
|
|
138
|
+
*/
|
|
139
|
+
prune() {
|
|
140
|
+
let removed = 0;
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
|
|
143
|
+
for (const [key, timestamp] of this._timestamps.entries()) {
|
|
144
|
+
if (now - timestamp > this.ttl) {
|
|
145
|
+
this.delete(key);
|
|
146
|
+
removed++;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return removed;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = { CacheManager };
|
|
@@ -8,36 +8,83 @@
|
|
|
8
8
|
* @license MIT
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
const {
|
|
12
|
+
escapeShell,
|
|
13
|
+
escapeSingleQuotes,
|
|
14
|
+
sanitizeExtension
|
|
15
|
+
} = require('./shell-escape');
|
|
16
|
+
|
|
11
17
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @param {string}
|
|
14
|
-
* @returns {string}
|
|
18
|
+
* Validate git branch name to prevent command injection
|
|
19
|
+
* @param {string} branch - Branch name to validate
|
|
20
|
+
* @returns {string} Validated branch name
|
|
21
|
+
* @throws {Error} If branch name contains invalid characters
|
|
15
22
|
*/
|
|
16
|
-
function
|
|
17
|
-
if (typeof
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
function validateBranchName(branch) {
|
|
24
|
+
if (typeof branch !== 'string' || branch.length === 0) {
|
|
25
|
+
throw new Error('Branch name must be a non-empty string');
|
|
26
|
+
}
|
|
27
|
+
if (branch.length > 255) {
|
|
28
|
+
throw new Error('Branch name too long (max 255 characters)');
|
|
29
|
+
}
|
|
30
|
+
// Allow alphanumeric, underscore, hyphen, forward slash, and dot
|
|
31
|
+
if (!/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
|
|
32
|
+
throw new Error('Branch name contains invalid characters');
|
|
33
|
+
}
|
|
34
|
+
// Prevent git option injection
|
|
35
|
+
if (branch.startsWith('-')) {
|
|
36
|
+
throw new Error('Branch name cannot start with hyphen');
|
|
37
|
+
}
|
|
38
|
+
return branch;
|
|
20
39
|
}
|
|
21
40
|
|
|
22
41
|
/**
|
|
23
|
-
*
|
|
24
|
-
* @param {string}
|
|
25
|
-
* @returns {string}
|
|
42
|
+
* Validate git reference to prevent command injection
|
|
43
|
+
* @param {string} ref - Git reference to validate
|
|
44
|
+
* @returns {string} Validated reference
|
|
45
|
+
* @throws {Error} If reference contains invalid characters
|
|
26
46
|
*/
|
|
27
|
-
function
|
|
28
|
-
if (typeof
|
|
29
|
-
|
|
47
|
+
function validateGitRef(ref) {
|
|
48
|
+
if (typeof ref !== 'string' || ref.length === 0) {
|
|
49
|
+
throw new Error('Git reference must be a non-empty string');
|
|
50
|
+
}
|
|
51
|
+
if (ref.length > 255) {
|
|
52
|
+
throw new Error('Git reference too long (max 255 characters)');
|
|
53
|
+
}
|
|
54
|
+
// Allow alphanumeric, tilde, caret, dot, hyphen, underscore, forward slash
|
|
55
|
+
if (!/^[a-zA-Z0-9~^._/-]+$/.test(ref)) {
|
|
56
|
+
throw new Error('Git reference contains invalid characters');
|
|
57
|
+
}
|
|
58
|
+
// Prevent git option injection
|
|
59
|
+
if (ref.startsWith('-')) {
|
|
60
|
+
throw new Error('Git reference cannot start with hyphen');
|
|
61
|
+
}
|
|
62
|
+
return ref;
|
|
30
63
|
}
|
|
31
64
|
|
|
32
65
|
/**
|
|
33
|
-
* Validate
|
|
34
|
-
* @param {
|
|
35
|
-
* @
|
|
66
|
+
* Validate numeric limit parameter
|
|
67
|
+
* @param {number} limit - Limit value to validate
|
|
68
|
+
* @param {number} max - Maximum allowed value (default: 1000)
|
|
69
|
+
* @returns {number} Validated limit
|
|
70
|
+
* @throws {Error} If limit is invalid
|
|
36
71
|
*/
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
72
|
+
function validateLimit(limit, max = 1000) {
|
|
73
|
+
// Strict type check - must be number or numeric string
|
|
74
|
+
if (typeof limit === 'string') {
|
|
75
|
+
// Only allow pure numeric strings
|
|
76
|
+
if (!/^\d+$/.test(limit)) {
|
|
77
|
+
throw new Error('Limit must be a positive integer');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const num = typeof limit === 'number' ? limit : parseInt(limit, 10);
|
|
81
|
+
if (!Number.isInteger(num) || num < 1) {
|
|
82
|
+
throw new Error('Limit must be a positive integer');
|
|
83
|
+
}
|
|
84
|
+
if (num > max) {
|
|
85
|
+
throw new Error(`Limit cannot exceed ${max}`);
|
|
86
|
+
}
|
|
87
|
+
return num;
|
|
41
88
|
}
|
|
42
89
|
|
|
43
90
|
/**
|
|
@@ -49,8 +96,10 @@ const contextOptimizer = {
|
|
|
49
96
|
* @param {number} limit - Number of commits to retrieve (default: 10)
|
|
50
97
|
* @returns {string} Git command
|
|
51
98
|
*/
|
|
52
|
-
recentCommits: (limit = 10) =>
|
|
53
|
-
|
|
99
|
+
recentCommits: (limit = 10) => {
|
|
100
|
+
const safeLimit = validateLimit(limit);
|
|
101
|
+
return `git log --oneline --no-decorate -${safeLimit} --format="%h %s"`;
|
|
102
|
+
},
|
|
54
103
|
|
|
55
104
|
/**
|
|
56
105
|
* Get compact git status (untracked files excluded)
|
|
@@ -64,8 +113,10 @@ const contextOptimizer = {
|
|
|
64
113
|
* @param {string} ref - Reference to compare from (default: 'HEAD~5')
|
|
65
114
|
* @returns {string} Git command
|
|
66
115
|
*/
|
|
67
|
-
fileChanges: (ref = 'HEAD~5') =>
|
|
68
|
-
|
|
116
|
+
fileChanges: (ref = 'HEAD~5') => {
|
|
117
|
+
const safeRef = validateGitRef(ref);
|
|
118
|
+
return `git diff ${safeRef}..HEAD --name-status`;
|
|
119
|
+
},
|
|
69
120
|
|
|
70
121
|
/**
|
|
71
122
|
* Get current branch name
|
|
@@ -102,11 +153,15 @@ const contextOptimizer = {
|
|
|
102
153
|
* @returns {string} Git command
|
|
103
154
|
*/
|
|
104
155
|
lineAge: (file, line) => {
|
|
105
|
-
// Validate line is a positive integer
|
|
156
|
+
// Validate line is a positive integer with reasonable bounds
|
|
106
157
|
const lineNum = parseInt(line, 10);
|
|
158
|
+
const MAX_LINE_NUMBER = 10000000; // 10 million lines - reasonable upper bound
|
|
107
159
|
if (!Number.isInteger(lineNum) || lineNum < 1) {
|
|
108
160
|
throw new Error('Line must be a positive integer');
|
|
109
161
|
}
|
|
162
|
+
if (lineNum > MAX_LINE_NUMBER) {
|
|
163
|
+
throw new Error(`Line number cannot exceed ${MAX_LINE_NUMBER}`);
|
|
164
|
+
}
|
|
110
165
|
// Escape file path for safe shell usage
|
|
111
166
|
const safeFile = escapeShell(file);
|
|
112
167
|
return `git blame -L ${lineNum},${lineNum} "${safeFile}" --porcelain | grep '^committer-time' | cut -d' ' -f2`;
|
|
@@ -127,8 +182,10 @@ const contextOptimizer = {
|
|
|
127
182
|
* @param {string} ref - Reference to compare from (default: 'HEAD~5')
|
|
128
183
|
* @returns {string} Git command
|
|
129
184
|
*/
|
|
130
|
-
diffStat: (ref = 'HEAD~5') =>
|
|
131
|
-
|
|
185
|
+
diffStat: (ref = 'HEAD~5') => {
|
|
186
|
+
const safeRef = validateGitRef(ref);
|
|
187
|
+
return `git diff ${safeRef}..HEAD --stat | head -20`;
|
|
188
|
+
},
|
|
132
189
|
|
|
133
190
|
/**
|
|
134
191
|
* Get contributors list (limited to top 10)
|
|
@@ -156,24 +213,30 @@ const contextOptimizer = {
|
|
|
156
213
|
* @param {number} limit - Number of branches (default: 10)
|
|
157
214
|
* @returns {string} Git command
|
|
158
215
|
*/
|
|
159
|
-
branches: (limit = 10) =>
|
|
160
|
-
|
|
216
|
+
branches: (limit = 10) => {
|
|
217
|
+
const safeLimit = validateLimit(limit);
|
|
218
|
+
return `git branch --format='%(refname:short)' | head -${safeLimit}`;
|
|
219
|
+
},
|
|
161
220
|
|
|
162
221
|
/**
|
|
163
222
|
* Get tags list (limited)
|
|
164
223
|
* @param {number} limit - Number of tags (default: 10)
|
|
165
224
|
* @returns {string} Git command
|
|
166
225
|
*/
|
|
167
|
-
tags: (limit = 10) =>
|
|
168
|
-
|
|
226
|
+
tags: (limit = 10) => {
|
|
227
|
+
const safeLimit = validateLimit(limit);
|
|
228
|
+
return `git tag --sort=-creatordate | head -${safeLimit}`;
|
|
229
|
+
},
|
|
169
230
|
|
|
170
231
|
/**
|
|
171
232
|
* Get count of commits on current branch since branching from main
|
|
172
233
|
* @param {string} mainBranch - Main branch name (default: 'main')
|
|
173
234
|
* @returns {string} Git command
|
|
174
235
|
*/
|
|
175
|
-
commitsSinceBranch: (mainBranch = 'main') =>
|
|
176
|
-
|
|
236
|
+
commitsSinceBranch: (mainBranch = 'main') => {
|
|
237
|
+
const safeBranch = validateBranchName(mainBranch);
|
|
238
|
+
return `git rev-list --count ${safeBranch}..HEAD`;
|
|
239
|
+
},
|
|
177
240
|
|
|
178
241
|
/**
|
|
179
242
|
* Check if working directory is clean
|
|
@@ -187,16 +250,20 @@ const contextOptimizer = {
|
|
|
187
250
|
* @param {string} mainBranch - Main branch name (default: 'main')
|
|
188
251
|
* @returns {string} Git command
|
|
189
252
|
*/
|
|
190
|
-
mergeBase: (mainBranch = 'main') =>
|
|
191
|
-
|
|
253
|
+
mergeBase: (mainBranch = 'main') => {
|
|
254
|
+
const safeBranch = validateBranchName(mainBranch);
|
|
255
|
+
return `git merge-base ${safeBranch} HEAD`;
|
|
256
|
+
},
|
|
192
257
|
|
|
193
258
|
/**
|
|
194
259
|
* Get files modified in current branch (since branching)
|
|
195
260
|
* @param {string} mainBranch - Main branch name (default: 'main')
|
|
196
261
|
* @returns {string} Git command
|
|
197
262
|
*/
|
|
198
|
-
branchChangedFiles: (mainBranch = 'main') =>
|
|
199
|
-
|
|
263
|
+
branchChangedFiles: (mainBranch = 'main') => {
|
|
264
|
+
const safeBranch = validateBranchName(mainBranch);
|
|
265
|
+
return `git diff ${safeBranch}...HEAD --name-only`;
|
|
266
|
+
},
|
|
200
267
|
|
|
201
268
|
/**
|
|
202
269
|
* Get commit count by author
|
|
@@ -219,4 +286,15 @@ const contextOptimizer = {
|
|
|
219
286
|
}
|
|
220
287
|
};
|
|
221
288
|
|
|
289
|
+
// Export main API
|
|
222
290
|
module.exports = contextOptimizer;
|
|
291
|
+
|
|
292
|
+
// Export internal functions for testing
|
|
293
|
+
module.exports._internal = {
|
|
294
|
+
escapeShell,
|
|
295
|
+
escapeSingleQuotes,
|
|
296
|
+
sanitizeExtension,
|
|
297
|
+
validateBranchName,
|
|
298
|
+
validateGitRef,
|
|
299
|
+
validateLimit
|
|
300
|
+
};
|