reflexive 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "reflexive",
3
+ "version": "0.1.0",
4
+ "description": "AI-powered introspection for Node.js apps. Monitor external processes or instrument your own with Claude Agent SDK.",
5
+ "type": "module",
6
+ "main": "./src/reflexive.js",
7
+ "bin": {
8
+ "reflexive": "./src/reflexive.js"
9
+ },
10
+ "exports": {
11
+ ".": "./src/reflexive.js",
12
+ "./inject": "./src/inject.cjs"
13
+ },
14
+ "scripts": {
15
+ "demo": "node demo-instrumented.js",
16
+ "demo:app": "node src/reflexive.js demo-app.js",
17
+ "demo:inject": "node src/reflexive.js --inject demo-inject.js",
18
+ "demo:eval": "node src/reflexive.js --eval demo-inject.js",
19
+ "demo:debug": "node src/reflexive.js --debug demo-app.js",
20
+ "demo:ai": "node demo-ai-features.js",
21
+ "demo:ai:inject": "node src/reflexive.js --inject demo-ai-features.js"
22
+ },
23
+ "keywords": [
24
+ "ai",
25
+ "claude",
26
+ "anthropic",
27
+ "introspection",
28
+ "debugging",
29
+ "agent",
30
+ "reflexive",
31
+ "llm",
32
+ "claude-code",
33
+ "claude-agent-sdk",
34
+ "cli"
35
+ ],
36
+ "author": "",
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "@anthropic-ai/claude-agent-sdk": "^0.1.0",
40
+ "express": "^5.2.1",
41
+ "ws": "^8.19.0",
42
+ "zod": "^3.24.1"
43
+ },
44
+ "engines": {
45
+ "node": ">=18.0.0"
46
+ }
47
+ }
package/play-story.sh ADDED
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+ echo "šŸŽ® Launching Reflexive AI Story Game..."
3
+ echo ""
4
+ echo "šŸ“” Reflexive Dashboard will be at: http://localhost:3100"
5
+ echo "šŸŽØ Story AI Dashboard at: http://localhost:3098/reflexive"
6
+ echo ""
7
+ echo "Press Ctrl+C to exit"
8
+ echo ""
9
+
10
+ npx reflexive -i story-game-reflexive.js
@@ -0,0 +1,3 @@
1
+ // Created by Reflexive
2
+
3
+ console.log("Hello from Reflexive!");
package/src/inject.cjs ADDED
@@ -0,0 +1,474 @@
1
+ /**
2
+ * Reflexive Injection Module
3
+ *
4
+ * This module is injected into child processes via --require
5
+ * It provides deep instrumentation without the app needing to import reflexive.
6
+ *
7
+ * Usage: node --require reflexive/inject ./app.js
8
+ * Or via CLI: reflexive --inject ./app.js
9
+ */
10
+
11
+ // Only run if we're in a child process spawned by reflexive
12
+ if (!process.send || !process.env.REFLEXIVE_INJECT) {
13
+ // Not running under reflexive, silently no-op
14
+ module.exports = {};
15
+ return;
16
+ }
17
+
18
+ const originalConsole = {
19
+ log: console.log.bind(console),
20
+ info: console.info.bind(console),
21
+ warn: console.warn.bind(console),
22
+ error: console.error.bind(console),
23
+ debug: console.debug.bind(console)
24
+ };
25
+
26
+ // Send message to parent reflexive process
27
+ function sendToParent(type, data) {
28
+ try {
29
+ process.send({ reflexive: true, type, data, timestamp: Date.now() });
30
+ } catch (e) {
31
+ // Parent may have disconnected
32
+ }
33
+ }
34
+
35
+ // Intercept console methods
36
+ function interceptConsole() {
37
+ console.log = (...args) => {
38
+ sendToParent('log', { level: 'info', message: args.map(String).join(' ') });
39
+ originalConsole.log(...args);
40
+ };
41
+
42
+ console.info = (...args) => {
43
+ sendToParent('log', { level: 'info', message: args.map(String).join(' ') });
44
+ originalConsole.info(...args);
45
+ };
46
+
47
+ console.warn = (...args) => {
48
+ sendToParent('log', { level: 'warn', message: args.map(String).join(' ') });
49
+ originalConsole.warn(...args);
50
+ };
51
+
52
+ console.error = (...args) => {
53
+ sendToParent('log', { level: 'error', message: args.map(String).join(' ') });
54
+ originalConsole.error(...args);
55
+ };
56
+
57
+ console.debug = (...args) => {
58
+ sendToParent('log', { level: 'debug', message: args.map(String).join(' ') });
59
+ originalConsole.debug(...args);
60
+ };
61
+ }
62
+
63
+ // Capture uncaught exceptions and rejections
64
+ function interceptErrors() {
65
+ process.on('uncaughtException', (err) => {
66
+ sendToParent('error', {
67
+ type: 'uncaughtException',
68
+ message: err.message,
69
+ stack: err.stack,
70
+ name: err.name
71
+ });
72
+ // Print error and exit gracefully instead of re-throwing
73
+ // (re-throwing adds inject.cjs to the stack trace which is confusing)
74
+ originalConsole.error('\n' + err.stack);
75
+ process.exit(1);
76
+ });
77
+
78
+ process.on('unhandledRejection', (reason, promise) => {
79
+ sendToParent('error', {
80
+ type: 'unhandledRejection',
81
+ message: reason?.message || String(reason),
82
+ stack: reason?.stack,
83
+ name: reason?.name
84
+ });
85
+ });
86
+ }
87
+
88
+ // Set up diagnostics_channel subscriptions if available
89
+ function setupDiagnostics() {
90
+ let dc;
91
+ try {
92
+ dc = require('diagnostics_channel');
93
+ } catch (e) {
94
+ return; // Not available in this Node version
95
+ }
96
+
97
+ // HTTP client requests
98
+ const httpClientStart = dc.channel('http.client.request.start');
99
+ if (httpClientStart.hasSubscribers !== false) {
100
+ httpClientStart.subscribe((message) => {
101
+ sendToParent('diagnostic', {
102
+ channel: 'http.client.request.start',
103
+ request: {
104
+ method: message.request?.method,
105
+ host: message.request?.host,
106
+ path: message.request?.path
107
+ }
108
+ });
109
+ });
110
+ }
111
+
112
+ // HTTP server requests
113
+ const httpServerStart = dc.channel('http.server.request.start');
114
+ if (httpServerStart.hasSubscribers !== false) {
115
+ httpServerStart.subscribe((message) => {
116
+ sendToParent('diagnostic', {
117
+ channel: 'http.server.request.start',
118
+ request: {
119
+ method: message.request?.method,
120
+ url: message.request?.url
121
+ }
122
+ });
123
+ });
124
+ }
125
+ }
126
+
127
+ // Set up perf_hooks for GC and event loop stats
128
+ function setupPerfHooks() {
129
+ let perf;
130
+ try {
131
+ perf = require('perf_hooks');
132
+ } catch (e) {
133
+ return;
134
+ }
135
+
136
+ // GC stats
137
+ const obs = new perf.PerformanceObserver((list) => {
138
+ for (const entry of list.getEntries()) {
139
+ if (entry.entryType === 'gc') {
140
+ sendToParent('perf', {
141
+ type: 'gc',
142
+ kind: entry.detail?.kind,
143
+ duration: entry.duration,
144
+ flags: entry.detail?.flags
145
+ });
146
+ }
147
+ }
148
+ });
149
+
150
+ try {
151
+ obs.observe({ entryTypes: ['gc'] });
152
+ } catch (e) {
153
+ // GC observation might not be available
154
+ }
155
+
156
+ // Event loop utilization (periodic)
157
+ if (perf.monitorEventLoopDelay) {
158
+ const h = perf.monitorEventLoopDelay({ resolution: 20 });
159
+ h.enable();
160
+
161
+ setInterval(() => {
162
+ sendToParent('perf', {
163
+ type: 'eventLoop',
164
+ min: h.min,
165
+ max: h.max,
166
+ mean: h.mean,
167
+ stddev: h.stddev,
168
+ p50: h.percentile(50),
169
+ p99: h.percentile(99)
170
+ });
171
+ h.reset();
172
+ }, 10000).unref(); // Don't keep process alive
173
+ }
174
+ }
175
+
176
+ // Breakpoint management
177
+ const breakpoints = new Map();
178
+ let breakpointIdCounter = 0;
179
+ let activeBreakpoint = null;
180
+ let breakpointResolve = null;
181
+
182
+ // Create process.reflexive API
183
+ function createReflexiveAPI() {
184
+ const state = {};
185
+
186
+ process.reflexive = {
187
+ // Set custom state that the agent can query
188
+ setState(key, value) {
189
+ state[key] = value;
190
+ sendToParent('state', { key, value });
191
+ },
192
+
193
+ // Get current state
194
+ getState(key) {
195
+ return key ? state[key] : { ...state };
196
+ },
197
+
198
+ // Log with custom level
199
+ log(level, message, meta = {}) {
200
+ sendToParent('log', { level, message, meta });
201
+ },
202
+
203
+ // Emit custom event
204
+ emit(event, data) {
205
+ sendToParent('event', { event, data });
206
+ },
207
+
208
+ // Set a breakpoint that pauses execution until resumed by the agent
209
+ async breakpoint(label = 'breakpoint', context = {}) {
210
+ const id = ++breakpointIdCounter;
211
+ const stack = new Error().stack.split('\n').slice(2).join('\n');
212
+
213
+ activeBreakpoint = { id, label, context, stack, timestamp: Date.now() };
214
+
215
+ sendToParent('breakpoint', {
216
+ action: 'hit',
217
+ id,
218
+ label,
219
+ context: serializeResult(context),
220
+ stack,
221
+ state: process.reflexive.getState()
222
+ });
223
+
224
+ originalConsole.log(`\nšŸ”“ BREAKPOINT [${label}] - Execution paused. Waiting for agent to resume...\n`);
225
+
226
+ // Wait for resume signal from parent
227
+ return new Promise((resolve) => {
228
+ breakpointResolve = resolve;
229
+ });
230
+ },
231
+
232
+ // List all breakpoints (for programmatic use)
233
+ getBreakpoints() {
234
+ return Array.from(breakpoints.values());
235
+ },
236
+
237
+ // Mark a span for tracing
238
+ span(name, fn) {
239
+ const start = Date.now();
240
+ sendToParent('span', { name, phase: 'start', timestamp: start });
241
+
242
+ const finish = (error) => {
243
+ const end = Date.now();
244
+ sendToParent('span', {
245
+ name,
246
+ phase: 'end',
247
+ timestamp: end,
248
+ duration: end - start,
249
+ error: error?.message
250
+ });
251
+ };
252
+
253
+ if (fn.constructor.name === 'AsyncFunction') {
254
+ return fn().then(
255
+ (result) => { finish(); return result; },
256
+ (error) => { finish(error); throw error; }
257
+ );
258
+ } else {
259
+ try {
260
+ const result = fn();
261
+ finish();
262
+ return result;
263
+ } catch (error) {
264
+ finish(error);
265
+ throw error;
266
+ }
267
+ }
268
+ }
269
+ };
270
+ }
271
+
272
+ // Handle messages from parent
273
+ function setupParentMessageHandler() {
274
+ process.on('message', (msg) => {
275
+ if (!msg || !msg.reflexive) return;
276
+
277
+ switch (msg.type) {
278
+ case 'getState':
279
+ sendToParent('stateResponse', { state: process.reflexive.getState() });
280
+ break;
281
+
282
+ case 'eval':
283
+ // Execute code in the app context
284
+ // DANGEROUS: Only enabled with explicit --eval flag
285
+ if (!process.env.REFLEXIVE_EVAL) {
286
+ sendToParent('evalResponse', {
287
+ id: msg.id,
288
+ error: 'Eval not enabled. Run with --eval flag.',
289
+ success: false
290
+ });
291
+ return;
292
+ }
293
+
294
+ try {
295
+ // Use indirect eval to run in global scope
296
+ const evalInGlobal = eval;
297
+ const result = evalInGlobal(msg.code);
298
+
299
+ // Handle promises
300
+ if (result && typeof result.then === 'function') {
301
+ result
302
+ .then((resolved) => {
303
+ sendToParent('evalResponse', {
304
+ id: msg.id,
305
+ result: serializeResult(resolved),
306
+ success: true
307
+ });
308
+ })
309
+ .catch((err) => {
310
+ sendToParent('evalResponse', {
311
+ id: msg.id,
312
+ error: err.message,
313
+ stack: err.stack,
314
+ success: false
315
+ });
316
+ });
317
+ } else {
318
+ sendToParent('evalResponse', {
319
+ id: msg.id,
320
+ result: serializeResult(result),
321
+ success: true
322
+ });
323
+ }
324
+ } catch (err) {
325
+ sendToParent('evalResponse', {
326
+ id: msg.id,
327
+ error: err.message,
328
+ stack: err.stack,
329
+ success: false
330
+ });
331
+ }
332
+ break;
333
+
334
+ case 'getGlobals':
335
+ // List available global variables
336
+ const globals = Object.keys(global).filter(k => !k.startsWith('_'));
337
+ sendToParent('globalsResponse', { globals });
338
+ break;
339
+
340
+ case 'resumeBreakpoint':
341
+ // Resume from a breakpoint
342
+ if (activeBreakpoint && breakpointResolve) {
343
+ const bp = activeBreakpoint;
344
+ originalConsole.log(`\n🟢 RESUMED [${bp.label}] - Continuing execution...\n`);
345
+ sendToParent('breakpoint', {
346
+ action: 'resumed',
347
+ id: bp.id,
348
+ label: bp.label,
349
+ pauseDuration: Date.now() - bp.timestamp
350
+ });
351
+ activeBreakpoint = null;
352
+ breakpointResolve(msg.returnValue);
353
+ breakpointResolve = null;
354
+ } else {
355
+ sendToParent('breakpointError', { error: 'No active breakpoint to resume' });
356
+ }
357
+ break;
358
+
359
+ case 'getActiveBreakpoint':
360
+ // Get info about current breakpoint
361
+ if (activeBreakpoint) {
362
+ sendToParent('activeBreakpointResponse', {
363
+ active: true,
364
+ breakpoint: {
365
+ id: activeBreakpoint.id,
366
+ label: activeBreakpoint.label,
367
+ context: serializeResult(activeBreakpoint.context),
368
+ stack: activeBreakpoint.stack,
369
+ pausedFor: Date.now() - activeBreakpoint.timestamp
370
+ }
371
+ });
372
+ } else {
373
+ sendToParent('activeBreakpointResponse', { active: false });
374
+ }
375
+ break;
376
+
377
+ case 'triggerBreakpoint':
378
+ // Remotely triggered breakpoint from dashboard
379
+ if (!activeBreakpoint) {
380
+ // Trigger breakpoint asynchronously so it doesn't block the message handler
381
+ setImmediate(async () => {
382
+ await process.reflexive.breakpoint(msg.label || 'remote', {
383
+ triggeredRemotely: true,
384
+ timestamp: new Date().toISOString()
385
+ });
386
+ });
387
+ } else {
388
+ sendToParent('breakpointError', { error: 'Already at a breakpoint' });
389
+ }
390
+ break;
391
+ }
392
+ });
393
+ }
394
+
395
+ // Serialize result for IPC (handle circular refs, functions, etc.)
396
+ function serializeResult(value, depth = 0) {
397
+ if (depth > 3) return '[Max depth reached]';
398
+
399
+ if (value === undefined) return 'undefined';
400
+ if (value === null) return null;
401
+ if (typeof value === 'function') return `[Function: ${value.name || 'anonymous'}]`;
402
+ if (typeof value === 'symbol') return value.toString();
403
+ if (typeof value === 'bigint') return value.toString() + 'n';
404
+
405
+ if (value instanceof Error) {
406
+ return { __type: 'Error', name: value.name, message: value.message, stack: value.stack };
407
+ }
408
+
409
+ if (value instanceof Map) {
410
+ return { __type: 'Map', entries: Array.from(value.entries()).slice(0, 20) };
411
+ }
412
+
413
+ if (value instanceof Set) {
414
+ return { __type: 'Set', values: Array.from(value.values()).slice(0, 20) };
415
+ }
416
+
417
+ if (Buffer.isBuffer(value)) {
418
+ return { __type: 'Buffer', length: value.length, preview: value.slice(0, 50).toString('hex') };
419
+ }
420
+
421
+ if (Array.isArray(value)) {
422
+ if (value.length > 100) {
423
+ return { __type: 'Array', length: value.length, preview: value.slice(0, 20).map(v => serializeResult(v, depth + 1)) };
424
+ }
425
+ return value.map(v => serializeResult(v, depth + 1));
426
+ }
427
+
428
+ if (typeof value === 'object') {
429
+ try {
430
+ const keys = Object.keys(value);
431
+ if (keys.length > 50) {
432
+ const preview = {};
433
+ keys.slice(0, 20).forEach(k => { preview[k] = serializeResult(value[k], depth + 1); });
434
+ return { __type: 'Object', keyCount: keys.length, preview };
435
+ }
436
+ const result = {};
437
+ keys.forEach(k => { result[k] = serializeResult(value[k], depth + 1); });
438
+ return result;
439
+ } catch (e) {
440
+ return `[Object: ${value.constructor?.name || 'unknown'}]`;
441
+ }
442
+ }
443
+
444
+ return value;
445
+ }
446
+
447
+ // Initialize everything
448
+ function init() {
449
+ createReflexiveAPI();
450
+ interceptConsole();
451
+ interceptErrors();
452
+ setupDiagnostics();
453
+ setupPerfHooks();
454
+ setupParentMessageHandler();
455
+
456
+ // Notify parent that injection is complete
457
+ sendToParent('ready', {
458
+ pid: process.pid,
459
+ nodeVersion: process.version,
460
+ platform: process.platform
461
+ });
462
+ }
463
+
464
+ init();
465
+
466
+ module.exports = {
467
+ // Export for programmatic use if someone imports this directly
468
+ setState: (key, value) => process.reflexive?.setState(key, value),
469
+ getState: (key) => process.reflexive?.getState(key),
470
+ log: (level, message, meta) => process.reflexive?.log(level, message, meta),
471
+ emit: (event, data) => process.reflexive?.emit(event, data),
472
+ span: (name, fn) => process.reflexive?.span(name, fn),
473
+ breakpoint: (label, context) => process.reflexive?.breakpoint(label, context)
474
+ };