@raquezha/notrace 0.0.5 → 0.0.6

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 (95) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +102 -55
  3. package/assets/notrace-logo.svg +20 -0
  4. package/assets/notrace-mark.svg +18 -0
  5. package/assets/notrace-wordmark.svg +4 -0
  6. package/dist/notrace/adapters.d.ts +32 -0
  7. package/dist/notrace/adapters.js +83 -0
  8. package/dist/notrace/index.d.ts +2 -0
  9. package/dist/notrace/index.js +295 -0
  10. package/dist/notrace/renderer.d.ts +4 -0
  11. package/dist/notrace/renderer.js +605 -0
  12. package/dist/notrace/types.d.ts +93 -0
  13. package/dist/notrace/types.js +1 -0
  14. package/extensions/notrace/adapters.ts +88 -0
  15. package/extensions/notrace/index.ts +359 -0
  16. package/extensions/notrace/renderer.ts +616 -0
  17. package/extensions/notrace/types.ts +108 -0
  18. package/package.json +4 -3
  19. package/templates/README.md +24 -0
  20. package/templates/dashboard.sample.html +361 -0
  21. package/templates/dashboard.sample.json +113 -0
  22. package/templates/notrace-logo.preview.png +0 -0
  23. package/templates/render-samples.mjs +44 -0
  24. package/templates/session.sample.html +431 -0
  25. package/templates/session.sample.json +127 -0
  26. package/templates/sessions/019ecec4-1c48-7b47-bdbf-cec3500978ed/notrace.html +313 -0
  27. package/templates/sessions/019ecec4-1c48-7b47-bdbf-cec3500978ed/notrace.json +129 -0
  28. package/templates/sessions/019ecfa1-7ac2-7c00-bbe9-541f37751201/notrace.html +313 -0
  29. package/templates/sessions/019ecfa1-7ac2-7c00-bbe9-541f37751201/notrace.json +129 -0
  30. package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.html +426 -0
  31. package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.json +134 -0
  32. package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.html +425 -0
  33. package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.json +133 -0
  34. package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.html +426 -0
  35. package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.json +134 -0
  36. package/templates/sessions/019ed2ee-1003-76ee-b353-000000000004/notrace.html +423 -0
  37. package/templates/sessions/019ed2ee-1003-76ee-b353-000000000004/notrace.json +130 -0
  38. package/templates/sessions/019ed2ee-1004-76ee-b353-000000000005/notrace.html +423 -0
  39. package/templates/sessions/019ed2ee-1004-76ee-b353-000000000005/notrace.json +130 -0
  40. package/templates/sessions/019ed2ee-1005-76ee-b353-000000000006/notrace.html +423 -0
  41. package/templates/sessions/019ed2ee-1005-76ee-b353-000000000006/notrace.json +130 -0
  42. package/templates/sessions/019ed2ee-1006-76ee-b353-000000000007/notrace.html +423 -0
  43. package/templates/sessions/019ed2ee-1006-76ee-b353-000000000007/notrace.json +130 -0
  44. package/templates/sessions/019ed2ee-1007-76ee-b353-000000000008/notrace.html +423 -0
  45. package/templates/sessions/019ed2ee-1007-76ee-b353-000000000008/notrace.json +130 -0
  46. package/templates/sessions/019ed2ee-1008-76ee-b353-000000000009/notrace.html +423 -0
  47. package/templates/sessions/019ed2ee-1008-76ee-b353-000000000009/notrace.json +130 -0
  48. package/templates/sessions/019ed2ee-1009-76ee-b353-000000000010/notrace.html +423 -0
  49. package/templates/sessions/019ed2ee-1009-76ee-b353-000000000010/notrace.json +130 -0
  50. package/templates/sessions/019ed2ee-1010-76ee-b353-000000000011/notrace.html +423 -0
  51. package/templates/sessions/019ed2ee-1010-76ee-b353-000000000011/notrace.json +130 -0
  52. package/templates/sessions/019ed2ee-1011-76ee-b353-000000000012/notrace.html +423 -0
  53. package/templates/sessions/019ed2ee-1011-76ee-b353-000000000012/notrace.json +130 -0
  54. package/templates/sessions/019ed2ee-1012-76ee-b353-000000000013/notrace.html +423 -0
  55. package/templates/sessions/019ed2ee-1012-76ee-b353-000000000013/notrace.json +130 -0
  56. package/templates/sessions/019ed2ee-1013-76ee-b353-000000000014/notrace.html +423 -0
  57. package/templates/sessions/019ed2ee-1013-76ee-b353-000000000014/notrace.json +130 -0
  58. package/templates/sessions/019ed2ee-1014-76ee-b353-000000000015/notrace.html +423 -0
  59. package/templates/sessions/019ed2ee-1014-76ee-b353-000000000015/notrace.json +130 -0
  60. package/templates/sessions/019ed2ee-1015-76ee-b353-000000000016/notrace.html +423 -0
  61. package/templates/sessions/019ed2ee-1015-76ee-b353-000000000016/notrace.json +130 -0
  62. package/templates/sessions/019ed2ee-1016-76ee-b353-000000000017/notrace.html +423 -0
  63. package/templates/sessions/019ed2ee-1016-76ee-b353-000000000017/notrace.json +130 -0
  64. package/templates/sessions/019ed2ee-1017-76ee-b353-000000000018/notrace.html +423 -0
  65. package/templates/sessions/019ed2ee-1017-76ee-b353-000000000018/notrace.json +130 -0
  66. package/templates/sessions/019ed2ee-1018-76ee-b353-000000000019/notrace.html +423 -0
  67. package/templates/sessions/019ed2ee-1018-76ee-b353-000000000019/notrace.json +130 -0
  68. package/templates/sessions/019ed2ee-1019-76ee-b353-000000000020/notrace.html +423 -0
  69. package/templates/sessions/019ed2ee-1019-76ee-b353-000000000020/notrace.json +130 -0
  70. package/templates/sessions/019ed2ee-1020-76ee-b353-000000000021/notrace.html +423 -0
  71. package/templates/sessions/019ed2ee-1020-76ee-b353-000000000021/notrace.json +130 -0
  72. package/templates/sessions/019ed2ee-1021-76ee-b353-000000000022/notrace.html +423 -0
  73. package/templates/sessions/019ed2ee-1021-76ee-b353-000000000022/notrace.json +130 -0
  74. package/templates/sessions/019ed2ee-1022-76ee-b353-000000000023/notrace.html +423 -0
  75. package/templates/sessions/019ed2ee-1022-76ee-b353-000000000023/notrace.json +130 -0
  76. package/templates/sessions/019ed2ee-1023-76ee-b353-000000000024/notrace.html +423 -0
  77. package/templates/sessions/019ed2ee-1023-76ee-b353-000000000024/notrace.json +130 -0
  78. package/templates/sessions/019ed2ee-1024-76ee-b353-000000000025/notrace.html +423 -0
  79. package/templates/sessions/019ed2ee-1024-76ee-b353-000000000025/notrace.json +130 -0
  80. package/templates/sessions/019ed2ee-1025-76ee-b353-000000000026/notrace.html +423 -0
  81. package/templates/sessions/019ed2ee-1025-76ee-b353-000000000026/notrace.json +130 -0
  82. package/templates/sessions/019ed2ee-1026-76ee-b353-000000000027/notrace.html +423 -0
  83. package/templates/sessions/019ed2ee-1026-76ee-b353-000000000027/notrace.json +130 -0
  84. package/templates/sessions/019ed2ee-1027-76ee-b353-000000000028/notrace.html +423 -0
  85. package/templates/sessions/019ed2ee-1027-76ee-b353-000000000028/notrace.json +130 -0
  86. package/templates/sessions/019ed2ee-1028-76ee-b353-000000000029/notrace.html +423 -0
  87. package/templates/sessions/019ed2ee-1028-76ee-b353-000000000029/notrace.json +130 -0
  88. package/templates/sessions/019ed2ee-1029-76ee-b353-000000000030/notrace.html +423 -0
  89. package/templates/sessions/019ed2ee-1029-76ee-b353-000000000030/notrace.json +130 -0
  90. package/templates/sessions/019ed2ee-5252-76ee-b353-ad925a6bad31/notrace.html +313 -0
  91. package/templates/sessions/019ed2ee-5252-76ee-b353-ad925a6bad31/notrace.json +129 -0
  92. package/tsconfig.json +1 -1
  93. package/dist/notrace.d.ts +0 -9
  94. package/dist/notrace.js +0 -914
  95. package/extensions/notrace.ts +0 -965
@@ -0,0 +1,295 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { getActiveAdapter } from "./adapters.js";
4
+ import { generateHtmlReport, generateDashboardHtml } from "./renderer.js";
5
+ const REDACTED = "[REDACTED by notrace]";
6
+ const SENSITIVE_VALUE_RE = /(bearer\s+[a-z0-9._~+/=-]{12,}|sk-[a-z0-9_-]{16,}|gh[pousr]_[a-z0-9_]{16,}|AKIA[0-9A-Z]{16})/gi;
7
+ const TELEMETRY_CHANNEL = "notrace.telemetry.extension";
8
+ const SCHEMA_VERSION = 2;
9
+ let currentMode = "full";
10
+ function getInitialMode() {
11
+ const env = process.env.NOTRACE_CAPTURE?.toLowerCase();
12
+ if (env === "metadata" || env === "redacted")
13
+ return env;
14
+ return "full";
15
+ }
16
+ const SENSITIVE_KEY_RE = /(authorization|cookie|setcookie|password|passwd|pwd|secret|token|apikey|accesskey|accesskeyid|accessid|accesstoken|privatekey|session|credential|refreshtoken|idtoken)/i;
17
+ function isSensitiveKey(key) {
18
+ const normalized = key.replace(/[^a-z0-9]/gi, "");
19
+ if (/^(inputtokens|outputtokens|totaltokens|prompttokens|completiontokens|reasoningtokens|cachedtokens|cachecreationinputtokens|cachereadinputtokens|cost|total|input|output|prompt|completion|reasoning|read|write)$/i.test(normalized))
20
+ return false;
21
+ return SENSITIVE_KEY_RE.test(normalized);
22
+ }
23
+ function sanitizeTraceValue(value) {
24
+ if (currentMode === "full")
25
+ return value;
26
+ if (value == null || typeof value !== "object") {
27
+ return typeof value === "string" ? value.replace(SENSITIVE_VALUE_RE, REDACTED).slice(0, 10000) : value;
28
+ }
29
+ if (Array.isArray(value))
30
+ return value.slice(0, 100).map(sanitizeTraceValue);
31
+ const out = {};
32
+ for (const [k, v] of Object.entries(value).slice(0, 100))
33
+ out[k] = isSensitiveKey(k) ? REDACTED : sanitizeTraceValue(v);
34
+ return out;
35
+ }
36
+ function asNumber(value) {
37
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
38
+ }
39
+ function normalizeUsage(raw) {
40
+ const usage = (raw && typeof raw === "object" ? raw : {});
41
+ return {
42
+ inputTokens: asNumber(usage.inputTokens ?? usage.input),
43
+ outputTokens: asNumber(usage.outputTokens ?? usage.output),
44
+ cacheReadTokens: asNumber(usage.cacheReadTokens ?? usage.cacheRead),
45
+ cacheWriteTokens: asNumber(usage.cacheWriteTokens ?? usage.cacheWrite),
46
+ totalTokens: asNumber(usage.totalTokens),
47
+ totalCostUsd: asNumber(usage.cost?.total),
48
+ };
49
+ }
50
+ function collectActivity(events, startedAt, endedAt) {
51
+ const activity = {
52
+ turnCount: 0,
53
+ llmCallCount: 0,
54
+ toolCallCount: 0,
55
+ toolErrorCount: 0,
56
+ durationMs: Math.max(0, endedAt - startedAt),
57
+ totals: {
58
+ inputTokens: 0,
59
+ outputTokens: 0,
60
+ cacheReadTokens: 0,
61
+ cacheWriteTokens: 0,
62
+ totalTokens: 0,
63
+ totalCostUsd: 0,
64
+ },
65
+ };
66
+ for (const e of events) {
67
+ if (e.type === "turn_start")
68
+ activity.turnCount++;
69
+ if (e.type === "tool_start")
70
+ activity.toolCallCount++;
71
+ if (e.type === "tool_end" && e.isError)
72
+ activity.toolErrorCount++;
73
+ if (e.type === "llm_completion") {
74
+ activity.llmCallCount++;
75
+ const usage = normalizeUsage(e.usage);
76
+ activity.totals.inputTokens += usage.inputTokens;
77
+ activity.totals.outputTokens += usage.outputTokens;
78
+ activity.totals.cacheReadTokens += usage.cacheReadTokens;
79
+ activity.totals.cacheWriteTokens += usage.cacheWriteTokens;
80
+ activity.totals.totalTokens += usage.totalTokens;
81
+ activity.totals.totalCostUsd += usage.totalCostUsd;
82
+ }
83
+ }
84
+ return activity;
85
+ }
86
+ function buildConditions(events, telemetry) {
87
+ const models = new Set();
88
+ const providers = new Set();
89
+ for (const event of events) {
90
+ if (event.type !== "llm_completion")
91
+ continue;
92
+ if (typeof event.model === "string" && event.model)
93
+ models.add(event.model);
94
+ if (typeof event.provider === "string" && event.provider)
95
+ providers.add(event.provider);
96
+ }
97
+ const extensions = ["notrace", ...Object.keys(telemetry).sort()];
98
+ return {
99
+ harness: {
100
+ name: "pi",
101
+ adapter: "pi-session-hooks",
102
+ version: null,
103
+ },
104
+ models: [...models],
105
+ providers: [...providers],
106
+ extensions,
107
+ };
108
+ }
109
+ function toTaskInfo(context) {
110
+ if (!context)
111
+ return null;
112
+ return {
113
+ workflow: context.workflow,
114
+ id: context.taskId,
115
+ path: context.taskPath,
116
+ dir: context.taskDir,
117
+ };
118
+ }
119
+ function createIndexEntry(record, cwd, htmlPath, recordPath) {
120
+ return {
121
+ sessionId: record.traceId,
122
+ startedAt: record.session.startedAt,
123
+ endedAt: record.session.endedAt,
124
+ captureMode: record.captureMode,
125
+ task: record.task,
126
+ conditions: record.conditions,
127
+ activity: record.activity,
128
+ artifacts: {
129
+ html: path.relative(cwd, htmlPath),
130
+ record: path.relative(cwd, recordPath),
131
+ },
132
+ };
133
+ }
134
+ function normalizeTelemetryPayload(raw) {
135
+ if (!raw || typeof raw !== "object")
136
+ return null;
137
+ const payload = raw;
138
+ if (typeof payload.extension !== "string" || !payload.extension.trim())
139
+ return null;
140
+ const status = payload.status === "absent" ||
141
+ payload.status === "loaded-disabled" ||
142
+ payload.status === "loaded-inactive" ||
143
+ payload.status === "active" ||
144
+ payload.status === "unknown"
145
+ ? payload.status
146
+ : "unknown";
147
+ return {
148
+ extension: payload.extension,
149
+ telemetry: {
150
+ loaded: payload.loaded !== false,
151
+ enabled: typeof payload.enabled === "boolean" ? payload.enabled : null,
152
+ active: typeof payload.active === "boolean" ? payload.active : null,
153
+ status,
154
+ summary: typeof payload.summary === "string" ? payload.summary : null,
155
+ details: payload.details && typeof payload.details === "object" ? payload.details : {},
156
+ },
157
+ };
158
+ }
159
+ export default function (pi) {
160
+ const events = [];
161
+ const startTime = Date.now();
162
+ let traceId = "";
163
+ let activeLlmPayload = null;
164
+ let shutdownReason = null;
165
+ const extensionTelemetry = new Map();
166
+ currentMode = getInitialMode();
167
+ if (typeof pi.events?.on === "function") {
168
+ pi.events.on(TELEMETRY_CHANNEL, (raw) => {
169
+ const normalized = normalizeTelemetryPayload(raw);
170
+ if (!normalized)
171
+ return;
172
+ extensionTelemetry.set(normalized.extension, normalized.telemetry);
173
+ });
174
+ }
175
+ pi.registerCommand("notrace", {
176
+ description: "Change notrace capture mode (full | redacted | metadata)",
177
+ handler: async (args, ctx) => {
178
+ const mode = args?.trim().toLowerCase();
179
+ if (mode === "full" || mode === "redacted" || mode === "metadata") {
180
+ currentMode = mode;
181
+ ctx.ui.notify(`notrace capture mode set to: ${currentMode}`, "info");
182
+ }
183
+ else {
184
+ ctx.ui.notify(`Current notrace mode: ${currentMode}. Usage: /notrace [full|redacted|metadata]`, "info");
185
+ }
186
+ }
187
+ });
188
+ pi.on("session_start", async (_e, ctx) => {
189
+ traceId = ctx.sessionManager.getSessionId() || `s-${Date.now()}`;
190
+ events.push({ type: "session_start", timestamp: Date.now() });
191
+ });
192
+ pi.on("turn_start", async () => events.push({ type: "turn_start", timestamp: Date.now() }));
193
+ pi.on("tool_execution_start", async (e) => {
194
+ events.push({ type: "tool_start", toolName: e.toolName, args: sanitizeTraceValue(e.args), timestamp: Date.now() });
195
+ });
196
+ pi.on("tool_execution_end", async (e) => {
197
+ events.push({ type: "tool_end", toolName: e.toolName, result: sanitizeTraceValue(e.result), isError: e.isError, timestamp: Date.now() });
198
+ });
199
+ pi.on("before_provider_request", async (e) => {
200
+ activeLlmPayload = sanitizeTraceValue(e.payload);
201
+ });
202
+ pi.on("message_end", async (e) => {
203
+ if (e.message.role === "assistant") {
204
+ events.push({
205
+ type: "llm_completion",
206
+ model: e.message.model,
207
+ provider: e.message.provider,
208
+ inputPayload: activeLlmPayload,
209
+ outputContent: sanitizeTraceValue(e.message.content),
210
+ usage: e.message.usage,
211
+ stopReason: typeof e.message.stopReason === "string" ? e.message.stopReason : undefined,
212
+ errorMessage: typeof e.message.errorMessage === "string" ? sanitizeTraceValue(e.message.errorMessage) : undefined,
213
+ timestamp: Date.now(),
214
+ });
215
+ activeLlmPayload = null;
216
+ }
217
+ });
218
+ pi.on("session_shutdown", async (e, ctx) => {
219
+ shutdownReason = typeof e?.reason === "string" ? e.reason : null;
220
+ const endedAt = Date.now();
221
+ const adapter = getActiveAdapter(ctx.cwd);
222
+ const context = adapter.getContext(ctx.cwd);
223
+ const notraceDir = path.resolve(ctx.cwd, ".notrace");
224
+ const finalTraceId = ctx.sessionManager?.getSessionId?.() || traceId;
225
+ const outputDir = path.join(notraceDir, "sessions", finalTraceId.replace(/[^a-z0-9]/gi, "-"));
226
+ const repositoryName = path.basename(ctx.cwd);
227
+ const recordPath = path.join(outputDir, "notrace.json");
228
+ let mergedEvents = events;
229
+ let originalStartedAt = startTime;
230
+ let originalTask = null;
231
+ if (existsSync(recordPath)) {
232
+ try {
233
+ const oldRecord = JSON.parse(readFileSync(recordPath, "utf-8"));
234
+ if (Array.isArray(oldRecord.events)) {
235
+ mergedEvents = [...oldRecord.events, ...events];
236
+ }
237
+ if (oldRecord.session?.startedAt) {
238
+ originalStartedAt = new Date(oldRecord.session.startedAt).getTime();
239
+ }
240
+ if (oldRecord.task) {
241
+ originalTask = oldRecord.task;
242
+ }
243
+ }
244
+ catch (err) {
245
+ // ignore parse errors
246
+ }
247
+ }
248
+ const activity = collectActivity(mergedEvents, originalStartedAt, endedAt);
249
+ // Do not index purely empty ghost sessions
250
+ const isGhostSession = activity.llmCallCount === 0 && activity.toolCallCount === 0 && activity.totals.totalTokens === 0;
251
+ const telemetry = Object.fromEntries([...extensionTelemetry.entries()].sort(([a], [b]) => a.localeCompare(b)));
252
+ const record = {
253
+ kind: "notrace-run",
254
+ schemaVersion: SCHEMA_VERSION,
255
+ traceId: finalTraceId,
256
+ repository: {
257
+ name: repositoryName,
258
+ cwd: ctx.cwd,
259
+ },
260
+ session: {
261
+ id: finalTraceId,
262
+ startedAt: new Date(originalStartedAt).toISOString(),
263
+ endedAt: new Date(endedAt).toISOString(),
264
+ durationMs: activity.durationMs,
265
+ shutdownReason,
266
+ },
267
+ task: toTaskInfo(context) || originalTask,
268
+ captureMode: currentMode,
269
+ conditions: buildConditions(mergedEvents, telemetry),
270
+ activity,
271
+ telemetry: { extensions: telemetry },
272
+ events: mergedEvents,
273
+ };
274
+ const html = generateHtmlReport(record);
275
+ mkdirSync(outputDir, { recursive: true });
276
+ const htmlPath = path.join(outputDir, "notrace.html");
277
+ writeFileSync(htmlPath, html);
278
+ writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}\n`);
279
+ const indexPath = path.join(notraceDir, "index.json");
280
+ const existing = existsSync(indexPath) ? JSON.parse(readFileSync(indexPath, "utf-8")) : { repositoryName, sessions: [] };
281
+ let sessions = Array.isArray(existing.sessions) ? existing.sessions.filter((s) => s.sessionId !== finalTraceId) : [];
282
+ if (!isGhostSession) {
283
+ sessions.push(createIndexEntry(record, ctx.cwd, htmlPath, recordPath));
284
+ }
285
+ writeFileSync(indexPath, `${JSON.stringify({ repositoryName, sessions }, null, 2)}\n`);
286
+ writeFileSync(path.join(notraceDir, "index.html"), generateDashboardHtml(sessions, { repositoryName }));
287
+ if (context) {
288
+ adapter.attach(context, {
289
+ html: path.relative(ctx.cwd, htmlPath),
290
+ record: path.relative(ctx.cwd, recordPath)
291
+ });
292
+ }
293
+ console.log(`\n\x1b[1m\x1b[38;5;208m[notrace] Session Retrospective: file://${htmlPath}\x1b[0m\n`);
294
+ });
295
+ }
@@ -0,0 +1,4 @@
1
+ export declare function escapeHtml(v: unknown): string;
2
+ export declare function safeJsonForScript(v: any): string;
3
+ export declare function generateDashboardHtml(sessions: any[], options?: any): string;
4
+ export declare function generateHtmlReport(data: any): string;