awesome-slash 2.4.4 → 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.
Files changed (144) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +88 -1
  4. package/README.md +173 -161
  5. package/SECURITY.md +25 -81
  6. package/adapters/codex/install.sh +58 -16
  7. package/adapters/opencode/install.sh +92 -23
  8. package/lib/index.js +47 -4
  9. package/lib/patterns/review-patterns.js +58 -11
  10. package/lib/patterns/slop-patterns.js +154 -147
  11. package/lib/platform/detect-platform.js +99 -350
  12. package/lib/platform/detection-configs.js +93 -0
  13. package/lib/platform/verify-tools.js +10 -78
  14. package/lib/schemas/README.md +195 -0
  15. package/lib/schemas/validator.js +247 -0
  16. package/lib/sources/custom-handler.js +199 -0
  17. package/lib/sources/policy-questions.js +239 -0
  18. package/lib/sources/source-cache.js +149 -0
  19. package/lib/state/workflow-state.js +363 -665
  20. package/lib/types/README.md +292 -0
  21. package/lib/types/agent-frontmatter.d.ts +134 -0
  22. package/lib/types/command-frontmatter.d.ts +107 -0
  23. package/lib/types/hook-frontmatter.d.ts +115 -0
  24. package/lib/types/index.d.ts +84 -0
  25. package/lib/types/plugin-manifest.d.ts +102 -0
  26. package/lib/types/skill-frontmatter.d.ts +89 -0
  27. package/lib/utils/cache-manager.js +154 -0
  28. package/lib/utils/context-optimizer.js +5 -36
  29. package/lib/utils/deprecation.js +37 -0
  30. package/lib/utils/shell-escape.js +88 -0
  31. package/mcp-server/index.js +513 -18
  32. package/package.json +6 -2
  33. package/plugins/deslop-around/.claude-plugin/plugin.json +1 -1
  34. package/plugins/deslop-around/lib/index.js +170 -0
  35. package/plugins/deslop-around/lib/patterns/review-patterns.js +58 -11
  36. package/plugins/deslop-around/lib/patterns/slop-patterns.js +170 -129
  37. package/plugins/deslop-around/lib/platform/detect-platform.js +212 -123
  38. package/plugins/deslop-around/lib/platform/detection-configs.js +93 -0
  39. package/plugins/deslop-around/lib/platform/verify-tools.js +10 -1
  40. package/plugins/deslop-around/lib/schemas/README.md +195 -0
  41. package/plugins/deslop-around/lib/schemas/validator.js +205 -0
  42. package/plugins/deslop-around/lib/sources/custom-handler.js +199 -0
  43. package/plugins/deslop-around/lib/sources/policy-questions.js +239 -0
  44. package/plugins/deslop-around/lib/sources/source-cache.js +149 -0
  45. package/plugins/deslop-around/lib/state/workflow-state.js +382 -484
  46. package/plugins/deslop-around/lib/types/README.md +292 -0
  47. package/plugins/deslop-around/lib/types/agent-frontmatter.d.ts +134 -0
  48. package/plugins/deslop-around/lib/types/command-frontmatter.d.ts +107 -0
  49. package/plugins/deslop-around/lib/types/hook-frontmatter.d.ts +115 -0
  50. package/plugins/deslop-around/lib/types/index.d.ts +84 -0
  51. package/plugins/deslop-around/lib/types/plugin-manifest.d.ts +102 -0
  52. package/plugins/deslop-around/lib/types/skill-frontmatter.d.ts +89 -0
  53. package/plugins/deslop-around/lib/utils/cache-manager.js +154 -0
  54. package/plugins/deslop-around/lib/utils/context-optimizer.js +115 -37
  55. package/plugins/deslop-around/lib/utils/deprecation.js +37 -0
  56. package/plugins/deslop-around/lib/utils/shell-escape.js +88 -0
  57. package/plugins/next-task/.claude-plugin/plugin.json +1 -1
  58. package/plugins/next-task/agents/delivery-validator.md +2 -2
  59. package/plugins/next-task/agents/implementation-agent.md +3 -4
  60. package/plugins/next-task/agents/planning-agent.md +77 -19
  61. package/plugins/next-task/agents/review-orchestrator.md +21 -122
  62. package/plugins/next-task/agents/task-discoverer.md +164 -23
  63. package/plugins/next-task/commands/next-task.md +180 -14
  64. package/plugins/next-task/lib/index.js +170 -0
  65. package/plugins/next-task/lib/patterns/review-patterns.js +58 -11
  66. package/plugins/next-task/lib/patterns/slop-patterns.js +170 -129
  67. package/plugins/next-task/lib/platform/detect-platform.js +212 -123
  68. package/plugins/next-task/lib/platform/detection-configs.js +93 -0
  69. package/plugins/next-task/lib/platform/verify-tools.js +10 -1
  70. package/plugins/next-task/lib/schemas/README.md +195 -0
  71. package/plugins/next-task/lib/schemas/validator.js +205 -0
  72. package/plugins/next-task/lib/sources/custom-handler.js +199 -0
  73. package/plugins/next-task/lib/sources/policy-questions.js +239 -0
  74. package/plugins/next-task/lib/sources/source-cache.js +149 -0
  75. package/plugins/next-task/lib/state/workflow-state.js +382 -484
  76. package/plugins/next-task/lib/types/README.md +292 -0
  77. package/plugins/next-task/lib/types/agent-frontmatter.d.ts +134 -0
  78. package/plugins/next-task/lib/types/command-frontmatter.d.ts +107 -0
  79. package/plugins/next-task/lib/types/hook-frontmatter.d.ts +115 -0
  80. package/plugins/next-task/lib/types/index.d.ts +84 -0
  81. package/plugins/next-task/lib/types/plugin-manifest.d.ts +102 -0
  82. package/plugins/next-task/lib/types/skill-frontmatter.d.ts +89 -0
  83. package/plugins/next-task/lib/utils/cache-manager.js +154 -0
  84. package/plugins/next-task/lib/utils/context-optimizer.js +115 -37
  85. package/plugins/next-task/lib/utils/deprecation.js +37 -0
  86. package/plugins/next-task/lib/utils/shell-escape.js +88 -0
  87. package/plugins/project-review/.claude-plugin/plugin.json +1 -1
  88. package/plugins/project-review/lib/index.js +170 -0
  89. package/plugins/project-review/lib/patterns/review-patterns.js +58 -11
  90. package/plugins/project-review/lib/patterns/slop-patterns.js +170 -129
  91. package/plugins/project-review/lib/platform/detect-platform.js +212 -123
  92. package/plugins/project-review/lib/platform/detection-configs.js +93 -0
  93. package/plugins/project-review/lib/platform/verify-tools.js +10 -1
  94. package/plugins/project-review/lib/schemas/README.md +195 -0
  95. package/plugins/project-review/lib/schemas/validator.js +205 -0
  96. package/plugins/project-review/lib/sources/custom-handler.js +199 -0
  97. package/plugins/project-review/lib/sources/policy-questions.js +239 -0
  98. package/plugins/project-review/lib/sources/source-cache.js +149 -0
  99. package/plugins/project-review/lib/state/workflow-state.js +382 -484
  100. package/plugins/project-review/lib/types/README.md +292 -0
  101. package/plugins/project-review/lib/types/agent-frontmatter.d.ts +134 -0
  102. package/plugins/project-review/lib/types/command-frontmatter.d.ts +107 -0
  103. package/plugins/project-review/lib/types/hook-frontmatter.d.ts +115 -0
  104. package/plugins/project-review/lib/types/index.d.ts +84 -0
  105. package/plugins/project-review/lib/types/plugin-manifest.d.ts +102 -0
  106. package/plugins/project-review/lib/types/skill-frontmatter.d.ts +89 -0
  107. package/plugins/project-review/lib/utils/cache-manager.js +154 -0
  108. package/plugins/project-review/lib/utils/context-optimizer.js +115 -37
  109. package/plugins/project-review/lib/utils/deprecation.js +37 -0
  110. package/plugins/project-review/lib/utils/shell-escape.js +88 -0
  111. package/plugins/reality-check/.claude-plugin/plugin.json +1 -1
  112. package/plugins/reality-check/agents/code-explorer.md +1 -1
  113. package/plugins/ship/.claude-plugin/plugin.json +1 -1
  114. package/plugins/ship/lib/index.js +170 -0
  115. package/plugins/ship/lib/patterns/review-patterns.js +58 -11
  116. package/plugins/ship/lib/patterns/slop-patterns.js +170 -129
  117. package/plugins/ship/lib/platform/detect-platform.js +212 -123
  118. package/plugins/ship/lib/platform/detection-configs.js +93 -0
  119. package/plugins/ship/lib/platform/verify-tools.js +10 -1
  120. package/plugins/ship/lib/schemas/README.md +195 -0
  121. package/plugins/ship/lib/schemas/validator.js +205 -0
  122. package/plugins/ship/lib/sources/custom-handler.js +199 -0
  123. package/plugins/ship/lib/sources/policy-questions.js +239 -0
  124. package/plugins/ship/lib/sources/source-cache.js +149 -0
  125. package/plugins/ship/lib/state/workflow-state.js +382 -484
  126. package/plugins/ship/lib/types/README.md +292 -0
  127. package/plugins/ship/lib/types/agent-frontmatter.d.ts +134 -0
  128. package/plugins/ship/lib/types/command-frontmatter.d.ts +107 -0
  129. package/plugins/ship/lib/types/hook-frontmatter.d.ts +115 -0
  130. package/plugins/ship/lib/types/index.d.ts +84 -0
  131. package/plugins/ship/lib/types/plugin-manifest.d.ts +102 -0
  132. package/plugins/ship/lib/types/skill-frontmatter.d.ts +89 -0
  133. package/plugins/ship/lib/utils/cache-manager.js +154 -0
  134. package/plugins/ship/lib/utils/context-optimizer.js +115 -37
  135. package/plugins/ship/lib/utils/deprecation.js +37 -0
  136. package/plugins/ship/lib/utils/shell-escape.js +88 -0
  137. package/lib/state/workflow-state.schema.json +0 -282
  138. package/plugins/deslop-around/lib/state/workflow-state.schema.json +0 -282
  139. package/plugins/next-task/agents/policy-selector.md +0 -248
  140. package/plugins/next-task/lib/state/tasks-registry.schema.json +0 -85
  141. package/plugins/next-task/lib/state/workflow-state.schema.json +0 -282
  142. package/plugins/next-task/lib/state/worktree-status.schema.json +0 -219
  143. package/plugins/project-review/lib/state/workflow-state.schema.json +0 -282
  144. 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
- * Escape shell special characters for safe command interpolation
13
- * @param {string} str - String to escape
14
- * @returns {string} Escaped string safe for shell use
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 escapeShell(str) {
17
- if (typeof str !== 'string') return '';
18
- // Escape characters that have special meaning in shell
19
- return str.replace(/["$`\\!]/g, '\\$&');
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
- * Escape single quotes for shell (replace ' with '\''
24
- * @param {string} str - String to escape
25
- * @returns {string} Escaped string safe for single-quoted shell use
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 escapeSingleQuotes(str) {
28
- if (typeof str !== 'string') return '';
29
- return str.replace(/'/g, "'\\''");
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 and sanitize file extension
34
- * @param {string} ext - Extension to validate
35
- * @returns {string} Safe extension (alphanumeric only)
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 sanitizeExtension(ext) {
38
- if (typeof ext !== 'string') return 'ts';
39
- const safe = ext.replace(/[^a-zA-Z0-9]/g, '');
40
- return safe || 'ts';
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
- `git log --oneline --no-decorate -${limit} --format="%h %s"`,
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
- `git diff ${ref}..HEAD --name-status`,
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
- `git diff ${ref}..HEAD --stat | head -20`,
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
- `git branch --format='%(refname:short)' | head -${limit}`,
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
- `git tag --sort=-creatordate | head -${limit}`,
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
- `git rev-list --count ${mainBranch}..HEAD`,
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
- `git merge-base ${mainBranch} HEAD`,
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
- `git diff ${mainBranch}...HEAD --name-only`,
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
+ };