create-walle 0.9.7 → 0.9.9
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 +1 -1
- package/bin/create-walle.js +32 -1
- package/bin/mcp-inject.js +60 -0
- package/package.json +11 -3
- package/template/bin/setup.js +2 -1
- package/template/claude-code-skill.md +35 -34
- package/template/claude-task-manager/api-prompts.js +113 -70
- package/template/claude-task-manager/approval-agent.js +127 -22
- package/template/claude-task-manager/atomic-write.js +22 -0
- package/template/claude-task-manager/db.js +515 -28
- package/template/claude-task-manager/git-utils.js +112 -1
- package/template/claude-task-manager/public/css/setup.css +191 -0
- package/template/claude-task-manager/public/css/walle-session.css +3 -0
- package/template/claude-task-manager/public/css/walle.css +305 -0
- package/template/claude-task-manager/public/index.html +3174 -296
- package/template/claude-task-manager/public/js/setup.js +698 -0
- package/template/claude-task-manager/public/js/walle-session.js +124 -1
- package/template/claude-task-manager/public/js/walle.js +1420 -42
- package/template/claude-task-manager/public/setup.html +693 -104
- package/template/claude-task-manager/server-state.js +8 -1
- package/template/claude-task-manager/server.js +2280 -227
- package/template/claude-task-manager/session-utils.js +50 -3
- package/template/claude-task-manager/workers/harvest-worker.js +36 -0
- package/template/claude-task-manager/workers/scrollback-worker.js +60 -0
- package/template/claude-task-manager/workers/vterm-worker.js +179 -0
- package/template/package.json +2 -2
- package/template/wall-e/agent.js +288 -146
- package/template/wall-e/api-walle.js +588 -4
- package/template/wall-e/brain.js +565 -10
- package/template/wall-e/chat.js +298 -51
- package/template/wall-e/coding-orchestrator.js +31 -0
- package/template/wall-e/context/compactor.js +21 -0
- package/template/wall-e/context/context-builder.js +216 -64
- package/template/wall-e/context/topic-matcher.js +40 -11
- package/template/wall-e/docs/prompt-architecture.md +121 -0
- package/template/wall-e/embeddings.js +810 -0
- package/template/wall-e/eval/aggregator.js +115 -0
- package/template/wall-e/eval/benchmarks/chat-eval.json +1041 -0
- package/template/wall-e/eval/benchmarks/chat.json +82 -0
- package/template/wall-e/eval/benchmarks/coding.json +122 -0
- package/template/wall-e/eval/benchmarks/memory-retrieval.json +82 -0
- package/template/wall-e/eval/benchmarks/reasoning.json +82 -0
- package/template/wall-e/eval/benchmarks.js +464 -0
- package/template/wall-e/eval/chat-eval.js +509 -0
- package/template/wall-e/eval/evaluate.js +202 -0
- package/template/wall-e/eval/evaluator.js +373 -0
- package/template/wall-e/eval/exporter.js +212 -0
- package/template/wall-e/eval/harvester.js +472 -0
- package/template/wall-e/eval/head-to-head.js +337 -0
- package/template/wall-e/eval/promoter.js +146 -0
- package/template/wall-e/eval/replay.js +381 -0
- package/template/wall-e/eval/shadow.js +144 -0
- package/template/wall-e/eval/train.py +320 -0
- package/template/wall-e/eval/trainer.js +232 -0
- package/template/wall-e/evaluation/complexity.js +3 -0
- package/template/wall-e/evaluation/index.js +1 -1
- package/template/wall-e/evaluation/learner.js +14 -2
- package/template/wall-e/evaluation/quorum-evaluator.js +544 -0
- package/template/wall-e/evaluation/router.js +237 -29
- package/template/wall-e/evaluation/scorecard.js +74 -2
- package/template/wall-e/evaluation/self-critique.js +188 -0
- package/template/wall-e/evaluation/tier-selector.js +166 -0
- package/template/wall-e/evaluation/user-signals.js +109 -0
- package/template/wall-e/extraction/knowledge-extractor.js +60 -17
- package/template/wall-e/lib/scheduler.js +389 -0
- package/template/wall-e/llm/client.js +74 -6
- package/template/wall-e/llm/index.js +1 -0
- package/template/wall-e/llm/mlx.js +220 -0
- package/template/wall-e/llm/ollama-setup.js +228 -0
- package/template/wall-e/llm/ollama.js +20 -3
- package/template/wall-e/llm/provider-availability.js +213 -0
- package/template/wall-e/llm/provider-detector.js +273 -0
- package/template/wall-e/loops/backfill.js +338 -0
- package/template/wall-e/loops/initiative.js +13 -0
- package/template/wall-e/loops/reflect.js +13 -0
- package/template/wall-e/loops/tasks.js +56 -11
- package/template/wall-e/loops/think.js +55 -4
- package/template/wall-e/mcp-server.js +235 -0
- package/template/wall-e/package.json +1 -0
- package/template/wall-e/scripts/eval-embeddings.js +311 -0
- package/template/wall-e/scripts/slack-channel-history.js +1 -1
- package/template/wall-e/server.js +17 -0
- package/template/wall-e/skills/_bundled/coding-agent/run.js +9 -1
- package/template/wall-e/skills/_bundled/email-sync/run.js +9 -1
- package/template/wall-e/skills/_bundled/file-ingest/run.js +6 -1
- package/template/wall-e/skills/_bundled/glean-team-sync/run.js +9 -4
- package/template/wall-e/skills/_bundled/google-calendar/run.js +40 -6
- package/template/wall-e/skills/_bundled/mcp-scan/run.js +9 -4
- package/template/wall-e/skills/_bundled/model-trainer/SKILL.md +1 -1
- package/template/wall-e/skills/_bundled/morning-briefing/run.js +82 -6
- package/template/wall-e/skills/_bundled/proactive-alerts/run.js +9 -4
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +24 -17
- package/template/wall-e/skills/_bundled/training-harvester/SKILL.md +43 -0
- package/template/wall-e/skills/_bundled/training-harvester/run.js +46 -0
- package/template/wall-e/skills/skill-planner.js +92 -64
- package/template/wall-e/telemetry.js +65 -1
- package/template/wall-e/test/eval/aggregator.test.js +180 -0
- package/template/wall-e/test/eval/benchmarks.test.js +373 -0
- package/template/wall-e/test/eval/brain-shadow.test.js +359 -0
- package/template/wall-e/test/eval/evaluate.test.js +221 -0
- package/template/wall-e/test/eval/evaluator.test.js +340 -0
- package/template/wall-e/test/eval/exporter.test.js +353 -0
- package/template/wall-e/test/eval/harvester.test.js +718 -0
- package/template/wall-e/test/eval/head-to-head.test.js +485 -0
- package/template/wall-e/test/eval/promoter.test.js +175 -0
- package/template/wall-e/test/eval/shadow.test.js +156 -0
- package/template/wall-e/test/eval/trainer.test.js +314 -0
- package/template/wall-e/test/evaluation/scorecard.test.js +33 -0
- package/template/wall-e/test/llm/client.test.js +74 -6
- package/template/wall-e/test/training/aggregator.test.js +180 -0
- package/template/wall-e/test/training/brain-shadow.test.js +359 -0
- package/template/wall-e/test/training/evaluator.test.js +340 -0
- package/template/wall-e/test/training/exporter.test.js +141 -1
- package/template/wall-e/test/training/harvester.test.js +718 -0
- package/template/wall-e/test/training/promoter.test.js +175 -0
- package/template/wall-e/test/training/shadow.test.js +156 -0
- package/template/wall-e/test/training/trainer.test.js +77 -4
- package/template/wall-e/tests/backfill.test.js +126 -0
- package/template/wall-e/tests/brain.test.js +4 -4
- package/template/wall-e/tests/coding-orchestrator.test.js +212 -217
- package/template/wall-e/tests/context-builder.test.js +42 -35
- package/template/wall-e/tests/embeddings.test.js +378 -0
- package/template/wall-e/tests/mcp-inject.test.js +68 -0
- package/template/wall-e/tests/mcp-server.test.js +219 -0
- package/template/wall-e/tests/ollama-setup.test.js +81 -0
- package/template/wall-e/tests/provider-availability.test.js +231 -0
- package/template/wall-e/tests/provider-detector.test.js +127 -0
- package/template/wall-e/tests/quorum-evaluator.test.js +277 -0
- package/template/wall-e/tests/scheduler.test.js +546 -0
- package/template/wall-e/tests/scorecard-evolution.test.js +96 -0
- package/template/wall-e/tests/self-critique.test.js +162 -0
- package/template/wall-e/tests/think.test.js +34 -15
- package/template/wall-e/tests/tier-selector.test.js +109 -0
- package/template/wall-e/tests/user-signals.test.js +73 -0
- package/template/wall-e/tools/local-tools.js +143 -14
- package/template/wall-e/tools/slack-mcp.js +20 -16
- package/template/wall-e/training/aggregator.js +115 -0
- package/template/wall-e/training/evaluator.js +373 -0
- package/template/wall-e/training/exporter.js +86 -1
- package/template/wall-e/training/harvester.js +476 -0
- package/template/wall-e/training/promoter.js +146 -0
- package/template/wall-e/training/replay.js +381 -0
- package/template/wall-e/training/shadow.js +144 -0
- package/template/wall-e/training/train.py +85 -34
- package/template/wall-e/training/trainer.js +35 -5
- package/template/wall-e/evaluation/quorum.js +0 -256
|
@@ -14,6 +14,15 @@ const dbModule = require('./db');
|
|
|
14
14
|
const PROCEED_PATTERN = /Do you want to (proceed|make this edit to .+|create .+|overwrite .+)\??/;
|
|
15
15
|
const YES_NO_PATTERN = /[>❯]\s*1\.\s*Yes/;
|
|
16
16
|
|
|
17
|
+
// Delay (ms) before sending the auto-approve keystroke. Lower = faster response.
|
|
18
|
+
const APPROVE_DELAY_MS = 100;
|
|
19
|
+
const ENTER_DELAY_MS = 30;
|
|
20
|
+
|
|
21
|
+
// Determine which option to send: "2" for "Yes, allow all" when available, "1" for plain "Yes"
|
|
22
|
+
function getApproveKeystroke(context) {
|
|
23
|
+
return context.hasAllowAll ? '2' : '1';
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
// Parse the terminal buffer to extract the approval context
|
|
18
27
|
function parseApprovalContext(cleanText) {
|
|
19
28
|
const lines = cleanText.split('\n').map(l => l.trim()).filter(Boolean);
|
|
@@ -78,14 +87,61 @@ function parseApprovalContext(cleanText) {
|
|
|
78
87
|
const ctxEnd = Math.min(lines.length, proceedIdx + 5); // include Yes/No options
|
|
79
88
|
const fullContext = lines.slice(ctxStart, ctxEnd).join('\n');
|
|
80
89
|
|
|
90
|
+
// Detect if "2. Yes, allow all..." option is available (Edit/Write prompts)
|
|
91
|
+
let hasAllowAll = false;
|
|
92
|
+
for (let i = proceedIdx + 1; i < Math.min(proceedIdx + 8, lines.length); i++) {
|
|
93
|
+
if (/2\.\s*Yes,\s*allow all/i.test(lines[i])) { hasAllowAll = true; break; }
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
return {
|
|
82
97
|
toolName: toolName || 'Unknown',
|
|
83
98
|
command: command.slice(0, 2000),
|
|
84
99
|
warning: warning || '',
|
|
85
100
|
fullContext: fullContext.slice(0, 2000),
|
|
101
|
+
hasAllowAll,
|
|
86
102
|
};
|
|
87
103
|
}
|
|
88
104
|
|
|
105
|
+
// Normalize a command into a stable "signature" by extracting the command structure
|
|
106
|
+
// and replacing variable parts (paths, strings, numbers) with placeholders.
|
|
107
|
+
// Examples:
|
|
108
|
+
// "node -e 'console.log(1+2)'" → "node -e <arg>"
|
|
109
|
+
// "git commit -m 'fix typo'" → "git commit -m <arg>"
|
|
110
|
+
// "cat /Users/bob/ws/foo/bar.js" → "cat <path>"
|
|
111
|
+
// "python3 -c 'import json; ...'" → "python3 -c <arg>"
|
|
112
|
+
// "ls -la /some/dir" → "ls -la <path>"
|
|
113
|
+
// "kill -9 12345" → "kill -9 <num>"
|
|
114
|
+
// "curl http://localhost:3000/api" → "curl <url>"
|
|
115
|
+
function normalizeCommandSignature(toolName, command) {
|
|
116
|
+
const tool = (toolName || '').replace(/^[⏺●\s]+/, '').trim();
|
|
117
|
+
const cmd = (command || '').trim();
|
|
118
|
+
if (!cmd) return tool.toLowerCase();
|
|
119
|
+
|
|
120
|
+
// For non-Bash tools (Edit, Write, Read, etc.), signature is just the tool name
|
|
121
|
+
if (!/bash/i.test(tool)) return tool.toLowerCase();
|
|
122
|
+
|
|
123
|
+
// Extract the shell command — take first line, strip leading whitespace
|
|
124
|
+
const firstLine = cmd.split('\n')[0].trim();
|
|
125
|
+
|
|
126
|
+
let sig = firstLine
|
|
127
|
+
// Replace quoted strings (single/double/backtick) with <arg>
|
|
128
|
+
.replace(/(["'`])(?:(?!\1).)*\1/g, '<arg>')
|
|
129
|
+
// Replace URLs with <url>
|
|
130
|
+
.replace(/https?:\/\/\S+/g, '<url>')
|
|
131
|
+
// Replace absolute paths with <path>
|
|
132
|
+
.replace(/(?:\/[\w._-]+){2,}/g, '<path>')
|
|
133
|
+
// Replace standalone numbers (PIDs, ports, line numbers) with <num>
|
|
134
|
+
.replace(/\b\d{2,}\b/g, '<num>')
|
|
135
|
+
// Collapse multiple spaces
|
|
136
|
+
.replace(/\s+/g, ' ')
|
|
137
|
+
.trim();
|
|
138
|
+
|
|
139
|
+
// Limit length to prevent bloated signatures
|
|
140
|
+
if (sig.length > 150) sig = sig.slice(0, 150);
|
|
141
|
+
|
|
142
|
+
return sig.toLowerCase();
|
|
143
|
+
}
|
|
144
|
+
|
|
89
145
|
// Reject regex patterns that could cause catastrophic backtracking (ReDoS).
|
|
90
146
|
// Looks for nested quantifiers like (a+)+, (a*)*, (a+|b+)+ etc.
|
|
91
147
|
function isSafeRegex(pattern) {
|
|
@@ -98,8 +154,20 @@ function isSafeRegex(pattern) {
|
|
|
98
154
|
return true;
|
|
99
155
|
}
|
|
100
156
|
|
|
101
|
-
// Check if a learned rule already covers this situation
|
|
157
|
+
// Check if a learned rule already covers this situation.
|
|
158
|
+
// Two-tier matching: (1) exact signature lookup in DB (fast, O(1)),
|
|
159
|
+
// then (2) regex scan over all rules (slower, backwards-compatible).
|
|
102
160
|
function findMatchingRule(context) {
|
|
161
|
+
// Tier 1: Signature-based lookup (indexed, instant)
|
|
162
|
+
const signature = normalizeCommandSignature(context.toolName, context.command);
|
|
163
|
+
if (signature) {
|
|
164
|
+
try {
|
|
165
|
+
const sigRule = dbModule.findApprovalRuleBySignature(signature);
|
|
166
|
+
if (sigRule) return sigRule;
|
|
167
|
+
} catch {}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Tier 2: Regex-based scan (backwards-compatible with existing rules)
|
|
103
171
|
let rules;
|
|
104
172
|
try {
|
|
105
173
|
rules = dbModule.listApprovalRules();
|
|
@@ -114,8 +182,8 @@ function findMatchingRule(context) {
|
|
|
114
182
|
for (const rule of rules) {
|
|
115
183
|
if (!rule.enabled) continue;
|
|
116
184
|
try {
|
|
117
|
-
if (!rule.pattern || rule.pattern.length > 200) continue;
|
|
118
|
-
if (!isSafeRegex(rule.pattern)) continue;
|
|
185
|
+
if (!rule.pattern || rule.pattern.length > 200) continue;
|
|
186
|
+
if (!isSafeRegex(rule.pattern)) continue;
|
|
119
187
|
const re = new RegExp(rule.pattern, 'i');
|
|
120
188
|
if (re.test(searchText) || re.test(cmdText) || re.test(warnText)) {
|
|
121
189
|
return rule;
|
|
@@ -172,6 +240,14 @@ function reviewWithHeuristics(context) {
|
|
|
172
240
|
{ re: /touch\s/, label: 'Create empty file', desc: 'Create or update file timestamps' },
|
|
173
241
|
{ re: /cp\s/, label: 'Copy files', desc: 'Copy files or directories' },
|
|
174
242
|
{ re: /mv\s/, label: 'Move/rename files', desc: 'Move or rename files' },
|
|
243
|
+
{ re: /curl\s+(-[sSwfkL]+\s+)*(https?:\/\/localhost|http:\/\/127\.0\.0\.1)/, label: 'Curl localhost (GET)', desc: 'Read-only HTTP requests to local dev servers' },
|
|
244
|
+
{ re: /grep\s+-?[crn]/, label: 'Grep search', desc: 'Search file contents with grep' },
|
|
245
|
+
{ re: /wc\s/, label: 'Word count', desc: 'Count lines/words/bytes' },
|
|
246
|
+
{ re: /head\s|tail\s/, label: 'Read file head/tail', desc: 'View beginning or end of files' },
|
|
247
|
+
{ re: /which\s|type\s/, label: 'Find command', desc: 'Locate commands in PATH' },
|
|
248
|
+
{ re: /echo\s[^|>]+$/, label: 'Echo output', desc: 'Print text to stdout (no redirect/pipe)' },
|
|
249
|
+
{ re: /find\s.*-name/, label: 'Find files', desc: 'Search for files by name' },
|
|
250
|
+
{ re: /sort\s|uniq\s/, label: 'Sort/unique', desc: 'Sort or deduplicate output' },
|
|
175
251
|
];
|
|
176
252
|
for (const { re, label, desc } of devSafe) {
|
|
177
253
|
if (re.test(cmd)) {
|
|
@@ -181,8 +257,8 @@ function reviewWithHeuristics(context) {
|
|
|
181
257
|
}
|
|
182
258
|
}
|
|
183
259
|
|
|
184
|
-
// Default: approve with medium risk (
|
|
185
|
-
return { decision: 'approve', reasoning: '
|
|
260
|
+
// Default: approve with medium risk — NOT auto-approved (sent to AI reviewer or held for user)
|
|
261
|
+
return { decision: 'approve', reasoning: 'Unrecognized command — needs review', riskLevel: 'medium', fallback: true,
|
|
186
262
|
ruleLabel: context.toolName || 'Unknown', rulePattern: '',
|
|
187
263
|
ruleDescription: 'Auto-approved without AI review' };
|
|
188
264
|
}
|
|
@@ -310,12 +386,15 @@ async function handleApprovalCheck(sessionId, session, cleanText, broadcastFn) {
|
|
|
310
386
|
};
|
|
311
387
|
|
|
312
388
|
// Record and execute
|
|
313
|
-
try {
|
|
389
|
+
try {
|
|
390
|
+
dbModule.addApprovalDecision(decision);
|
|
391
|
+
dbModule.incrementApprovalRuleMatch(matchingRule.id);
|
|
392
|
+
} catch (e) { console.error('[approval-agent] DB error:', e.message); }
|
|
314
393
|
|
|
315
|
-
// Send "1" for Yes
|
|
394
|
+
// Send approval keystroke ("2" for allow-all when available, "1" for plain Yes)
|
|
316
395
|
setTimeout(() => {
|
|
317
|
-
session.ptyProcess.write(
|
|
318
|
-
setTimeout(() => session.ptyProcess.write('\r'),
|
|
396
|
+
session.ptyProcess.write(getApproveKeystroke(context));
|
|
397
|
+
setTimeout(() => session.ptyProcess.write('\r'), ENTER_DELAY_MS);
|
|
319
398
|
// Notify clients
|
|
320
399
|
broadcastFn(sessionId, session, {
|
|
321
400
|
type: 'approval-decision',
|
|
@@ -326,15 +405,15 @@ async function handleApprovalCheck(sessionId, session, cleanText, broadcastFn) {
|
|
|
326
405
|
reasoning: decision.reasoning,
|
|
327
406
|
riskLevel: decision.riskLevel,
|
|
328
407
|
});
|
|
329
|
-
},
|
|
408
|
+
}, APPROVE_DELAY_MS);
|
|
330
409
|
|
|
331
410
|
return true;
|
|
332
411
|
}
|
|
333
412
|
|
|
334
413
|
// No matching rule — check heuristics for obvious safe/dangerous patterns first
|
|
335
414
|
const heuristic = reviewWithHeuristics(context);
|
|
336
|
-
if (heuristic.riskLevel === 'low') {
|
|
337
|
-
//
|
|
415
|
+
if (heuristic.riskLevel === 'low' || (heuristic.riskLevel === 'medium' && heuristic.decision === 'approve' && !heuristic.fallback)) {
|
|
416
|
+
// Low risk or medium with explicit rule match: auto-approve without AI call
|
|
338
417
|
const decision = {
|
|
339
418
|
sessionId,
|
|
340
419
|
toolName: context.toolName,
|
|
@@ -347,14 +426,34 @@ async function handleApprovalCheck(sessionId, session, cleanText, broadcastFn) {
|
|
|
347
426
|
riskLevel: 'low',
|
|
348
427
|
};
|
|
349
428
|
try { dbModule.addApprovalDecision(decision); } catch (e) { console.error('[approval-agent] DB error:', e.message); }
|
|
429
|
+
|
|
430
|
+
// Learn signature from heuristic approval so future matches use fast DB path.
|
|
431
|
+
// Only write if no signature rule exists yet (avoid DB write on every approval).
|
|
432
|
+
const heuristicSig = normalizeCommandSignature(context.toolName, context.command);
|
|
433
|
+
if (heuristicSig && heuristic.rulePattern) {
|
|
434
|
+
try {
|
|
435
|
+
if (!dbModule.findApprovalRuleBySignature(heuristicSig)) {
|
|
436
|
+
dbModule.upsertApprovalRule({
|
|
437
|
+
pattern: heuristic.rulePattern,
|
|
438
|
+
label: heuristic.ruleLabel || context.toolName,
|
|
439
|
+
description: heuristic.ruleDescription || '',
|
|
440
|
+
category: context.toolName.toLowerCase().replace(/\s+/g, '-'),
|
|
441
|
+
riskLevel: 'low',
|
|
442
|
+
enabled: true,
|
|
443
|
+
commandSignature: heuristicSig,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
} catch {}
|
|
447
|
+
}
|
|
448
|
+
|
|
350
449
|
setTimeout(() => {
|
|
351
|
-
session.ptyProcess.write(
|
|
352
|
-
setTimeout(() => session.ptyProcess.write('\r'),
|
|
450
|
+
session.ptyProcess.write(getApproveKeystroke(context));
|
|
451
|
+
setTimeout(() => session.ptyProcess.write('\r'), ENTER_DELAY_MS);
|
|
353
452
|
broadcastFn(sessionId, session, {
|
|
354
453
|
type: 'approval-decision', sessionId, decision: 'approved', decidedBy: 'heuristic',
|
|
355
454
|
label: heuristic.ruleLabel || context.toolName, reasoning: heuristic.reasoning, riskLevel: 'low',
|
|
356
455
|
});
|
|
357
|
-
},
|
|
456
|
+
}, APPROVE_DELAY_MS);
|
|
358
457
|
return true;
|
|
359
458
|
}
|
|
360
459
|
if (heuristic.riskLevel === 'high') {
|
|
@@ -401,23 +500,28 @@ async function handleApprovalCheck(sessionId, session, cleanText, broadcastFn) {
|
|
|
401
500
|
try { decisionId = dbModule.addApprovalDecision(decision); } catch (e) { console.error('[approval-agent] DB error:', e.message); }
|
|
402
501
|
|
|
403
502
|
if (review.decision === 'approve') {
|
|
404
|
-
// Auto-approve and learn a new rule
|
|
405
|
-
|
|
503
|
+
// Auto-approve and learn a new rule with command signature for fast future matching
|
|
504
|
+
const signature = normalizeCommandSignature(context.toolName, context.command);
|
|
505
|
+
const rulePattern = review.rulePattern || signature || '';
|
|
506
|
+
const ruleLabel = review.ruleLabel || context.toolName || 'Unknown';
|
|
507
|
+
if (rulePattern) {
|
|
406
508
|
try {
|
|
407
509
|
dbModule.upsertApprovalRule({
|
|
408
|
-
pattern:
|
|
409
|
-
label:
|
|
510
|
+
pattern: rulePattern,
|
|
511
|
+
label: ruleLabel,
|
|
410
512
|
description: review.ruleDescription || '',
|
|
411
513
|
category: context.toolName.toLowerCase().replace(/\s+/g, '-'),
|
|
412
514
|
riskLevel: review.riskLevel || 'low',
|
|
413
515
|
enabled: true,
|
|
516
|
+
commandSignature: signature,
|
|
414
517
|
});
|
|
518
|
+
console.log(`[approval-agent] Learned rule: "${ruleLabel}" sig="${signature}" pattern="${rulePattern}"`);
|
|
415
519
|
} catch (e) { console.error('[approval-agent] Rule save error:', e.message); }
|
|
416
520
|
}
|
|
417
521
|
|
|
418
522
|
setTimeout(() => {
|
|
419
|
-
session.ptyProcess.write(
|
|
420
|
-
setTimeout(() => session.ptyProcess.write('\r'),
|
|
523
|
+
session.ptyProcess.write(getApproveKeystroke(context));
|
|
524
|
+
setTimeout(() => session.ptyProcess.write('\r'), ENTER_DELAY_MS);
|
|
421
525
|
broadcastFn(sessionId, session, {
|
|
422
526
|
type: 'approval-decision',
|
|
423
527
|
sessionId,
|
|
@@ -427,7 +531,7 @@ async function handleApprovalCheck(sessionId, session, cleanText, broadcastFn) {
|
|
|
427
531
|
reasoning: review.reasoning,
|
|
428
532
|
riskLevel: review.riskLevel,
|
|
429
533
|
});
|
|
430
|
-
},
|
|
534
|
+
}, APPROVE_DELAY_MS);
|
|
431
535
|
} else {
|
|
432
536
|
// Escalate to user
|
|
433
537
|
broadcastFn(sessionId, session, {
|
|
@@ -449,6 +553,7 @@ async function handleApprovalCheck(sessionId, session, cleanText, broadcastFn) {
|
|
|
449
553
|
|
|
450
554
|
module.exports = {
|
|
451
555
|
parseApprovalContext,
|
|
556
|
+
normalizeCommandSignature,
|
|
452
557
|
findMatchingRule,
|
|
453
558
|
reviewWithAI,
|
|
454
559
|
handleApprovalCheck,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Atomic file write using tmp + rename (POSIX rename is atomic on same filesystem).
|
|
7
|
+
* Prevents read-modify-write races when multiple sessions write to .env concurrently.
|
|
8
|
+
*/
|
|
9
|
+
function atomicWriteFileSync(filePath, data, options) {
|
|
10
|
+
const resolved = path.resolve(filePath);
|
|
11
|
+
const tmp = resolved + '.tmp.' + process.pid + '.' + Date.now();
|
|
12
|
+
try {
|
|
13
|
+
fs.writeFileSync(tmp, data, options);
|
|
14
|
+
fs.renameSync(tmp, resolved);
|
|
15
|
+
} catch (e) {
|
|
16
|
+
// Clean up tmp file on failure
|
|
17
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
18
|
+
throw e;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { atomicWriteFileSync };
|