create-walle 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/bin/create-walle.js +134 -0
- package/package.json +18 -0
- package/template/.env.example +40 -0
- package/template/CLAUDE.md +12 -0
- package/template/LICENSE +21 -0
- package/template/README.md +167 -0
- package/template/bin/setup.js +100 -0
- package/template/claude-code-skill.md +60 -0
- package/template/claude-task-manager/api-prompts.js +1841 -0
- package/template/claude-task-manager/api-reviews.js +275 -0
- package/template/claude-task-manager/approval-agent.js +454 -0
- package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
- package/template/claude-task-manager/db.js +1721 -0
- package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
- package/template/claude-task-manager/git-utils.js +214 -0
- package/template/claude-task-manager/package-lock.json +1607 -0
- package/template/claude-task-manager/package.json +31 -0
- package/template/claude-task-manager/prompt-harvest.js +1148 -0
- package/template/claude-task-manager/public/css/prompts.css +880 -0
- package/template/claude-task-manager/public/css/reviews.css +430 -0
- package/template/claude-task-manager/public/css/walle.css +732 -0
- package/template/claude-task-manager/public/favicon.ico +0 -0
- package/template/claude-task-manager/public/icon.svg +37 -0
- package/template/claude-task-manager/public/index.html +8346 -0
- package/template/claude-task-manager/public/js/prompts.js +3159 -0
- package/template/claude-task-manager/public/js/reviews.js +1292 -0
- package/template/claude-task-manager/public/js/walle.js +3081 -0
- package/template/claude-task-manager/public/manifest.json +13 -0
- package/template/claude-task-manager/public/prompts.html +4353 -0
- package/template/claude-task-manager/public/setup.html +216 -0
- package/template/claude-task-manager/queue-engine.js +404 -0
- package/template/claude-task-manager/server-state.js +5 -0
- package/template/claude-task-manager/server.js +2254 -0
- package/template/claude-task-manager/session-utils.js +124 -0
- package/template/claude-task-manager/start.sh +17 -0
- package/template/claude-task-manager/tests/test-ai-search.js +61 -0
- package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
- package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
- package/template/claude-task-manager/tests/test-features-v2.js +127 -0
- package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
- package/template/claude-task-manager/tests/test-insights.js +124 -0
- package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
- package/template/claude-task-manager/tests/test-permissions.js +122 -0
- package/template/claude-task-manager/tests/test-pin.js +51 -0
- package/template/claude-task-manager/tests/test-prompts.js +164 -0
- package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
- package/template/claude-task-manager/tests/test-review.js +104 -0
- package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
- package/template/claude-task-manager/tests/test-send-final.js +30 -0
- package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
- package/template/claude-task-manager/tests/test-send-integration.js +107 -0
- package/template/claude-task-manager/tests/test-send-visual.js +34 -0
- package/template/claude-task-manager/tests/test-session-create.js +147 -0
- package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
- package/template/claude-task-manager/tests/test-url-hash.js +68 -0
- package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
- package/template/claude-task-manager/tests/test-ux-review.js +130 -0
- package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
- package/template/claude-task-manager/tests/test-zoom.js +92 -0
- package/template/claude-task-manager/tests/test-zoom2.js +67 -0
- package/template/docs/site/api/README.md +187 -0
- package/template/docs/site/guides/claude-code.md +58 -0
- package/template/docs/site/guides/configuration.md +96 -0
- package/template/docs/site/guides/quickstart.md +158 -0
- package/template/docs/site/index.md +14 -0
- package/template/docs/site/skills/README.md +135 -0
- package/template/wall-e/.dockerignore +11 -0
- package/template/wall-e/Dockerfile +25 -0
- package/template/wall-e/adapters/adapter-base.js +37 -0
- package/template/wall-e/adapters/ctm.js +193 -0
- package/template/wall-e/adapters/slack.js +56 -0
- package/template/wall-e/agent.js +319 -0
- package/template/wall-e/api-walle.js +1073 -0
- package/template/wall-e/brain.js +1235 -0
- package/template/wall-e/channels/agent-api.js +172 -0
- package/template/wall-e/channels/channel-base.js +14 -0
- package/template/wall-e/channels/imessage-channel.js +113 -0
- package/template/wall-e/channels/slack-channel.js +118 -0
- package/template/wall-e/chat.js +778 -0
- package/template/wall-e/decision/confidence.js +93 -0
- package/template/wall-e/deploy.sh +35 -0
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
- package/template/wall-e/extraction/contradiction.js +168 -0
- package/template/wall-e/extraction/knowledge-extractor.js +190 -0
- package/template/wall-e/fly.toml +24 -0
- package/template/wall-e/loops/ingest.js +34 -0
- package/template/wall-e/loops/reflect.js +63 -0
- package/template/wall-e/loops/tasks.js +487 -0
- package/template/wall-e/loops/think.js +125 -0
- package/template/wall-e/package-lock.json +533 -0
- package/template/wall-e/package.json +18 -0
- package/template/wall-e/scripts/ingest-slack-search.js +85 -0
- package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
- package/template/wall-e/scripts/slack-backfill.js +295 -0
- package/template/wall-e/scripts/slack-channel-history.js +454 -0
- package/template/wall-e/server.js +93 -0
- package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
- package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
- package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
- package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
- package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
- package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
- package/template/wall-e/skills/claude-code-reader.js +144 -0
- package/template/wall-e/skills/mcp-client.js +407 -0
- package/template/wall-e/skills/skill-executor.js +163 -0
- package/template/wall-e/skills/skill-loader.js +410 -0
- package/template/wall-e/skills/skill-planner.js +88 -0
- package/template/wall-e/skills/slack-ingest.js +329 -0
- package/template/wall-e/skills/slack-pull-live.js +270 -0
- package/template/wall-e/skills/tool-executor.js +188 -0
- package/template/wall-e/tests/adapter-base.test.js +20 -0
- package/template/wall-e/tests/adapter-ctm.test.js +122 -0
- package/template/wall-e/tests/adapter-slack.test.js +98 -0
- package/template/wall-e/tests/agent-api.test.js +256 -0
- package/template/wall-e/tests/api-walle.test.js +222 -0
- package/template/wall-e/tests/brain.test.js +602 -0
- package/template/wall-e/tests/channels.test.js +104 -0
- package/template/wall-e/tests/chat.test.js +103 -0
- package/template/wall-e/tests/confidence.test.js +134 -0
- package/template/wall-e/tests/contradiction.test.js +217 -0
- package/template/wall-e/tests/ingest.test.js +113 -0
- package/template/wall-e/tests/mcp-client.test.js +71 -0
- package/template/wall-e/tests/reflect.test.js +103 -0
- package/template/wall-e/tests/server.test.js +111 -0
- package/template/wall-e/tests/skills.test.js +198 -0
- package/template/wall-e/tests/slack-ingest.test.js +103 -0
- package/template/wall-e/tests/think.test.js +435 -0
- package/template/wall-e/tools/local-tools.js +697 -0
- package/template/wall-e/tools/slack-mcp.js +290 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
// --- Shadow Approver Agent ---
|
|
2
|
+
// Shadows user approvals in Claude Code terminal sessions.
|
|
3
|
+
// Monitors PTY output for "Do you want to proceed?" prompts,
|
|
4
|
+
// checks learned rules first, then uses AI to decide whether to
|
|
5
|
+
// auto-approve or escalate to the user.
|
|
6
|
+
|
|
7
|
+
const dbModule = require('./db');
|
|
8
|
+
|
|
9
|
+
// Prompt patterns: Claude Code asks various "Do you want to ...?" prompts with numbered options
|
|
10
|
+
// - "Do you want to proceed?" (Bash commands)
|
|
11
|
+
// - "Do you want to make this edit to <file>?" (Edit tool)
|
|
12
|
+
// - "Do you want to create <file>?" (Write tool)
|
|
13
|
+
const PROCEED_PATTERN = /Do you want to (proceed|make this edit to .+|create .+)\??/;
|
|
14
|
+
const YES_NO_PATTERN = /[>❯]\s*1\.\s*Yes/;
|
|
15
|
+
|
|
16
|
+
// Parse the terminal buffer to extract the approval context
|
|
17
|
+
function parseApprovalContext(cleanText) {
|
|
18
|
+
const lines = cleanText.split('\n').map(l => l.trim()).filter(Boolean);
|
|
19
|
+
|
|
20
|
+
// Find the "Do you want to proceed?" line
|
|
21
|
+
let proceedIdx = -1;
|
|
22
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
23
|
+
if (PROCEED_PATTERN.test(lines[i])) {
|
|
24
|
+
proceedIdx = i;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (proceedIdx < 0) return null;
|
|
29
|
+
|
|
30
|
+
// Find "1. Yes" after it (Edit prompts may have more options so search further)
|
|
31
|
+
let hasYesNo = false;
|
|
32
|
+
for (let i = proceedIdx + 1; i < Math.min(proceedIdx + 6, lines.length); i++) {
|
|
33
|
+
if (/1\.\s*Yes/.test(lines[i])) { hasYesNo = true; break; }
|
|
34
|
+
}
|
|
35
|
+
if (!hasYesNo) return null;
|
|
36
|
+
|
|
37
|
+
// Extract warning (line before "Do you want to proceed?")
|
|
38
|
+
let warning = '';
|
|
39
|
+
for (let i = proceedIdx - 1; i >= Math.max(proceedIdx - 3, 0); i--) {
|
|
40
|
+
const line = lines[i];
|
|
41
|
+
if (!line) continue;
|
|
42
|
+
// Warning lines typically describe the risk
|
|
43
|
+
if (/command contains|could write|could modify|could delete|could overwrite|which can|permission|dangerous|destructive|overwrite|will modify|will delete|will overwrite|execute arbitrary|shell command substitution/i.test(line)) {
|
|
44
|
+
warning = line;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Find the warning line index (if any)
|
|
50
|
+
let warningIdx = -1;
|
|
51
|
+
if (warning) {
|
|
52
|
+
for (let i = proceedIdx - 1; i >= Math.max(proceedIdx - 3, 0); i--) {
|
|
53
|
+
if (lines[i] === warning) { warningIdx = i; break; }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Extract the tool/command block above the warning
|
|
58
|
+
// Look for tool header like "Bash command", "Edit", "Write", etc.
|
|
59
|
+
let toolName = '';
|
|
60
|
+
const contextLines = [];
|
|
61
|
+
const endIdx = warningIdx > 0 ? warningIdx : proceedIdx;
|
|
62
|
+
|
|
63
|
+
for (let i = endIdx - 1; i >= Math.max(0, endIdx - 30); i--) {
|
|
64
|
+
const line = lines[i];
|
|
65
|
+
// Detect tool headers (Claude Code shows "Bash command", "⏺ Bash(...)", "Edit", etc.)
|
|
66
|
+
if (/^[⏺●]?\s*(Bash command|Bash|Edit|Write|Read|Glob|Grep|WebFetch|NotebookEdit|TodoWrite|Agent)\b/.test(line)) {
|
|
67
|
+
toolName = line.trim();
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
contextLines.unshift(line);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const command = contextLines.join('\n').trim();
|
|
74
|
+
|
|
75
|
+
// Build focused context: tool header + command + warning + prompt (not the whole screen)
|
|
76
|
+
const ctxStart = Math.max(0, endIdx - (contextLines.length + 1));
|
|
77
|
+
const ctxEnd = Math.min(lines.length, proceedIdx + 5); // include Yes/No options
|
|
78
|
+
const fullContext = lines.slice(ctxStart, ctxEnd).join('\n');
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
toolName: toolName || 'Unknown',
|
|
82
|
+
command: command.slice(0, 2000),
|
|
83
|
+
warning: warning || '',
|
|
84
|
+
fullContext: fullContext.slice(0, 2000),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Reject regex patterns that could cause catastrophic backtracking (ReDoS).
|
|
89
|
+
// Looks for nested quantifiers like (a+)+, (a*)*, (a+|b+)+ etc.
|
|
90
|
+
function isSafeRegex(pattern) {
|
|
91
|
+
// Reject patterns with nested quantifiers — the main ReDoS vector
|
|
92
|
+
if (/(\+|\*|\{)\)?(\+|\*|\?)/.test(pattern)) return false;
|
|
93
|
+
// Reject patterns with excessive alternation groups
|
|
94
|
+
if ((pattern.match(/\|/g) || []).length > 20) return false;
|
|
95
|
+
// Quick compile test — reject if it takes too long conceptually
|
|
96
|
+
try { new RegExp(pattern); } catch { return false; }
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check if a learned rule already covers this situation
|
|
101
|
+
function findMatchingRule(context) {
|
|
102
|
+
let rules;
|
|
103
|
+
try {
|
|
104
|
+
rules = dbModule.listApprovalRules();
|
|
105
|
+
} catch { return null; }
|
|
106
|
+
|
|
107
|
+
if (!rules || rules.length === 0) return null;
|
|
108
|
+
|
|
109
|
+
const searchText = `${context.toolName} ${context.command} ${context.warning}`.slice(0, 500).toLowerCase();
|
|
110
|
+
const cmdText = (context.command || '').slice(0, 500);
|
|
111
|
+
const warnText = (context.warning || '').slice(0, 500);
|
|
112
|
+
|
|
113
|
+
for (const rule of rules) {
|
|
114
|
+
if (!rule.enabled) continue;
|
|
115
|
+
try {
|
|
116
|
+
if (!rule.pattern || rule.pattern.length > 200) continue; // skip overly complex patterns
|
|
117
|
+
if (!isSafeRegex(rule.pattern)) continue; // skip ReDoS-prone patterns
|
|
118
|
+
const re = new RegExp(rule.pattern, 'i');
|
|
119
|
+
if (re.test(searchText) || re.test(cmdText) || re.test(warnText)) {
|
|
120
|
+
return rule;
|
|
121
|
+
}
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Simple heuristic review when no API key is available
|
|
128
|
+
function reviewWithHeuristics(context) {
|
|
129
|
+
const cmd = (context.command || '').toLowerCase();
|
|
130
|
+
// Strip Unicode bullets/icons (⏺●) from tool name before matching
|
|
131
|
+
const tool = (context.toolName || '').replace(/^[⏺●\s]+/, '').toLowerCase();
|
|
132
|
+
const warning = (context.warning || '').toLowerCase();
|
|
133
|
+
|
|
134
|
+
// High-risk patterns — always escalate
|
|
135
|
+
const highRisk = [
|
|
136
|
+
/rm\s+-rf?\s+[\/~]/, /force.?push/, /--force/, /drop\s+table/i,
|
|
137
|
+
/delete.*production/i, /sudo\s/, /chmod\s+777/, /curl.*\|\s*sh/,
|
|
138
|
+
/>\s*\/etc\//, />\s*\/usr\//, />\s*\/var\//, /mkfs/, /dd\s+if=/,
|
|
139
|
+
];
|
|
140
|
+
for (const re of highRisk) {
|
|
141
|
+
if (re.test(cmd) || re.test(warning)) {
|
|
142
|
+
return { decision: 'escalate', reasoning: 'High-risk operation detected (heuristic)', riskLevel: 'high' };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Low-risk patterns — auto-approve
|
|
147
|
+
const lowRisk = [
|
|
148
|
+
/^(read|glob|grep|webfetch|notebookedit)/, // Read-only tools
|
|
149
|
+
/^(edit|write)\b/, // Edit/Write to project files — normal dev workflow
|
|
150
|
+
];
|
|
151
|
+
for (const re of lowRisk) {
|
|
152
|
+
if (re.test(tool)) {
|
|
153
|
+
return { decision: 'approve', reasoning: 'Standard dev tool (heuristic)', riskLevel: 'low',
|
|
154
|
+
ruleLabel: `${context.toolName} operations`, rulePattern: context.toolName.replace(/\s+/g, '\\\\s+'),
|
|
155
|
+
ruleDescription: `Auto-approve ${context.toolName} operations` };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Medium: approve most local dev operations
|
|
160
|
+
const devSafe = [
|
|
161
|
+
{ re: /echo\s+.*>\s*\/tmp\//, label: 'Write to /tmp', desc: 'Echo output to temp files' },
|
|
162
|
+
{ re: /cat\s/, label: 'Read file contents', desc: 'View file contents with cat' },
|
|
163
|
+
{ re: /ls\s/, label: 'List directory', desc: 'List files and directories' },
|
|
164
|
+
{ re: /pwd/, label: 'Print working directory', desc: 'Show current directory path' },
|
|
165
|
+
{ re: /git\s+(status|log|diff|branch|show)/, label: 'Git read operations', desc: 'Read-only git commands (status, log, diff, branch, show)' },
|
|
166
|
+
{ re: /node\s+-e/, label: 'Node one-liner', desc: 'Run inline Node.js expression' },
|
|
167
|
+
{ re: /python3?\s+-c/, label: 'Python one-liner', desc: 'Run inline Python expression' },
|
|
168
|
+
{ re: /npm\s+(run|test|start)/, label: 'npm script', desc: 'Run npm scripts (run, test, start)' },
|
|
169
|
+
{ re: /mkdir\s+-?p?\s/, label: 'Create directory', desc: 'Create directories with mkdir' },
|
|
170
|
+
{ re: />\s*\/tmp\//, label: 'Write to /tmp', desc: 'Redirect output to temp files' },
|
|
171
|
+
{ re: /touch\s/, label: 'Create empty file', desc: 'Create or update file timestamps' },
|
|
172
|
+
{ re: /cp\s/, label: 'Copy files', desc: 'Copy files or directories' },
|
|
173
|
+
{ re: /mv\s/, label: 'Move/rename files', desc: 'Move or rename files' },
|
|
174
|
+
];
|
|
175
|
+
for (const { re, label, desc } of devSafe) {
|
|
176
|
+
if (re.test(cmd)) {
|
|
177
|
+
return { decision: 'approve', reasoning: 'Common dev operation (heuristic)', riskLevel: 'low',
|
|
178
|
+
ruleLabel: label, rulePattern: re.source,
|
|
179
|
+
ruleDescription: desc };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Default: approve with medium risk (user can review in the decisions log)
|
|
184
|
+
return { decision: 'approve', reasoning: 'No API key; auto-approved with medium risk (heuristic)', riskLevel: 'medium',
|
|
185
|
+
ruleLabel: context.toolName || 'Unknown', rulePattern: '',
|
|
186
|
+
ruleDescription: 'Auto-approved without AI review' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Call Claude API to review the command as a TL/Code Reviewer
|
|
190
|
+
async function reviewWithAI(context, learnedRules) {
|
|
191
|
+
const baseUrl = process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com';
|
|
192
|
+
const apiKey = process.env.ANTHROPIC_API_KEY || '';
|
|
193
|
+
if (!apiKey) return reviewWithHeuristics(context);
|
|
194
|
+
|
|
195
|
+
// Build custom headers if any
|
|
196
|
+
let customHeaders = {};
|
|
197
|
+
try {
|
|
198
|
+
const headerStr = process.env.ANTHROPIC_CUSTOM_HEADERS || '';
|
|
199
|
+
if (headerStr) {
|
|
200
|
+
for (const pair of headerStr.split(',')) {
|
|
201
|
+
const [k, ...v] = pair.split(':');
|
|
202
|
+
if (k && v.length) customHeaders[k.trim()] = v.join(':').trim();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch {}
|
|
206
|
+
|
|
207
|
+
const rulesContext = learnedRules.length > 0
|
|
208
|
+
? `\nPreviously approved patterns (the user always approves these):\n${learnedRules.map(r => `- ${r.label}: ${r.description || r.pattern}`).join('\n')}\n`
|
|
209
|
+
: '';
|
|
210
|
+
|
|
211
|
+
const prompt = `You are a senior TL/Code Reviewer acting as a gatekeeper for a developer's Claude Code sessions.
|
|
212
|
+
|
|
213
|
+
Your job: Review commands that Claude Code wants to execute and decide whether to AUTO-APPROVE (safe) or ESCALATE to the developer (risky).
|
|
214
|
+
|
|
215
|
+
The developer's general approach:
|
|
216
|
+
- They approve most read-only operations, file reads, searches
|
|
217
|
+
- They approve code editing within their project
|
|
218
|
+
- They approve running their own scripts (python3 -c, node -e) for data analysis
|
|
219
|
+
- They approve git operations like commit, status, diff, log, branch
|
|
220
|
+
- They approve server restarts (kill + restart node server)
|
|
221
|
+
- They approve npm/pip install for known dependencies
|
|
222
|
+
- They are cautious about: force push, deleting production data, modifying CI/CD, running unknown binaries, writing to system directories
|
|
223
|
+
${rulesContext}
|
|
224
|
+
Current request being reviewed:
|
|
225
|
+
Tool: ${context.toolName}
|
|
226
|
+
Command/Content:
|
|
227
|
+
${context.command.slice(0, 1500)}
|
|
228
|
+
|
|
229
|
+
Safety Warning: ${context.warning || 'None'}
|
|
230
|
+
|
|
231
|
+
Analyze the risk and decide.
|
|
232
|
+
|
|
233
|
+
Return ONLY valid JSON (no markdown fences):
|
|
234
|
+
{
|
|
235
|
+
"decision": "approve" or "escalate",
|
|
236
|
+
"riskLevel": "low" or "medium" or "high",
|
|
237
|
+
"reasoning": "brief explanation (1-2 sentences)",
|
|
238
|
+
"ruleLabel": "short label for this type of operation (e.g. 'Read JSONL files', 'Restart dev server')",
|
|
239
|
+
"rulePattern": "regex pattern that would match similar future requests",
|
|
240
|
+
"ruleDescription": "human-readable description of what this rule covers"
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
Be pragmatic. Most development operations in a local dev environment are safe. Only escalate things that could cause irreversible damage or affect production/shared systems.`;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const res = await fetch(`${baseUrl}/messages`, {
|
|
247
|
+
method: 'POST',
|
|
248
|
+
headers: {
|
|
249
|
+
'Content-Type': 'application/json',
|
|
250
|
+
'x-api-key': apiKey,
|
|
251
|
+
'anthropic-version': '2023-06-01',
|
|
252
|
+
...customHeaders,
|
|
253
|
+
},
|
|
254
|
+
body: JSON.stringify({
|
|
255
|
+
model: 'claude-sonnet-4-20250514',
|
|
256
|
+
max_tokens: 512,
|
|
257
|
+
messages: [{ role: 'user', content: prompt }],
|
|
258
|
+
}),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (!res.ok) {
|
|
262
|
+
const text = await res.text();
|
|
263
|
+
console.error('[approval-agent] Claude API error:', res.status, text);
|
|
264
|
+
return { decision: 'escalate', reasoning: `API error: ${res.status}`, riskLevel: 'unknown' };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const data = await res.json();
|
|
268
|
+
const text = data.content?.[0]?.text || '';
|
|
269
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
270
|
+
if (!match) {
|
|
271
|
+
return { decision: 'escalate', reasoning: 'Could not parse AI response', riskLevel: 'unknown' };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const result = JSON.parse(match[0]);
|
|
275
|
+
return {
|
|
276
|
+
decision: result.decision || 'escalate',
|
|
277
|
+
riskLevel: result.riskLevel || 'medium',
|
|
278
|
+
reasoning: result.reasoning || '',
|
|
279
|
+
ruleLabel: result.ruleLabel || '',
|
|
280
|
+
rulePattern: result.rulePattern || '',
|
|
281
|
+
ruleDescription: result.ruleDescription || '',
|
|
282
|
+
};
|
|
283
|
+
} catch (e) {
|
|
284
|
+
console.error('[approval-agent] Review failed:', e.message);
|
|
285
|
+
return { decision: 'escalate', reasoning: `Review failed: ${e.message}`, riskLevel: 'unknown' };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Main entry point: check terminal buffer for approval prompts and handle them
|
|
290
|
+
async function handleApprovalCheck(sessionId, session, cleanText, broadcastFn) {
|
|
291
|
+
const context = parseApprovalContext(cleanText);
|
|
292
|
+
if (!context) return false;
|
|
293
|
+
|
|
294
|
+
// Check learned rules first (fast path)
|
|
295
|
+
const matchingRule = findMatchingRule(context);
|
|
296
|
+
if (matchingRule) {
|
|
297
|
+
// Auto-approve based on learned rule
|
|
298
|
+
const decision = {
|
|
299
|
+
sessionId,
|
|
300
|
+
toolName: context.toolName,
|
|
301
|
+
commandSummary: matchingRule.label,
|
|
302
|
+
fullContext: context.fullContext.slice(0, 2000),
|
|
303
|
+
warning: context.warning,
|
|
304
|
+
decision: 'approved',
|
|
305
|
+
reasoning: `Matched learned rule: ${matchingRule.label}`,
|
|
306
|
+
decidedBy: 'rule',
|
|
307
|
+
ruleId: matchingRule.id,
|
|
308
|
+
riskLevel: matchingRule.risk_level || 'low',
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Record and execute
|
|
312
|
+
try { dbModule.addApprovalDecision(decision); } catch (e) { console.error('[approval-agent] DB error:', e.message); }
|
|
313
|
+
|
|
314
|
+
// Send "1" for Yes then Enter
|
|
315
|
+
setTimeout(() => {
|
|
316
|
+
session.ptyProcess.write('1');
|
|
317
|
+
setTimeout(() => session.ptyProcess.write('\r'), 50);
|
|
318
|
+
// Notify clients
|
|
319
|
+
broadcastFn(sessionId, session, {
|
|
320
|
+
type: 'approval-decision',
|
|
321
|
+
sessionId,
|
|
322
|
+
decision: 'approved',
|
|
323
|
+
decidedBy: 'rule',
|
|
324
|
+
label: matchingRule.label,
|
|
325
|
+
reasoning: decision.reasoning,
|
|
326
|
+
riskLevel: decision.riskLevel,
|
|
327
|
+
});
|
|
328
|
+
}, 400);
|
|
329
|
+
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// No matching rule — check heuristics for obvious safe/dangerous patterns first
|
|
334
|
+
const heuristic = reviewWithHeuristics(context);
|
|
335
|
+
if (heuristic.riskLevel === 'low') {
|
|
336
|
+
// Heuristic says it's safe — auto-approve without AI call
|
|
337
|
+
const decision = {
|
|
338
|
+
sessionId,
|
|
339
|
+
toolName: context.toolName,
|
|
340
|
+
commandSummary: heuristic.ruleLabel || context.toolName,
|
|
341
|
+
fullContext: context.fullContext.slice(0, 2000),
|
|
342
|
+
warning: context.warning,
|
|
343
|
+
decision: 'approved',
|
|
344
|
+
reasoning: heuristic.reasoning,
|
|
345
|
+
decidedBy: 'heuristic',
|
|
346
|
+
riskLevel: 'low',
|
|
347
|
+
};
|
|
348
|
+
try { dbModule.addApprovalDecision(decision); } catch (e) { console.error('[approval-agent] DB error:', e.message); }
|
|
349
|
+
setTimeout(() => {
|
|
350
|
+
session.ptyProcess.write('1');
|
|
351
|
+
setTimeout(() => session.ptyProcess.write('\r'), 50);
|
|
352
|
+
broadcastFn(sessionId, session, {
|
|
353
|
+
type: 'approval-decision', sessionId, decision: 'approved', decidedBy: 'heuristic',
|
|
354
|
+
label: heuristic.ruleLabel || context.toolName, reasoning: heuristic.reasoning, riskLevel: 'low',
|
|
355
|
+
});
|
|
356
|
+
}, 400);
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
if (heuristic.riskLevel === 'high') {
|
|
360
|
+
// Heuristic says it's dangerous — escalate immediately
|
|
361
|
+
const decision = {
|
|
362
|
+
sessionId,
|
|
363
|
+
toolName: context.toolName,
|
|
364
|
+
commandSummary: heuristic.ruleLabel || context.toolName,
|
|
365
|
+
fullContext: context.fullContext.slice(0, 2000),
|
|
366
|
+
warning: context.warning,
|
|
367
|
+
decision: 'escalated',
|
|
368
|
+
reasoning: heuristic.reasoning,
|
|
369
|
+
decidedBy: 'heuristic',
|
|
370
|
+
riskLevel: 'high',
|
|
371
|
+
};
|
|
372
|
+
try { dbModule.addApprovalDecision(decision); } catch (e) { console.error('[approval-agent] DB error:', e.message); }
|
|
373
|
+
broadcastFn(sessionId, session, {
|
|
374
|
+
type: 'approval-decision', sessionId, decision: 'escalated', decidedBy: 'heuristic',
|
|
375
|
+
label: context.toolName, reasoning: heuristic.reasoning, riskLevel: 'high',
|
|
376
|
+
});
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Medium risk — call AI for review
|
|
381
|
+
let learnedRules;
|
|
382
|
+
try { learnedRules = dbModule.listApprovalRules(); } catch { learnedRules = []; }
|
|
383
|
+
|
|
384
|
+
const review = await reviewWithAI(context, learnedRules);
|
|
385
|
+
|
|
386
|
+
const decision = {
|
|
387
|
+
sessionId,
|
|
388
|
+
toolName: context.toolName,
|
|
389
|
+
commandSummary: review.ruleLabel || context.toolName,
|
|
390
|
+
fullContext: context.fullContext.slice(0, 2000),
|
|
391
|
+
warning: context.warning,
|
|
392
|
+
decision: review.decision === 'approve' ? 'approved' : 'escalated',
|
|
393
|
+
reasoning: review.reasoning,
|
|
394
|
+
decidedBy: 'ai',
|
|
395
|
+
riskLevel: review.riskLevel,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// Record decision
|
|
399
|
+
let decisionId;
|
|
400
|
+
try { decisionId = dbModule.addApprovalDecision(decision); } catch (e) { console.error('[approval-agent] DB error:', e.message); }
|
|
401
|
+
|
|
402
|
+
if (review.decision === 'approve') {
|
|
403
|
+
// Auto-approve and learn a new rule
|
|
404
|
+
if (review.rulePattern && review.ruleLabel) {
|
|
405
|
+
try {
|
|
406
|
+
dbModule.upsertApprovalRule({
|
|
407
|
+
pattern: review.rulePattern,
|
|
408
|
+
label: review.ruleLabel,
|
|
409
|
+
description: review.ruleDescription || '',
|
|
410
|
+
category: context.toolName.toLowerCase().replace(/\s+/g, '-'),
|
|
411
|
+
riskLevel: review.riskLevel || 'low',
|
|
412
|
+
enabled: true,
|
|
413
|
+
});
|
|
414
|
+
} catch (e) { console.error('[approval-agent] Rule save error:', e.message); }
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
setTimeout(() => {
|
|
418
|
+
session.ptyProcess.write('1');
|
|
419
|
+
setTimeout(() => session.ptyProcess.write('\r'), 50);
|
|
420
|
+
broadcastFn(sessionId, session, {
|
|
421
|
+
type: 'approval-decision',
|
|
422
|
+
sessionId,
|
|
423
|
+
decision: 'approved',
|
|
424
|
+
decidedBy: 'ai',
|
|
425
|
+
label: review.ruleLabel || context.toolName,
|
|
426
|
+
reasoning: review.reasoning,
|
|
427
|
+
riskLevel: review.riskLevel,
|
|
428
|
+
});
|
|
429
|
+
}, 400);
|
|
430
|
+
} else {
|
|
431
|
+
// Escalate to user
|
|
432
|
+
broadcastFn(sessionId, session, {
|
|
433
|
+
type: 'approval-decision',
|
|
434
|
+
sessionId,
|
|
435
|
+
decision: 'escalated',
|
|
436
|
+
decidedBy: 'ai',
|
|
437
|
+
decisionId,
|
|
438
|
+
label: review.ruleLabel || context.toolName,
|
|
439
|
+
reasoning: review.reasoning,
|
|
440
|
+
riskLevel: review.riskLevel,
|
|
441
|
+
command: context.command.slice(0, 500),
|
|
442
|
+
warning: context.warning,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
module.exports = {
|
|
450
|
+
parseApprovalContext,
|
|
451
|
+
findMatchingRule,
|
|
452
|
+
reviewWithAI,
|
|
453
|
+
handleApprovalCheck,
|
|
454
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Graceful CTM restart — spawns a watcher process, then exits cleanly.
|
|
3
|
+
# Use this instead of killing the process directly.
|
|
4
|
+
curl -s -X POST "http://localhost:3456/api/restart/ctm" | cat
|
|
5
|
+
echo ""
|
|
6
|
+
echo "CTM server restarting... waiting for it to come back."
|
|
7
|
+
sleep 2
|
|
8
|
+
for i in $(seq 1 15); do
|
|
9
|
+
if curl -sf "http://localhost:3456/api/services/status" > /dev/null 2>&1; then
|
|
10
|
+
echo "CTM server is back up."
|
|
11
|
+
exit 0
|
|
12
|
+
fi
|
|
13
|
+
sleep 1
|
|
14
|
+
done
|
|
15
|
+
echo "CTM server did not come back within 15 seconds."
|
|
16
|
+
exit 1
|