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.
Files changed (136) hide show
  1. package/bin/create-walle.js +134 -0
  2. package/package.json +18 -0
  3. package/template/.env.example +40 -0
  4. package/template/CLAUDE.md +12 -0
  5. package/template/LICENSE +21 -0
  6. package/template/README.md +167 -0
  7. package/template/bin/setup.js +100 -0
  8. package/template/claude-code-skill.md +60 -0
  9. package/template/claude-task-manager/api-prompts.js +1841 -0
  10. package/template/claude-task-manager/api-reviews.js +275 -0
  11. package/template/claude-task-manager/approval-agent.js +454 -0
  12. package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
  13. package/template/claude-task-manager/db.js +1721 -0
  14. package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
  15. package/template/claude-task-manager/git-utils.js +214 -0
  16. package/template/claude-task-manager/package-lock.json +1607 -0
  17. package/template/claude-task-manager/package.json +31 -0
  18. package/template/claude-task-manager/prompt-harvest.js +1148 -0
  19. package/template/claude-task-manager/public/css/prompts.css +880 -0
  20. package/template/claude-task-manager/public/css/reviews.css +430 -0
  21. package/template/claude-task-manager/public/css/walle.css +732 -0
  22. package/template/claude-task-manager/public/favicon.ico +0 -0
  23. package/template/claude-task-manager/public/icon.svg +37 -0
  24. package/template/claude-task-manager/public/index.html +8346 -0
  25. package/template/claude-task-manager/public/js/prompts.js +3159 -0
  26. package/template/claude-task-manager/public/js/reviews.js +1292 -0
  27. package/template/claude-task-manager/public/js/walle.js +3081 -0
  28. package/template/claude-task-manager/public/manifest.json +13 -0
  29. package/template/claude-task-manager/public/prompts.html +4353 -0
  30. package/template/claude-task-manager/public/setup.html +216 -0
  31. package/template/claude-task-manager/queue-engine.js +404 -0
  32. package/template/claude-task-manager/server-state.js +5 -0
  33. package/template/claude-task-manager/server.js +2254 -0
  34. package/template/claude-task-manager/session-utils.js +124 -0
  35. package/template/claude-task-manager/start.sh +17 -0
  36. package/template/claude-task-manager/tests/test-ai-search.js +61 -0
  37. package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
  38. package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
  39. package/template/claude-task-manager/tests/test-features-v2.js +127 -0
  40. package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
  41. package/template/claude-task-manager/tests/test-insights.js +124 -0
  42. package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
  43. package/template/claude-task-manager/tests/test-permissions.js +122 -0
  44. package/template/claude-task-manager/tests/test-pin.js +51 -0
  45. package/template/claude-task-manager/tests/test-prompts.js +164 -0
  46. package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
  47. package/template/claude-task-manager/tests/test-review.js +104 -0
  48. package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
  49. package/template/claude-task-manager/tests/test-send-final.js +30 -0
  50. package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
  51. package/template/claude-task-manager/tests/test-send-integration.js +107 -0
  52. package/template/claude-task-manager/tests/test-send-visual.js +34 -0
  53. package/template/claude-task-manager/tests/test-session-create.js +147 -0
  54. package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
  55. package/template/claude-task-manager/tests/test-url-hash.js +68 -0
  56. package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
  57. package/template/claude-task-manager/tests/test-ux-review.js +130 -0
  58. package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
  59. package/template/claude-task-manager/tests/test-zoom.js +92 -0
  60. package/template/claude-task-manager/tests/test-zoom2.js +67 -0
  61. package/template/docs/site/api/README.md +187 -0
  62. package/template/docs/site/guides/claude-code.md +58 -0
  63. package/template/docs/site/guides/configuration.md +96 -0
  64. package/template/docs/site/guides/quickstart.md +158 -0
  65. package/template/docs/site/index.md +14 -0
  66. package/template/docs/site/skills/README.md +135 -0
  67. package/template/wall-e/.dockerignore +11 -0
  68. package/template/wall-e/Dockerfile +25 -0
  69. package/template/wall-e/adapters/adapter-base.js +37 -0
  70. package/template/wall-e/adapters/ctm.js +193 -0
  71. package/template/wall-e/adapters/slack.js +56 -0
  72. package/template/wall-e/agent.js +319 -0
  73. package/template/wall-e/api-walle.js +1073 -0
  74. package/template/wall-e/brain.js +1235 -0
  75. package/template/wall-e/channels/agent-api.js +172 -0
  76. package/template/wall-e/channels/channel-base.js +14 -0
  77. package/template/wall-e/channels/imessage-channel.js +113 -0
  78. package/template/wall-e/channels/slack-channel.js +118 -0
  79. package/template/wall-e/chat.js +778 -0
  80. package/template/wall-e/decision/confidence.js +93 -0
  81. package/template/wall-e/deploy.sh +35 -0
  82. package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
  83. package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
  84. package/template/wall-e/extraction/contradiction.js +168 -0
  85. package/template/wall-e/extraction/knowledge-extractor.js +190 -0
  86. package/template/wall-e/fly.toml +24 -0
  87. package/template/wall-e/loops/ingest.js +34 -0
  88. package/template/wall-e/loops/reflect.js +63 -0
  89. package/template/wall-e/loops/tasks.js +487 -0
  90. package/template/wall-e/loops/think.js +125 -0
  91. package/template/wall-e/package-lock.json +533 -0
  92. package/template/wall-e/package.json +18 -0
  93. package/template/wall-e/scripts/ingest-slack-search.js +85 -0
  94. package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
  95. package/template/wall-e/scripts/slack-backfill.js +295 -0
  96. package/template/wall-e/scripts/slack-channel-history.js +454 -0
  97. package/template/wall-e/server.js +93 -0
  98. package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
  99. package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
  100. package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
  101. package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
  102. package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
  103. package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
  104. package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
  105. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
  106. package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
  107. package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
  108. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
  109. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
  110. package/template/wall-e/skills/claude-code-reader.js +144 -0
  111. package/template/wall-e/skills/mcp-client.js +407 -0
  112. package/template/wall-e/skills/skill-executor.js +163 -0
  113. package/template/wall-e/skills/skill-loader.js +410 -0
  114. package/template/wall-e/skills/skill-planner.js +88 -0
  115. package/template/wall-e/skills/slack-ingest.js +329 -0
  116. package/template/wall-e/skills/slack-pull-live.js +270 -0
  117. package/template/wall-e/skills/tool-executor.js +188 -0
  118. package/template/wall-e/tests/adapter-base.test.js +20 -0
  119. package/template/wall-e/tests/adapter-ctm.test.js +122 -0
  120. package/template/wall-e/tests/adapter-slack.test.js +98 -0
  121. package/template/wall-e/tests/agent-api.test.js +256 -0
  122. package/template/wall-e/tests/api-walle.test.js +222 -0
  123. package/template/wall-e/tests/brain.test.js +602 -0
  124. package/template/wall-e/tests/channels.test.js +104 -0
  125. package/template/wall-e/tests/chat.test.js +103 -0
  126. package/template/wall-e/tests/confidence.test.js +134 -0
  127. package/template/wall-e/tests/contradiction.test.js +217 -0
  128. package/template/wall-e/tests/ingest.test.js +113 -0
  129. package/template/wall-e/tests/mcp-client.test.js +71 -0
  130. package/template/wall-e/tests/reflect.test.js +103 -0
  131. package/template/wall-e/tests/server.test.js +111 -0
  132. package/template/wall-e/tests/skills.test.js +198 -0
  133. package/template/wall-e/tests/slack-ingest.test.js +103 -0
  134. package/template/wall-e/tests/think.test.js +435 -0
  135. package/template/wall-e/tools/local-tools.js +697 -0
  136. 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