agileflow 2.77.0 → 2.79.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/README.md +6 -6
- package/package.json +6 -1
- package/scripts/agileflow-configure.js +174 -2
- package/scripts/agileflow-statusline.sh +171 -78
- package/scripts/agileflow-welcome.js +88 -64
- package/scripts/auto-self-improve.js +23 -45
- package/scripts/check-update.js +35 -42
- package/scripts/damage-control/bash-tool-damage-control.js +257 -0
- package/scripts/damage-control/edit-tool-damage-control.js +279 -0
- package/scripts/damage-control/patterns.yaml +227 -0
- package/scripts/damage-control/write-tool-damage-control.js +274 -0
- package/scripts/damage-control-bash.js +232 -0
- package/scripts/damage-control-edit.js +243 -0
- package/scripts/damage-control-write.js +243 -0
- package/scripts/obtain-context.js +22 -3
- package/scripts/ralph-loop.js +191 -63
- package/scripts/screenshot-verifier.js +213 -0
- package/scripts/session-manager.js +12 -33
- package/src/core/agents/accessibility.md +124 -53
- package/src/core/agents/adr-writer.md +192 -52
- package/src/core/agents/analytics.md +139 -60
- package/src/core/agents/api.md +173 -63
- package/src/core/agents/ci.md +139 -57
- package/src/core/agents/compliance.md +159 -68
- package/src/core/agents/configuration/damage-control.md +356 -0
- package/src/core/agents/configuration-damage-control.md +248 -0
- package/src/core/agents/database.md +162 -61
- package/src/core/agents/datamigration.md +179 -66
- package/src/core/agents/design.md +179 -57
- package/src/core/agents/devops.md +160 -3
- package/src/core/agents/documentation.md +204 -60
- package/src/core/agents/epic-planner.md +147 -55
- package/src/core/agents/integrations.md +197 -69
- package/src/core/agents/mentor.md +158 -57
- package/src/core/agents/mobile.md +159 -67
- package/src/core/agents/monitoring.md +154 -65
- package/src/core/agents/multi-expert.md +115 -43
- package/src/core/agents/orchestrator.md +77 -24
- package/src/core/agents/performance.md +130 -75
- package/src/core/agents/product.md +151 -55
- package/src/core/agents/qa.md +162 -74
- package/src/core/agents/readme-updater.md +178 -76
- package/src/core/agents/refactor.md +148 -95
- package/src/core/agents/research.md +143 -72
- package/src/core/agents/security.md +154 -65
- package/src/core/agents/testing.md +176 -97
- package/src/core/agents/ui.md +170 -79
- package/src/core/commands/adr/list.md +171 -0
- package/src/core/commands/adr/update.md +235 -0
- package/src/core/commands/adr/view.md +252 -0
- package/src/core/commands/adr.md +207 -50
- package/src/core/commands/agent.md +16 -0
- package/src/core/commands/assign.md +148 -44
- package/src/core/commands/auto.md +18 -1
- package/src/core/commands/babysit.md +391 -38
- package/src/core/commands/baseline.md +14 -0
- package/src/core/commands/blockers.md +170 -51
- package/src/core/commands/board.md +144 -66
- package/src/core/commands/changelog.md +15 -0
- package/src/core/commands/ci.md +179 -69
- package/src/core/commands/compress.md +18 -0
- package/src/core/commands/configure.md +16 -0
- package/src/core/commands/context/export.md +193 -4
- package/src/core/commands/context/full.md +191 -18
- package/src/core/commands/context/note.md +248 -4
- package/src/core/commands/debt.md +17 -0
- package/src/core/commands/deploy.md +208 -65
- package/src/core/commands/deps.md +15 -0
- package/src/core/commands/diagnose.md +16 -0
- package/src/core/commands/docs.md +196 -64
- package/src/core/commands/epic/list.md +170 -0
- package/src/core/commands/epic/view.md +242 -0
- package/src/core/commands/epic.md +192 -69
- package/src/core/commands/feedback.md +191 -71
- package/src/core/commands/handoff.md +162 -48
- package/src/core/commands/help.md +9 -0
- package/src/core/commands/ideate.md +446 -0
- package/src/core/commands/impact.md +16 -0
- package/src/core/commands/metrics.md +141 -37
- package/src/core/commands/multi-expert.md +77 -0
- package/src/core/commands/packages.md +16 -0
- package/src/core/commands/pr.md +161 -67
- package/src/core/commands/readme-sync.md +16 -0
- package/src/core/commands/research/analyze.md +568 -0
- package/src/core/commands/research/ask.md +345 -20
- package/src/core/commands/research/import.md +562 -19
- package/src/core/commands/research/list.md +173 -5
- package/src/core/commands/research/view.md +181 -8
- package/src/core/commands/retro.md +135 -48
- package/src/core/commands/review.md +219 -47
- package/src/core/commands/session/end.md +209 -0
- package/src/core/commands/session/history.md +210 -0
- package/src/core/commands/session/init.md +116 -0
- package/src/core/commands/session/new.md +296 -0
- package/src/core/commands/session/resume.md +166 -0
- package/src/core/commands/session/status.md +166 -0
- package/src/core/commands/setup/visual-e2e.md +462 -0
- package/src/core/commands/skill/create.md +115 -17
- package/src/core/commands/skill/delete.md +117 -0
- package/src/core/commands/skill/edit.md +104 -0
- package/src/core/commands/skill/list.md +128 -0
- package/src/core/commands/skill/test.md +135 -0
- package/src/core/commands/skill/upgrade.md +542 -0
- package/src/core/commands/sprint.md +17 -1
- package/src/core/commands/status.md +133 -21
- package/src/core/commands/story/list.md +176 -0
- package/src/core/commands/story/view.md +265 -0
- package/src/core/commands/story-validate.md +101 -1
- package/src/core/commands/story.md +204 -51
- package/src/core/commands/template.md +16 -1
- package/src/core/commands/tests.md +226 -64
- package/src/core/commands/update.md +17 -1
- package/src/core/commands/validate-expertise.md +16 -0
- package/src/core/commands/velocity.md +140 -36
- package/src/core/commands/verify.md +14 -0
- package/src/core/commands/whats-new.md +30 -0
- package/src/core/skills/_learnings/README.md +91 -0
- package/src/core/skills/_learnings/_template.yaml +106 -0
- package/src/core/skills/_learnings/code-review.yaml +118 -0
- package/src/core/skills/_learnings/commit.yaml +69 -0
- package/src/core/skills/_learnings/story-writer.yaml +71 -0
- package/src/core/templates/damage-control-patterns.yaml +234 -0
- package/src/core/templates/skill-template.md +53 -11
- package/tools/cli/commands/start.js +180 -0
- package/tools/cli/installers/ide/claude-code.js +127 -0
- package/tools/cli/tui/Dashboard.js +66 -0
- package/tools/cli/tui/StoryList.js +69 -0
- package/tools/cli/tui/index.js +16 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* damage-control-edit.js - PreToolUse hook for Edit tool
|
|
4
|
+
*
|
|
5
|
+
* Validates file paths against access control patterns in damage-control-patterns.yaml
|
|
6
|
+
* before allowing file edits. Part of AgileFlow's damage control system.
|
|
7
|
+
*
|
|
8
|
+
* Exit codes:
|
|
9
|
+
* 0 - Allow operation
|
|
10
|
+
* 2 - Block operation
|
|
11
|
+
*
|
|
12
|
+
* Usage: Configured as PreToolUse hook in .claude/settings.json
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
|
|
19
|
+
// Color codes for output
|
|
20
|
+
const c = {
|
|
21
|
+
red: '\x1b[38;5;203m',
|
|
22
|
+
reset: '\x1b[0m',
|
|
23
|
+
dim: '\x1b[2m'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Find project root by looking for .agileflow directory
|
|
28
|
+
*/
|
|
29
|
+
function findProjectRoot() {
|
|
30
|
+
let dir = process.cwd();
|
|
31
|
+
while (dir !== '/') {
|
|
32
|
+
if (fs.existsSync(path.join(dir, '.agileflow'))) {
|
|
33
|
+
return dir;
|
|
34
|
+
}
|
|
35
|
+
dir = path.dirname(dir);
|
|
36
|
+
}
|
|
37
|
+
return process.cwd();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Expand ~ to home directory
|
|
42
|
+
*/
|
|
43
|
+
function expandPath(p) {
|
|
44
|
+
if (p.startsWith('~/')) {
|
|
45
|
+
return path.join(os.homedir(), p.slice(2));
|
|
46
|
+
}
|
|
47
|
+
return p;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse simplified YAML for path patterns
|
|
52
|
+
*/
|
|
53
|
+
function parseSimpleYAML(content) {
|
|
54
|
+
const config = {
|
|
55
|
+
zeroAccessPaths: [],
|
|
56
|
+
readOnlyPaths: [],
|
|
57
|
+
noDeletePaths: []
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let currentSection = null;
|
|
61
|
+
|
|
62
|
+
for (const line of content.split('\n')) {
|
|
63
|
+
const trimmed = line.trim();
|
|
64
|
+
|
|
65
|
+
// Skip empty lines and comments
|
|
66
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
67
|
+
|
|
68
|
+
// Detect section headers
|
|
69
|
+
if (trimmed === 'zeroAccessPaths:') {
|
|
70
|
+
currentSection = 'zeroAccessPaths';
|
|
71
|
+
} else if (trimmed === 'readOnlyPaths:') {
|
|
72
|
+
currentSection = 'readOnlyPaths';
|
|
73
|
+
} else if (trimmed === 'noDeletePaths:') {
|
|
74
|
+
currentSection = 'noDeletePaths';
|
|
75
|
+
} else if (trimmed.endsWith(':') && !trimmed.startsWith('-')) {
|
|
76
|
+
// Other sections we don't care about for path validation
|
|
77
|
+
currentSection = null;
|
|
78
|
+
} else if (trimmed.startsWith('- ') && currentSection && config[currentSection]) {
|
|
79
|
+
// Path entry
|
|
80
|
+
const pathValue = trimmed.slice(2).replace(/^["']|["']$/g, '');
|
|
81
|
+
config[currentSection].push(pathValue);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return config;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Load patterns configuration from YAML file
|
|
90
|
+
*/
|
|
91
|
+
function loadPatterns(projectRoot) {
|
|
92
|
+
const configPaths = [
|
|
93
|
+
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yaml'),
|
|
94
|
+
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yml'),
|
|
95
|
+
path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml')
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
for (const configPath of configPaths) {
|
|
99
|
+
if (fs.existsSync(configPath)) {
|
|
100
|
+
try {
|
|
101
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
102
|
+
return parseSimpleYAML(content);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
// Continue to next path
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Return empty config if no file found (fail-open)
|
|
110
|
+
return { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if a file path matches any of the protected patterns
|
|
115
|
+
*/
|
|
116
|
+
function pathMatches(filePath, patterns) {
|
|
117
|
+
if (!filePath) return null;
|
|
118
|
+
|
|
119
|
+
const normalizedPath = path.resolve(filePath);
|
|
120
|
+
const relativePath = path.relative(process.cwd(), normalizedPath);
|
|
121
|
+
|
|
122
|
+
for (const pattern of patterns) {
|
|
123
|
+
const expandedPattern = expandPath(pattern);
|
|
124
|
+
|
|
125
|
+
// Check if pattern is a directory prefix
|
|
126
|
+
if (pattern.endsWith('/')) {
|
|
127
|
+
const patternDir = expandedPattern.slice(0, -1);
|
|
128
|
+
if (normalizedPath.startsWith(patternDir)) {
|
|
129
|
+
return pattern;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check exact match
|
|
134
|
+
if (normalizedPath === expandedPattern) {
|
|
135
|
+
return pattern;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check if normalized path ends with pattern (for filenames like "id_rsa")
|
|
139
|
+
if (normalizedPath.endsWith(pattern) || relativePath.endsWith(pattern)) {
|
|
140
|
+
return pattern;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check if pattern appears in path (for patterns like "*.pem")
|
|
144
|
+
if (pattern.startsWith('*')) {
|
|
145
|
+
const ext = pattern.slice(1);
|
|
146
|
+
if (normalizedPath.endsWith(ext) || relativePath.endsWith(ext)) {
|
|
147
|
+
return pattern;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check if path contains pattern (for things like ".env.production")
|
|
152
|
+
const patternBase = path.basename(pattern);
|
|
153
|
+
if (path.basename(normalizedPath) === patternBase) {
|
|
154
|
+
return pattern;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Validate file path for edit operation
|
|
163
|
+
*/
|
|
164
|
+
function validatePath(filePath, config) {
|
|
165
|
+
// Check zero access paths - completely blocked
|
|
166
|
+
const zeroMatch = pathMatches(filePath, config.zeroAccessPaths || []);
|
|
167
|
+
if (zeroMatch) {
|
|
168
|
+
return {
|
|
169
|
+
action: 'block',
|
|
170
|
+
reason: `Zero-access path: ${zeroMatch}`,
|
|
171
|
+
detail: 'This file is protected and cannot be accessed'
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check read-only paths - cannot edit
|
|
176
|
+
const readOnlyMatch = pathMatches(filePath, config.readOnlyPaths || []);
|
|
177
|
+
if (readOnlyMatch) {
|
|
178
|
+
return {
|
|
179
|
+
action: 'block',
|
|
180
|
+
reason: `Read-only path: ${readOnlyMatch}`,
|
|
181
|
+
detail: 'This file is read-only and cannot be edited'
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Allow by default
|
|
186
|
+
return { action: 'allow' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Main function - read input and validate
|
|
191
|
+
*/
|
|
192
|
+
function main() {
|
|
193
|
+
const projectRoot = findProjectRoot();
|
|
194
|
+
let inputData = '';
|
|
195
|
+
|
|
196
|
+
process.stdin.setEncoding('utf8');
|
|
197
|
+
|
|
198
|
+
process.stdin.on('data', chunk => {
|
|
199
|
+
inputData += chunk;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
process.stdin.on('end', () => {
|
|
203
|
+
try {
|
|
204
|
+
// Parse tool input from Claude Code
|
|
205
|
+
const input = JSON.parse(inputData);
|
|
206
|
+
const filePath = input.file_path || input.tool_input?.file_path || '';
|
|
207
|
+
|
|
208
|
+
if (!filePath) {
|
|
209
|
+
// No path to validate - allow
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Load patterns and validate
|
|
214
|
+
const config = loadPatterns(projectRoot);
|
|
215
|
+
const result = validatePath(filePath, config);
|
|
216
|
+
|
|
217
|
+
if (result.action === 'block') {
|
|
218
|
+
console.error(`${c.red}[BLOCKED]${c.reset} ${result.reason}`);
|
|
219
|
+
console.error(`${c.dim}${result.detail}${c.reset}`);
|
|
220
|
+
console.error(`${c.dim}File: ${filePath}${c.reset}`);
|
|
221
|
+
process.exit(2);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Allow
|
|
225
|
+
process.exit(0);
|
|
226
|
+
} catch (e) {
|
|
227
|
+
// Parse error or other issue - fail open
|
|
228
|
+
process.exit(0);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Handle no stdin
|
|
233
|
+
process.stdin.on('error', () => {
|
|
234
|
+
process.exit(0);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Set timeout to prevent hanging
|
|
238
|
+
setTimeout(() => {
|
|
239
|
+
process.exit(0);
|
|
240
|
+
}, 4000);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
main();
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* damage-control-write.js - PreToolUse hook for Write tool
|
|
4
|
+
*
|
|
5
|
+
* Validates file paths against access control patterns in damage-control-patterns.yaml
|
|
6
|
+
* before allowing file writes. Part of AgileFlow's damage control system.
|
|
7
|
+
*
|
|
8
|
+
* Exit codes:
|
|
9
|
+
* 0 - Allow operation
|
|
10
|
+
* 2 - Block operation
|
|
11
|
+
*
|
|
12
|
+
* Usage: Configured as PreToolUse hook in .claude/settings.json
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
|
|
19
|
+
// Color codes for output
|
|
20
|
+
const c = {
|
|
21
|
+
red: '\x1b[38;5;203m',
|
|
22
|
+
reset: '\x1b[0m',
|
|
23
|
+
dim: '\x1b[2m'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Find project root by looking for .agileflow directory
|
|
28
|
+
*/
|
|
29
|
+
function findProjectRoot() {
|
|
30
|
+
let dir = process.cwd();
|
|
31
|
+
while (dir !== '/') {
|
|
32
|
+
if (fs.existsSync(path.join(dir, '.agileflow'))) {
|
|
33
|
+
return dir;
|
|
34
|
+
}
|
|
35
|
+
dir = path.dirname(dir);
|
|
36
|
+
}
|
|
37
|
+
return process.cwd();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Expand ~ to home directory
|
|
42
|
+
*/
|
|
43
|
+
function expandPath(p) {
|
|
44
|
+
if (p.startsWith('~/')) {
|
|
45
|
+
return path.join(os.homedir(), p.slice(2));
|
|
46
|
+
}
|
|
47
|
+
return p;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse simplified YAML for path patterns
|
|
52
|
+
*/
|
|
53
|
+
function parseSimpleYAML(content) {
|
|
54
|
+
const config = {
|
|
55
|
+
zeroAccessPaths: [],
|
|
56
|
+
readOnlyPaths: [],
|
|
57
|
+
noDeletePaths: []
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let currentSection = null;
|
|
61
|
+
|
|
62
|
+
for (const line of content.split('\n')) {
|
|
63
|
+
const trimmed = line.trim();
|
|
64
|
+
|
|
65
|
+
// Skip empty lines and comments
|
|
66
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
67
|
+
|
|
68
|
+
// Detect section headers
|
|
69
|
+
if (trimmed === 'zeroAccessPaths:') {
|
|
70
|
+
currentSection = 'zeroAccessPaths';
|
|
71
|
+
} else if (trimmed === 'readOnlyPaths:') {
|
|
72
|
+
currentSection = 'readOnlyPaths';
|
|
73
|
+
} else if (trimmed === 'noDeletePaths:') {
|
|
74
|
+
currentSection = 'noDeletePaths';
|
|
75
|
+
} else if (trimmed.endsWith(':') && !trimmed.startsWith('-')) {
|
|
76
|
+
// Other sections we don't care about for path validation
|
|
77
|
+
currentSection = null;
|
|
78
|
+
} else if (trimmed.startsWith('- ') && currentSection && config[currentSection]) {
|
|
79
|
+
// Path entry
|
|
80
|
+
const pathValue = trimmed.slice(2).replace(/^["']|["']$/g, '');
|
|
81
|
+
config[currentSection].push(pathValue);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return config;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Load patterns configuration from YAML file
|
|
90
|
+
*/
|
|
91
|
+
function loadPatterns(projectRoot) {
|
|
92
|
+
const configPaths = [
|
|
93
|
+
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yaml'),
|
|
94
|
+
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yml'),
|
|
95
|
+
path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml')
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
for (const configPath of configPaths) {
|
|
99
|
+
if (fs.existsSync(configPath)) {
|
|
100
|
+
try {
|
|
101
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
102
|
+
return parseSimpleYAML(content);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
// Continue to next path
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Return empty config if no file found (fail-open)
|
|
110
|
+
return { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if a file path matches any of the protected patterns
|
|
115
|
+
*/
|
|
116
|
+
function pathMatches(filePath, patterns) {
|
|
117
|
+
if (!filePath) return null;
|
|
118
|
+
|
|
119
|
+
const normalizedPath = path.resolve(filePath);
|
|
120
|
+
const relativePath = path.relative(process.cwd(), normalizedPath);
|
|
121
|
+
|
|
122
|
+
for (const pattern of patterns) {
|
|
123
|
+
const expandedPattern = expandPath(pattern);
|
|
124
|
+
|
|
125
|
+
// Check if pattern is a directory prefix
|
|
126
|
+
if (pattern.endsWith('/')) {
|
|
127
|
+
const patternDir = expandedPattern.slice(0, -1);
|
|
128
|
+
if (normalizedPath.startsWith(patternDir)) {
|
|
129
|
+
return pattern;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check exact match
|
|
134
|
+
if (normalizedPath === expandedPattern) {
|
|
135
|
+
return pattern;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check if normalized path ends with pattern (for filenames like "id_rsa")
|
|
139
|
+
if (normalizedPath.endsWith(pattern) || relativePath.endsWith(pattern)) {
|
|
140
|
+
return pattern;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check if pattern appears in path (for patterns like "*.pem")
|
|
144
|
+
if (pattern.startsWith('*')) {
|
|
145
|
+
const ext = pattern.slice(1);
|
|
146
|
+
if (normalizedPath.endsWith(ext) || relativePath.endsWith(ext)) {
|
|
147
|
+
return pattern;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check if path contains pattern (for things like ".env.production")
|
|
152
|
+
const patternBase = path.basename(pattern);
|
|
153
|
+
if (path.basename(normalizedPath) === patternBase) {
|
|
154
|
+
return pattern;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Validate file path for write operation
|
|
163
|
+
*/
|
|
164
|
+
function validatePath(filePath, config) {
|
|
165
|
+
// Check zero access paths - completely blocked
|
|
166
|
+
const zeroMatch = pathMatches(filePath, config.zeroAccessPaths || []);
|
|
167
|
+
if (zeroMatch) {
|
|
168
|
+
return {
|
|
169
|
+
action: 'block',
|
|
170
|
+
reason: `Zero-access path: ${zeroMatch}`,
|
|
171
|
+
detail: 'This file is protected and cannot be accessed'
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check read-only paths - cannot write
|
|
176
|
+
const readOnlyMatch = pathMatches(filePath, config.readOnlyPaths || []);
|
|
177
|
+
if (readOnlyMatch) {
|
|
178
|
+
return {
|
|
179
|
+
action: 'block',
|
|
180
|
+
reason: `Read-only path: ${readOnlyMatch}`,
|
|
181
|
+
detail: 'This file is read-only and cannot be written to'
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Allow by default
|
|
186
|
+
return { action: 'allow' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Main function - read input and validate
|
|
191
|
+
*/
|
|
192
|
+
function main() {
|
|
193
|
+
const projectRoot = findProjectRoot();
|
|
194
|
+
let inputData = '';
|
|
195
|
+
|
|
196
|
+
process.stdin.setEncoding('utf8');
|
|
197
|
+
|
|
198
|
+
process.stdin.on('data', chunk => {
|
|
199
|
+
inputData += chunk;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
process.stdin.on('end', () => {
|
|
203
|
+
try {
|
|
204
|
+
// Parse tool input from Claude Code
|
|
205
|
+
const input = JSON.parse(inputData);
|
|
206
|
+
const filePath = input.file_path || input.tool_input?.file_path || '';
|
|
207
|
+
|
|
208
|
+
if (!filePath) {
|
|
209
|
+
// No path to validate - allow
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Load patterns and validate
|
|
214
|
+
const config = loadPatterns(projectRoot);
|
|
215
|
+
const result = validatePath(filePath, config);
|
|
216
|
+
|
|
217
|
+
if (result.action === 'block') {
|
|
218
|
+
console.error(`${c.red}[BLOCKED]${c.reset} ${result.reason}`);
|
|
219
|
+
console.error(`${c.dim}${result.detail}${c.reset}`);
|
|
220
|
+
console.error(`${c.dim}File: ${filePath}${c.reset}`);
|
|
221
|
+
process.exit(2);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Allow
|
|
225
|
+
process.exit(0);
|
|
226
|
+
} catch (e) {
|
|
227
|
+
// Parse error or other issue - fail open
|
|
228
|
+
process.exit(0);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Handle no stdin
|
|
233
|
+
process.stdin.on('error', () => {
|
|
234
|
+
process.exit(0);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Set timeout to prevent hanging
|
|
238
|
+
setTimeout(() => {
|
|
239
|
+
process.exit(0);
|
|
240
|
+
}, 4000);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
main();
|
|
@@ -28,11 +28,25 @@ if (commandName) {
|
|
|
28
28
|
if (fs.existsSync(sessionStatePath)) {
|
|
29
29
|
try {
|
|
30
30
|
const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
31
|
-
|
|
31
|
+
|
|
32
|
+
// Initialize active_commands array if not present
|
|
33
|
+
if (!Array.isArray(state.active_commands)) {
|
|
34
|
+
state.active_commands = [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Remove any existing entry for this command (avoid duplicates)
|
|
38
|
+
state.active_commands = state.active_commands.filter(c => c.name !== commandName);
|
|
39
|
+
|
|
40
|
+
// Add the new command
|
|
41
|
+
state.active_commands.push({
|
|
32
42
|
name: commandName,
|
|
33
43
|
activated_at: new Date().toISOString(),
|
|
34
44
|
state: {},
|
|
35
|
-
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Keep backwards compatibility - also set singular active_command to most recent
|
|
48
|
+
state.active_command = state.active_commands[state.active_commands.length - 1];
|
|
49
|
+
|
|
36
50
|
fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
|
|
37
51
|
} catch (e) {
|
|
38
52
|
// Silently continue if session state can't be updated
|
|
@@ -370,7 +384,12 @@ function generateFullContent() {
|
|
|
370
384
|
} else {
|
|
371
385
|
content += `${C.dim}No active session${C.reset}\n`;
|
|
372
386
|
}
|
|
373
|
-
|
|
387
|
+
// Show all active commands (array)
|
|
388
|
+
if (Array.isArray(sessionState.active_commands) && sessionState.active_commands.length > 0) {
|
|
389
|
+
const cmdNames = sessionState.active_commands.map(c => c.name).join(', ');
|
|
390
|
+
content += `Active commands: ${C.skyBlue}${cmdNames}${C.reset}\n`;
|
|
391
|
+
} else if (sessionState.active_command) {
|
|
392
|
+
// Backwards compatibility for old format
|
|
374
393
|
content += `Active command: ${C.skyBlue}${sessionState.active_command.name}${C.reset}\n`;
|
|
375
394
|
}
|
|
376
395
|
} else {
|