trickle-observe 0.2.88 → 0.2.90

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/dist/trace-var.js CHANGED
@@ -238,4 +238,61 @@ if (typeof process !== 'undefined' && process.on) {
238
238
  process.on('beforeExit', exitFlush);
239
239
  process.on('SIGTERM', exitFlush);
240
240
  process.on('SIGINT', exitFlush);
241
+ // Capture uncaught exceptions with variable context for agent debugging.
242
+ // Write error + nearby variable values to errors.jsonl before the process exits.
243
+ process.on('uncaughtException', (err) => {
244
+ flushVarBuffer();
245
+ try {
246
+ const dir = varsFilePath
247
+ ? path.dirname(varsFilePath)
248
+ : process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
249
+ try {
250
+ fs.mkdirSync(dir, { recursive: true });
251
+ }
252
+ catch { }
253
+ const errorsFile = path.join(dir, 'errors.jsonl');
254
+ // Extract file and line from stack trace
255
+ const stackLines = (err.stack || '').split('\n');
256
+ let errorFile;
257
+ let errorLine;
258
+ for (const sl of stackLines.slice(1)) {
259
+ const m = sl.match(/\((.+):(\d+):\d+\)/) || sl.match(/at (.+):(\d+):\d+/);
260
+ if (m && !m[1].includes('node_modules') && !m[1].includes('node:') && !m[1].includes('trickle')) {
261
+ errorFile = m[1];
262
+ errorLine = parseInt(m[2]);
263
+ break;
264
+ }
265
+ }
266
+ // Find nearby variable values from the cache
267
+ const nearbyVars = {};
268
+ if (errorFile && errorLine) {
269
+ for (const [key, entry] of varCache) {
270
+ const parts = key.split(':');
271
+ const file = parts[0];
272
+ const line = parseInt(parts[1]);
273
+ const varName = parts.slice(2).join(':');
274
+ if (file === errorFile && Math.abs(line - errorLine) <= 10) {
275
+ nearbyVars[`L${parts[1]} ${varName}`] = entry.fp;
276
+ }
277
+ }
278
+ }
279
+ const record = {
280
+ kind: 'error',
281
+ error: err.message,
282
+ type: err.constructor.name,
283
+ file: errorFile,
284
+ line: errorLine,
285
+ stack: stackLines.slice(0, 6).join('\n'),
286
+ nearbyVariables: Object.keys(nearbyVars).length > 0 ? nearbyVars : undefined,
287
+ timestamp: new Date().toISOString(),
288
+ };
289
+ fs.appendFileSync(errorsFile, JSON.stringify(record) + '\n');
290
+ }
291
+ catch {
292
+ // Never suppress the original error
293
+ }
294
+ // Print the original error and exit (don't re-throw to preserve original stack)
295
+ console.error(err.stack || err.message);
296
+ process.exit(1);
297
+ });
241
298
  }
@@ -236,6 +236,10 @@ function findVarDeclarationsESM(source) {
236
236
  const restOfLine = source.slice(match.index + match[0].length - 1, match.index + match[0].length + 200);
237
237
  if (/^\s*require\s*\(/.test(restOfLine)) continue;
238
238
 
239
+ // Skip variable declarations inside for-loop headers
240
+ const beforeDecl = source.slice(Math.max(0, match.index - 50), match.index);
241
+ if (/\bfor\s*\(\s*$/.test(beforeDecl)) continue;
242
+
239
243
  // Calculate line number where the declaration starts
240
244
  let lineNo = 1;
241
245
  for (let i = 0; i < match.index; i++) {
@@ -260,6 +264,19 @@ function findVarDeclarationsESM(source) {
260
264
  } else if (ch === '\n' && depth === 0) {
261
265
  const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
262
266
  if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
267
+ // Check if a recent non-empty line ends with an operator (continuation across empty lines)
268
+ let checkPos = pos;
269
+ let lastCh = '';
270
+ for (let back = 0; back < 5; back++) {
271
+ const prevNL = source.lastIndexOf('\n', checkPos - 1);
272
+ const prevLine = source.slice(prevNL + 1, checkPos).trimEnd();
273
+ if (prevLine.length > 0) { lastCh = prevLine[prevLine.length - 1]; break; }
274
+ checkPos = prevNL;
275
+ if (prevNL <= 0) break;
276
+ }
277
+ if (lastCh && '=+-*/%&|^~<>?:,({['.includes(lastCh)) {
278
+ pos++; continue;
279
+ }
263
280
  foundEnd = pos;
264
281
  break;
265
282
  }
@@ -270,6 +287,21 @@ function findVarDeclarationsESM(source) {
270
287
  else if (source[pos] === quote) break;
271
288
  pos++;
272
289
  }
290
+ } else if (ch === '/' && pos + 1 < source.length && source[pos + 1] !== '/' && source[pos + 1] !== '*') {
291
+ // Possible regex literal
292
+ let rp = pos - 1;
293
+ while (rp >= 0 && (source[rp] === ' ' || source[rp] === '\t')) rp--;
294
+ const rpCh = rp >= 0 ? source[rp] : '';
295
+ if ('=(!,;:?[{&|^~+-><%'.includes(rpCh) || source.slice(Math.max(0, rp - 5), rp + 1).match(/\b(return|typeof|instanceof|in|of|void|delete|throw|new|case)\s*$/)) {
296
+ pos++;
297
+ while (pos < source.length) {
298
+ if (source[pos] === '\\') pos++;
299
+ else if (source[pos] === '[') { pos++; while (pos < source.length && source[pos] !== ']') { if (source[pos] === '\\') pos++; pos++; } }
300
+ else if (source[pos] === '/') break;
301
+ pos++;
302
+ }
303
+ while (pos + 1 < source.length && /[gimsuy]/.test(source[pos + 1])) pos++;
304
+ }
273
305
  } else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
274
306
  while (pos < source.length && source[pos] !== '\n') pos++;
275
307
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.88",
3
+ "version": "0.2.90",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/trace-var.ts CHANGED
@@ -221,4 +221,61 @@ if (typeof process !== 'undefined' && process.on) {
221
221
  process.on('beforeExit', exitFlush);
222
222
  process.on('SIGTERM', exitFlush);
223
223
  process.on('SIGINT', exitFlush);
224
+
225
+ // Capture uncaught exceptions with variable context for agent debugging.
226
+ // Write error + nearby variable values to errors.jsonl before the process exits.
227
+ process.on('uncaughtException', (err: Error) => {
228
+ flushVarBuffer();
229
+ try {
230
+ const dir = varsFilePath
231
+ ? path.dirname(varsFilePath)
232
+ : process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
233
+ try { fs.mkdirSync(dir, { recursive: true }); } catch {}
234
+ const errorsFile = path.join(dir, 'errors.jsonl');
235
+
236
+ // Extract file and line from stack trace
237
+ const stackLines = (err.stack || '').split('\n');
238
+ let errorFile: string | undefined;
239
+ let errorLine: number | undefined;
240
+ for (const sl of stackLines.slice(1)) {
241
+ const m = sl.match(/\((.+):(\d+):\d+\)/) || sl.match(/at (.+):(\d+):\d+/);
242
+ if (m && !m[1].includes('node_modules') && !m[1].includes('node:') && !m[1].includes('trickle')) {
243
+ errorFile = m[1];
244
+ errorLine = parseInt(m[2]);
245
+ break;
246
+ }
247
+ }
248
+
249
+ // Find nearby variable values from the cache
250
+ const nearbyVars: Record<string, string> = {};
251
+ if (errorFile && errorLine) {
252
+ for (const [key, entry] of varCache) {
253
+ const parts = key.split(':');
254
+ const file = parts[0];
255
+ const line = parseInt(parts[1]);
256
+ const varName = parts.slice(2).join(':');
257
+ if (file === errorFile && Math.abs(line - errorLine) <= 10) {
258
+ nearbyVars[`L${parts[1]} ${varName}`] = entry.fp;
259
+ }
260
+ }
261
+ }
262
+
263
+ const record = {
264
+ kind: 'error',
265
+ error: err.message,
266
+ type: err.constructor.name,
267
+ file: errorFile,
268
+ line: errorLine,
269
+ stack: stackLines.slice(0, 6).join('\n'),
270
+ nearbyVariables: Object.keys(nearbyVars).length > 0 ? nearbyVars : undefined,
271
+ timestamp: new Date().toISOString(),
272
+ };
273
+ fs.appendFileSync(errorsFile, JSON.stringify(record) + '\n');
274
+ } catch {
275
+ // Never suppress the original error
276
+ }
277
+ // Print the original error and exit (don't re-throw to preserve original stack)
278
+ console.error(err.stack || err.message);
279
+ process.exit(1);
280
+ });
224
281
  }