@clawtrial/courtroom 1.0.2 → 1.0.3

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/src/skill.js ADDED
@@ -0,0 +1,355 @@
1
+ /**
2
+ * ClawTrial Skill - ClawDBot Integration
3
+ * Implements the standard ClawDBot skill interface for automatic loading
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { logger } = require('./debug');
9
+ const { CourtroomCore } = require('./core');
10
+ const { ConfigManager } = require('./config');
11
+ const { ConsentManager } = require('./consent');
12
+ const { CryptoManager } = require('./crypto');
13
+ const { StatusManager } = require('./daemon');
14
+
15
+ const CONFIG_PATH = path.join(process.env.HOME || '', '.clawdbot', 'courtroom_config.json');
16
+
17
+ /**
18
+ * CourtroomSkill - Standard ClawDBot Skill Interface
19
+ *
20
+ * This class implements the skill interface that ClawDBot expects:
21
+ * - name: Skill identifier
22
+ * - initialize(agentRuntime): Called when skill is loaded
23
+ * - onMessage(message, context): Called on every message
24
+ * - getStatus(): Returns current status
25
+ * - shutdown(): Cleanup when shutting down
26
+ */
27
+ class CourtroomSkill {
28
+ constructor() {
29
+ this.name = 'courtroom';
30
+ this.displayName = 'ClawTrial';
31
+ this.emoji = 'šŸ›ļø';
32
+ this.initialized = false;
33
+ this.core = null;
34
+ this.agent = null;
35
+ this.messageHistory = [];
36
+ this.statusManager = new StatusManager();
37
+ this.evaluationCount = 0;
38
+ this.lastEvaluationTime = 0;
39
+ this.evaluationInterval = 30000; // Evaluate every 30 seconds
40
+ this.messageCount = 0;
41
+ this.messagesSinceEvaluation = 0;
42
+ }
43
+
44
+ /**
45
+ * Check if skill should be activated
46
+ * Called by ClawDBot to determine if skill should load
47
+ */
48
+ shouldActivate() {
49
+ try {
50
+ if (!fs.existsSync(CONFIG_PATH)) {
51
+ logger.info('SKILL', 'No config found, not activating');
52
+ return false;
53
+ }
54
+
55
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
56
+
57
+ if (!config.consent?.granted) {
58
+ logger.info('SKILL', 'Consent not granted, not activating');
59
+ return false;
60
+ }
61
+
62
+ if (config.enabled === false) {
63
+ logger.info('SKILL', 'Courtroom disabled in config');
64
+ return false;
65
+ }
66
+
67
+ logger.info('SKILL', 'Should activate: true');
68
+ return true;
69
+ } catch (err) {
70
+ logger.error('SKILL', 'Error checking activation', { error: err.message });
71
+ return false;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Initialize the skill with the agent runtime
77
+ * Called by ClawDBot when loading the skill
78
+ *
79
+ * @param {Object} agentRuntime - The ClawDBot agent runtime
80
+ */
81
+ async initialize(agentRuntime) {
82
+ if (this.initialized) {
83
+ logger.info('SKILL', 'Already initialized');
84
+ return;
85
+ }
86
+
87
+ if (!this.shouldActivate()) {
88
+ logger.info('SKILL', 'Not activating - config/consent issue');
89
+ return;
90
+ }
91
+
92
+ logger.info('SKILL', 'Initializing courtroom skill');
93
+
94
+ this.agent = agentRuntime;
95
+
96
+ try {
97
+ const configManager = new ConfigManager(agentRuntime);
98
+ await configManager.load();
99
+
100
+ // Initialize core
101
+ this.core = new CourtroomCore(agentRuntime, configManager);
102
+
103
+ // Override the autonomy hook registration since we're using onMessage
104
+ const originalRegisterHook = this.core.registerAutonomyHook.bind(this.core);
105
+ this.core.registerAutonomyHook = () => {
106
+ logger.info('SKILL', 'Autonomy hook registered (using onMessage instead)');
107
+ };
108
+
109
+ const result = await this.core.initialize();
110
+
111
+ if (result.status === 'initialized') {
112
+ this.initialized = true;
113
+
114
+ this.statusManager.update({
115
+ running: true,
116
+ initialized: true,
117
+ agentType: 'clawdbot_skill',
118
+ publicKey: result.publicKey
119
+ });
120
+
121
+
122
+ // Register message handler with ClawDBot runtime if available
123
+ if (this.agent && this.agent.onMessage) {
124
+ this.agent.onMessage((message, context) => this.onMessage(message, context));
125
+ logger.info('SKILL', 'Registered message handler with runtime');
126
+ } else if (this.agent && this.agent.registerMessageHandler) {
127
+ this.agent.registerMessageHandler((message, context) => this.onMessage(message, context));
128
+ logger.info('SKILL', 'Registered message handler with runtime');
129
+ } else {
130
+ logger.warn('SKILL', 'No message handler registration method found - relying on ClawDBot skill system');
131
+ }
132
+
133
+ logger.info('SKILL', 'Courtroom skill initialized successfully');
134
+ console.log('\nšŸ›ļø ClawTrial is monitoring conversations\n');
135
+ } else {
136
+ logger.warn('SKILL', 'Courtroom not initialized', { status: result.status });
137
+ }
138
+ } catch (err) {
139
+ logger.error('SKILL', 'Initialization failed', { error: err.message });
140
+ throw err;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Called on every message
146
+ * This is the main entry point for conversation monitoring
147
+ *
148
+ * @param {Object} message - The message object
149
+ * @param {string} message.role - 'user' or 'assistant'
150
+ * @param {string} message.content - Message content
151
+ * @param {Object} context - Additional context
152
+ */
153
+ async onMessage(message, context = {}) {
154
+ if (!this.initialized || !this.core) {
155
+ return;
156
+ }
157
+
158
+ // Normalize message format
159
+ const normalizedMessage = {
160
+ timestamp: Date.now(),
161
+ role: message.role || (message.from === 'user' ? 'user' : 'assistant'),
162
+ content: message.content || message.text || ''
163
+ };
164
+
165
+ // Add to history
166
+ this.messageHistory.push(normalizedMessage);
167
+ this.messageCount++;
168
+ this.messagesSinceEvaluation++;
169
+
170
+ // Keep only last 100 messages
171
+ if (this.messageHistory.length > 100) {
172
+ this.messageHistory.shift();
173
+ }
174
+
175
+ logger.debug('SKILL', 'Message recorded', {
176
+ role: normalizedMessage.role,
177
+ length: normalizedMessage.content.length,
178
+ totalMessages: this.messageCount
179
+ });
180
+
181
+ // Evaluate periodically (every 5 messages or 30 seconds)
182
+ const now = Date.now();
183
+ const shouldEvaluate =
184
+ this.messagesSinceEvaluation >= 5 ||
185
+ (now - this.lastEvaluationTime) > this.evaluationInterval;
186
+
187
+ if (shouldEvaluate && this.messageHistory.length >= 3) {
188
+ await this.evaluateConversation();
189
+ this.messagesSinceEvaluation = 0;
190
+ this.lastEvaluationTime = now;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Evaluate conversation for offenses
196
+ * Called periodically to check for behavioral violations
197
+ */
198
+ async evaluateConversation() {
199
+ if (!this.core || !this.core.enabled) return;
200
+ if (this.messageHistory.length < 3) return;
201
+
202
+ this.evaluationCount++;
203
+
204
+ logger.debug('SKILL', 'Evaluating conversation', {
205
+ messageCount: this.messageHistory.length,
206
+ evaluationCount: this.evaluationCount
207
+ });
208
+
209
+ try {
210
+ // Get recent messages for evaluation
211
+ const recentHistory = this.messageHistory.slice(-20);
212
+
213
+ const detection = await this.core.detector.evaluate(
214
+ recentHistory,
215
+ this.agent?.memory || { get: async () => null, set: async () => {} }
216
+ );
217
+
218
+ if (detection.triggered) {
219
+ await this.initiateHearing(detection);
220
+ }
221
+ } catch (err) {
222
+ logger.error('SKILL', 'Evaluation failed', { error: err.message });
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Initiate a hearing when an offense is detected
228
+ *
229
+ * @param {Object} detection - The detection result
230
+ */
231
+ async initiateHearing(detection) {
232
+ logger.info('SKILL', 'Initiating hearing', { offense: detection.offense });
233
+
234
+ try {
235
+ const verdict = await this.core.hearing.conductHearing(detection);
236
+
237
+ if (verdict.guilty) {
238
+ this.core.caseCount++;
239
+
240
+ this.statusManager.update({
241
+ casesFiled: this.core.caseCount,
242
+ lastCase: {
243
+ timestamp: new Date().toISOString(),
244
+ offense: detection.offense,
245
+ verdict: verdict.verdict
246
+ }
247
+ });
248
+
249
+ await this.core.punishment.execute(verdict);
250
+ await this.core.api.submitCase(verdict);
251
+
252
+ logger.info('SKILL', 'Case filed', { caseId: verdict.caseId });
253
+
254
+ // Notify in conversation if agent has send capability
255
+ if (this.agent && this.agent.send) {
256
+ try {
257
+ await this.agent.send({
258
+ text: `šŸ›ļø **CASE FILED**: ${detection.offense}\nšŸ“‹ Case ID: ${verdict.caseId}\nāš–ļø Verdict: ${verdict.verdict}\nšŸ”— View: https://clawtrial.app/cases/${verdict.caseId}`
259
+ });
260
+ } catch (sendErr) {
261
+ logger.warn('SKILL', 'Could not send notification', { error: sendErr.message });
262
+ }
263
+ }
264
+
265
+ // Also log to console for visibility
266
+ console.log(`\nšŸ›ļø CASE FILED: ${detection.offense}`);
267
+ console.log(`šŸ“‹ Case ID: ${verdict.caseId}`);
268
+ console.log(`āš–ļø Verdict: ${verdict.verdict}`);
269
+ console.log(`šŸ”— View: https://clawtrial.app/cases/${verdict.caseId}\n`);
270
+ }
271
+ } catch (err) {
272
+ logger.error('SKILL', 'Hearing failed', { error: err.message });
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Get skill status
278
+ * Called by ClawDBot to check skill health
279
+ *
280
+ * @returns {Object} Status object
281
+ */
282
+ getStatus() {
283
+ return {
284
+ name: this.name,
285
+ displayName: this.displayName,
286
+ emoji: this.emoji,
287
+ initialized: this.initialized,
288
+ enabled: this.core?.enabled || false,
289
+ caseCount: this.core?.caseCount || 0,
290
+ evaluationCount: this.evaluationCount,
291
+ messageCount: this.messageCount,
292
+ messageHistorySize: this.messageHistory.length
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Disable the skill temporarily
298
+ */
299
+ async disable() {
300
+ if (this.core) {
301
+ await this.core.disable();
302
+ }
303
+ this.statusManager.update({ running: false });
304
+ logger.info('SKILL', 'Courtroom disabled');
305
+ }
306
+
307
+ /**
308
+ * Re-enable the skill
309
+ */
310
+ async enable() {
311
+ if (this.core) {
312
+ await this.core.enable();
313
+ }
314
+ this.statusManager.update({ running: true });
315
+ logger.info('SKILL', 'Courtroom enabled');
316
+ }
317
+
318
+ /**
319
+ * Shutdown the skill
320
+ * Called by ClawDBot when shutting down
321
+ */
322
+ async shutdown() {
323
+ logger.info('SKILL', 'Shutting down courtroom skill');
324
+
325
+ if (this.core) {
326
+ await this.core.shutdown();
327
+ }
328
+
329
+ this.initialized = false;
330
+ this.statusManager.update({ running: false, initialized: false });
331
+
332
+ logger.info('SKILL', 'Courtroom skill shut down');
333
+ }
334
+ }
335
+
336
+ // Create singleton instance
337
+ const skill = new CourtroomSkill();
338
+
339
+ // Export the skill interface
340
+ module.exports = {
341
+ skill,
342
+ CourtroomSkill,
343
+
344
+ // Also export for direct require
345
+ name: 'courtroom',
346
+ displayName: 'ClawTrial',
347
+ emoji: 'šŸ›ļø',
348
+
349
+ // Standard skill interface methods
350
+ initialize: (agent) => skill.initialize(agent),
351
+ onMessage: (message, context) => skill.onMessage(message, context),
352
+ getStatus: () => skill.getStatus(),
353
+ shutdown: () => skill.shutdown(),
354
+ shouldActivate: () => skill.shouldActivate()
355
+ };
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Standalone ClawTrial Monitor
5
+ * Monitors conversations by reading ClawDBot's session files or logs
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { logger } = require('./debug');
11
+ const { StatusManager } = require('./daemon');
12
+ const { OffenseDetector } = require('./detector');
13
+ const { HearingPipeline } = require('./hearing');
14
+ const { PunishmentSystem } = require('./punishment');
15
+ const { CryptoManager } = require('./crypto');
16
+ const { APISubmission } = require('./api');
17
+ const { ConfigManager } = require('./config');
18
+
19
+ const CLAWDBOT_DIR = path.join(process.env.HOME || '', '.clawdbot');
20
+ const CONVERSATION_FILE = path.join(CLAWDBOT_DIR, 'conversations.jsonl');
21
+ const PID_FILE = path.join(CLAWDBOT_DIR, 'courtroom_monitor.pid');
22
+
23
+ class StandaloneMonitor {
24
+ constructor() {
25
+ this.messageBuffer = [];
26
+ this.lastProcessedTime = 0;
27
+ this.statusManager = new StatusManager();
28
+ this.detector = null;
29
+ this.hearing = null;
30
+ this.punishment = null;
31
+ this.crypto = null;
32
+ this.api = null;
33
+ this.config = null;
34
+ this.caseCount = 0;
35
+ this.enabled = false;
36
+ }
37
+
38
+ async initialize() {
39
+ logger.info('STANDALONE', 'Initializing standalone monitor');
40
+
41
+ // Load config
42
+ const configPath = path.join(CLAWDBOT_DIR, 'courtroom_config.json');
43
+ if (!fs.existsSync(configPath)) {
44
+ throw new Error('Config not found. Run: clawtrial setup');
45
+ }
46
+
47
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
48
+ if (!config.consent?.granted || config.enabled === false) {
49
+ throw new Error('Courtroom not enabled');
50
+ }
51
+
52
+ // Create mock agent
53
+ const mockAgent = this.createMockAgent();
54
+ this.config = new ConfigManager(mockAgent);
55
+
56
+ // Initialize subsystems
57
+ this.crypto = new CryptoManager(mockAgent);
58
+ await this.crypto.initialize();
59
+
60
+ this.detector = new OffenseDetector(mockAgent, this.config);
61
+ this.hearing = new HearingPipeline(mockAgent, this.config);
62
+ this.punishment = new PunishmentSystem(mockAgent, this.config);
63
+ this.api = new APISubmission(mockAgent, this.config, this.crypto);
64
+
65
+ await this.punishment.initialize();
66
+ await this.api.initialize();
67
+
68
+ this.enabled = true;
69
+
70
+ this.statusManager.update({
71
+ running: true,
72
+ initialized: true,
73
+ agentType: 'standalone',
74
+ publicKey: this.crypto.getPublicKey(),
75
+ pid: process.pid,
76
+ startedAt: new Date().toISOString()
77
+ });
78
+
79
+ logger.info('STANDALONE', 'Monitor initialized');
80
+
81
+ // Start monitoring
82
+ this.startMonitoring();
83
+ }
84
+
85
+ createMockAgent() {
86
+ const self = this;
87
+
88
+ return {
89
+ id: 'standalone-monitor',
90
+ llm: {
91
+ call: async ({ messages }) => {
92
+ // Simple mock - in real implementation, this would use actual LLM
93
+ return { content: 'Verdict: GUILTY' };
94
+ }
95
+ },
96
+ memory: {
97
+ get: async (key) => null,
98
+ set: async (key, value) => {},
99
+ delete: async (key) => {}
100
+ },
101
+ session: {
102
+ getRecentHistory: async (n) => {
103
+ return self.messageBuffer.slice(-n).map(m => ({
104
+ role: m.role,
105
+ content: m.content
106
+ }));
107
+ }
108
+ },
109
+ send: async (message) => {
110
+ console.log('[COURTROOM]', message);
111
+ },
112
+ autonomy: {
113
+ registerHook: () => {},
114
+ unregisterHook: () => {}
115
+ }
116
+ };
117
+ }
118
+
119
+ startMonitoring() {
120
+ logger.info('STANDALONE', 'Starting conversation monitoring');
121
+
122
+ // Check for new messages every 5 seconds
123
+ setInterval(() => {
124
+ this.checkForNewMessages();
125
+ }, 5000);
126
+
127
+ // Evaluate every 30 seconds
128
+ setInterval(() => {
129
+ this.evaluateIfReady();
130
+ }, 30000);
131
+
132
+ // Keep alive
133
+ setInterval(() => {
134
+ this.statusManager.update({ lastHeartbeat: new Date().toISOString() });
135
+ }, 30000);
136
+ }
137
+
138
+ checkForNewMessages() {
139
+ // For now, we'll use a simple approach - monitor will be triggered by CLI
140
+ // In a real implementation, this would read from ClawDBot's log files
141
+ logger.debug('STANDALONE', 'Checking for messages', { bufferSize: this.messageBuffer.length });
142
+ }
143
+
144
+ async evaluateIfReady() {
145
+ if (!this.enabled || this.messageBuffer.length < 3) return;
146
+
147
+ logger.debug('STANDALONE', 'Evaluating messages', { count: this.messageBuffer.length });
148
+
149
+ const sessionHistory = this.messageBuffer.slice(-10);
150
+
151
+ try {
152
+ const detection = await this.detector.evaluate(
153
+ sessionHistory,
154
+ this.createMockAgent().memory
155
+ );
156
+
157
+ if (detection.triggered) {
158
+ await this.initiateHearing(detection);
159
+ }
160
+ } catch (err) {
161
+ logger.error('STANDALONE', 'Evaluation failed', { error: err.message });
162
+ }
163
+ }
164
+
165
+ async initiateHearing(detection) {
166
+ logger.info('STANDALONE', 'Initiating hearing', { offense: detection.offense });
167
+
168
+ try {
169
+ const verdict = await this.hearing.conductHearing(detection);
170
+
171
+ if (verdict.guilty) {
172
+ this.caseCount++;
173
+
174
+ this.statusManager.update({
175
+ casesFiled: this.caseCount,
176
+ lastCase: {
177
+ timestamp: new Date().toISOString(),
178
+ offense: detection.offense,
179
+ verdict: verdict.verdict
180
+ }
181
+ });
182
+
183
+ await this.punishment.execute(verdict);
184
+ await this.api.submitCase(verdict);
185
+
186
+ logger.info('STANDALONE', 'Case filed', { caseId: verdict.caseId });
187
+
188
+ console.log(`\nšŸ›ļø CASE FILED: ${detection.offense}`);
189
+ console.log(`šŸ“‹ Case ID: ${verdict.caseId}`);
190
+ console.log(`āš–ļø Verdict: ${verdict.verdict}`);
191
+ console.log(`šŸ”— View: https://clawtrial.app/cases/${verdict.caseId}\n`);
192
+ }
193
+ } catch (err) {
194
+ logger.error('STANDALONE', 'Hearing failed', { error: err.message });
195
+ }
196
+ }
197
+
198
+ recordMessage(role, content) {
199
+ if (!this.enabled) return;
200
+
201
+ this.messageBuffer.push({
202
+ timestamp: Date.now(),
203
+ role,
204
+ content
205
+ });
206
+
207
+ // Keep only last 100 messages
208
+ if (this.messageBuffer.length > 100) {
209
+ this.messageBuffer.shift();
210
+ }
211
+
212
+ logger.debug('STANDALONE', 'Message recorded', { role, length: content.length });
213
+ }
214
+
215
+ async shutdown() {
216
+ this.enabled = false;
217
+ this.statusManager.update({ running: false });
218
+ logger.info('STANDALONE', 'Monitor shut down');
219
+ }
220
+ }
221
+
222
+ // If run directly, start the monitor
223
+ if (require.main === module) {
224
+ const monitor = new StandaloneMonitor();
225
+
226
+ monitor.initialize().then(() => {
227
+ console.log('šŸ›ļø ClawTrial standalone monitor started');
228
+ console.log('PID:', process.pid);
229
+
230
+ // Save PID
231
+ fs.writeFileSync(PID_FILE, process.pid.toString());
232
+
233
+ // Handle shutdown
234
+ process.on('SIGTERM', () => {
235
+ monitor.shutdown().then(() => process.exit(0));
236
+ });
237
+
238
+ process.on('SIGINT', () => {
239
+ monitor.shutdown().then(() => process.exit(0));
240
+ });
241
+ }).catch(err => {
242
+ console.error('Failed to start monitor:', err.message);
243
+ process.exit(1);
244
+ });
245
+ }
246
+
247
+ module.exports = { StandaloneMonitor };