watchmyagents 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.
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ // WatchMyAgents log inspector
3
+ // Usage:
4
+ // node scripts/inspect.js [path]
5
+ //
6
+ // path can be:
7
+ // - a single .ndjson file
8
+ // - a directory (recursively scans for .ndjson)
9
+ // - omitted → defaults to ./watchmyagents-logs
10
+
11
+ import { readFile, readdir, stat } from 'node:fs/promises';
12
+ import { join, resolve } from 'node:path';
13
+ import { TokenTracker } from '../src/tokens.js';
14
+
15
+ const target = resolve(process.argv[2] || './watchmyagents-logs');
16
+
17
+ async function collectFiles(p) {
18
+ const s = await stat(p).catch(() => null);
19
+ if (!s) return [];
20
+ if (s.isFile()) return p.endsWith('.ndjson') ? [p] : [];
21
+ const out = [];
22
+ for (const name of await readdir(p)) {
23
+ out.push(...(await collectFiles(join(p, name))));
24
+ }
25
+ return out;
26
+ }
27
+
28
+ function fmt(n) { return n.toLocaleString('en-US'); }
29
+ function ms(n) { return n == null ? '—' : `${n.toLocaleString('en-US')} ms`; }
30
+ function pct(num, den) { return den ? `${((num / den) * 100).toFixed(1)}%` : '—'; }
31
+ function truncate(s, n) { s = String(s); return s.length > n ? s.slice(0, n - 1) + '…' : s; }
32
+
33
+ function percentile(arr, p) {
34
+ if (arr.length === 0) return null;
35
+ const sorted = [...arr].sort((a, b) => a - b);
36
+ const idx = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length));
37
+ return sorted[idx];
38
+ }
39
+
40
+ // Best-effort destination extraction from a tool_use input payload.
41
+ // Returns a short identifier of *what* the tool acted on (URL, query, path…).
42
+ function extractDestination(input) {
43
+ if (input == null) return null;
44
+ if (typeof input === 'string') return truncate(input, 60);
45
+ if (typeof input !== 'object') return null;
46
+ const url = input.url || input.uri || input.endpoint;
47
+ if (url) return truncate(url, 60);
48
+ if (input.query) return `"${truncate(input.query, 60)}"`;
49
+ if (input.path || input.file_path) return truncate(input.path || input.file_path, 60);
50
+ if (input.command) return `$ ${truncate(input.command, 60)}`;
51
+ // Fallback: stringify first key/value
52
+ const k = Object.keys(input)[0];
53
+ return k ? `${k}=${truncate(JSON.stringify(input[k]), 50)}` : null;
54
+ }
55
+
56
+ async function main() {
57
+ const files = await collectFiles(target);
58
+ if (files.length === 0) {
59
+ process.stderr.write(`No .ndjson files found under ${target}\n`); process.exit(1);
60
+ }
61
+
62
+ const tracker = new TokenTracker();
63
+ const entries = [];
64
+ const errors = [];
65
+ const sessionEnds = [];
66
+ const bySession = new Map();
67
+ const byStatus = { ok: 0, error: 0 };
68
+ const models = new Set();
69
+ let firstTs = null, lastTs = null;
70
+
71
+ for (const f of files) {
72
+ const raw = await readFile(f, 'utf8');
73
+ for (const line of raw.split('\n')) {
74
+ if (!line.trim()) continue;
75
+ let e; try { e = JSON.parse(line); } catch { continue; }
76
+ entries.push(e);
77
+ tracker.record(e);
78
+ byStatus[e.status] = (byStatus[e.status] || 0) + 1;
79
+ if (e.status === 'error') errors.push(e);
80
+ if (e.action_type === 'session_end') sessionEnds.push(e);
81
+ if (e.session_id) bySession.set(e.session_id, (bySession.get(e.session_id) || 0) + 1);
82
+ if (e.model) models.add(typeof e.model === 'object' ? (e.model.id || JSON.stringify(e.model)) : e.model);
83
+ if (!firstTs || e.timestamp < firstTs) firstTs = e.timestamp;
84
+ if (!lastTs || e.timestamp > lastTs) lastTs = e.timestamp;
85
+ }
86
+ }
87
+
88
+ const stats = tracker.stats();
89
+ const t = stats.total;
90
+ const durationMs = firstTs && lastTs ? new Date(lastTs) - new Date(firstTs) : 0;
91
+
92
+ const out = [];
93
+ out.push('━━━ WatchMyAgents log inspector ━━━');
94
+ out.push(`source : ${target}`);
95
+ out.push(`files scanned : ${files.length}`);
96
+ out.push(`entries : ${fmt(entries.length)}`);
97
+ out.push(`sessions : ${bySession.size} (session_end entries: ${sessionEnds.length})`);
98
+ out.push(`model : ${models.size ? [...models].join(', ') : '—'}`);
99
+ out.push(`window : ${firstTs || '—'} → ${lastTs || '—'}`);
100
+ out.push(`elapsed : ${ms(durationMs)}`);
101
+ out.push(`status : ok=${byStatus.ok || 0} error=${byStatus.error || 0}`);
102
+ out.push('');
103
+ out.push('── Tokens ──');
104
+ out.push(`total : ${fmt(t.sum)} (in=${fmt(t.input)} out=${fmt(t.output)} cache_r=${fmt(t.cache_read)} cache_w=${fmt(t.cache_creation)})`);
105
+ out.push('');
106
+
107
+ const topRows = (obj, label, max = 10) => {
108
+ const rows = Object.entries(obj)
109
+ .sort((a, b) => b[1].sum - a[1].sum)
110
+ .slice(0, max);
111
+ if (rows.length === 0) return;
112
+ out.push(`── By ${label} ──`);
113
+ for (const [k, v] of rows) {
114
+ out.push(` ${k.padEnd(40)} calls=${String(v.calls).padStart(4)} tokens=${String(v.sum).padStart(8)}`);
115
+ }
116
+ out.push('');
117
+ };
118
+ topRows(stats.by_tool, 'tool');
119
+ topRows(stats.by_action, 'action_type');
120
+ // "By model" is redundant when only one model is in use — shown in the header.
121
+ if (Object.keys(stats.by_model).length > 1) topRows(stats.by_model, 'model');
122
+
123
+ if (errors.length) {
124
+ out.push(`── Errors (${errors.length}) ──`);
125
+ for (const e of errors.slice(0, 10)) {
126
+ out.push(` [${e.timestamp}] ${e.tool_name || e.action_type}: ${e.error}`);
127
+ }
128
+ if (errors.length > 10) out.push(` … and ${errors.length - 10} more`);
129
+ out.push('');
130
+ }
131
+
132
+ const slowest = entries
133
+ .filter(e => typeof e.duration_ms === 'number')
134
+ .sort((a, b) => b.duration_ms - a.duration_ms).slice(0, 5);
135
+ if (slowest.length) {
136
+ out.push('── Slowest actions ──');
137
+ for (const e of slowest) {
138
+ out.push(` ${ms(e.duration_ms).padStart(12)} ${e.action_type.padEnd(14)} ${e.tool_name || ''}`);
139
+ }
140
+ out.push('');
141
+ }
142
+
143
+ // ── Security Section 1: Top destinations (URLs / queries / paths) ──
144
+ // Reveals where the agent reached: data exfiltration targets, repeated
145
+ // queries (replay), unexpected hosts. Aggregated per (tool_name, destination).
146
+ const destCounts = new Map();
147
+ for (const e of entries) {
148
+ if (!e.tool_name || (e.action_type !== 'tool_use' && e.action_type !== 'mcp_tool_use' && e.action_type !== 'custom_tool_use')) continue;
149
+ const dest = extractDestination(e.input);
150
+ if (!dest) continue;
151
+ const key = `${e.tool_name}\t${dest}`;
152
+ destCounts.set(key, (destCounts.get(key) || 0) + 1);
153
+ }
154
+ if (destCounts.size) {
155
+ out.push('── Top destinations (tool inputs) ──');
156
+ const rows = [...destCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
157
+ for (const [k, n] of rows) {
158
+ const [tool, dest] = k.split('\t');
159
+ out.push(` ${String(n).padStart(3)}× ${tool.padEnd(16)} ${dest}`);
160
+ }
161
+ out.push('');
162
+ }
163
+
164
+ // ── Security Section 2: Action sequences (Markov transitions) ──
165
+ // Reveals attack patterns: e.g. "tool_use(web_fetch) → tool_use(bash)" or
166
+ // "context_compacted → message" (loss of safety context).
167
+ const sorted = [...entries].sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
168
+ const seqCounts = new Map();
169
+ for (let i = 0; i < sorted.length - 1; i++) {
170
+ const a = sorted[i], b = sorted[i + 1];
171
+ if (a.action_type === 'session_end' || b.action_type === 'session_end') continue;
172
+ if (a.session_id && b.session_id && a.session_id !== b.session_id) continue;
173
+ const key = `${a.action_type} → ${b.action_type}`;
174
+ seqCounts.set(key, (seqCounts.get(key) || 0) + 1);
175
+ }
176
+ if (seqCounts.size) {
177
+ out.push('── Action sequences (top transitions) ──');
178
+ const rows = [...seqCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
179
+ const totalSeq = [...seqCounts.values()].reduce((s, n) => s + n, 0);
180
+ for (const [k, n] of rows) {
181
+ out.push(` ${String(n).padStart(3)}× ${pct(n, totalSeq).padStart(5)} ${k}`);
182
+ }
183
+ out.push('');
184
+ }
185
+
186
+ // ── Security Section 3: Tool error rate ──
187
+ // High error rate on a tool may signal exploit attempts (malformed inputs).
188
+ const toolStats = new Map();
189
+ for (const e of entries) {
190
+ if (!e.tool_name) continue;
191
+ const s = toolStats.get(e.tool_name) || { calls: 0, errors: 0, durations: [] };
192
+ s.calls++;
193
+ if (e.status === 'error') s.errors++;
194
+ if (typeof e.duration_ms === 'number') s.durations.push(e.duration_ms);
195
+ toolStats.set(e.tool_name, s);
196
+ }
197
+ const toolsWithErrors = [...toolStats.entries()].filter(([, s]) => s.errors > 0);
198
+ if (toolsWithErrors.length) {
199
+ out.push('── Tool error rate ──');
200
+ for (const [name, s] of toolsWithErrors.sort((a, b) => b[1].errors - a[1].errors)) {
201
+ out.push(` ${name.padEnd(20)} ${s.errors}/${s.calls} (${pct(s.errors, s.calls)})`);
202
+ }
203
+ out.push('');
204
+ }
205
+
206
+ // ── Security Section 4: Tool latency p50/p95 ──
207
+ // Outliers can hide exfiltration via timing channels or compromised MCPs.
208
+ const toolsWithLatency = [...toolStats.entries()].filter(([, s]) => s.durations.length > 0);
209
+ if (toolsWithLatency.length) {
210
+ out.push('── Tool latency ──');
211
+ for (const [name, s] of toolsWithLatency.sort((a, b) => percentile(b[1].durations, 95) - percentile(a[1].durations, 95))) {
212
+ const p50 = percentile(s.durations, 50);
213
+ const p95 = percentile(s.durations, 95);
214
+ const maxv = Math.max(...s.durations);
215
+ out.push(` ${name.padEnd(20)} n=${String(s.durations.length).padStart(3)} p50=${ms(p50).padStart(10)} p95=${ms(p95).padStart(10)} max=${ms(maxv).padStart(10)}`);
216
+ }
217
+ out.push('');
218
+ }
219
+
220
+ // ── Security Section 5: Rate metrics ──
221
+ // Helps detect abuse loops, runaway agents, or cost spikes.
222
+ if (durationMs > 0) {
223
+ const minutes = durationMs / 60000;
224
+ out.push('── Rate metrics ──');
225
+ out.push(` tokens/min : ${fmt(Math.round(t.sum / minutes))}`);
226
+ out.push(` calls/min : ${(entries.length / minutes).toFixed(2)}`);
227
+ out.push(` llm_calls/min : ${((stats.by_action.llm_call?.calls || 0) / minutes).toFixed(2)}`);
228
+ out.push('');
229
+ }
230
+
231
+ for (const se of sessionEnds) {
232
+ const st = se.session_tokens || {};
233
+ out.push(`── Session ${se.session_id.slice(0, 8)} (from session_end) ──`);
234
+ out.push(` tokens : total=${fmt(st.total || 0)} in=${fmt(st.input || 0)} out=${fmt(st.output || 0)} cache_r=${fmt(st.cache_read || 0)} cache_w=${fmt(st.cache_creation || 0)}`);
235
+ out.push('');
236
+ }
237
+
238
+ process.stdout.write(out.join('\n') + '\n');
239
+ }
240
+
241
+ main().catch(e => { process.stderr.write(`error: ${e.stack || e.message}\n`); process.exit(1); });
@@ -0,0 +1,46 @@
1
+ import { WatchMyAgents } from '../collector.js';
2
+
3
+ export function createClaudeMonitor(opts = {}) {
4
+ const wma = WatchMyAgents.current() || new WatchMyAgents({ ...opts, framework: 'claude' });
5
+
6
+ return {
7
+ wma,
8
+ wrap(client) {
9
+ const m = client?.messages;
10
+ if (!m?.create) return client;
11
+ const orig = m.create.bind(m);
12
+ m.create = async (params) => {
13
+ const start = Date.now();
14
+ let status = 'ok', error = null, res;
15
+ try { res = await orig(params); return res; }
16
+ catch (e) { status = 'error'; error = e?.message || String(e); throw e; }
17
+ finally {
18
+ const u = res?.usage || {};
19
+ const inT = u.input_tokens || 0;
20
+ const outT = u.output_tokens || 0;
21
+ const cr = u.cache_read_input_tokens || 0;
22
+ const cw = u.cache_creation_input_tokens || 0;
23
+ const toolUses = Array.isArray(res?.content)
24
+ ? res.content.filter(b => b?.type === 'tool_use').map(b => b.name) : [];
25
+ await wma.logAction({
26
+ framework: 'claude', action_type: 'llm_call',
27
+ tool_name: params?.model || 'messages.create',
28
+ model: params?.model || null,
29
+ duration_ms: Date.now() - start,
30
+ input_tokens: inT || null,
31
+ output_tokens: outT || null,
32
+ cache_read_tokens: cr || null,
33
+ cache_creation_tokens: cw || null,
34
+ tokens_used: (inT + outT + cr + cw) || null,
35
+ status, error,
36
+ input: { model: params?.model, message_count: params?.messages?.length, tool_count: params?.tools?.length || 0 },
37
+ output: { stop_reason: res?.stop_reason || null, tool_uses: toolUses },
38
+ });
39
+ }
40
+ };
41
+ return client;
42
+ },
43
+ logToolUse: (name, input, output, duration_ms) =>
44
+ wma.logAction({ framework: 'claude', action_type: 'tool_use', tool_name: name, duration_ms: duration_ms ?? null, status: 'ok', input, output }),
45
+ };
46
+ }
@@ -0,0 +1,21 @@
1
+ import { WatchMyAgents } from '../collector.js';
2
+
3
+ export async function watch(toolName, params, fn, meta = {}) {
4
+ const wma = WatchMyAgents.getOrCreate();
5
+ return wma.watch(toolName, params, fn, { framework: 'generic', ...meta });
6
+ }
7
+
8
+ export function createGenericMonitor(opts = {}) {
9
+ const wma = WatchMyAgents.current() || new WatchMyAgents(opts);
10
+ return {
11
+ watch: (toolName, params, fn, meta) => wma.watch(toolName, params, fn, { framework: 'generic', ...meta }),
12
+ wrap(obj, methodNames) {
13
+ const names = methodNames || Object.keys(obj).filter(k => typeof obj[k] === 'function');
14
+ const wrapped = {};
15
+ for (const name of names) {
16
+ wrapped[name] = (...args) => wma.watch(name, args, () => obj[name](...args), { framework: 'generic' });
17
+ }
18
+ return { ...obj, ...wrapped };
19
+ },
20
+ };
21
+ }
@@ -0,0 +1,42 @@
1
+ import { WatchMyAgents } from '../collector.js';
2
+
3
+ export function createLangChainHandler(opts = {}) {
4
+ const wma = WatchMyAgents.current() || new WatchMyAgents({ ...opts, framework: 'langchain' });
5
+ const starts = new Map();
6
+ const begin = id => starts.set(id, Date.now());
7
+ const elapsed = id => { const t = starts.get(id); starts.delete(id); return t ? Date.now() - t : null; };
8
+
9
+ return {
10
+ name: 'WatchMyAgentsHandler',
11
+ handleLLMStart: async (_l, _p, runId) => begin(runId),
12
+ handleLLMEnd: async (out, runId) => {
13
+ const u = out?.llmOutput?.tokenUsage || {};
14
+ const inT = u.promptTokens || 0, outT = u.completionTokens || 0;
15
+ return wma.logAction({
16
+ framework: 'langchain', action_type: 'llm_call', tool_name: 'llm',
17
+ model: out?.llmOutput?.modelName || null,
18
+ duration_ms: elapsed(runId),
19
+ input_tokens: inT || null, output_tokens: outT || null,
20
+ tokens_used: (inT + outT) || null, status: 'ok',
21
+ });
22
+ },
23
+ handleLLMError: async (err, runId) => wma.logAction({
24
+ framework: 'langchain', action_type: 'llm_call', tool_name: 'llm',
25
+ duration_ms: elapsed(runId), status: 'error', error: err?.message || String(err),
26
+ }),
27
+ handleToolStart: async (_t, _i, runId) => begin(runId),
28
+ handleToolEnd: async (_o, runId) => wma.logAction({
29
+ framework: 'langchain', action_type: 'tool_call', tool_name: 'tool',
30
+ duration_ms: elapsed(runId), status: 'ok',
31
+ }),
32
+ handleToolError: async (err, runId) => wma.logAction({
33
+ framework: 'langchain', action_type: 'tool_call', tool_name: 'tool',
34
+ duration_ms: elapsed(runId), status: 'error', error: err?.message || String(err),
35
+ }),
36
+ handleChainStart: async (_c, _i, runId) => begin(runId),
37
+ handleChainEnd: async (_o, runId) => wma.logAction({
38
+ framework: 'langchain', action_type: 'chain', tool_name: 'chain',
39
+ duration_ms: elapsed(runId), status: 'ok',
40
+ }),
41
+ };
42
+ }
@@ -0,0 +1,47 @@
1
+ import { WatchMyAgents } from '../collector.js';
2
+
3
+ export function createOpenAIMonitor(opts = {}) {
4
+ const wma = WatchMyAgents.current() || new WatchMyAgents({ ...opts, framework: 'openai' });
5
+
6
+ function wrapMethod(obj, method, action_type) {
7
+ if (!obj || typeof obj[method] !== 'function') return;
8
+ const orig = obj[method].bind(obj);
9
+ obj[method] = async (params) => {
10
+ const start = Date.now();
11
+ let status = 'ok', error = null, res;
12
+ try { res = await orig(params); return res; }
13
+ catch (e) { status = 'error'; error = e?.message || String(e); throw e; }
14
+ finally {
15
+ const u = res?.usage || {};
16
+ const inT = u.prompt_tokens || u.input_tokens || 0;
17
+ const outT = u.completion_tokens || u.output_tokens || 0;
18
+ const cr = u.prompt_tokens_details?.cached_tokens || 0;
19
+ await wma.logAction({
20
+ framework: 'openai', action_type,
21
+ tool_name: params?.model || params?.assistant_id || method,
22
+ model: params?.model || null,
23
+ duration_ms: Date.now() - start,
24
+ input_tokens: inT || null,
25
+ output_tokens: outT || null,
26
+ cache_read_tokens: cr || null,
27
+ tokens_used: (inT + outT) || null,
28
+ status, error,
29
+ input: { model: params?.model, assistant_id: params?.assistant_id },
30
+ output: { id: res?.id, status: res?.status },
31
+ });
32
+ }
33
+ };
34
+ }
35
+
36
+ return {
37
+ wma,
38
+ wrap(client) {
39
+ wrapMethod(client?.chat?.completions, 'create', 'llm_call');
40
+ wrapMethod(client?.completions, 'create', 'llm_call');
41
+ wrapMethod(client?.beta?.threads?.runs, 'create', 'assistant_run');
42
+ wrapMethod(client?.beta?.threads?.runs, 'createAndPoll', 'assistant_run');
43
+ wrapMethod(client?.responses, 'create', 'llm_call');
44
+ return client;
45
+ },
46
+ };
47
+ }
@@ -0,0 +1,48 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ const PII_PATTERNS = [
4
+ [/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, '[EMAIL]'],
5
+ [/Bearer\s+[A-Za-z0-9\-_\.=]+/gi, '[TOKEN]'],
6
+ [/eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+/g, '[TOKEN]'],
7
+ [/\b(sk|pk|rk)-[A-Za-z0-9_\-]{16,}\b/g, '[API_KEY]'],
8
+ [/\bwma_[A-Za-z0-9_\-]{8,}\b/g, '[API_KEY]'],
9
+ [/\b(?:\d[ -]*?){13,19}\b/g, '[CARD]'],
10
+ [/\b\+?\d{1,3}[\s.-]?\(?\d{2,4}\)?[\s.-]?\d{3,4}[\s.-]?\d{3,4}\b/g, '[PHONE]'],
11
+ [/https?:\/\/[^\s"'<>]+/gi, '[URL]'],
12
+ [/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, '[IP]'],
13
+ [/\b(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}\b/gi, '[IP]'],
14
+ ];
15
+
16
+ const HASH_FIELDS = new Set(['user_id', 'session_id', 'agent_id']);
17
+
18
+ export function scrubString(str) {
19
+ if (typeof str !== 'string') return str;
20
+ let out = str;
21
+ for (const [re, tag] of PII_PATTERNS) out = out.replace(re, tag);
22
+ return out;
23
+ }
24
+
25
+ export function hashId(value) {
26
+ if (value == null) return value;
27
+ return 'h_' + createHash('sha256').update(String(value)).digest('hex').slice(0, 24);
28
+ }
29
+
30
+ export function anonymize(obj) {
31
+ if (obj == null) return obj;
32
+ if (typeof obj === 'string') return scrubString(obj);
33
+ if (typeof obj !== 'object') return obj;
34
+ if (Array.isArray(obj)) return obj.map(anonymize);
35
+ const out = {};
36
+ for (const [k, v] of Object.entries(obj)) {
37
+ if (HASH_FIELDS.has(k) && (typeof v === 'string' || typeof v === 'number')) {
38
+ out[k] = hashId(v);
39
+ } else if (typeof v === 'string') {
40
+ out[k] = scrubString(v);
41
+ } else if (typeof v === 'object') {
42
+ out[k] = anonymize(v);
43
+ } else {
44
+ out[k] = v;
45
+ }
46
+ }
47
+ return out;
48
+ }
@@ -0,0 +1,113 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { Logger } from './logger.js';
3
+ import { Exporter } from './exporter.js';
4
+ import { TokenTracker, estimateCost } from './tokens.js';
5
+
6
+ let _instance = null;
7
+
8
+ export class WatchMyAgents {
9
+ constructor(opts = {}) {
10
+ this.apiKey = opts.apiKey || process.env.WMA_API_KEY || null;
11
+ this.agentId = opts.agentId || 'default-agent';
12
+ this.logDir = opts.logDir || process.env.WMA_LOG_DIR || './watchmyagents-logs';
13
+ this.exportUrl = opts.exportUrl || process.env.WMA_EXPORT_URL || null;
14
+ this.silent = opts.silent !== false;
15
+ this.sessionId = opts.sessionId || randomUUID();
16
+ this.framework = opts.framework || 'generic';
17
+ this.tokenPricing = opts.tokenPricing || null;
18
+ this.tracker = new TokenTracker();
19
+ this.logger = new Logger({ logDir: this.logDir, agentId: this.agentId, sessionId: this.sessionId, silent: this.silent });
20
+ this.exporter = new Exporter({
21
+ apiKey: this.apiKey, exportUrl: this.exportUrl, agentId: this.agentId,
22
+ batchInterval: opts.batchInterval ?? 30000, silent: this.silent,
23
+ });
24
+ this.exporter.start();
25
+ _instance = this;
26
+ }
27
+
28
+ static current() { return _instance; }
29
+ static getOrCreate(opts) { return _instance || new WatchMyAgents(opts); }
30
+
31
+ summarize(v) {
32
+ if (v == null) return null;
33
+ const t = typeof v;
34
+ if (t === 'string') return { type: 'string', length: v.length };
35
+ if (t === 'number' || t === 'boolean') return { type: t };
36
+ if (Array.isArray(v)) return { type: 'array', length: v.length };
37
+ if (t === 'object') return { type: 'object', keys: Object.keys(v).length };
38
+ return { type: t };
39
+ }
40
+
41
+ _enrichTokens(entry) {
42
+ const i = entry.input_tokens, o = entry.output_tokens;
43
+ const cr = entry.cache_read_tokens || 0, cw = entry.cache_creation_tokens || 0;
44
+ if (entry.tokens_used == null && (i != null || o != null)) {
45
+ entry.tokens_used = (i || 0) + (o || 0) + cr + cw;
46
+ }
47
+ if (entry.cost_usd == null && entry.model) {
48
+ entry.cost_usd = estimateCost(entry.model, {
49
+ input_tokens: i, output_tokens: o,
50
+ cache_read_tokens: cr, cache_creation_tokens: cw,
51
+ }, this.tokenPricing);
52
+ }
53
+ return entry;
54
+ }
55
+
56
+ async watch(toolName, params, fn, meta = {}) {
57
+ const start = Date.now();
58
+ const id = randomUUID();
59
+ let status = 'ok', error = null, result;
60
+ try { result = await fn(); return result; }
61
+ catch (e) { status = 'error'; error = e?.message || String(e); throw e; }
62
+ finally {
63
+ const entry = await this.logger.write(this._enrichTokens({
64
+ id, framework: meta.framework || this.framework,
65
+ action_type: meta.action_type || 'tool_call',
66
+ tool_name: toolName, duration_ms: Date.now() - start,
67
+ model: meta.model ?? null,
68
+ tokens_used: meta.tokens_used ?? null,
69
+ input_tokens: meta.input_tokens ?? null,
70
+ output_tokens: meta.output_tokens ?? null,
71
+ cache_read_tokens: meta.cache_read_tokens ?? null,
72
+ cache_creation_tokens: meta.cache_creation_tokens ?? null,
73
+ status, error, input: params, output: this.summarize(result),
74
+ }));
75
+ this.tracker.record(entry);
76
+ this.exporter.enqueue(this.logger.toExportRecord(entry));
77
+ }
78
+ }
79
+
80
+ async logAction(entry) {
81
+ const written = await this.logger.write(this._enrichTokens({ ...entry }));
82
+ this.tracker.record(written);
83
+ this.exporter.enqueue(this.logger.toExportRecord(written));
84
+ return written;
85
+ }
86
+
87
+ tokenStats() { return this.tracker.stats(); }
88
+
89
+ async flush() { await this.exporter.flush(); }
90
+
91
+ async shutdown() {
92
+ const stats = this.tracker.stats().total;
93
+ const entry = await this.logger.write({
94
+ action_type: 'session_end',
95
+ tool_name: null,
96
+ framework: this.framework,
97
+ status: 'ok',
98
+ session_tokens: {
99
+ input: stats.input,
100
+ output: stats.output,
101
+ cache_read: stats.cache_read,
102
+ cache_creation: stats.cache_creation,
103
+ total: stats.sum,
104
+ },
105
+ session_cost_usd: stats.cost_usd,
106
+ });
107
+ this.exporter.enqueue(this.logger.toExportRecord(entry));
108
+ this.exporter.stop();
109
+ await this.exporter.flush();
110
+ }
111
+ get logPath() { return this.logger._pathForToday(); }
112
+ get actionCount() { return this.logger.count; }
113
+ }
@@ -0,0 +1,71 @@
1
+ import { request } from 'node:https';
2
+ import { URL } from 'node:url';
3
+ import { createCipheriv, randomBytes, scryptSync } from 'node:crypto';
4
+ import { anonymize } from './anonymizer.js';
5
+
6
+ const SALT = Buffer.from('watchmyagents.v1.salt', 'utf8');
7
+
8
+ export class Exporter {
9
+ constructor({ apiKey, exportUrl, agentId, batchInterval = 30000, silent = true }) {
10
+ this.apiKey = apiKey;
11
+ this.exportUrl = exportUrl;
12
+ this.agentId = agentId;
13
+ this.silent = silent;
14
+ this.queue = [];
15
+ this.enabled = !!(apiKey && exportUrl);
16
+ this.key = this.enabled ? scryptSync(apiKey, SALT, 32) : null;
17
+ this.timer = null;
18
+ this.batchInterval = batchInterval;
19
+ }
20
+
21
+ start() {
22
+ if (this.timer || !this.enabled) return;
23
+ this.timer = setInterval(() => this.flush().catch(() => {}), this.batchInterval);
24
+ if (this.timer.unref) this.timer.unref();
25
+ }
26
+ stop() { if (this.timer) { clearInterval(this.timer); this.timer = null; } }
27
+ enqueue(record) { if (this.enabled) this.queue.push(anonymize(record)); }
28
+
29
+ _encrypt(payload) {
30
+ const iv = randomBytes(16);
31
+ const c = createCipheriv('aes-256-gcm', this.key, iv);
32
+ const data = Buffer.concat([c.update(payload, 'utf8'), c.final()]);
33
+ return { iv: iv.toString('base64'), tag: c.getAuthTag().toString('base64'), data: data.toString('base64') };
34
+ }
35
+
36
+ async flush() {
37
+ if (!this.exportUrl || !this.apiKey || this.queue.length === 0) return;
38
+ const batch = this.queue.splice(0, this.queue.length);
39
+ const body = JSON.stringify({ agent_id: this.agentId, count: batch.length, records: batch });
40
+ const payload = JSON.stringify({ v: 1, agent_id: this.agentId, ...this._encrypt(body) });
41
+ for (let attempt = 1; attempt <= 3; attempt++) {
42
+ try { await this._post(payload); return; }
43
+ catch (e) {
44
+ if (attempt === 3) {
45
+ this.queue.unshift(...batch);
46
+ if (!this.silent) process.stderr.write(`[wma] export failed: ${e.message}\n`);
47
+ return;
48
+ }
49
+ await new Promise(r => setTimeout(r, 1000 * 2 ** attempt));
50
+ }
51
+ }
52
+ }
53
+
54
+ _post(payload) {
55
+ return new Promise((resolve, reject) => {
56
+ let u; try { u = new URL(this.exportUrl); } catch (e) { return reject(e); }
57
+ if (u.protocol !== 'https:') return reject(new Error('HTTPS only'));
58
+ const req = request({
59
+ host: u.hostname, port: u.port || 443, path: u.pathname + u.search,
60
+ method: 'POST', rejectUnauthorized: true,
61
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload), 'X-WMA-Key': this.apiKey },
62
+ }, res => {
63
+ res.resume();
64
+ if (res.statusCode >= 200 && res.statusCode < 300) resolve();
65
+ else reject(new Error(`HTTP ${res.statusCode}`));
66
+ });
67
+ req.on('error', reject);
68
+ req.write(payload); req.end();
69
+ });
70
+ }
71
+ }
package/src/index.cjs ADDED
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ // CommonJS entrypoint — re-exports the ESM build via dynamic import.
4
+ // All consumers receive the same singleton-backed API.
5
+
6
+ const esmPromise = import('./index.js');
7
+
8
+ function bind(name) {
9
+ return async function (...args) {
10
+ const mod = await esmPromise;
11
+ return mod[name](...args);
12
+ };
13
+ }
14
+
15
+ class WatchMyAgentsLazy {
16
+ constructor(opts) {
17
+ this._ready = esmPromise.then(mod => new mod.WatchMyAgents(opts));
18
+ }
19
+ async watch(...args) { return (await this._ready).watch(...args); }
20
+ async logAction(...args) { return (await this._ready).logAction(...args); }
21
+ async flush() { return (await this._ready).flush(); }
22
+ async shutdown() { return (await this._ready).shutdown(); }
23
+ get instance() { return this._ready; }
24
+ }
25
+
26
+ module.exports = WatchMyAgentsLazy;
27
+ module.exports.default = WatchMyAgentsLazy;
28
+ module.exports.WatchMyAgents = WatchMyAgentsLazy;
29
+ module.exports.watch = bind('watch');
30
+ module.exports.createGenericMonitor = bind('createGenericMonitor');
31
+ module.exports.createClaudeMonitor = bind('createClaudeMonitor');
32
+ module.exports.createOpenAIMonitor = bind('createOpenAIMonitor');
33
+ module.exports.createLangChainHandler = bind('createLangChainHandler');
34
+ module.exports.anonymize = bind('anonymize');
35
+ module.exports.scrubString = bind('scrubString');
36
+ module.exports.hashId = bind('hashId');