reflexes 0.0.0-stage → 0.1.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/package.json +13 -3
- package/src/index.js +173 -0
- package/test/index.test.js +87 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reflexes",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pre-action verification for AI agents. Read .reflex files and check rules before actions.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node test/index.test.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"ai",
|
|
11
|
+
"reflex",
|
|
12
|
+
"verification",
|
|
13
|
+
"safety"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT"
|
|
6
16
|
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Reflex — Pre-action verification for AI agents
|
|
2
|
+
// Usage: const reflex = require('ai-reflex'); reflex.check(action, context);
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
class Reflex {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.rules = [];
|
|
11
|
+
this.directory = options.directory || process.cwd();
|
|
12
|
+
this.globalPath = options.globalPath || path.join(process.env.HOME || '', '.reflex');
|
|
13
|
+
this.load();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
load() {
|
|
17
|
+
this.rules = [];
|
|
18
|
+
// Load global
|
|
19
|
+
if (fs.existsSync(this.globalPath)) {
|
|
20
|
+
this.rules.push(...this.parse(fs.readFileSync(this.globalPath, 'utf-8')));
|
|
21
|
+
}
|
|
22
|
+
// Load project
|
|
23
|
+
const projectPath = path.join(this.directory, '.reflex');
|
|
24
|
+
if (fs.existsSync(projectPath)) {
|
|
25
|
+
this.rules.push(...this.parse(fs.readFileSync(projectPath, 'utf-8')));
|
|
26
|
+
}
|
|
27
|
+
// Load from .reflex.d
|
|
28
|
+
const dirPath = path.join(this.directory, '.reflex.d');
|
|
29
|
+
if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
|
|
30
|
+
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.reflex'));
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
this.rules.push(...this.parse(fs.readFileSync(path.join(dirPath, file), 'utf-8')));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
parse(content) {
|
|
38
|
+
const lines = content.split('\n');
|
|
39
|
+
const reflexes = [];
|
|
40
|
+
let current = null;
|
|
41
|
+
let currentKey = null;
|
|
42
|
+
|
|
43
|
+
for (const rawLine of lines) {
|
|
44
|
+
const line = rawLine.trimEnd();
|
|
45
|
+
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
|
|
48
|
+
if (trimmed.startsWith('reflex ')) {
|
|
49
|
+
if (current) reflexes.push(current);
|
|
50
|
+
current = { name: trimmed.slice(7).trim(), rules: {} };
|
|
51
|
+
currentKey = null;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (current && /^\s+\w+:/.test(rawLine)) {
|
|
56
|
+
const colonIdx = trimmed.indexOf(':');
|
|
57
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
58
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
59
|
+
current.rules[key] = value;
|
|
60
|
+
currentKey = key;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (current && currentKey && /^\s+/.test(rawLine) && !trimmed.includes(':')) {
|
|
65
|
+
current.rules[currentKey] += ' ' + trimmed;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (current) reflexes.push(current);
|
|
71
|
+
return reflexes;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
check(context) {
|
|
75
|
+
const results = [];
|
|
76
|
+
for (const reflex of this.rules) {
|
|
77
|
+
const r = reflex.rules;
|
|
78
|
+
const trigger = r.trigger;
|
|
79
|
+
const check = r.check;
|
|
80
|
+
const condition = r.condition;
|
|
81
|
+
const onPass = r.on_pass || 'allow';
|
|
82
|
+
const onFail = r.on_fail || 'allow';
|
|
83
|
+
const message = r.message || '';
|
|
84
|
+
|
|
85
|
+
// Check trigger
|
|
86
|
+
if (trigger && trigger !== context.triggerType && trigger !== 'always') {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check condition
|
|
91
|
+
if (condition) {
|
|
92
|
+
const parts = condition.split(/\s+/);
|
|
93
|
+
if (parts[1] === 'is' && context.action !== parts[2]) continue;
|
|
94
|
+
if (parts[1] === 'matches') {
|
|
95
|
+
const pattern = parts[2].replace(/\*/g, '.*');
|
|
96
|
+
if (!new RegExp('^' + pattern + '$').test(context.target || '')) continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Run check
|
|
101
|
+
let checkResult = this.runCheck(check, context);
|
|
102
|
+
|
|
103
|
+
const action = checkResult ? onPass : onFail;
|
|
104
|
+
results.push({
|
|
105
|
+
name: reflex.name,
|
|
106
|
+
check,
|
|
107
|
+
result: checkResult,
|
|
108
|
+
action,
|
|
109
|
+
message: message.replace(/^"|"$/g, '')
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (action === 'block') break;
|
|
113
|
+
}
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
runCheck(check, context) {
|
|
118
|
+
switch (check) {
|
|
119
|
+
case 'file_exists': return context.fileExists || false;
|
|
120
|
+
case 'file_not_exists': return !context.fileExists;
|
|
121
|
+
case 'has_tests': return context.hasTests || false;
|
|
122
|
+
case 'tests_pass': return context.testsPass || false;
|
|
123
|
+
case 'git_clean': return context.gitClean || false;
|
|
124
|
+
case 'no_secrets': return !context.hasSecrets;
|
|
125
|
+
case 'rate_limit': return !context.rateLimitExceeded;
|
|
126
|
+
case 'backup_exists': return context.hasBackup || false;
|
|
127
|
+
case 'user_confirmed': return context.userConfirmed || false;
|
|
128
|
+
default:
|
|
129
|
+
if (check.startsWith('custom ')) {
|
|
130
|
+
const cmdMatch = check.match(/"([^"]*)"/);
|
|
131
|
+
if (cmdMatch) {
|
|
132
|
+
try {
|
|
133
|
+
execSync(cmdMatch[1], { stdio: 'pipe' });
|
|
134
|
+
return true;
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Convenience methods
|
|
145
|
+
beforeAction(action, target, extra = {}) {
|
|
146
|
+
return this.check({
|
|
147
|
+
triggerType: 'before_action',
|
|
148
|
+
action,
|
|
149
|
+
target,
|
|
150
|
+
...extra
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
beforeCommit(extra = {}) {
|
|
155
|
+
return this.check({
|
|
156
|
+
triggerType: 'before_commit',
|
|
157
|
+
action: 'commit',
|
|
158
|
+
...extra
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
beforeDeploy(extra = {}) {
|
|
163
|
+
return this.check({
|
|
164
|
+
triggerType: 'before_deploy',
|
|
165
|
+
action: 'deploy',
|
|
166
|
+
...extra
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = Reflex;
|
|
172
|
+
module.exports.Reflex = Reflex;
|
|
173
|
+
module.exports.default = Reflex;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const Reflex = require('../src/index.js');
|
|
2
|
+
|
|
3
|
+
let passed = 0;
|
|
4
|
+
let failed = 0;
|
|
5
|
+
|
|
6
|
+
function test(name, fn) {
|
|
7
|
+
try {
|
|
8
|
+
fn();
|
|
9
|
+
passed++;
|
|
10
|
+
console.log(` ✓ ${name}`);
|
|
11
|
+
} catch (e) {
|
|
12
|
+
failed++;
|
|
13
|
+
console.log(` ✗ ${name}: ${e.message}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function assert(condition, message) {
|
|
18
|
+
if (!condition) throw new Error(message || 'Assertion failed');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log('Reflex Tests');
|
|
22
|
+
|
|
23
|
+
const reflexContent = `
|
|
24
|
+
reflex prevent_overwrite
|
|
25
|
+
trigger: before_action
|
|
26
|
+
check: file_exists
|
|
27
|
+
condition: action is write_file
|
|
28
|
+
on_fail: allow
|
|
29
|
+
on_pass: ask_user
|
|
30
|
+
message: "File exists. Overwrite?"
|
|
31
|
+
|
|
32
|
+
reflex block_dangerous
|
|
33
|
+
trigger: before_action
|
|
34
|
+
check: custom "echo test"
|
|
35
|
+
on_pass: block
|
|
36
|
+
on_fail: allow
|
|
37
|
+
message: "Dangerous command blocked"
|
|
38
|
+
|
|
39
|
+
reflex require_tests
|
|
40
|
+
trigger: before_commit
|
|
41
|
+
check: tests_pass
|
|
42
|
+
on_pass: allow
|
|
43
|
+
on_fail: block
|
|
44
|
+
message: "Tests must pass"
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
test('parse reflex file', () => {
|
|
48
|
+
const r = new Reflex({ directory: '/tmp' });
|
|
49
|
+
const parsed = r.parse(reflexContent);
|
|
50
|
+
assert(parsed.length === 3, `Expected 3 reflexes, got ${parsed.length}`);
|
|
51
|
+
assert(parsed[0].name === 'prevent_overwrite', 'Wrong name');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('check file_exists when file exists', () => {
|
|
55
|
+
const r = new Reflex({ directory: '/tmp' });
|
|
56
|
+
r.rules = r.parse(reflexContent);
|
|
57
|
+
const results = r.beforeAction('write_file', 'app.js', { fileExists: true });
|
|
58
|
+
assert(results.length > 0, 'No reflex fired');
|
|
59
|
+
assert(results[0].action === 'ask_user', `Expected ask_user, got ${results[0].action}`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('check file_exists when file does not exist', () => {
|
|
63
|
+
const r = new Reflex({ directory: '/tmp' });
|
|
64
|
+
r.rules = r.parse(reflexContent);
|
|
65
|
+
const results = r.beforeAction('write_file', 'app.js', { fileExists: false });
|
|
66
|
+
const preventResult = results.find(r => r.name === 'prevent_overwrite');
|
|
67
|
+
assert(preventResult && preventResult.action === 'allow', 'Should allow when file does not exist');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('beforeCommit triggers require_tests', () => {
|
|
71
|
+
const r = new Reflex({ directory: '/tmp' });
|
|
72
|
+
r.rules = r.parse(reflexContent);
|
|
73
|
+
const results = r.beforeCommit({ testsPass: false });
|
|
74
|
+
assert(results.length > 0, 'No reflex fired');
|
|
75
|
+
assert(results[0].action === 'block', `Expected block, got ${results[0].action}`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('beforeCommit passes when tests pass', () => {
|
|
79
|
+
const r = new Reflex({ directory: '/tmp' });
|
|
80
|
+
r.rules = r.parse(reflexContent);
|
|
81
|
+
const results = r.beforeCommit({ testsPass: true });
|
|
82
|
+
const reqResult = results.find(r => r.name === 'require_tests');
|
|
83
|
+
assert(reqResult && reqResult.action === 'allow', 'Should allow when tests pass');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
87
|
+
process.exit(failed > 0 ? 1 : 0);
|
package/README.md
DELETED